Added docstrings and some exception handling

This commit is contained in:
Brychan Dempsey 2021-06-08 21:13:05 +12:00
parent 6e000492b0
commit 22a75f5559
7 changed files with 329 additions and 162 deletions

View File

@ -21,21 +21,79 @@ import java.util.ArrayList;
class ComponentArray{ class ComponentArray{
List<Object> componentArray = new ArrayList<>(); // The object data array
private List<Object> componentArray = new ArrayList<>();
// The mappings between data and entity
private Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
private Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
Map<Integer, Integer> entityComponentDataMap = new HashMap<>(); /**
Map<Integer, Integer> componentDataEntityMap = new HashMap<>(); * 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 entityDestroyed(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)){ if (entityComponentDataMap.containsKey(entity)){
removeData(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;
} }
} }
public void removeData(int entity){ /**
* 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)){ if (!entityComponentDataMap.containsKey(entity)){
ECS.writeErr("Attempted to remove non-existent entity"); ECS.writeErr("Attempted to remove non-existent entity");
return; return false;
} }
// Get the componentData index of the entity // Get the componentData index of the entity
int removedComponentDataIndex = entityComponentDataMap.get(entity); int removedComponentDataIndex = entityComponentDataMap.get(entity);
@ -48,40 +106,7 @@ class ComponentArray{
// Finally, remomve the last elements // Finally, remomve the last elements
entityComponentDataMap.remove(entity); entityComponentDataMap.remove(entity);
componentDataEntityMap.remove(componentArray.size() -1); componentDataEntityMap.remove(componentArray.size() -1);
componentArray.remove(componentArray.size() -1); componentArray.remove(componentArray.size() -1);
return true;
}
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));
} }
} }

View File

@ -10,54 +10,123 @@ package nz.ac.massey.javaecs;
*/ */
import java.util.Map; import java.util.Map;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.BitSet;
import java.util.HashMap; import java.util.HashMap;
class ComponentManager{ class ComponentManager{
Map<Type, ComponentArray> componentArrays = new HashMap<>(); private Map<Type, ComponentArray> componentArrays = new HashMap<>();
Map<Type, Integer> componentPosIndex = new HashMap<>(); private Map<Type, Integer> componentPosIndex = new HashMap<>();
int componentPos = 0; private Map<Integer, Type> 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)){ if (componentArrays.containsKey(type)){
ECS.writeErr("Component " + type.getTypeName() + " is already registered"); ECS.writeErr("Component " + type.getTypeName() + " is already registered");
return false; return false;
} }
componentArrays.put(type, new ComponentArray()); componentArrays.put(type, new ComponentArray());
componentPosIndex.put(type, componentPos++); indexComponentType.put(componentPosIndex.size(), type);
componentPosIndex.put(type, componentPosIndex.size());
return true; 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 * Removes the specified component from the entity
* @param sourceEntity * @param componentType the class type of the component to remove
* @param destinationEntity * @param entity the entity to remove the component from
*/ */
public void moveComponentData(int sourceEntity, int destinationEntity){ public boolean removeComponentFromEntity(Type componentType, int entity){
for (Type key : componentArrays.keySet()) { return componentArrays.get(componentType).removeData(entity);
componentArrays.get(key).moveData(sourceEntity, destinationEntity);
}
} }
} }

View File

@ -18,7 +18,7 @@ import java.util.BitSet;
/** /**
* The ECS manager. * The ECS manager.
* <p> * <p>
* See <href src="https://git.software.kauripeak.co.nz/BrychanD/JavaECS">https://git.software.kauripeak.co.nz/BrychanD/JavaECS</href> * See https://git.software.kauripeak.co.nz/BrychanD/JavaECS
* for documentation and more information. * for documentation and more information.
*/ */
public class ECS { public class ECS {
@ -153,7 +153,7 @@ public class ECS {
systemManager.setRegistrationSignature(system, signature); systemManager.setRegistrationSignature(system, signature);
} }
Integer getMaxEntities(){ Integer getMaxEntities(){
return entityManager.currentSize; return entityManager.getMaxSize();
} }
// Encapsulate syserr writes so they may be redirected out of the lib // Encapsulate syserr writes so they may be redirected out of the lib

View File

@ -18,7 +18,15 @@ import java.util.HashSet;
abstract class ECSSystem{ abstract class ECSSystem{
Set<Integer> entities = new HashSet<>(); Set<Integer> 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(); 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(); abstract void update();
} }

View File

@ -1,10 +1,7 @@
package nz.ac.massey.javaecs; package nz.ac.massey.javaecs;
/** /**
* Entity Manager * Entity Manager
* This class manages entity allocations; keeping a list * Controls adding and removing entities, and registration and unregistration of components to specific entities.
* of all unassigned entities values.
* Additionally, handles setting the registered component
* flags for an entity.
* *
* Contributors: * Contributors:
* Brychan Dempsey - brychand@hotmail.com * 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. * I.e. Controls adding and removing entities, and registration and deregistration of components.
*/ */
class EntityManager{ class EntityManager{
Queue<Integer> unusedEntities; private Queue<Integer> unusedEntities;
List<BitSet> entityRegistrations; private List<BitSet> entityRegistrations;
int currentSize; private int maxSize = 1024;
/**
* Initialise the EntityManager with the default max size of 1024
*/
public EntityManager(){ public EntityManager(){
currentSize = 1024;
unusedEntities = new LinkedList<>(); unusedEntities = new LinkedList<>();
entityRegistrations = new ArrayList<>(); entityRegistrations = new ArrayList<>();
for (int i = 0; i < 1024; i++) { for (int i = 0; i < maxSize; i++) {
unusedEntities.add(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){ public EntityManager(int maxEntities){
currentSize = maxEntities; maxSize = maxEntities;
unusedEntities = new LinkedList<>(); unusedEntities = new LinkedList<>();
entityRegistrations = new ArrayList<>(); entityRegistrations = new ArrayList<>();
for (int i = 0; i < maxEntities; i++) { for (int i = 0; i < maxEntities; i++) {
unusedEntities.add(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){ if (unusedEntities.size() == 0){
ECS.writeErr("No available space to create a new entity"); ECS.writeErr("No available space to create a new entity");
return -1; return -1;
} }
return unusedEntities.remove(); int result = unusedEntities.remove();
entityRegistrations.set(result, new BitSet());
return result;
} }
public void removeEntity(int entity){ /**
unusedEntities.add(entity); * Gets the current maximum size
entityRegistrations.get(entity).clear(); * @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); ECS.writeErr("Attempted to assign a component to non-existent entity: " + entity);
return false; return false;
} }
@ -80,45 +112,67 @@ class EntityManager{
} }
} }
public boolean unregisterComponent(int component, int entity){ /**
* Adds the entity index back into unusedEntities, and sets the registrations to null
* <p>
* <b>Does not handle associated data</b> 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
* <p>
* <b>Does not handle component data</b> 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{ try{
if (entityRegistrations.get(entity).get(component))
{
entityRegistrations.get(entity).clear(component); entityRegistrations.get(entity).clear(component);
return true; return true;
} }
else return false;
}
catch (IndexOutOfBoundsException e) catch (IndexOutOfBoundsException e)
{ {
return false; return false;
} }
} }
public BitSet getRegistrations(int entity){ /**
return entityRegistrations.get(entity); * Resizes the currentSize of the entity manager.
} * @param newSize the new maximum size
* @param systemManager reference to the instanced SystemManager
public void setRegistrations(int entity, BitSet registrations){ * @param componentManager reference to the insanced ComponentManager
entityRegistrations.set(entity, registrations); * @return true if the operation succeeded, otherwise false
} */
protected boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){
public boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){ if (newSize < maxSize - unusedEntities.size()){
if (newSize < currentSize - unusedEntities.size()){
ECS.writeErr("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count."); ECS.writeErr("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count.");
return false; return false;
} }
else if (newSize == currentSize){ else if (newSize == maxSize){
ECS.writeErr("Attempted to set the newSize to the current size"); ECS.writeErr("Attempted to set the newSize to the current size");
return true; return true;
} }
else{ else{
// Consistency should be maintained. // Consistency should be maintained.
// This is computationally expensive; we must re-order every assigned entity above newSize, if the newSize is smaller // 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<Integer> outOfBounds = new ArrayList<>(); List<Integer> outOfBounds = new ArrayList<>();
for (int i = newSize; i < currentSize; i++) { for (int i = newSize; i < maxSize; i++) {
if (!unusedEntities.remove(i)){ if (!unusedEntities.remove(i)){
// Could not remove element, as it didn't exist (already assigned). // Could not remove element, as it didn't exist (already assigned).
// must find it and reassign it a new in-bounds value // must find it and reassign it a new in-bounds value
@ -132,23 +186,23 @@ class EntityManager{
systemManager.entityDestroyed(integer); systemManager.entityDestroyed(integer);
systemManager.entityRegistrationsChanged(newPos, getRegistrations(newPos)); systemManager.entityRegistrationsChanged(newPos, getRegistrations(newPos));
// Invoke the change in the components // Invoke the change in the components
componentManager.moveComponentData(integer, newPos); componentManager.moveAllComponentData(integer, newPos, getRegistrations(integer));
componentManager.entityDestroyed(integer); componentManager.entityDestroyed(integer);
} }
for (int i = newSize; i < currentSize; i++) { for (int i = newSize; i < maxSize; i++) {
// Remove out-of-bounds data // Remove out-of-bounds data
entityRegistrations.remove(newSize); entityRegistrations.remove(newSize);
} }
} }
else{ else{
// Init unassigned values // Init unassigned values
for (int i = currentSize; i < newSize; i++) { for (int i = maxSize; i < newSize; i++) {
unusedEntities.add(i); unusedEntities.add(i);
entityRegistrations.add(new BitSet()); entityRegistrations.add(new BitSet());
} }
} }
// Finally, set the current size // Finally, set the current size
currentSize = newSize; maxSize = newSize;
return true; return true;
} }
} }

View File

@ -15,39 +15,27 @@ import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
class SystemManager{ class SystemManager{
Map<Type, BitSet> registrationSignatures = new HashMap<>(); private Map<Type, BitSet> registrationSignatures = new HashMap<>();
Map<Type, ECSSystem> systems = new HashMap<>(); private Map<Type, ECSSystem> 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 * Signals the SystemManager that an entity was destroyed.
// system that can be called from the main thread. * Removes the entity from each system's tracked entities
// It returns this object. * @param entity the destroyed entity
// 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 protected void entityDestroyed(int entity){
// 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){
for (Type key : systems.keySet()) { for (Type key : systems.keySet()) {
systems.get(key).entities.remove(entity); 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()) { for (Type key : systems.keySet()) {
// Check if the signature is null // Check if the signature is null
if (!entityRegistrations.equals(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);
}
} }

View File

@ -2,7 +2,6 @@ package nz.ac.massey.javaecs;
import org.junit.jupiter.api.Test; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; 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 static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.BitSet; import java.util.BitSet;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -193,15 +189,15 @@ class AppTest {
@Test @Test
void testEqualResize(){ void testEqualResize(){
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream orig = System.err; PrintStream orig = ECS.getErr();
PrintStream newErr = new PrintStream(bytes); PrintStream newErr = new PrintStream(bytes);
System.setErr(newErr); ECS.setErr(newErr);
assertTrue(gameEngine.resizeMaximum(1024)); assertTrue(gameEngine.resizeMaximum(1024));
System.setErr(orig); ECS.setErr(orig);
newErr.flush(); // ensure the bytes are recieved newErr.flush(); // ensure the bytes are recieved
byte[] errBytes = bytes.toByteArray(); byte[] errBytes = bytes.toByteArray();
String result = new String(errBytes); 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")); assertTrue(result.trim().equals("Attempted to set the newSize to the current size"));
} }