Fully implemented Austin Morgan's ECS in Java
(minus generic typing)
This commit is contained in:
parent
44412dae46
commit
48a0fe2e1e
@ -0,0 +1,59 @@
|
||||
import java.util.*;
|
||||
|
||||
|
||||
class ComponentArray{
|
||||
List<Object> componentArray = new ArrayList<>();
|
||||
|
||||
Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
|
||||
Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
|
||||
|
||||
public void entityDestroyed(int entity){
|
||||
Optional<Integer> pos = entityComponentDataMap.get(entity);
|
||||
if (pos.isEmpty()){
|
||||
removeData(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeData(int entity){
|
||||
if (!entityComponentDataMap.containsKey(entity)){
|
||||
System.err.println("Attempted to remove non-existent entity");
|
||||
return;
|
||||
}
|
||||
// Get the componentData index of the entity
|
||||
int removedComponentDataIndex = entityComponentDataMap.get(entity);
|
||||
// Replace the removed component with the last component in the array
|
||||
componentArray.set(removedComponentDataIndex, componentArray.get(componentArray.size() -1));
|
||||
// update the data positions in the map
|
||||
int lastEntity = componentDataEntityMap.get(componentArray.size()-1);
|
||||
entityComponentDataMap.replace(lastEntity, removedComponentDataIndex);
|
||||
componentDataEntityMap.replace(removedComponentDataIndex, lastEntity);
|
||||
// Finally, remomve the last elements
|
||||
entityComponentDataMap.remove(entity);
|
||||
componentDataEntityMap.remove(componentArray.size() -1);
|
||||
|
||||
componentArray.remove(componentArray.size() -1);
|
||||
|
||||
}
|
||||
|
||||
public void insertData(int entity, Object component){
|
||||
Optional<Integer> pos = entityComponentDataMap.get(entity);
|
||||
if (!pos.isEmpty()){
|
||||
System.err.println("Entity is already subscribed to the component");
|
||||
return;
|
||||
}
|
||||
int index = componentArray.size();
|
||||
entityComponentDataMap.put(entity, index);
|
||||
componentDataEntityMap.put(index, entity);
|
||||
|
||||
componentArray.add(component);
|
||||
}
|
||||
|
||||
public Object getData(int entity){
|
||||
if (!entityComponentDataMap.containsKey(entity)){
|
||||
System.err.println("Attempted to retrieve non-existent data");
|
||||
return null;
|
||||
}
|
||||
|
||||
return componentArray.get(entityComponentDataMap.get(entity));
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import java.util.BitSet;
|
||||
import java.util.Map;
|
||||
|
||||
class ComponentManager{
|
||||
Map<String, ComponentArray> componentArrays = new HashMap<>();
|
||||
Map<String, Integer> componentPosIndex = new HashMap<>();
|
||||
int componentPos = 0;
|
||||
|
||||
public void registerComponent(String name){
|
||||
if (componentArrays.containsKey(name)){
|
||||
System.err.println("Component " + name + " is already registered");
|
||||
return;
|
||||
}
|
||||
componentArrays.put(name, new ComponentArray());
|
||||
componentPosIndex.put(name, componentPos++);
|
||||
}
|
||||
|
||||
public void addComponentToEntity(String componentName, Object componentData, int entity){
|
||||
componentArrays.get(componentName).insertData(entity, componentData);
|
||||
}
|
||||
|
||||
public void removeComponentFromEntity(String componentName, int entity){
|
||||
componentArrays.get(componentName).removeData(entity);
|
||||
}
|
||||
|
||||
// Java does not allow reflective typing, so must cast this in the retrieving function.
|
||||
public Object getComponent(String componentType, int entity){
|
||||
return componentArrays.get(componentType).getData(entity);
|
||||
}
|
||||
|
||||
public void entityDestroyed(int entity){
|
||||
for (String key : componentArrays.keySet()) {
|
||||
componentArrays.get(key).entityDestroyed(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getComponentIndex(String name){
|
||||
return componentPosIndex.get(name);
|
||||
}
|
||||
}
|
@ -12,135 +12,48 @@
|
||||
import java.util.*;
|
||||
|
||||
public class ECS {
|
||||
// As mentioned by Austin Morlan, a queue is a better choice of data structure for the entity list
|
||||
// - it allows the first element to be efficiently popped from the list
|
||||
Queue<Integer> unusedEntities = new ArrayList<>();
|
||||
EntityManager entityManager;
|
||||
ComponentManager componentManager;
|
||||
SystemManager systemManager;
|
||||
|
||||
// As the entity subscribes (or does not) to a component, we can use a boolean state to represent the subscription
|
||||
// If we instead store the list of indicies the component is subscribed to, we would use significantly more
|
||||
// memory space due to the components being defined as a 32-bit integer.
|
||||
// In a BitSet, each bit is a flag. Therefore we can represent subscription to the first 32 components in the
|
||||
// same space as one as a 32-bit int.
|
||||
//
|
||||
// This list therefore uses the index of the BitSet as its reference to the entity.
|
||||
List<BitSet> entityRegistrations = new ArrayList<>();
|
||||
|
||||
// Each component can be assigned to every possible entity. There is a reasonably unlimited number of possible components;
|
||||
// so therefore we get an array of arrays containing all of a component's data allocations.
|
||||
// Access is in [componentType][entityNumber] format; getting the data from a subscribed entity is performed like so:
|
||||
// (<datatype>)componentDataArrays.get(ComponentIndex).get(Entity);
|
||||
// Unfortunately, given Java's constraints on dynamic typing (run-time type determination), the returned data must be
|
||||
// explicitly cast. Therefore, all components must implement the IComponent interface - which mandates the getType()
|
||||
// function
|
||||
|
||||
// As discussed by Austin Morlan, this array must be packed, otherwise we waste time iterating over non-subscribed entities
|
||||
// This also means we cannot use the location implicitly - we must keep track of exactly which data instance associates with
|
||||
// which entity.
|
||||
// Therefore, we need a custom data structure that keeps reversable references to component instances and entities:
|
||||
interface IComponentArray{
|
||||
void entityDestroyed(int entity);
|
||||
}
|
||||
class ComponentArray<E> implements IComponentArray{
|
||||
List<E> componentArray = new ArrayList<>();
|
||||
Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
|
||||
Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
|
||||
|
||||
public void entityDestroyed(int entity){
|
||||
Optional<Integer> pos = entityComponentDataMap.get(entity);
|
||||
if (pos.isEmpty()){
|
||||
removeData(entity);
|
||||
}
|
||||
public ECS(){
|
||||
entityManager = new EntityManager();
|
||||
componentManager = new ComponentManager();
|
||||
systemManager = new SystemManager();
|
||||
}
|
||||
|
||||
void removeData(int entity){
|
||||
if (!entityComponentDataMap.containsKey(entity)){
|
||||
System.err.println("Attempted to remove non-existent entity");
|
||||
return;
|
||||
}
|
||||
// Get the componentData index of the entity
|
||||
int removedComponentDataIndex = entityComponentDataMap.get(entity);
|
||||
// Replace the removed component with the last component in the array
|
||||
componentArray.set(removedComponentDataIndex, componentArray.get(componentArray.size() -1));
|
||||
// update the data positions in the map
|
||||
int lastEntity = componentDataEntityMap.get(componentArray.size()-1);
|
||||
entityComponentDataMap.replace(lastEntity, removedComponentDataIndex);
|
||||
componentDataEntityMap.replace(removedComponentDataIndex, lastEntity);
|
||||
// Finally, remomve the last elements
|
||||
entityComponentDataMap.remove(entity);
|
||||
componentDataEntityMap.remove(componentArray.size() -1);
|
||||
|
||||
componentArray.remove(componentArray.size() -1);
|
||||
}
|
||||
|
||||
void insertData(int entity, E component){
|
||||
Optional<Integer> pos = entityComponentDataMap.get(entity);
|
||||
if (!pos.isEmpty()){
|
||||
System.err.println("Entity is already subscribed to the component");
|
||||
return;
|
||||
}
|
||||
int index = componentArray.size();
|
||||
entityComponentDataMap.put(entity, index);
|
||||
componentDataEntityMap.put(index, entity);
|
||||
|
||||
componentArray.add(component);
|
||||
}
|
||||
|
||||
E getData(int entity){
|
||||
if (!entityComponentDataMap.containsKey(entity)){
|
||||
System.err.println("Attempted to retrieve non-existent data");
|
||||
return null;
|
||||
}
|
||||
|
||||
return componentArray.get(entityComponentDataMap.get(entity));
|
||||
}
|
||||
}
|
||||
// The actual list of data arrays
|
||||
List<IComponentArray> componentDataArrays = new ArrayList<>();
|
||||
|
||||
|
||||
// Components are either primitive types or class/struct definitions, with an additional list associating
|
||||
// the entities the component is assigned to
|
||||
List<Object> components = new ArrayList<>();
|
||||
|
||||
// Systems are user-defined functions that are called by the game engine, usually at a regular interval such
|
||||
// as between every render frame. It must be able to find all instances of a specific component, in order
|
||||
// to update/read its data. A component
|
||||
List<Object> systems = new ArrayList<>();
|
||||
|
||||
|
||||
int entityIndex = 0;
|
||||
int componentIndex = 0;
|
||||
int systemIndex = 0;
|
||||
|
||||
/**
|
||||
* Takes the next element off the list of unassigned entities
|
||||
* @return the value of the new entity
|
||||
*/
|
||||
Integer createEntity(){
|
||||
int newEntity = entities.remove(0);
|
||||
if (componentAssociations.size() <= newEntity){
|
||||
for (int i = componentAssociations.size(); i < newEntity+1 - componentAssociations.size(); i++) {
|
||||
componentAssociations.add(i, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
componentAssociations.add(newEntity, element);
|
||||
return newEntity;
|
||||
return entityManager.addEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the entity back to the list of unassigned entities,
|
||||
* and empty all component associations
|
||||
* @param entity
|
||||
*/
|
||||
void destroyEntity(int entity){
|
||||
entities.add(entity);
|
||||
componentAssociations.set(entity, new ArrayList<>());
|
||||
entityManager.removeEntity(entity);
|
||||
componentManager.entityDestroyed(entity);
|
||||
systemManager.entityDestroyed(entity);
|
||||
}
|
||||
|
||||
void init(int maxEntities){
|
||||
for (int i = 0; i < maxEntities; i++) {
|
||||
entities.add(i);
|
||||
}
|
||||
void registerComponent(String name){
|
||||
componentManager.registerComponent(name);
|
||||
}
|
||||
|
||||
void addComponent(int entity, String componentName, Object component){
|
||||
componentManager.addComponentToEntity(componentName, component, entity);
|
||||
entityManager.registerComponent(componentManager.getComponentIndex(componentName), entity);
|
||||
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity));
|
||||
}
|
||||
|
||||
void removeComponent(int entity, String componentName){
|
||||
componentManager.removeComponentFromEntity(componentName, entity);
|
||||
entityManager.unregisterComponent(componentManager.getComponentIndex(componentName), entity);
|
||||
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity));
|
||||
}
|
||||
|
||||
Object getComponentData(int entity, String componentName){
|
||||
return componentManager.getComponentData(entity, componentName);
|
||||
}
|
||||
|
||||
void setSystemSignature(String system, BitSet signature){
|
||||
systemManager.setSignature(system, signature);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import java.util.Set;
|
||||
|
||||
class ECSSystem{
|
||||
Set<Integer> entities = new HashSet<>();
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.swing.text.html.parser.Entity;
|
||||
|
||||
// Define the manager classes internally - should be moved to seperate source files as appropriate
|
||||
/**
|
||||
* Manages data from the perspective of the entity.
|
||||
* <p>
|
||||
* I.e. Controls adding and removing entities, and registration and deregistration of components.
|
||||
*/
|
||||
class EntityManager{
|
||||
Queue<Integer> unusedEntities;
|
||||
List<BitSet> entityRegistrations;
|
||||
|
||||
public EntityManager(){
|
||||
unusedEntities = new ArrayList<>();
|
||||
entityRegistrations = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
unusedEntities.add(i);
|
||||
entityRegistrations.add(new BitSet());
|
||||
}
|
||||
}
|
||||
|
||||
public EntityManager(int maxEntities){
|
||||
unusedEntities = new ArrayList<>();
|
||||
entityRegistrations = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < maxEntities; i++) {
|
||||
unusedEntities.add(i);
|
||||
entityRegistrations.add(new BitSet());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Integer addEntity(){
|
||||
if (unusedEntities.size() == 0){
|
||||
System.err.println("No available space to create a new entity");
|
||||
return -1;
|
||||
}
|
||||
return unusedEntities.remove();
|
||||
}
|
||||
|
||||
public void removeEntity(int entity){
|
||||
unusedEntities.add(entity);
|
||||
entityRegistrations.set(entity, new BitSet());
|
||||
}
|
||||
|
||||
public void registerComponent(int component, int entity){
|
||||
entityRegistrations.get(entity).set(component);
|
||||
}
|
||||
|
||||
public void unregisterComponent(int component, int entity){
|
||||
entityRegistrations.get(entity).clear(component);
|
||||
}
|
||||
|
||||
public BitSet getRegistrations(int entity){
|
||||
return entityRegistrations.get(entity);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import java.util.BitSet;
|
||||
import java.util.Map;
|
||||
|
||||
class SystemManager{
|
||||
Map<String, BitSet> signatures = new HashMap<>();
|
||||
Map<String, ECSSystem> systems = new HashMap<>();
|
||||
|
||||
|
||||
public SystemManager(ECS baseECS){
|
||||
this.baseECS = baseECS;
|
||||
systemIndex = 0;
|
||||
}
|
||||
// Registering the system adds it to the array of systems.
|
||||
// In Austin Morlan's implementation, this also creates an instance of the
|
||||
// system that can be called from the main thread.
|
||||
// It returns this object.
|
||||
// In Java, we need to initialise the object first (before this is called),
|
||||
// then register that object. Enactment of the system is performed on that
|
||||
// instance.
|
||||
// I.e., create an object that represents the system class; then store that class in the system
|
||||
// table.
|
||||
public boolean registerSystem(String system, ECSSystem action){
|
||||
if (systems.containsKey(system)){
|
||||
System.err.println("System already registered");
|
||||
return false;
|
||||
}
|
||||
systems.put(system, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setSignature(String system, BitSet registrations){
|
||||
signatures.put(system, registrations);
|
||||
}
|
||||
|
||||
public void entityDestroyed(int entity){
|
||||
for (String key : systems.keySet()) {
|
||||
systems.get(key).entities.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public void entitySignatureChanged(int entity, BitSet entitySignature){
|
||||
for (String key : systems.keySet()) {
|
||||
if (entitySignature.and(signatures.get(key)) == signatures.get(key)){ // Bitwise check if the entity is subscribed to this system
|
||||
systems.get(key).entities.add(entity);
|
||||
}
|
||||
else{
|
||||
systems.get(key).entities.remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user