3 Commits
Author SHA1 Message Date
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
7 changed files with 94 additions and 152 deletions
+24 -1
View File
@@ -1,4 +1,27 @@
# 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 a conceptually simple 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.
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.
This project is inspired by a C++ implementation by [Austin Morlan](https://austinmorlan.com/posts/entity_component_system/).
## 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.0</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;
}
}
@@ -17,9 +17,11 @@ 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{ class ComponentManager{
private Map<Type, ComponentArray> componentArrays = new HashMap<>(); private 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<>(); private Map<Integer, Type> indexComponentType = new HashMap<>();
private Map<Type, Integer> componentTypeIndex = new HashMap<>();
//
/** /**
* Adds the specified component to the provided entity. * Adds the specified component to the provided entity.
@@ -30,7 +32,8 @@ class ComponentManager{
* @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){ protected 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 +44,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 +56,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 +66,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 +95,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 +110,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 +135,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 +149,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,24 +16,45 @@ 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<>(); protected Set<Entity> entities = new HashSet<>();
protected BitSet registrationSet; protected BitSet registrationSet;
public ECSSystem(){
registrationSet = new BitSet();
}
public BitSet getRegistrationSet(){ public BitSet getRegistrationSet(){
return registrationSet; return registrationSet;
} }
/** /**
* 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 **
***************************************/ ***************************************/
@@ -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
*/ */