Compare commits

...

25 Commits

Author SHA1 Message Date
244bb38e43 Fixed a typo 2022-01-10 13:39:43 +13:00
4731981977 Update 'README.md' 2021-06-15 18:28:27 +12:00
fc480cfe2e Update 'README.md' 2021-06-15 18:26:43 +12:00
2c96e92a6a Changed access modifiers to public
Changed access so data may be manually changed, if needed
2021-06-15 17:59:03 +12:00
ccabef5226 Made Manager classes public 2021-06-14 18:20:29 +12:00
6d8574b951 Update 'README.md' 2021-06-13 14:21:09 +12:00
e578e72b46 Version change to 1.0.0 2021-06-13 14:06:26 +12:00
84d71c0df7 Restructured component data
Removed the now superfluous ComponentArray
Flattened the Type->Map<Entity, Object> array
2021-06-13 13:45:30 +12:00
051836852c Fixed javadocs, cleaned many lines
Prepped for release at v0.9.9
Breaking changes for all implementations
2021-06-12 00:51:35 +12:00
999ffa41dd Changed array types, increasing performance 2021-06-11 20:55:05 +12:00
529f99abbd Cleaned and minor restructure 2021-06-11 14:41:22 +12:00
4f4e1bec7b Improved entity destruction handling 2021-06-10 22:45:12 +12:00
edd8a3faa7 Worked on tests, fixed issues with entities
Implemented hashValue() for the entity so equality can be compared correctly.
I.e. hashValue is the actual value of the entity, as they *should* be unique
2021-06-10 15:57:58 +12:00
86ef1e30e3 Changed component data handling
Adding empty (null) component data defaults to false
Also added getNumEntities() function
Added a test for getRegistrationsOutOfRange
2021-06-10 15:28:15 +12:00
0571986059 Changed: Version; System Registration behaviour 2021-06-09 12:51:50 +12:00
d86c07352a Refactored ECS to Engine 2021-06-09 12:25:22 +12:00
d021b7815b Set visibilites 2021-06-09 11:34:14 +12:00
6ada4110e1 Switched to using a new Entity class
While likely less performant, it should provide good readability,
and good explaination of how an entity is related in the engine
2021-06-09 11:18:58 +12:00
0b3553d46e Fixed to meet tests 2021-06-08 21:34:57 +12:00
22a75f5559 Added docstrings and some exception handling 2021-06-08 21:13:05 +12:00
6e000492b0 .. 2021-06-08 17:08:57 +12:00
23abf57b6e Tests added, some function changes
Functions changed to return booleans
2021-06-08 17:08:47 +12:00
b1efb9802d Removed implementation classes
Leaves only the framework in the compiled clause.
Find the removed files in ../examples/misc/
2021-06-08 14:47:10 +12:00
7524a0096a Added abstraction to ECSSystem
Allows the library to signal the required implementations of inti() and update()
2021-06-08 14:44:34 +12:00
4354eac0cf Fixed referencing to package name 2021-06-07 21:25:01 +12:00
19 changed files with 974 additions and 415 deletions

2
.vscode/launch.json vendored
View File

@ -14,7 +14,7 @@
"type": "java",
"name": "Launch App",
"request": "launch",
"mainClass": "nz.ac.massey.programming_project_159333_s1_2021.App",
"mainClass": "nz.ac.massey.javaecs.App",
"projectName": "javaecs"
}
]

View File

@ -1,4 +1,28 @@
# 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.
See the [documentation](https://git.software.kauripeak.co.nz/BrychanD/JavaECS-Docs/wiki) for implmentation details
## 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 implementation details

View File

@ -1,9 +1,15 @@
package nz.ac.massey.javaecs;
public class FrameRateSystem extends ECSSystem{
@Override
void init() {}
@Override
void update() {}
void update(double dt, double idleTime){
System.out.print(String.format("dt: %.3g (%.3g idle) ", dt, idleTime));
}
}

View File

@ -6,12 +6,18 @@ public class LogVec2DSystem extends ECSSystem{
this.gameEngine = gameEngine;
}
@Override
void init() {}
@Override
void update() {}
void update(double dt){
for (Integer entity : entities) {
Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class);
System.out.println(String.format("X: %.6g, Y: %.6g", pos.x, pos.y));
}
}
}

View File

@ -6,9 +6,13 @@ public class PhysicsSystem extends ECSSystem{
this.gameEngine = gameEngine;
}
@Override
void init() {}
void update(double dt){
@Override
void update() {}
void update(Double dt){
for (Integer entity : entities) {
Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class);
RidgidBody ridgidBody = (RidgidBody)gameEngine.getComponentData(entity, RidgidBody.class);

View File

@ -1,8 +1,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nz.ac.massey.programming_project_159333_s1_2021</groupId>
<groupId>nz.ac.massey.javaecs</groupId>
<artifactId>javaecs</artifactId>
<version>1.1-SNAPSHOT</version>
<version>1.0.1</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
@ -17,10 +17,10 @@
<maven-javadoc-plugin.version>3.0.0</maven-javadoc-plugin.version>
<coveralls-maven-plugin.version>4.3.0</coveralls-maven-plugin.version>
<!-- JaCoCo thresholds. Increase gradually as you add tests. -->
<jacoco.unit-tests.limit.instruction-ratio>0%</jacoco.unit-tests.limit.instruction-ratio>
<jacoco.unit-tests.limit.branch-ratio>0%</jacoco.unit-tests.limit.branch-ratio>
<jacoco.unit-tests.limit.class-complexity>20</jacoco.unit-tests.limit.class-complexity>
<jacoco.unit-tests.limit.method-complexity>5</jacoco.unit-tests.limit.method-complexity>
<jacoco.unit-tests.limit.instruction-ratio>75%</jacoco.unit-tests.limit.instruction-ratio>
<jacoco.unit-tests.limit.branch-ratio>50%</jacoco.unit-tests.limit.branch-ratio>
<jacoco.unit-tests.limit.class-complexity>30</jacoco.unit-tests.limit.class-complexity>
<jacoco.unit-tests.limit.method-complexity>10</jacoco.unit-tests.limit.method-complexity>
</properties>
<dependencies>
<dependency>
@ -60,6 +60,18 @@
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
@ -134,7 +146,7 @@
<goal>report</goal>
</goals>
</execution>
<!-- <execution>
<execution>
<id>check-unit-test</id>
<phase>test</phase>
<goals>
@ -180,7 +192,7 @@
</rule>
</rules>
</configuration>
</execution> -->
</execution>
</executions>
</plugin>
</plugins>

View File

@ -1,87 +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;
class ComponentArray{
List<Object> componentArray = new ArrayList<>();
Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
public void entityDestroyed(int entity){
if (entityComponentDataMap.containsKey(entity)){
removeData(entity);
}
}
public void removeData(int entity){
if (!entityComponentDataMap.containsKey(entity)){
System.err.println("Attempted to remove non-existent entity");
return;
}
// Get the componentData index of the entity
int removedComponentDataIndex = entityComponentDataMap.get(entity);
// Replace the removed component with the last component in the array
componentArray.set(removedComponentDataIndex, componentArray.get(componentArray.size() -1));
// update the data positions in the map
int lastEntity = componentDataEntityMap.get(componentArray.size()-1);
entityComponentDataMap.replace(lastEntity, removedComponentDataIndex);
componentDataEntityMap.replace(removedComponentDataIndex, lastEntity);
// Finally, remomve the last elements
entityComponentDataMap.remove(entity);
componentDataEntityMap.remove(componentArray.size() -1);
componentArray.remove(componentArray.size() -1);
}
public void insertData(int entity, Object component){
if (entityComponentDataMap.containsKey(entity)){
System.err.println("Entity is already subscribed to the component");
return;
}
int index = componentArray.size();
entityComponentDataMap.put(entity, index);
componentDataEntityMap.put(index, entity);
componentArray.add(component);
}
/**
* 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)){
System.err.println("Attempted to retrieve non-existent data");
return null;
}
return componentArray.get(entityComponentDataMap.get(entity));
}
}

View File

@ -10,53 +10,156 @@ 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<Type, ComponentArray> componentArrays = new HashMap<>();
Map<Type, Integer> componentPosIndex = new HashMap<>();
int componentPos = 0;
/**
* Manages the addition, sorting and retrieving of components and component data
*/
public class ComponentManager{
public Map<Type, Map<Entity, Object>> componentArrays = new HashMap<>();
// Need to be able to map bit indices and component types
public Map<Integer, Type> indexComponentType = new HashMap<>();
public Map<Type, Integer> componentTypeIndex = new HashMap<>();
//
public void registerComponent(Type type){
if (componentArrays.containsKey(type)){
System.err.println("Component " + type.getTypeName() + " is already registered");
return;
}
componentArrays.put(type, new ComponentArray());
componentPosIndex.put(type, componentPos++);
}
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);
/**
* Adds the specified component to the provided entity.
* Does not ensure synchronisation with the other managers; ensure this is done!
* @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
*/
public boolean addComponentToEntity(Type componentType, Object componentData, Entity entity){
componentArrays.get(componentType).put(entity, componentData);
return true;
}
/**
* Moves component data from one entity to another
* @param sourceEntity
* @param destinationEntity
* Signals to the ComponentManager the entity was destroyed. All component data references should be removed.
* @param entity the entity that was destroyed.
*/
public void moveComponentData(int sourceEntity, int destinationEntity){
for (Type key : componentArrays.keySet()) {
componentArrays.get(key).moveData(sourceEntity, destinationEntity);
public void entityDestroyed(Entity entity, BitSet entityRegistrations){
// HashMap lookups take time, use the known bitstates to avoid
int index = entityRegistrations.nextSetBit(0);
while(index != -1){
componentArrays.get(indexComponentType.get(index)).remove(entity);
index = entityRegistrations.nextSetBit(index+1);
}
}
/**
* 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, Entity entity){
return componentArrays.get(componentType).get(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
*/
public Integer getComponentIndex(Type type){
try{
return componentTypeIndex.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
*/
public 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
*/
public boolean moveComponentData(Entity sourceEntity, Entity destinationEntity, Type component){
Object data = componentArrays.get(component).get(sourceEntity);
componentArrays.get(component).put(destinationEntity, data);
componentArrays.get(component).remove(sourceEntity);
return true;
}
/**
* 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
*/
public void moveAllComponentData(Entity sourceEntity, Entity destinationEntity, BitSet sourceRegistrations){
int result = sourceRegistrations.nextSetBit(0);
while (result != -1){
moveComponentData(sourceEntity, destinationEntity, indexComponentType.get(result));
result = sourceRegistrations.nextSetBit(result +1);
}
}
/**
* Registers the component type
* @param type the class type to register
* @return true if the component was registered successfully, else false
*/
public boolean registerComponent(Type type){
return registerComponent(type, 16);
}
/**
* Registers the component type
* @param type the class type to register
* @param arraySize the number of elements to prereserve space for.
* @return true if the component was registered successfully, else false
*/
public boolean registerComponent(Type type, int arraySize){
if (componentArrays.containsKey(type)){
Engine.writeErr("Component " + type.getTypeName() + " is already registered");
return false;
}
componentArrays.put(type, new HashMap<>());
indexComponentType.put(componentTypeIndex.size(), type);
componentTypeIndex.put(type, componentTypeIndex.size());
return true;
}
/**
* Removes the specified component from the entity.
* <p>
* Be sure to call SystemManager.entityRegistrationsChanged after calling this function
* @param componentType the class type of the component to remove
* @param entity the entity to remove the component from
*/
public boolean removeComponentFromEntity(Type componentType, Entity 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;
}
}

View File

@ -1,145 +0,0 @@
package nz.ac.massey.javaecs;
/**
* ECS manager class.
* Call this class and its functions to interact correctly with the ECS system.
*
* Contributors:
* Brychan Dempsey - brychand@hotmail.com
*
* References:
* Based on the implementation by Austin Morlan:
* https://code.austinmorlan.com/austin/ecs - 'A simple C++ Entity Component System' - released under MIT licence
*
*/
import java.lang.reflect.Type;
import java.util.BitSet;
/**
* The ECS manager.
* <p>
* See <href src="https://git.software.kauripeak.co.nz/BrychanD/JavaECS">https://git.software.kauripeak.co.nz/BrychanD/JavaECS</href>
* for documentation and more information.
*/
public class ECS {
protected EntityManager entityManager;
protected ComponentManager componentManager;
protected SystemManager systemManager;
/**
* Initialises the ECS with default values
* <p>
* Maximum 1024 enitites default
*/
public ECS(){
entityManager = new EntityManager();
componentManager = new ComponentManager();
systemManager = new SystemManager();
}
/**
* Initialises the ECS with the specified value(s)
* @param maxEntities the maximum number of entities to allow
*/
public ECS(int maxEntities){
entityManager = new EntityManager(maxEntities);
componentManager = new ComponentManager();
systemManager = new SystemManager();
}
/**
* /**
* Creates a new entity
* @return the index of the new entity
* @throws IndexOutOfBoundsException
*/
Integer createEntity() throws IndexOutOfBoundsException{
int newEntity = entityManager.addEntity();
if (newEntity == -1) throw new IndexOutOfBoundsException("Could not create a new entity");
return newEntity;
}
/**
* Attempts to resize the maximum number of entities
* @param newSize the new maximum number of entities
* @return true if the operation succeeded
*/
boolean resizeMaximum(int newSize){
return entityManager.resize(newSize, systemManager, componentManager);
}
/**
* Signals each manager to remove the specified entity
* @param entity the entity to destroy
*/
void destroyEntity(int entity){
entityManager.removeEntity(entity);
componentManager.entityDestroyed(entity);
systemManager.entityDestroyed(entity);
}
/**
* Registers the specified name in the component manager
* @param name the name to register. Should be the component class name or a suitable name for primitive types
*/
void registerComponent(Type type){
componentManager.registerComponent(type);
}
Integer getComponentIndex(Type type){
return componentManager.getComponentIndex(type);
}
/**
* Adds an exisiting component to an exisiting entity
* @param entity the entity to add the component to
* @param componentName the class name of the component to add
* @param component the actual component data
*/
void addComponent(int entity, Type componentName, Object component){
componentManager.addComponentToEntity(componentName, component, entity);
entityManager.registerComponent(componentManager.getComponentIndex(componentName), entity);
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity));
}
/**
* Removes the component from the specified entity
* @param entity the entity to remove the component from
* @param componentName the class name of the component
*/
void removeComponent(int entity, Type componentType){
componentManager.removeComponentFromEntity(componentType, entity);
entityManager.unregisterComponent(componentManager.getComponentIndex(componentType), entity);
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity));
}
/**
* Gets the actual data of the component associated to the entity.
* May require casting from Object to the known data type
* @param entity the entity to retrieve the data for
* @param componentType the class type of the component
* @return the component data Object associated with the entity
*/
Object getComponentData(int entity, Type componentType){
return componentManager.getComponent(componentType, entity);
}
/**
*
* @param systemName
* @param action
*/
void registerSystem(Type systemType, ECSSystem action){
systemManager.registerSystem(systemType, action);
}
/**
* Sets the specified system's signature to the provided signature
* @param system the class name of the system to set the signature of
* @param signature the new signature data
*/
void setSystemSignature(Type system, BitSet signature){
systemManager.setSignature(system, signature);
}
Integer getMaxEntities(){
return entityManager.currentSize;
}
}

View File

@ -13,8 +13,49 @@ package nz.ac.massey.javaecs;
*/
import java.util.Set;
import java.util.BitSet;
import java.util.HashSet;
class ECSSystem{
Set<Integer> entities = new 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 Set<Entity> entities = new HashSet<>();
public BitSet registrationSet;
public ECSSystem(){
registrationSet = new BitSet();
}
public BitSet getRegistrationSet(){
return registrationSet;
}
/**
* Functionality that should be run only once.
* 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() throws Exception;
/**
* Functionality that is expected to be called regularly
* Intended as a template only; may be superficially implemented.
* 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
*/
public abstract void update(double dt);
}

View File

@ -0,0 +1,288 @@
package nz.ac.massey.javaecs;
/**
* ECS engine class.
* Call this class and its functions to interact correctly with the ECS system.
* This is the entry-point to the library
*
* Contributors:
* Brychan Dempsey - brychand@hotmail.com
*
* References:
* Based on the implementation by Austin Morlan:
* https://austinmorlan.com/posts/entity_component_system/ - 'A simple C++ Entity Component System'
*
*/
import java.io.PrintStream;
import java.lang.reflect.Type;
import java.util.BitSet;
import java.util.NoSuchElementException;
/**
* The ECS management engine. Supplemented by the EntityManager, ComponentManager, and System Manager
* These four classes provide the actual functionality of the ecs
* <p>
* See https://git.software.kauripeak.co.nz/BrychanD/JavaECS
* for documentation and more information.
*/
public class Engine {
// All internal functions write error messages to errorStream; which defaults to System.err
// Can be set to a different PrintStream, to allow errors to be logged elsewhere
protected static PrintStream errorStream = System.err;
// References to the manager classes
protected EntityManager entityManager;
protected ComponentManager componentManager;
protected SystemManager systemManager;
/***************************************
** Engine Constructors **
***************************************/
public Engine(){
entityManager = new EntityManager();
componentManager = new ComponentManager();
systemManager = new SystemManager();
// In a non-ECS type manner (instead OO-like), provide an additional method to
// add components to an entity: entity.addComponent().
Entity.engineRef = this;
}
/**
* Initialises the ECS with the specified value
* @param maxEntities the maximum number of entities to allow
*/
public Engine(int maxEntities){
entityManager = new EntityManager(maxEntities);
componentManager = new ComponentManager();
systemManager = new SystemManager();
Entity.engineRef = this;
}
/**
* Attempts to resize the maximum number of entities
* @param newSize the new maximum number of entities
* @return true if the operation succeeded
*/
public boolean resizeMaximum(int newSize){
return entityManager.resize(newSize, systemManager, componentManager);
}
/***************************************
** Manage Entities **
***************************************/
/**
* Create a new Entity (dequeues the first element from unusedEntities)
* @return the next unused Entity, now set to used
* @throws NoSuchElementException if there are no more entities available
*/
public Entity createEntity() throws NoSuchElementException{
try{
Entity newEntity = entityManager.addEntity();
return newEntity;
}
catch (NoSuchElementException e){
// Catch the exception to log the error message
writeErr("No more available entities");
// Throw again to pass up to the original calling function
throw e;
}
}
/**
* Signals each manager to remove the specified entity
* @param entity the entity to destroy
*/
public void destroyEntity(Entity entity){
// Pass the registration list to the componentManager first, to avoid having to iterate through each component type
componentManager.entityDestroyed(entity, entityManager.getRegistrations(entity));
entityManager.removeEntity(entity);
systemManager.entityDestroyed(entity);
}
/**
* Gets the set maximum number of entities
* @return an integer of the maximum number of entities
*/
public Integer getMaxEntities(){
return entityManager.getMaxSize();
}
/**
* Gets the current number of entities
* @return the number of currently active entities
*/
public int 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 **
***************************************/
/**
* Registers the specified name in the component manager
* @param type the name to register. Should be the component class name or a suitable name for primitive types
* @return true if the component type was registered successfully
*/
public boolean registerComponent(Type type){
return componentManager.registerComponent(type);
}
/**
* Adds an exisiting component to an exisiting entity
* @param entity the entity to add the component to
* @param componentType the class name of the component to add
* @param component the actual component data
* @return true if the compnent was added to the entity
*/
public boolean addComponent(Entity entity, Type componentType, Object component){
// Get the old registrations of the entity so that we can update only the relevant systems
int componentIndex = componentManager.getComponentIndex(componentType);
if (entityManager.registerComponent(componentIndex, entity))
{
systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity));
componentManager.addComponentToEntity(componentType, component, entity);
return true;
}
else {
writeErr("(" + componentType.getTypeName() + ")");
return false;
}
}
/**
* Removes the component from the specified entity
* @param entity the entity to remove the component from
* @param componentType the class type of the component
* @return true if the component was removed
*/
public boolean removeComponent(Entity entity, Type componentType){
if (entityManager.unregisterComponent(componentManager.getComponentIndex(componentType), entity))
{
componentManager.removeComponentFromEntity(componentType, entity);
systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity));
return true;
}
else return false;
}
/**
* Gets the actual data of the component associated to the entity.
* Requires casting from Object to the known data type
* @param entity the entity to retrieve the data for
* @param componentType the class type of the component
* @return the component data Object associated with the entity
*/
public Object getComponentData(Entity entity, Type componentType){
return componentManager.getComponent(componentType, entity);
}
/**
* Gets the component index of the provided type
* @param type the type to get the index of
* @return the index of the component
*/
public Integer getComponentIndex(Type type){
return componentManager.getComponentIndex(type);
}
/****************************************
** Manage Systems **
****************************************/
/**
* Registers the system to the SystemManager
* @param systemType the type of the system
* @param instance the instance of the system
* @return true if successful
*/
public boolean registerSystem(Type systemType, ECSSystem instance){
return systemManager.registerSystem(systemType, instance);
}
/**
* Sets the specified system's signature to the provided signature
* @param system the class name of the system to set the signature of
* @param signature the new signature data
*/
public void setSystemSignature(Type system, BitSet signature){
systemManager.setSystemRegistraions(system, signature);
}
/****************************************
** Helper Functions & Accessors **
****************************************/
/**
* Writes an error message to the set PrintStream
* <p>
* <i>Default is System.err
* @param message the message to write
*/
static void writeErr(String message){
errorStream.println(message);
}
/**
* Writes an object to the set PrintStream
* <p>
* <i>Default System.err
* @param obj the object to write
*/
static void writeErr(Object obj){
errorStream.println(obj);
}
/**
* Sets the error PrintStream to the provided PrintStream
* @param newErr the PrintStream to set as the write destination
*/
public static void setErr(PrintStream newErr){
errorStream = newErr;
}
/**
* Gets the current error PrintStream
* @return the current PrintStream that errors are written to
*/
public static PrintStream getErr(){
return errorStream;
}
/**
* Gets a the current component manager
* @return the current active component manager
*/
public ComponentManager getComponentManager() {
return componentManager;
}
/**
* Gets a the current enitity manager
* @return the current active enitity manager
*/
public EntityManager getEntityManager() {
return entityManager;
}
/**
* Gets a the current systems manager
* @return the current active systems manager
*/
public SystemManager getSystemManager() {
return systemManager;
}
}

View File

@ -0,0 +1,104 @@
package nz.ac.massey.javaecs;
import java.lang.reflect.Type;
/**
* Entity class.
* Whilst an entity is just an Integer, and using Integer values
* 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
* like names, grouping, specific ordering etc
*/
public class Entity {
protected static Engine engineRef;
private int value;
public Entity(int value){
this.value = value;
}
public int getValue() {
return value;
}
/**
* Returns the int value wrapped as an entity.
* Used to provide a distinction between creating a new entity, and
* using an int value we assume is a valid entity.
* <p>
* Functionally, this is no different to creating a new entity.
*
* @param value the integer value to read as an Entity
* @return an Entity object representing the ID value
*/
public static Entity asEntity(int value){
return new Entity(value);
}
@Override
public boolean equals(Object obj)
{
if (obj instanceof Entity){
return (this.getValue() == ((Entity)obj).getValue());
}
else if (obj instanceof Integer){
return (this.value == (Integer)obj);
}
else return false;
}
@Override
public int hashCode(){
return value; // each entity is an id; the value is implicitly unique, so use it as the hash code
}
/**
* Adds the provided component to this entity.
* <p>
* <b>This function calls <i>Engine.addComponent(Entity, Type, Object)</i></b>, so
* that should be used instead.
* @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design
* @param componentType the class type of the component to add
* @param componentData the component data
* @return true if successful
*/
@Deprecated
public boolean addComponent(Type componentType, Object componentData){
return engineRef.addComponent(this, componentType, componentData);
}
/**
* Removes the provided component from this entity.
* <p>
* <b>This function calls <i>Engine.removeComponent(Entity, Type)</i></b>, so
* that should be used instead.
* @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design
* @param componentType the class type of the component to remove
* @return true if successful
*/
@Deprecated
public boolean removeComponent(Type componentType){
return engineRef.removeComponent(this, componentType);
}
/**
* Gets the component data associated to this entity.
* <p>
* <b>This function calls <i>Engine.getComponentData()</i></b>, so
* that should be used instead.
* @deprecated This function is not ECS-like.
* It is provided as an auxilliary method, that will be more
* intuitive to those familiar with OO design
* @param componentType the class type of the component to fetch data from
* @return the data Object, which requires casting
*/
@Deprecated
public Object getComponent(Type componentType){
return engineRef.getComponentData(this, componentType);
}
}

View File

@ -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
@ -12,124 +9,229 @@ package nz.ac.massey.javaecs;
*/
import java.util.BitSet;
import java.util.LinkedList;
import java.util.Deque;
import java.util.List;
import java.util.Queue;
import java.util.NoSuchElementException;
import java.util.ArrayDeque;
import java.util.ArrayList;
// Define the manager classes internally - should be moved to seperate source files as appropriate
/**
* Manages data from the perspective of the entity.
* <p>
* 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{
Queue<Integer> unusedEntities;
List<BitSet> entityRegistrations;
int currentSize;
public class EntityManager{
// 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.
// 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)
public Deque<Entity> unusedEntities;
public List<BitSet> entityRegistrations;
public int maxSize = 1024;
/**
* Initialise the EntityManager with the default max size of 1024
*/
public EntityManager(){
currentSize = 1024;
unusedEntities = new LinkedList<>();
entityRegistrations = new ArrayList<>();
unusedEntities = new ArrayDeque<>(maxSize);
entityRegistrations = new ArrayList<>(maxSize); // Init to maxSize to increase performance
for (int i = 0; i < 1024; i++) {
unusedEntities.add(i);
entityRegistrations.add(new BitSet());
for (int i = 0; i < maxSize; i++) {
unusedEntities.add(new Entity(i));
entityRegistrations.add(null); // Annul the entries, to set list size, but keep the unused
// entities in an invalid state
}
}
/***
* Initialise the EntityManager with the provided maximum size
* @param maxEntities the maximum number of entities to allow
*/
public EntityManager(int maxEntities){
currentSize = maxEntities;
unusedEntities = new LinkedList<>();
entityRegistrations = new ArrayList<>();
maxSize = maxEntities;
unusedEntities = new ArrayDeque<>(maxSize);
entityRegistrations = new ArrayList<>(maxEntities);
for (int i = 0; i < maxEntities; i++) {
unusedEntities.add(i);
entityRegistrations.add(new BitSet());
unusedEntities.add(new Entity(i));
entityRegistrations.add(null);
}
}
/**
* Creates a new entity
* @return the index of the new entity
* @throws NoSuchElementException an exception if there are no more unused entities
*/
public Entity addEntity() throws NoSuchElementException{
Entity result = unusedEntities.remove();
entityRegistrations.set(result.getValue(), new BitSet());
return result;
}
public Integer addEntity(){
if (unusedEntities.size() == 0){
System.err.println("No available space to create a new entity");
return -1;
/**
* Gets the current maximum size
* @return the value of currentSize
*/
public Integer getMaxSize(){
return maxSize;
}
/**
* Gets the BitSet containing the registrations of the entity.
* @param entity the entity whose BitSet to retrieve
* @return the BitSet of the provided entity, or a new, empty BitSet if the result was null or out of bounds
*/
public BitSet getRegistrations(Entity entity){
try{
BitSet registrations = entityRegistrations.get(entity.getValue());
if (registrations != null){
return registrations;
}
else{
Engine.writeErr("Registrations not initialised for entity: " + entity.getValue() + "; The entity does not exist");
return new BitSet();
}
}
return unusedEntities.remove();
}
public void removeEntity(int entity){
unusedEntities.add(entity);
entityRegistrations.get(entity).clear();
}
public void registerComponent(int component, int entity){
if (entity >= currentSize){
System.err.println("Attempted to assign a component to non-existent entity: " + entity);
return;
catch (IndexOutOfBoundsException e){
Engine.writeErr("Index out of bounds error getting registrations for " + entity.getValue() + "; The entity might not exist");
return new BitSet(); // Using a blank BitSet will retain data safety (that is, no data will be modified)
}
entityRegistrations.get(entity).set(component);
}
public void unregisterComponent(int component, int entity){
entityRegistrations.get(entity).clear(component);
}
public BitSet getRegistrations(int entity){
return entityRegistrations.get(entity);
}
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()){
System.err.println("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count.");
/**
* 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
*/
public boolean registerComponent(int component, Entity entity){
if (entity.getValue() >= maxSize){
Engine.writeErr("Attempted to assign a component to non-existent entity: " + entity.getValue());
return false;
}
else if (newSize == currentSize){
System.err.println("Attempted to set the newSize to the current size");
else if (entityRegistrations.get(entity.getValue()).get(component))
{
Engine.writeErr("Entity: " + entity.getValue() + " is already assigned to component " + component);
return false;
}
else{
entityRegistrations.get(entity.getValue()).set(component);
return true;
}
}
/**
* 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,
* or otherwise ensure the component data and systems are updated!
* @param entity the entity to remove
*/
public void removeEntity(Entity entity){
unusedEntities.add(entity);
entityRegistrations.set(entity.getValue(), null);
}
/**
* 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 registrations the preset registrations
*/
public void setRegistrations(Entity entity, BitSet registrations){
entityRegistrations.set(entity.getValue(), 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,
* or otherwise ensure the component data and systems are updated!
* @param component the component index to remove
* @param entity the entity to remove
* @return true if successful
*/
public boolean unregisterComponent(int component, Entity entity){
try{
entityRegistrations.get(entity.getValue()).clear(component);
return true;
}
catch (NullPointerException e){
Engine.writeErr("Entity not initialised");
return false;
}
catch (IndexOutOfBoundsException e)
{
return false;
}
}
/**
* 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
*/
public boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){
if (newSize < maxSize - unusedEntities.size()){
Engine.writeErr("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count.");
return false;
}
else if (newSize == maxSize){
Engine.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){
List<Integer> outOfBounds = new ArrayList<>();
for (int i = newSize; i < currentSize; i++) {
if (!unusedEntities.remove(i)){
// Every element above the index must be reordered
if (newSize < maxSize){
// Loop through every entity value above our desired index
// and attempt to remove it from the unused entity queue
// If that fails, the entity is in use, and must have its
// data copied to a lower index
Deque<Entity> outOfBounds = new ArrayDeque<>(maxSize - newSize);
for (int i = newSize; i < maxSize; i++) {
Entity entityI = Entity.asEntity(i);
if (!unusedEntities.remove(entityI)){
// Could not remove element, as it didn't exist (already assigned).
// must find it and reassign it a new in-bounds value
outOfBounds.add(i);
outOfBounds.add(entityI);
}
}
for (Integer integer : outOfBounds) {
int newPos = addEntity();
setRegistrations(newPos, getRegistrations(integer));
// Process all out-of-bounds entities, and destroy them as we go
while(outOfBounds.size() > 0){
Entity old = outOfBounds.remove();
Entity newPos = addEntity();
setRegistrations(newPos, getRegistrations(old));
// Invoke an update in the SystemManager
systemManager.entityDestroyed(integer);
systemManager.entitySignatureChanged(newPos, getRegistrations(newPos));
systemManager.entityDestroyed(old);
systemManager.entityRegistrationsChanged(newPos, getRegistrations(newPos));
// Invoke the change in the components
componentManager.moveComponentData(integer, newPos);
componentManager.entityDestroyed(integer);
componentManager.moveAllComponentData(old, newPos, getRegistrations(old));
}
for (int i = newSize; i < currentSize; i++) {
// Remove out-of-bounds data
for (int i = newSize; i < maxSize; i++) {
// Remove out-of-bounds registration data
entityRegistrations.remove(newSize);
}
}
else{
// Init unassigned values
for (int i = currentSize; i < newSize; i++) {
unusedEntities.add(i);
for (int i = maxSize; i < newSize; i++) {
unusedEntities.add(new Entity(i));
entityRegistrations.add(new BitSet());
}
}
// Finally, set the current size
currentSize = newSize;
maxSize = newSize;
return true;
}
}
public int getNumEntities(){
return maxSize - unusedEntities.size();
}
}

View File

@ -14,52 +14,73 @@ import java.util.BitSet;
import java.util.Map;
import java.util.HashMap;
class SystemManager{
Map<Type, BitSet> signatures = new HashMap<>();
Map<Type, ECSSystem> systems = new HashMap<>();
/**
* Manages system-focused aspects, such as ensuring a system has the correct list of current entities.
* Manages registration of new systems
* <p>
* Do not update data in this class unless you have reviewed the JavaECS source.
*/
public class SystemManager{
public 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
// 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)){
System.err.println("System already registered");
return false;
}
systems.put(systemType, system);
signatures.put(systemType, new BitSet());
return true;
}
public void setSignature(Type system, BitSet registrations){
signatures.put(system, 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
*/
public void entityDestroyed(Entity entity){
// Unlike components, this isn't simply indexed.
for (Type key : systems.keySet()) {
systems.get(key).entities.remove(entity);
}
}
public void entitySignatureChanged(int entity, BitSet entitySignature){
/**
* 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
*/
public void entityRegistrationsChanged(Entity entity, BitSet entityRegistrations){
for (Type key : systems.keySet()) {
// Check if the signature is null
if (!entitySignature.equals(null)){
BitSet srcCpy = (BitSet)entitySignature.clone();
srcCpy.and(signatures.get(key));
if (srcCpy.equals(signatures.get(key))){ // Bitwise check if the entity is subscribed to this system
systems.get(key).entities.add(entity);
if (!entityRegistrations.equals(null)){
BitSet srcCpy = (BitSet)entityRegistrations.clone();
ECSSystem sys = systems.get(key);
srcCpy.and(sys.registrationSet);
if (srcCpy.equals(sys.registrationSet)){ // Bitwise check if the entity is subscribed to this system
sys.entities.add(entity);
}
else{
systems.get(key).entities.remove(entity);
sys.entities.remove(entity);
}
}
}
}
/**
* 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
*/
public boolean registerSystem(Type systemType, ECSSystem system){
if (systems.containsKey(systemType)){
Engine.writeErr("System \'" + systemType.getTypeName() + "\' already registered");
return false;
}
systems.put(systemType, system);
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
*/
public void setSystemRegistraions(Type systemType, BitSet registrations){
systems.get(systemType).registrationSet = registrations;
}
}

View File

@ -2,13 +2,16 @@ package nz.ac.massey.javaecs;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.BitSet;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.BeforeEach;
@ -17,17 +20,21 @@ import org.junit.jupiter.api.BeforeEach;
*/
class AppTest {
ECS gameEngine;
Engine gameEngine;
TestSystem system;
class TestObject{
TestObject() {}
TestObject(int i){
this.i = i;
}
int i = 1;
}
class TestSystem extends ECSSystem{
public void init(){}
public void update() {
for (Integer entity : entities) {
public void update(double dt) {
for (Entity entity : entities) {
TestObject entityComponent = (TestObject)gameEngine.getComponentData(entity, TestObject.class);
entityComponent.i++;
}
@ -38,7 +45,7 @@ class AppTest {
*/
@Test
void testAssignOne() {
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertEquals(1, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
}
@ -49,7 +56,7 @@ class AppTest {
@Test
void testAssignMax(){
for (int i = 0; i < gameEngine.getMaxEntities(); i++) {
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertEquals(1, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
}
@ -61,12 +68,12 @@ class AppTest {
void testAssignMoreThanMax(){
for (int i = 0; i < gameEngine.getMaxEntities() + 5; i++) {
if (i >= gameEngine.getMaxEntities()){
assertThrows(IndexOutOfBoundsException.class, () -> {
int entity = gameEngine.createEntity();
assertThrows(NoSuchElementException.class, () -> {
gameEngine.createEntity();
});
}
else{
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
}
}
@ -77,7 +84,7 @@ class AppTest {
*/
@Test
void testRemoveOne(){
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
gameEngine.destroyEntity(entity);
assertNull(((TestObject)gameEngine.getComponentData(entity, TestObject.class)));
@ -88,11 +95,11 @@ class AppTest {
*/
@Test
void testRunSystem(){
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
for (int i = 0; i < 5; i++) {
system.update();
system.update(0.1);
}
assertEquals(6, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
@ -104,7 +111,7 @@ class AppTest {
*/
@Test
void testResizeMinimal(){
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
gameEngine.resizeMaximum(10);
@ -118,12 +125,12 @@ class AppTest {
@Test
void testLargeCreateDelete(){
for (int i = 0; i < gameEngine.getMaxEntities(); i++) {
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertEquals(1, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
}
for (int i = 256; i < 512; i++) {
gameEngine.destroyEntity(i);
gameEngine.destroyEntity(Entity.asEntity(i));
}
}
@ -133,12 +140,12 @@ class AppTest {
@Test
void testLargeCreateDeleteResize(){
for (int i = 0; i < gameEngine.getMaxEntities(); i++) {
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertEquals(1, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
}
for (int i = 256; i < 512; i++) {
gameEngine.destroyEntity(i);
gameEngine.destroyEntity(Entity.asEntity(i));
}
gameEngine.resizeMaximum(1024 - 255);
@ -150,24 +157,97 @@ class AppTest {
@Test
void testLargeCreateDeleteResizeTooSmall(){
for (int i = 0; i < gameEngine.getMaxEntities(); i++) {
int entity = gameEngine.createEntity();
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertEquals(1, ((TestObject)gameEngine.getComponentData(entity, TestObject.class)).i);
}
for (int i = 256; i < 512; i++) {
gameEngine.destroyEntity(i);
gameEngine.destroyEntity(Entity.asEntity(i));
}
assertFalse(gameEngine.resizeMaximum(512));
}
@Test
void testEntityManagerConstructor(){
gameEngine = new Engine(5);
assertEquals(5, gameEngine.getMaxEntities());
}
@Test
void testAssignToNonExistentEntity(){
assertFalse(gameEngine.addComponent(Entity.asEntity(1025), TestObject.class, new TestObject()));
}
@Test
void testRemoveComponent(){
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
gameEngine.removeComponent(entity, TestObject.class);
assertFalse(gameEngine.entityManager.getRegistrations(entity).get(gameEngine.getComponentIndex(TestObject.class)));
}
@Test
void testEqualResize(){
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream orig = Engine.getErr();
PrintStream newErr = new PrintStream(bytes);
Engine.setErr(newErr);
assertTrue(gameEngine.resizeMaximum(1024));
Engine.setErr(orig);
newErr.flush(); // ensure the bytes are recieved
byte[] errBytes = bytes.toByteArray();
String result = new String(errBytes);
Engine.writeErr("Captured in redirect: " + result);
assertTrue(result.trim().equals("Attempted to set the newSize to the current size"));
}
@Test
void testLargerResize(){
assertTrue(gameEngine.resizeMaximum(2048));
assertEquals(2048, gameEngine.getMaxEntities());
}
@Test
void testComponentAlreadyRegistered(){
assertFalse(gameEngine.registerComponent(TestObject.class));
}
@Test
void testRemoveNonExistentComponentData(){
assertFalse(gameEngine.removeComponent(Entity.asEntity(1), TestObject.class));
}
@Test
void testAssignComponentAlreadyAssigned(){
Entity entity = gameEngine.createEntity();
gameEngine.addComponent(entity, TestObject.class, new TestObject());
assertFalse(gameEngine.addComponent(entity, TestObject.class, new TestObject()));
}
@Test
void testRegisterExistingSystem(){
assertFalse(gameEngine.registerSystem(TestSystem.class, new TestSystem()));
}
@Test
void testGetRegistrationsOutOfRange(){
Entity entity = Entity.asEntity(-1);
assertEquals(new BitSet(), gameEngine.entityManager.getRegistrations(entity));
}
@Test
void testGetRegistrationsUnassignedEntity(){
Entity entity = Entity.asEntity(25);
assertEquals(new BitSet(), gameEngine.entityManager.getRegistrations(entity));
}
/**
* Establish a simple ECS, with a single system and component
*/
@BeforeEach
void testSetup(){
gameEngine = new ECS();
gameEngine = new Engine();
gameEngine.registerComponent(TestObject.class);
system = new TestSystem();