From 22a75f5559c84b4d6b186302b10e422b129be202 Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Tue, 8 Jun 2021 21:13:05 +1200 Subject: [PATCH] Added docstrings and some exception handling --- .../nz/ac/massey/javaecs/ComponentArray.java | 111 +++++++------ .../ac/massey/javaecs/ComponentManager.java | 139 ++++++++++++----- .../main/java/nz/ac/massey/javaecs/ECS.java | 4 +- .../java/nz/ac/massey/javaecs/ECSSystem.java | 10 +- .../nz/ac/massey/javaecs/EntityManager.java | 146 ++++++++++++------ .../nz/ac/massey/javaecs/SystemManager.java | 69 +++++---- .../java/nz/ac/massey/javaecs/AppTest.java | 12 +- 7 files changed, 329 insertions(+), 162 deletions(-) diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentArray.java b/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentArray.java index f6ce8fd..4df9595 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentArray.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentArray.java @@ -21,21 +21,79 @@ import java.util.ArrayList; class ComponentArray{ - List componentArray = new ArrayList<>(); + // The object data array + private List componentArray = new ArrayList<>(); + // The mappings between data and entity + private Map entityComponentDataMap = new HashMap<>(); + private Map componentDataEntityMap = new HashMap<>(); - Map entityComponentDataMap = new HashMap<>(); - Map componentDataEntityMap = new HashMap<>(); - - public void entityDestroyed(int entity){ - if (entityComponentDataMap.containsKey(entity)){ - removeData(entity); + /** + * Gets the data Object associated with the entity. + * @param entity the entity to find data for + * @return the Object if the data exists, else null + */ + protected Object getData(int entity){ + try{ + Object result = componentArray.get(entityComponentDataMap.get(entity)); + if (result == null) ECS.writeErr("Attempted to retrieve non-existent data"); + return result; + } + catch (IndexOutOfBoundsException e){ + ECS.writeErr("Index out-of-bounds"); + return null; } } - public void removeData(int entity){ + /** + * Inserts the provided component and associates it with the entity. + * @param entity the entity to associate data to + * @param component the component data + * @return true if successful, false if the entity is already subscribed to the component + */ + protected boolean insertData(int entity, Object component){ + if (entityComponentDataMap.containsKey(entity)){ + ECS.writeErr("Entity is already subscribed to the component"); + return false; + } + // Put data at the end of the componentArray + int index = componentArray.size(); + entityComponentDataMap.put(entity, index); + componentDataEntityMap.put(index, entity); + componentArray.add(component); + return true; + } + + /** + * Moves component data to another entity. (Copies, deletes and inserts data) + * @param sourceEntity + * @param destinationEntity + * @return 0 if successful, -1 if the object is null, -2 if a NullPointerException occurred, and -3 if inserting data failed + */ + protected int moveData(int sourceEntity, int destinationEntity){ + try{ + Object data = entityComponentDataMap.get(sourceEntity); + if (data == null) return -1; + else if (insertData(destinationEntity, data)) + { + removeData(sourceEntity); + return 0; + } + else return -3; + } + catch (NullPointerException e){ + return -2; + } + } + + /** + * Removes the data associated with the entity. + * @param entity the entity to remove the data from + * @return true if the data was removed. If the data isn't found then returns false + */ + protected boolean removeData(int entity){ if (!entityComponentDataMap.containsKey(entity)){ ECS.writeErr("Attempted to remove non-existent entity"); - return; + return false; } // Get the componentData index of the entity int removedComponentDataIndex = entityComponentDataMap.get(entity); @@ -48,40 +106,7 @@ class ComponentArray{ // 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){ - if (entityComponentDataMap.containsKey(entity)){ - ECS.writeErr("Entity is already subscribed to the component"); - return; - } - int index = componentArray.size(); - entityComponentDataMap.put(entity, index); - componentDataEntityMap.put(index, entity); - - componentArray.add(component); - } - - /** - * Moves component data to another entity. (Copies, deletes and inserts data) - * @param sourceEntity - * @param destinationEntity - */ - public void moveData(int sourceEntity, int destinationEntity){ - Object data = entityComponentDataMap.get(sourceEntity); - removeData(sourceEntity); - insertData(destinationEntity, data); - } - - public Object getData(int entity){ - if (!entityComponentDataMap.containsKey(entity)){ - ECS.writeErr("Attempted to retrieve non-existent data"); - return null; - } - - return componentArray.get(entityComponentDataMap.get(entity)); + return true; } } \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentManager.java b/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentManager.java index 82d6599..26ddafd 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentManager.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/ComponentManager.java @@ -10,54 +10,123 @@ package nz.ac.massey.javaecs; */ import java.util.Map; import java.lang.reflect.Type; +import java.util.BitSet; import java.util.HashMap; class ComponentManager{ - Map componentArrays = new HashMap<>(); - Map componentPosIndex = new HashMap<>(); - int componentPos = 0; + private Map componentArrays = new HashMap<>(); + private Map componentPosIndex = new HashMap<>(); + private Map indexComponentType = new HashMap<>(); - public boolean registerComponent(Type type){ + /** + * Adds the specified component to the provided entity + * @param componentType the class type of the component to add + * @param componentData the component data to associate + * @param entity the entity to associate data to + */ + protected boolean addComponentToEntity(Type componentType, Object componentData, int entity){ + return componentArrays.get(componentType).insertData(entity, componentData); + } + + /** + * Signals to the ComponentManager the entity was destroyed. All component data references should be removed. + * @param entity the entity that was destroyed. + */ + public void entityDestroyed(int entity){ + for (Type key : componentArrays.keySet()) { + componentArrays.get(key).removeData(entity); + } + } + + /** + * Gets the component data associated with the entity + * @param componentType the class type of data to look for + * @param entity the entity to find data for + * @return the Object data found, or null if it was not found + */ + public Object getComponent(Type componentType, int entity){ + return componentArrays.get(componentType).getData(entity); + } + + /** + * Gets the registration index of the component type + * @param type the class type of the component + * @return the index of the component type, or -1 if it isn't found + */ + protected Integer getComponentIndex(Type type){ + try{ + return componentPosIndex.get(type); + } + catch (NullPointerException e){ + return -1; + } + } + + /** + * Gets the type of the component at the provided index + * @param index the index of the component + * @return the class type of the index. `null` if not found + */ + protected Type getComponentType(Integer index){ + try{ + return indexComponentType.get(index); + } + catch (NullPointerException e){ + return null; + } + } + + /** + * Moves a single component data from one entity to another + * @param sourceEntity the entity to move data from + * @param destinationEntity the entity to move data to + * @param component the component class type to consider + * @return true if the component was moved successfully, else false + */ + protected boolean moveComponentData(int sourceEntity, int destinationEntity, Type component){ + if (componentArrays.get(component).moveData(sourceEntity, destinationEntity) == 0){ + return true; + } + else return false; + } + + /** + * Moves all component data from one entity to another + * @param sourceEntity the entity to move data from + * @param destinationEntity the entity to move data to + * @param sourceRegistrations the component registrations of the source entity + */ + protected void moveAllComponentData(int sourceEntity, int destinationEntity, BitSet sourceRegistrations){ + int result = sourceRegistrations.nextSetBit(0); + while (result != -1){ + Type key = indexComponentType.get(result); + componentArrays.get(key).moveData(sourceEntity, destinationEntity); + sourceRegistrations.nextSetBit(result); + } + } + + /** + * Registers the component type + * @param type the class type to register + * @return true if the component was registered successfully, else false + */ + protected boolean registerComponent(Type type){ if (componentArrays.containsKey(type)){ ECS.writeErr("Component " + type.getTypeName() + " is already registered"); return false; } componentArrays.put(type, new ComponentArray()); - componentPosIndex.put(type, componentPos++); + indexComponentType.put(componentPosIndex.size(), type); + componentPosIndex.put(type, componentPosIndex.size()); return true; } - public void addComponentToEntity(Type componentName, Object componentData, int entity){ - componentArrays.get(componentName).insertData(entity, componentData); - } - - public void removeComponentFromEntity(Type 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(Type componentType, int entity){ - return componentArrays.get(componentType).getData(entity); - } - - public void entityDestroyed(int entity){ - for (Type key : componentArrays.keySet()) { - componentArrays.get(key).entityDestroyed(entity); - } - } - - public Integer getComponentIndex(Type type){ - return componentPosIndex.get(type); - } - /** - * Moves component data from one entity to another - * @param sourceEntity - * @param destinationEntity + * Removes the specified component from the entity + * @param componentType the class type of the component to remove + * @param entity the entity to remove the component from */ - public void moveComponentData(int sourceEntity, int destinationEntity){ - for (Type key : componentArrays.keySet()) { - componentArrays.get(key).moveData(sourceEntity, destinationEntity); - } + public boolean removeComponentFromEntity(Type componentType, int entity){ + return componentArrays.get(componentType).removeData(entity); } } \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/ECS.java b/javaecs/src/main/java/nz/ac/massey/javaecs/ECS.java index de0a6ae..6899ce9 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/ECS.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/ECS.java @@ -18,7 +18,7 @@ import java.util.BitSet; /** * The ECS manager. *

- * See https://git.software.kauripeak.co.nz/BrychanD/JavaECS + * See https://git.software.kauripeak.co.nz/BrychanD/JavaECS * for documentation and more information. */ public class ECS { @@ -153,7 +153,7 @@ public class ECS { systemManager.setRegistrationSignature(system, signature); } Integer getMaxEntities(){ - return entityManager.currentSize; + return entityManager.getMaxSize(); } // Encapsulate syserr writes so they may be redirected out of the lib diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/ECSSystem.java b/javaecs/src/main/java/nz/ac/massey/javaecs/ECSSystem.java index 596256c..b419bd4 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/ECSSystem.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/ECSSystem.java @@ -18,7 +18,15 @@ import java.util.HashSet; abstract class ECSSystem{ Set entities = new HashSet<>(); - // Abstractions are included to remind the library user these two implementations (and/or variations) should be implemented + /** + * Implement additional parameterised init() functions as required. + * These should run once, when the system is first initialised. + */ abstract void init(); + + /** + * Implement additional parameterised update() functions as required. + * These should be run each game loop or otherwise sensible regular interval + */ abstract void update(); } \ No newline at end of file diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/EntityManager.java b/javaecs/src/main/java/nz/ac/massey/javaecs/EntityManager.java index 70c8537..72271b6 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/EntityManager.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/EntityManager.java @@ -1,10 +1,7 @@ package nz.ac.massey.javaecs; /** * Entity Manager - * This class manages entity allocations; keeping a list - * of all unassigned entities values. - * Additionally, handles setting the registered component - * flags for an entity. + * Controls adding and removing entities, and registration and unregistration of components to specific entities. * * Contributors: * Brychan Dempsey - brychand@hotmail.com @@ -24,48 +21,83 @@ import java.util.ArrayList; * I.e. Controls adding and removing entities, and registration and deregistration of components. */ class EntityManager{ - Queue unusedEntities; - List entityRegistrations; - int currentSize; + private Queue unusedEntities; + private List entityRegistrations; + private int maxSize = 1024; + /** + * Initialise the EntityManager with the default max size of 1024 + */ public EntityManager(){ - currentSize = 1024; unusedEntities = new LinkedList<>(); entityRegistrations = new ArrayList<>(); - for (int i = 0; i < 1024; i++) { + for (int i = 0; i < maxSize; i++) { unusedEntities.add(i); - entityRegistrations.add(new BitSet()); + entityRegistrations.add(null); // Leave bitsets out as if they are null the entity isn't initialised properly } } + /*** + * Initialise the EntityManager with the provided maximum size + * @param maxEntities the maximum number of entities to allow + */ public EntityManager(int maxEntities){ - currentSize = maxEntities; + maxSize = maxEntities; unusedEntities = new LinkedList<>(); entityRegistrations = new ArrayList<>(); for (int i = 0; i < maxEntities; i++) { unusedEntities.add(i); - entityRegistrations.add(new BitSet()); + entityRegistrations.add(null); } } - - public Integer addEntity(){ + /** + * Creates a new entity + * @return the index of the new entity, or -1 if there is no more available entities + */ + protected Integer addEntity(){ if (unusedEntities.size() == 0){ ECS.writeErr("No available space to create a new entity"); return -1; } - return unusedEntities.remove(); + int result = unusedEntities.remove(); + entityRegistrations.set(result, new BitSet()); + return result; } - public void removeEntity(int entity){ - unusedEntities.add(entity); - entityRegistrations.get(entity).clear(); + /** + * Gets the current maximum size + * @return the value of currentSize + */ + protected Integer getMaxSize(){ + return maxSize; } - public boolean registerComponent(int component, int entity){ - if (entity >= currentSize){ + /** + * Gets the BitSet containing the registrations of the entity + * @param entity the entity whose BitSet to retrieve + * @return the BitSet of the provided entity + */ + protected BitSet getRegistrations(int entity){ + try{ + return entityRegistrations.get(entity); + } + catch (IndexOutOfBoundsException e){ + ECS.writeErr("Index out of bounds error getting registrations for " + entity + ";\nThe entity might not exist"); + return new BitSet(); // Using a blank BitSet will retain data safety (that is, no data will be modified) + } + } + + /** + * Registers the specified component index to the entity + * @param component the index of the component to register + * @param entity the entity to register + * @return true if the operation was successful + */ + protected boolean registerComponent(int component, int entity){ + if (entity >= maxSize){ ECS.writeErr("Attempted to assign a component to non-existent entity: " + entity); return false; } @@ -76,49 +108,71 @@ class EntityManager{ } else{ entityRegistrations.get(entity).set(component); - return true; + return true; } } - public boolean unregisterComponent(int component, int entity){ + /** + * Adds the entity index back into unusedEntities, and sets the registrations to null + *

+ * Does not handle associated data Use the method in ECS to remove entities cleanly + * @param entity the entity to remove + */ + protected void removeEntity(int entity){ + unusedEntities.add(entity); + entityRegistrations.set(entity, null); + } + + /** + * Sets the entity's registrations to the provided BitSet + * @param entity the entity to set + * @param registrations the preset registrations + */ + protected void setRegistrations(int entity, BitSet registrations){ + entityRegistrations.set(entity, registrations); + } + + /** + * Unregisters the specified component from the entity + *

+ * Does not handle component data Use the method in ECS to remove components cleanly + * @param component the component index to remove + * @param entity the entity to remove + * @return true if successful + */ + protected boolean unregisterComponent(int component, int entity){ try{ - if (entityRegistrations.get(entity).get(component)) - { - entityRegistrations.get(entity).clear(component); - return true; - } - else return false; + entityRegistrations.get(entity).clear(component); + return true; } catch (IndexOutOfBoundsException e) { return false; } - } - public BitSet getRegistrations(int entity){ - return entityRegistrations.get(entity); - } - - public void setRegistrations(int entity, BitSet registrations){ - entityRegistrations.set(entity, registrations); - } - - public boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){ - if (newSize < currentSize - unusedEntities.size()){ + /** + * Resizes the currentSize of the entity manager. + * @param newSize the new maximum size + * @param systemManager reference to the instanced SystemManager + * @param componentManager reference to the insanced ComponentManager + * @return true if the operation succeeded, otherwise false + */ + protected boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){ + if (newSize < maxSize - unusedEntities.size()){ ECS.writeErr("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count."); return false; } - else if (newSize == currentSize){ + else if (newSize == maxSize){ ECS.writeErr("Attempted to set the newSize to the current size"); return true; } else{ // Consistency should be maintained. // This is computationally expensive; we must re-order every assigned entity above newSize, if the newSize is smaller - if (newSize < currentSize){ + if (newSize < maxSize){ List outOfBounds = new ArrayList<>(); - for (int i = newSize; i < currentSize; i++) { + for (int i = newSize; i < maxSize; i++) { if (!unusedEntities.remove(i)){ // Could not remove element, as it didn't exist (already assigned). // must find it and reassign it a new in-bounds value @@ -132,23 +186,23 @@ class EntityManager{ systemManager.entityDestroyed(integer); systemManager.entityRegistrationsChanged(newPos, getRegistrations(newPos)); // Invoke the change in the components - componentManager.moveComponentData(integer, newPos); + componentManager.moveAllComponentData(integer, newPos, getRegistrations(integer)); componentManager.entityDestroyed(integer); } - for (int i = newSize; i < currentSize; i++) { + for (int i = newSize; i < maxSize; i++) { // Remove out-of-bounds data entityRegistrations.remove(newSize); } } else{ // Init unassigned values - for (int i = currentSize; i < newSize; i++) { + for (int i = maxSize; i < newSize; i++) { unusedEntities.add(i); entityRegistrations.add(new BitSet()); } } // Finally, set the current size - currentSize = newSize; + maxSize = newSize; return true; } } diff --git a/javaecs/src/main/java/nz/ac/massey/javaecs/SystemManager.java b/javaecs/src/main/java/nz/ac/massey/javaecs/SystemManager.java index 388014a..ba80e9f 100644 --- a/javaecs/src/main/java/nz/ac/massey/javaecs/SystemManager.java +++ b/javaecs/src/main/java/nz/ac/massey/javaecs/SystemManager.java @@ -15,39 +15,27 @@ import java.util.Map; import java.util.HashMap; class SystemManager{ - Map registrationSignatures = new HashMap<>(); - Map systems = new HashMap<>(); + private Map registrationSignatures = new HashMap<>(); + private Map systems = new HashMap<>(); - // 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(Type systemType, ECSSystem system){ - if (systems.containsKey(systemType)){ - ECS.writeErr("System already registered"); - return false; - } - systems.put(systemType, system); - registrationSignatures.put(systemType, new BitSet()); - return true; - } - - public void setRegistrationSignature(Type systemType, BitSet registrations){ - registrationSignatures.put(systemType, registrations); - } - - public void entityDestroyed(int entity){ + /** + * Signals the SystemManager that an entity was destroyed. + * Removes the entity from each system's tracked entities + * @param entity the destroyed entity + */ + protected void entityDestroyed(int entity){ for (Type key : systems.keySet()) { systems.get(key).entities.remove(entity); } } - public void entityRegistrationsChanged(int entity, BitSet entityRegistrations){ + /** + * Signals the SystemManager that an entity had its registrations changed, so + * evaluate if the entity is still relevant to each system + * @param entity the entity that was modified + * @param entityRegistrations the new registrations of the entity + */ + protected void entityRegistrationsChanged(int entity, BitSet entityRegistrations){ for (Type key : systems.keySet()) { // Check if the signature is null if (!entityRegistrations.equals(null)){ @@ -62,4 +50,31 @@ class SystemManager{ } } } + + /** + * Registers the specified system name and system reference + * @param systemType the class type of the system + * @param system the instance of the system + * @return true if the system was added successfully. False if it was already registered; with an error message written to the log + */ + protected boolean registerSystem(Type systemType, ECSSystem system){ + if (systems.containsKey(systemType)){ + ECS.writeErr("System already registered"); + return false; + } + systems.put(systemType, system); + registrationSignatures.put(systemType, new BitSet()); + return true; + } + + /** + * Sets the registrations the system requires + * @param systemType the class type of the system + * @param registrations the BitSet containing the required registrations set to true + */ + protected void setRegistrationSignature(Type systemType, BitSet registrations){ + registrationSignatures.put(systemType, registrations); + } + + } \ No newline at end of file diff --git a/javaecs/src/test/java/nz/ac/massey/javaecs/AppTest.java b/javaecs/src/test/java/nz/ac/massey/javaecs/AppTest.java index b708c33..c7de0e4 100644 --- a/javaecs/src/test/java/nz/ac/massey/javaecs/AppTest.java +++ b/javaecs/src/test/java/nz/ac/massey/javaecs/AppTest.java @@ -2,7 +2,6 @@ package nz.ac.massey.javaecs; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -10,10 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.OutputStream; import java.io.PrintStream; -import java.nio.charset.Charset; import java.util.BitSet; import org.junit.jupiter.api.BeforeEach; @@ -193,15 +189,15 @@ class AppTest { @Test void testEqualResize(){ ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PrintStream orig = System.err; + PrintStream orig = ECS.getErr(); PrintStream newErr = new PrintStream(bytes); - System.setErr(newErr); + ECS.setErr(newErr); assertTrue(gameEngine.resizeMaximum(1024)); - System.setErr(orig); + ECS.setErr(orig); newErr.flush(); // ensure the bytes are recieved byte[] errBytes = bytes.toByteArray(); String result = new String(errBytes); - System.err.println("Captured in redirect: " + result); + ECS.writeErr("Captured in redirect: " + result); assertTrue(result.trim().equals("Attempted to set the newSize to the current size")); }