From 48a0fe2e1ec40a847ceaaf15cc467898c7518ce2 Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Thu, 15 Apr 2021 16:17:04 +1200 Subject: [PATCH] Fully implemented Austin Morgan's ECS in Java (minus generic typing) --- .../ComponentArray.java | 59 +++++++ .../ComponentManager.java | 40 +++++ .../ECS.java | 151 ++++-------------- .../ECSSystem.java | 5 + .../EntityManager.java | 62 +++++++ .../SystemManager.java | 51 ++++++ 6 files changed, 249 insertions(+), 119 deletions(-) create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java new file mode 100644 index 0000000..886a07e --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java @@ -0,0 +1,59 @@ +import java.util.*; + + +class ComponentArray{ + List componentArray = new ArrayList<>(); + + Map entityComponentDataMap = new HashMap<>(); + Map componentDataEntityMap = new HashMap<>(); + + public void entityDestroyed(int entity){ + Optional 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 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)); + } +} \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java new file mode 100644 index 0000000..071e493 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java @@ -0,0 +1,40 @@ +import java.util.BitSet; +import java.util.Map; + +class ComponentManager{ + Map componentArrays = new HashMap<>(); + Map 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); + } +} \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java index 1cee030..36554a4 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java @@ -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 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 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: - // ()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); + public ECS(){ + entityManager = new EntityManager(); + componentManager = new ComponentManager(); + systemManager = new SystemManager(); } - class ComponentArray implements IComponentArray{ - List componentArray = new ArrayList<>(); - Map entityComponentDataMap = new HashMap<>(); - Map componentDataEntityMap = new HashMap<>(); - - public void entityDestroyed(int entity){ - Optional pos = entityComponentDataMap.get(entity); - if (pos.isEmpty()){ - removeData(entity); - } - } - - 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 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 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 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 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); + } } \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java new file mode 100644 index 0000000..f3a01c0 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java @@ -0,0 +1,5 @@ +import java.util.Set; + +class ECSSystem{ + Set entities = new HashSet<>(); +} \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java new file mode 100644 index 0000000..e4d7255 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java @@ -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. + *

+ * I.e. Controls adding and removing entities, and registration and deregistration of components. + */ +class EntityManager{ + Queue unusedEntities; + List 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); + } +} \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java new file mode 100644 index 0000000..5984685 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java @@ -0,0 +1,51 @@ +import java.util.BitSet; +import java.util.Map; + +class SystemManager{ + Map signatures = new HashMap<>(); + Map 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); + } + } + } +} \ No newline at end of file