7 Commits
Author SHA1 Message Date
BrychanD 4731981977 Update 'README.md' 2021-06-15 18:28:27 +12:00
BrychanD fc480cfe2e Update 'README.md' 2021-06-15 18:26:43 +12:00
BrychanD 2c96e92a6a Changed access modifiers to public
Changed access so data may be manually changed, if needed
2021-06-15 17:59:03 +12:00
BrychanD ccabef5226 Made Manager classes public 2021-06-14 18:20:29 +12:00
BrychanD 6d8574b951 Update 'README.md' 2021-06-13 14:21:09 +12:00
BrychanD e578e72b46 Version change to 1.0.0 2021-06-13 14:06:26 +12:00
BrychanD 84d71c0df7 Restructured component data
Removed the now superfluous ComponentArray
Flattened the Type->Map<Entity, Object> array
2021-06-13 13:45:30 +12:00
9 changed files with 124 additions and 175 deletions
+25 -1
View File
@@ -1,4 +1,28 @@
# JavaECS # JavaECS
An implementation of an Entity-Component-System in Java. Based on the C++ implementation by [Austin Morlan](https://code.austinmorlan.com/austin/ecs). An implementation of an Entity-Component-System written in Java.
## Introduction to ECS
The primary goal of an ECS is to provide fast access to many entities; especially where those entities share many of the same properties. It also solves issues regarding adaptability in an inheritance-based engine.
ECS is more of a conceptual idea rather than an actual engine structure. There exists many variations of the concept, each adding in different features and models as required.
The primary example of an ECS is [EnTT](https://github.com/skypjack/entt), which is used in Mojang's Minecraft.
There are four key elements to an ECS:
1. The **entity**, which is a simple ID, usually an index.
2. The **component**, which is a struct or class that stores data
3. The **system**, which is a functionality that is executed regularly by the engine.
4. The **engine** (*sometimes split into parts called 'managers'*), which controls the interaction between these components, and provides access to the external program.
## About JavaECS
The focus of JavaECS is more about the structure rather than raw performance. It remains performant, but there may be multiple areas where improvements can be made.
In a quick port of Alex Beimler's [ECS Benchmark](https://github.com/abeimler/ecs_benchmark), JavaECS performs at about the same speed as [ECS](https://github.com/redxdev/ECS) (~90 ms). The results aren't normalised between test environments, so take them with a grain of salt; but it tends to suggest that the project has decent performance.
This project is inspired by:
* [C++ implementation](https://austinmorlan.com/posts/entity_component_system/) by Austin Morlan.
* [Nomad Game Engine](https://medium.com/@savas/nomad-game-engine-part-2-ecs-9132829188e5) by Niko Savas
* [EntityX](https://github.com/alecthomas/entityx) by Alec Thomas.
## Implementation
See the [documentation](https://git.software.kauripeak.co.nz/BrychanD/JavaECS-Docs/wiki) for implmentation details See the [documentation](https://git.software.kauripeak.co.nz/BrychanD/JavaECS-Docs/wiki) for implmentation details
+1 -1
View File
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>nz.ac.massey.javaecs</groupId> <groupId>nz.ac.massey.javaecs</groupId>
<artifactId>javaecs</artifactId> <artifactId>javaecs</artifactId>
<version>0.9.9-RELEASE_CANDIDATE</version> <version>1.0.1</version>
<properties> <properties>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>
@@ -1,121 +0,0 @@
package nz.ac.massey.javaecs;
/**
* Component Array
* Defines the data structure that component data is stored under.
* Has a list of defined objects, and two associative maps that link
* the position of the data with the entity number.
*
* Therefore, every entity in entityComponentDataMap is valid, so
* no additional sorting is required
*
*
* Contributors:
* Brychan Dempsey - brychand@hotmail.com
*
*/
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
/**
* Stores component data in a packed interal list, and provides methods for accessing this data
*/
class ComponentArray{
// The object data array
private List<Object> componentArray;
// The mappings between data and entity
private Map<Entity, Integer> entityComponentDataMap;
private Map<Integer, Entity> componentDataEntityMap;
public ComponentArray(int initialSize){
componentArray = new ArrayList<>(initialSize);
entityComponentDataMap = new HashMap<>(initialSize);
componentDataEntityMap = new HashMap<>(initialSize);
}
/**
* 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(Entity entity){
try{
return componentArray.get(entityComponentDataMap.get(entity));
}
catch (NullPointerException ex){
Engine.writeErr("Attempted to retrieve non-existent data for unassigned entity: " + entity.getValue());
return null;
}
catch (IndexOutOfBoundsException e){
Engine.writeErr("Index out-of-bounds for entity: " + entity.getValue());
return null;
}
}
/**
* 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(Entity entity, Object component){
if (entityComponentDataMap.containsKey(entity)){
Engine.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(Entity sourceEntity, Entity 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(Entity entity){
if (!entityComponentDataMap.containsKey(entity)){
Engine.writeErr("Attempted to remove non-existent entity: " + entity.getValue());
return false;
}
// 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
Entity 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);
return true;
}
}
@@ -16,21 +16,23 @@ import java.util.HashMap;
/** /**
* Manages the addition, sorting and retrieving of components and component data * Manages the addition, sorting and retrieving of components and component data
*/ */
class ComponentManager{ public class ComponentManager{
private Map<Type, ComponentArray> componentArrays = new HashMap<>(); public Map<Type, Map<Entity, Object>> componentArrays = new HashMap<>();
private Map<Type, Integer> componentPosIndex = new HashMap<>(); // Need to be able to map bit indices and component types
private Map<Integer, Type> indexComponentType = new HashMap<>(); public Map<Integer, Type> indexComponentType = new HashMap<>();
public Map<Type, Integer> componentTypeIndex = new HashMap<>();
//
/** /**
* Adds the specified component to the provided entity. * Adds the specified component to the provided entity.
* <p> * Does not ensure synchronisation with the other managers; ensure this is done!
* Providing a null Object defaults the value to boolean false
* @param componentType the class type of the component to add * @param componentType the class type of the component to add
* @param componentData the component data to associate * @param componentData the component data to associate
* @param entity the entity to associate data to * @param entity the entity to associate data to
*/ */
protected boolean addComponentToEntity(Type componentType, Object componentData, Entity entity){ public boolean addComponentToEntity(Type componentType, Object componentData, Entity entity){
return componentArrays.get(componentType).insertData(entity, componentData); componentArrays.get(componentType).put(entity, componentData);
return true;
} }
/** /**
@@ -41,9 +43,7 @@ class ComponentManager{
// HashMap lookups take time, use the known bitstates to avoid // HashMap lookups take time, use the known bitstates to avoid
int index = entityRegistrations.nextSetBit(0); int index = entityRegistrations.nextSetBit(0);
while(index != -1){ while(index != -1){
if (!componentArrays.get(indexComponentType.get(index)).removeData(entity)){ componentArrays.get(indexComponentType.get(index)).remove(entity);
Engine.writeErr(indexComponentType.get(index).getTypeName());
}
index = entityRegistrations.nextSetBit(index+1); index = entityRegistrations.nextSetBit(index+1);
} }
} }
@@ -55,7 +55,7 @@ class ComponentManager{
* @return the Object data found, or null if it was not found * @return the Object data found, or null if it was not found
*/ */
public Object getComponent(Type componentType, Entity entity){ public Object getComponent(Type componentType, Entity entity){
return componentArrays.get(componentType).getData(entity); return componentArrays.get(componentType).get(entity);
} }
/** /**
@@ -65,7 +65,7 @@ class ComponentManager{
*/ */
public Integer getComponentIndex(Type type){ public Integer getComponentIndex(Type type){
try{ try{
return componentPosIndex.get(type); return componentTypeIndex.get(type);
} }
catch (NullPointerException e){ catch (NullPointerException e){
return -1; return -1;
@@ -94,11 +94,11 @@ class ComponentManager{
* @return true if the component was moved successfully, else false * @return true if the component was moved successfully, else false
*/ */
public boolean moveComponentData(Entity sourceEntity, Entity destinationEntity, Type component){ public boolean moveComponentData(Entity sourceEntity, Entity destinationEntity, Type component){
if (componentArrays.get(component).moveData(sourceEntity, destinationEntity) == 0){ Object data = componentArrays.get(component).get(sourceEntity);
componentArrays.get(component).put(destinationEntity, data);
componentArrays.get(component).remove(sourceEntity);
return true; return true;
} }
else return false;
}
/** /**
* Moves all component data from one entity to another * Moves all component data from one entity to another
@@ -109,8 +109,7 @@ class ComponentManager{
public void moveAllComponentData(Entity sourceEntity, Entity destinationEntity, BitSet sourceRegistrations){ public void moveAllComponentData(Entity sourceEntity, Entity destinationEntity, BitSet sourceRegistrations){
int result = sourceRegistrations.nextSetBit(0); int result = sourceRegistrations.nextSetBit(0);
while (result != -1){ while (result != -1){
Type key = indexComponentType.get(result); moveComponentData(sourceEntity, destinationEntity, indexComponentType.get(result));
componentArrays.get(key).moveData(sourceEntity, destinationEntity);
result = sourceRegistrations.nextSetBit(result +1); result = sourceRegistrations.nextSetBit(result +1);
} }
} }
@@ -135,9 +134,9 @@ class ComponentManager{
Engine.writeErr("Component " + type.getTypeName() + " is already registered"); Engine.writeErr("Component " + type.getTypeName() + " is already registered");
return false; return false;
} }
componentArrays.put(type, new ComponentArray(arraySize)); componentArrays.put(type, new HashMap<>());
indexComponentType.put(componentPosIndex.size(), type); indexComponentType.put(componentTypeIndex.size(), type);
componentPosIndex.put(type, componentPosIndex.size()); componentTypeIndex.put(type, componentTypeIndex.size());
return true; return true;
} }
@@ -149,6 +148,18 @@ class ComponentManager{
* @param entity the entity to remove the component from * @param entity the entity to remove the component from
*/ */
public boolean removeComponentFromEntity(Type componentType, Entity entity){ public boolean removeComponentFromEntity(Type componentType, Entity entity){
return componentArrays.get(componentType).removeData(entity); componentArrays.get(componentType).remove(entity);
return true;
}
/**
* Checks if the entity contains data for the provided component
* @param entity the entity to check
* @param componentType the component class type
* @return true if the entity has component data
*/
public boolean entityHasComponentData(Entity entity, Type componentType){
if (componentArrays.get(componentType).containsKey(entity)) return true;
else return false;
} }
} }
@@ -16,9 +16,25 @@ import java.util.Set;
import java.util.BitSet; import java.util.BitSet;
import java.util.HashSet; import java.util.HashSet;
/**
* Abstract class that all systems should inherit from<p>
* Defines four required components:
* <ol>
* <li>The list of <b>entities</b> that have all the components required for this system</li>
* <li>The <b>BitSet</b> of component registrations required for this system</li>
* <li>The <b>init()</b> function, where logic that needs to be run once is performed</li>
* <li>The <b>update(dt)</b> function, where logic that needs to be run regularly is performed. <i>dt is the delta time in millseconds</i></li>
* </ol>
* Additionally, the object constructor should define the actual values of the BitSet.<p>
* Additional functions can be implemented as required.
*/
public abstract class ECSSystem{ public abstract class ECSSystem{
protected Set<Entity> entities = new HashSet<>(); public Set<Entity> entities = new HashSet<>();
protected BitSet registrationSet; public BitSet registrationSet;
public ECSSystem(){
registrationSet = new BitSet();
}
public BitSet getRegistrationSet(){ public BitSet getRegistrationSet(){
return registrationSet; return registrationSet;
@@ -26,14 +42,19 @@ public abstract class ECSSystem{
/** /**
* Functionality that should be run only once. * Functionality that should be run only once.
* Implement additional parameterised init() functions as required. * Implement additional parameterised <b>init()</b> functions as required.
* <p>
* This is distinct from the constructor as this may be run as required any time between
* the construction and <b>update(dt)</b>. It is not intended to set the actual bits of <b>registrationSet</b>
* in this function; that should be performed in the constructor.
* @throws Exception can return exceptions to the main program if an issue occurred.
*/ */
public abstract void init(); public abstract void init() throws Exception;
/** /**
* Functionality that is expected to be called regularly * Functionality that is expected to be called regularly
* Intended as a template only; may be superficially implemented. * Intended as a template only; may be superficially implemented.
* Implement additional parameterised update() functions as required. * Implement additional parameterised <b>update()</b> functions as required.
* @param dt delta-time; the change in time in milliseconds since this function was last run * @param dt delta-time; the change in time in milliseconds since this function was last run
*/ */
public abstract void update(double dt); public abstract void update(double dt);
@@ -116,6 +116,16 @@ public class Engine {
return entityManager.getNumEntities(); return entityManager.getNumEntities();
} }
/**
* Checks if the entity is subscribed to the provided type
* @param entity the entity to search
* @param componentType the class type of the component to search
* @return true if the entity is subscribed
*/
public boolean entityHasComponent(Entity entity, Type componentType){
return entityManager.getRegistrations(entity).get(componentManager.getComponentIndex(componentType));
}
/*************************************** /***************************************
** Manage Components ** ** Manage Components **
***************************************/ ***************************************/
@@ -141,7 +151,6 @@ public class Engine {
int componentIndex = componentManager.getComponentIndex(componentType); int componentIndex = componentManager.getComponentIndex(componentType);
if (entityManager.registerComponent(componentIndex, entity)) if (entityManager.registerComponent(componentIndex, entity))
{ {
//systemManager.entityRegistrationAdded(entity, componentIndex);
systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity)); systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity));
componentManager.addComponentToEntity(componentType, component, entity); componentManager.addComponentToEntity(componentType, component, entity);
return true; return true;
@@ -5,7 +5,7 @@ import java.lang.reflect.Type;
/** /**
* Entity class. * Entity class.
* Whilst an entity is just an Integer, and using Integer values * Whilst an entity is just an Integer, and using Integer values
* would be more performant (no function call, primitive arrays & etc.), it is encapsulated to help * would be more performant (no function call, primitive arrays and etc.), it is encapsulated to help
* distinguish between entities and Integers. Also, in this form it may be extended to also include things * distinguish between entities and Integers. Also, in this form it may be extended to also include things
* like names, grouping, specific ordering etc * like names, grouping, specific ordering etc
*/ */
@@ -56,12 +56,11 @@ public class Entity {
/** /**
* Adds the provided component to this entity. * Adds the provided component to this entity.
* <p> * <p>
* <b>This function calls addComponent() in the engine,</b> so * <b>This function calls <i>Engine.addComponent(Entity, Type, Object)</i></b>, so
* that should be used instead. * that should be used instead.
* @deprecated This function is not ECS-like. * @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more * It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design * intuitive to those familiar with OO design
* @see Engine.addComponent() - which this function makes a call to
* @param componentType the class type of the component to add * @param componentType the class type of the component to add
* @param componentData the component data * @param componentData the component data
* @return true if successful * @return true if successful
@@ -74,12 +73,11 @@ public class Entity {
/** /**
* Removes the provided component from this entity. * Removes the provided component from this entity.
* <p> * <p>
* <b>This function calls removeComponent() in the engine,</b> so * <b>This function calls <i>Engine.removeComponent(Entity, Type)</i></b>, so
* that should be used instead. * that should be used instead.
* @deprecated This function is not ECS-like. * @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more * It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design * intuitive to those familiar with OO design
* @see Engine.removeComponent() - which this function makes a call to
* @param componentType the class type of the component to remove * @param componentType the class type of the component to remove
* @return true if successful * @return true if successful
*/ */
@@ -91,12 +89,11 @@ public class Entity {
/** /**
* Gets the component data associated to this entity. * Gets the component data associated to this entity.
* <p> * <p>
* <b>This function calls getComponentData() in the engine,</b> so * <b>This function calls <i>Engine.getComponentData()</i></b>, so
* that should be used instead. * that should be used instead.
* @deprecated This function is not ECS-like. * @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more * It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design * intuitive to those familiar with OO design
* @see Engine.getComponentData() - which this function makes a call to
* @param componentType the class type of the component to fetch data from * @param componentType the class type of the component to fetch data from
* @return the data Object, which requires casting * @return the data Object, which requires casting
*/ */
@@ -19,15 +19,17 @@ import java.util.ArrayList;
* Manages data from the perspective of the entity. * Manages data from the perspective of the entity.
* <p> * <p>
* 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.
* <p>
* Do not update values in this class unless you have reviewed the JavaECS source!
*/ */
class EntityManager{ public class EntityManager{
// According to https://stackoverflow.com/questions/12524826/why-should-i-use-deque-over-stack // According to https://stackoverflow.com/questions/12524826/why-should-i-use-deque-over-stack
// ArrayDeque is likely faster than a LinkedList, when used in place of one. // ArrayDeque is likely faster than a LinkedList, when used in place of one.
// We can also supply a size to the constructor of ArrayDeque, which avoids resizing the collection // We can also supply a size to the constructor of ArrayDeque, which avoids resizing the collection
// at initialisation time (took 1.4s vs 1.8s for 1M) // at initialisation time (took 1.4s vs 1.8s for 1M)
private Deque<Entity> unusedEntities; public Deque<Entity> unusedEntities;
private List<BitSet> entityRegistrations; public List<BitSet> entityRegistrations;
private int maxSize = 1024; public int maxSize = 1024;
/** /**
* Initialise the EntityManager with the default max size of 1024 * Initialise the EntityManager with the default max size of 1024
@@ -63,7 +65,7 @@ class EntityManager{
* @return the index of the new entity * @return the index of the new entity
* @throws NoSuchElementException an exception if there are no more unused entities * @throws NoSuchElementException an exception if there are no more unused entities
*/ */
protected Entity addEntity() throws NoSuchElementException{ public Entity addEntity() throws NoSuchElementException{
Entity result = unusedEntities.remove(); Entity result = unusedEntities.remove();
entityRegistrations.set(result.getValue(), new BitSet()); entityRegistrations.set(result.getValue(), new BitSet());
return result; return result;
@@ -105,7 +107,7 @@ class EntityManager{
* @param entity the entity to register * @param entity the entity to register
* @return true if the operation was successful * @return true if the operation was successful
*/ */
protected boolean registerComponent(int component, Entity entity){ public boolean registerComponent(int component, Entity entity){
if (entity.getValue() >= maxSize){ if (entity.getValue() >= maxSize){
Engine.writeErr("Attempted to assign a component to non-existent entity: " + entity.getValue()); Engine.writeErr("Attempted to assign a component to non-existent entity: " + entity.getValue());
return false; return false;
@@ -124,32 +126,36 @@ class EntityManager{
/** /**
* Adds the entity index back into unusedEntities, and sets the registrations to null * Adds the entity index back into unusedEntities, and sets the registrations to null
* <p> * <p>
* <b>Does not handle associated data</b> Use the method in ECS to remove entities cleanly * <b>Does not handle associated data</b> Use the method in ECS to remove entities cleanly,
* or otherwise ensure the component data and systems are updated!
* @param entity the entity to remove * @param entity the entity to remove
*/ */
protected void removeEntity(Entity entity){ public void removeEntity(Entity entity){
unusedEntities.add(entity); unusedEntities.add(entity);
entityRegistrations.set(entity.getValue(), null); entityRegistrations.set(entity.getValue(), null);
} }
/** /**
* Sets the entity's registrations to the provided BitSet * Sets the entity's registrations to the provided BitSet.
* <p>
* Does <b>not</b> ensure the systems registrations are updated.
* @param entity the entity to set * @param entity the entity to set
* @param registrations the preset registrations * @param registrations the preset registrations
*/ */
protected void setRegistrations(Entity entity, BitSet registrations){ public void setRegistrations(Entity entity, BitSet registrations){
entityRegistrations.set(entity.getValue(), registrations); entityRegistrations.set(entity.getValue(), registrations);
} }
/** /**
* Unregisters the specified component from the entity * Unregisters the specified component from the entity
* <p> * <p>
* <b>Does not handle component data</b> Use the method in ECS to remove components cleanly * <b>Does not handle component data</b> Use the method in ECS to remove components cleanly,
* or otherwise ensure the component data and systems are updated!
* @param component the component index to remove * @param component the component index to remove
* @param entity the entity to remove * @param entity the entity to remove
* @return true if successful * @return true if successful
*/ */
protected boolean unregisterComponent(int component, Entity entity){ public boolean unregisterComponent(int component, Entity entity){
try{ try{
entityRegistrations.get(entity.getValue()).clear(component); entityRegistrations.get(entity.getValue()).clear(component);
return true; return true;
@@ -17,16 +17,18 @@ import java.util.HashMap;
/** /**
* Manages system-focused aspects, such as ensuring a system has the correct list of current entities. * Manages system-focused aspects, such as ensuring a system has the correct list of current entities.
* Manages registration of new systems * Manages registration of new systems
* <p>
* Do not update data in this class unless you have reviewed the JavaECS source.
*/ */
class SystemManager{ public class SystemManager{
private Map<Type, ECSSystem> systems = new HashMap<>(); public Map<Type, ECSSystem> systems = new HashMap<>();
/** /**
* Signals the SystemManager that an entity was destroyed. * Signals the SystemManager that an entity was destroyed.
* Removes the entity from each system's tracked entities * Removes the entity from each system's tracked entities
* @param entity the destroyed entity * @param entity the destroyed entity
*/ */
protected void entityDestroyed(Entity entity){ public void entityDestroyed(Entity entity){
// Unlike components, this isn't simply indexed. // Unlike components, this isn't simply indexed.
for (Type key : systems.keySet()) { for (Type key : systems.keySet()) {
systems.get(key).entities.remove(entity); systems.get(key).entities.remove(entity);