Author SHA1 Message Date
BrychanD 6e000492b0 .. 2021-06-08 17:08:57 +12:00
BrychanD 23abf57b6e Tests added, some function changes
Functions changed to return booleans
2021-06-08 17:08:47 +12:00
BrychanD 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
BrychanD 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
BrychanD 4354eac0cf Fixed referencing to package name 2021-06-07 21:25:01 +12:00
16 changed files with 216 additions and 53 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
"type": "java", "type": "java",
"name": "Launch App", "name": "Launch App",
"request": "launch", "request": "launch",
"mainClass": "nz.ac.massey.programming_project_159333_s1_2021.App", "mainClass": "nz.ac.massey.javaecs.App",
"projectName": "javaecs" "projectName": "javaecs"
} }
] ]
@@ -1,9 +1,15 @@
package nz.ac.massey.javaecs; package nz.ac.massey.javaecs;
public class FrameRateSystem extends ECSSystem{ public class FrameRateSystem extends ECSSystem{
@Override
void init() {} void init() {}
@Override
void update() {}
void update(double dt, double idleTime){ void update(double dt, double idleTime){
System.out.print(String.format("dt: %.3g (%.3g idle) ", dt, idleTime)); System.out.print(String.format("dt: %.3g (%.3g idle) ", dt, idleTime));
} }
} }
@@ -6,12 +6,18 @@ public class LogVec2DSystem extends ECSSystem{
this.gameEngine = gameEngine; this.gameEngine = gameEngine;
} }
@Override
void init() {} void init() {}
@Override
void update() {}
void update(double dt){ void update(double dt){
for (Integer entity : entities) { for (Integer entity : entities) {
Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class); Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class);
System.out.println(String.format("X: %.6g, Y: %.6g", pos.x, pos.y)); System.out.println(String.format("X: %.6g, Y: %.6g", pos.x, pos.y));
} }
} }
} }
@@ -6,9 +6,13 @@ public class PhysicsSystem extends ECSSystem{
this.gameEngine = gameEngine; this.gameEngine = gameEngine;
} }
@Override
void init() {} void init() {}
void update(double dt){ @Override
void update() {}
void update(Double dt){
for (Integer entity : entities) { for (Integer entity : entities) {
Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class); Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class);
RidgidBody ridgidBody = (RidgidBody)gameEngine.getComponentData(entity, RidgidBody.class); RidgidBody ridgidBody = (RidgidBody)gameEngine.getComponentData(entity, RidgidBody.class);
+8 -8
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"> <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> <modelVersion>4.0.0</modelVersion>
<groupId>nz.ac.massey.programming_project_159333_s1_2021</groupId> <groupId>nz.ac.massey.javaecs</groupId>
<artifactId>javaecs</artifactId> <artifactId>javaecs</artifactId>
<version>1.1-SNAPSHOT</version> <version>1.2-PRERELEASE</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>
@@ -17,10 +17,10 @@
<maven-javadoc-plugin.version>3.0.0</maven-javadoc-plugin.version> <maven-javadoc-plugin.version>3.0.0</maven-javadoc-plugin.version>
<coveralls-maven-plugin.version>4.3.0</coveralls-maven-plugin.version> <coveralls-maven-plugin.version>4.3.0</coveralls-maven-plugin.version>
<!-- JaCoCo thresholds. Increase gradually as you add tests. --> <!-- 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.instruction-ratio>75%</jacoco.unit-tests.limit.instruction-ratio>
<jacoco.unit-tests.limit.branch-ratio>0%</jacoco.unit-tests.limit.branch-ratio> <jacoco.unit-tests.limit.branch-ratio>50%</jacoco.unit-tests.limit.branch-ratio>
<jacoco.unit-tests.limit.class-complexity>20</jacoco.unit-tests.limit.class-complexity> <jacoco.unit-tests.limit.class-complexity>30</jacoco.unit-tests.limit.class-complexity>
<jacoco.unit-tests.limit.method-complexity>5</jacoco.unit-tests.limit.method-complexity> <jacoco.unit-tests.limit.method-complexity>10</jacoco.unit-tests.limit.method-complexity>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -134,7 +134,7 @@
<goal>report</goal> <goal>report</goal>
</goals> </goals>
</execution> </execution>
<!-- <execution> <execution>
<id>check-unit-test</id> <id>check-unit-test</id>
<phase>test</phase> <phase>test</phase>
<goals> <goals>
@@ -180,7 +180,7 @@
</rule> </rule>
</rules> </rules>
</configuration> </configuration>
</execution> --> </execution>
</executions> </executions>
</plugin> </plugin>
</plugins> </plugins>
@@ -34,7 +34,7 @@ class ComponentArray{
public void removeData(int entity){ public void removeData(int entity){
if (!entityComponentDataMap.containsKey(entity)){ if (!entityComponentDataMap.containsKey(entity)){
System.err.println("Attempted to remove non-existent entity"); ECS.writeErr("Attempted to remove non-existent entity");
return; return;
} }
// Get the componentData index of the entity // Get the componentData index of the entity
@@ -55,7 +55,7 @@ class ComponentArray{
public void insertData(int entity, Object component){ public void insertData(int entity, Object component){
if (entityComponentDataMap.containsKey(entity)){ if (entityComponentDataMap.containsKey(entity)){
System.err.println("Entity is already subscribed to the component"); ECS.writeErr("Entity is already subscribed to the component");
return; return;
} }
int index = componentArray.size(); int index = componentArray.size();
@@ -78,7 +78,7 @@ class ComponentArray{
public Object getData(int entity){ public Object getData(int entity){
if (!entityComponentDataMap.containsKey(entity)){ if (!entityComponentDataMap.containsKey(entity)){
System.err.println("Attempted to retrieve non-existent data"); ECS.writeErr("Attempted to retrieve non-existent data");
return null; return null;
} }
@@ -17,13 +17,14 @@ class ComponentManager{
Map<Type, Integer> componentPosIndex = new HashMap<>(); Map<Type, Integer> componentPosIndex = new HashMap<>();
int componentPos = 0; int componentPos = 0;
public void registerComponent(Type type){ public boolean registerComponent(Type type){
if (componentArrays.containsKey(type)){ if (componentArrays.containsKey(type)){
System.err.println("Component " + type.getTypeName() + " is already registered"); ECS.writeErr("Component " + type.getTypeName() + " is already registered");
return; return false;
} }
componentArrays.put(type, new ComponentArray()); componentArrays.put(type, new ComponentArray());
componentPosIndex.put(type, componentPos++); componentPosIndex.put(type, componentPos++);
return true;
} }
public void addComponentToEntity(Type componentName, Object componentData, int entity){ public void addComponentToEntity(Type componentName, Object componentData, int entity){
@@ -12,6 +12,7 @@ package nz.ac.massey.javaecs;
* *
*/ */
import java.io.PrintStream;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.BitSet; import java.util.BitSet;
/** /**
@@ -21,6 +22,10 @@ import java.util.BitSet;
* for documentation and more information. * for documentation and more information.
*/ */
public class ECS { public class ECS {
// 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 EntityManager entityManager;
protected ComponentManager componentManager; protected ComponentManager componentManager;
protected SystemManager systemManager; protected SystemManager systemManager;
@@ -36,7 +41,7 @@ public class ECS {
} }
/** /**
* Initialises the ECS with the specified value(s) * Initialises the ECS with the specified value
* @param maxEntities the maximum number of entities to allow * @param maxEntities the maximum number of entities to allow
*/ */
public ECS(int maxEntities){ public ECS(int maxEntities){
@@ -80,8 +85,8 @@ public class ECS {
* Registers the specified name in the component manager * 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 * @param name the name to register. Should be the component class name or a suitable name for primitive types
*/ */
void registerComponent(Type type){ boolean registerComponent(Type type){
componentManager.registerComponent(type); return componentManager.registerComponent(type);
} }
Integer getComponentIndex(Type type){ Integer getComponentIndex(Type type){
@@ -94,10 +99,14 @@ public class ECS {
* @param componentName the class name of the component to add * @param componentName the class name of the component to add
* @param component the actual component data * @param component the actual component data
*/ */
void addComponent(int entity, Type componentName, Object component){ boolean addComponent(int entity, Type componentName, Object component){
if (entityManager.registerComponent(componentManager.getComponentIndex(componentName), entity))
{
systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity));
componentManager.addComponentToEntity(componentName, component, entity); componentManager.addComponentToEntity(componentName, component, entity);
entityManager.registerComponent(componentManager.getComponentIndex(componentName), entity); return true;
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity)); }
else return false;
} }
/** /**
@@ -105,10 +114,14 @@ public class ECS {
* @param entity the entity to remove the component from * @param entity the entity to remove the component from
* @param componentName the class name of the component * @param componentName the class name of the component
*/ */
void removeComponent(int entity, Type componentType){ boolean removeComponent(int entity, Type componentType){
if (entityManager.unregisterComponent(componentManager.getComponentIndex(componentType), entity))
{
componentManager.removeComponentFromEntity(componentType, entity); componentManager.removeComponentFromEntity(componentType, entity);
entityManager.unregisterComponent(componentManager.getComponentIndex(componentType), entity); systemManager.entityRegistrationsChanged(entity, entityManager.getRegistrations(entity));
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity)); return true;
}
else return false;
} }
/** /**
@@ -127,8 +140,8 @@ public class ECS {
* @param systemName * @param systemName
* @param action * @param action
*/ */
void registerSystem(Type systemType, ECSSystem action){ boolean registerSystem(Type systemType, ECSSystem action){
systemManager.registerSystem(systemType, action); return systemManager.registerSystem(systemType, action);
} }
/** /**
@@ -137,9 +150,47 @@ public class ECS {
* @param signature the new signature data * @param signature the new signature data
*/ */
void setSystemSignature(Type system, BitSet signature){ void setSystemSignature(Type system, BitSet signature){
systemManager.setSignature(system, signature); systemManager.setRegistrationSignature(system, signature);
} }
Integer getMaxEntities(){ Integer getMaxEntities(){
return entityManager.currentSize; return entityManager.currentSize;
} }
// Encapsulate syserr writes so they may be redirected out of the lib
/**
* Writes an error message to the set PrintStream
* <p>
* <i>Default 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
*/
static void setErr(PrintStream newErr){
errorStream = newErr;
}
/**
* Gets the current error PrintStream
* @return the current PrintStream that errors are written to
*/
static PrintStream getErr(){
return errorStream;
}
} }
@@ -15,6 +15,10 @@ package nz.ac.massey.javaecs;
import java.util.Set; import java.util.Set;
import java.util.HashSet; import java.util.HashSet;
class ECSSystem{ abstract class ECSSystem{
Set<Integer> entities = new HashSet<>(); Set<Integer> entities = new HashSet<>();
// Abstractions are included to remind the library user these two implementations (and/or variations) should be implemented
abstract void init();
abstract void update();
} }
@@ -53,7 +53,7 @@ class EntityManager{
public Integer addEntity(){ public Integer addEntity(){
if (unusedEntities.size() == 0){ if (unusedEntities.size() == 0){
System.err.println("No available space to create a new entity"); ECS.writeErr("No available space to create a new entity");
return -1; return -1;
} }
return unusedEntities.remove(); return unusedEntities.remove();
@@ -64,16 +64,36 @@ class EntityManager{
entityRegistrations.get(entity).clear(); entityRegistrations.get(entity).clear();
} }
public void registerComponent(int component, int entity){ public boolean registerComponent(int component, int entity){
if (entity >= currentSize){ if (entity >= currentSize){
System.err.println("Attempted to assign a component to non-existent entity: " + entity); ECS.writeErr("Attempted to assign a component to non-existent entity: " + entity);
return; return false;
} }
else if (entityRegistrations.get(entity).get(component))
{
ECS.writeErr("Entity is already assigned to the component");
return false;
}
else{
entityRegistrations.get(entity).set(component); entityRegistrations.get(entity).set(component);
return true;
}
} }
public void unregisterComponent(int component, int entity){ public boolean unregisterComponent(int component, int entity){
try{
if (entityRegistrations.get(entity).get(component))
{
entityRegistrations.get(entity).clear(component); entityRegistrations.get(entity).clear(component);
return true;
}
else return false;
}
catch (IndexOutOfBoundsException e)
{
return false;
}
} }
public BitSet getRegistrations(int entity){ public BitSet getRegistrations(int entity){
@@ -86,11 +106,11 @@ class EntityManager{
public boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){ public boolean resize(int newSize, SystemManager systemManager, ComponentManager componentManager){
if (newSize < currentSize - unusedEntities.size()){ 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."); ECS.writeErr("Attempted to resize the maximum entity count to a number smaller than the current assigned entity count.");
return false; return false;
} }
else if (newSize == currentSize){ else if (newSize == currentSize){
System.err.println("Attempted to set the newSize to the current size"); ECS.writeErr("Attempted to set the newSize to the current size");
return true; return true;
} }
else{ else{
@@ -110,7 +130,7 @@ class EntityManager{
setRegistrations(newPos, getRegistrations(integer)); setRegistrations(newPos, getRegistrations(integer));
// Invoke an update in the SystemManager // Invoke an update in the SystemManager
systemManager.entityDestroyed(integer); systemManager.entityDestroyed(integer);
systemManager.entitySignatureChanged(newPos, getRegistrations(newPos)); systemManager.entityRegistrationsChanged(newPos, getRegistrations(newPos));
// Invoke the change in the components // Invoke the change in the components
componentManager.moveComponentData(integer, newPos); componentManager.moveComponentData(integer, newPos);
componentManager.entityDestroyed(integer); componentManager.entityDestroyed(integer);
@@ -15,7 +15,7 @@ import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
class SystemManager{ class SystemManager{
Map<Type, BitSet> signatures = new HashMap<>(); Map<Type, BitSet> registrationSignatures = new HashMap<>();
Map<Type, ECSSystem> systems = new HashMap<>(); Map<Type, ECSSystem> systems = new HashMap<>();
// Registering the system adds it to the array of systems. // Registering the system adds it to the array of systems.
@@ -29,16 +29,16 @@ class SystemManager{
// table. // table.
public boolean registerSystem(Type systemType, ECSSystem system){ public boolean registerSystem(Type systemType, ECSSystem system){
if (systems.containsKey(systemType)){ if (systems.containsKey(systemType)){
System.err.println("System already registered"); ECS.writeErr("System already registered");
return false; return false;
} }
systems.put(systemType, system); systems.put(systemType, system);
signatures.put(systemType, new BitSet()); registrationSignatures.put(systemType, new BitSet());
return true; return true;
} }
public void setSignature(Type system, BitSet registrations){ public void setRegistrationSignature(Type systemType, BitSet registrations){
signatures.put(system, registrations); registrationSignatures.put(systemType, registrations);
} }
public void entityDestroyed(int entity){ public void entityDestroyed(int entity){
@@ -47,13 +47,13 @@ class SystemManager{
} }
} }
public void entitySignatureChanged(int entity, BitSet entitySignature){ public void entityRegistrationsChanged(int entity, BitSet entityRegistrations){
for (Type key : systems.keySet()) { for (Type key : systems.keySet()) {
// Check if the signature is null // Check if the signature is null
if (!entitySignature.equals(null)){ if (!entityRegistrations.equals(null)){
BitSet srcCpy = (BitSet)entitySignature.clone(); BitSet srcCpy = (BitSet)entityRegistrations.clone();
srcCpy.and(signatures.get(key)); srcCpy.and(registrationSignatures.get(key));
if (srcCpy.equals(signatures.get(key))){ // Bitwise check if the entity is subscribed to this system if (srcCpy.equals(registrationSignatures.get(key))){ // Bitwise check if the entity is subscribed to this system
systems.get(key).entities.add(entity); systems.get(key).entities.add(entity);
} }
else{ else{
@@ -2,12 +2,18 @@ package nz.ac.massey.javaecs;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.BitSet; import java.util.BitSet;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -21,6 +27,10 @@ class AppTest {
TestSystem system; TestSystem system;
class TestObject{ class TestObject{
TestObject() {}
TestObject(int i){
this.i = i;
}
int i = 1; int i = 1;
} }
@@ -62,7 +72,7 @@ class AppTest {
for (int i = 0; i < gameEngine.getMaxEntities() + 5; i++) { for (int i = 0; i < gameEngine.getMaxEntities() + 5; i++) {
if (i >= gameEngine.getMaxEntities()){ if (i >= gameEngine.getMaxEntities()){
assertThrows(IndexOutOfBoundsException.class, () -> { assertThrows(IndexOutOfBoundsException.class, () -> {
int entity = gameEngine.createEntity(); gameEngine.createEntity();
}); });
} }
else{ else{
@@ -161,6 +171,67 @@ class AppTest {
} }
@Test
void testEntityManagerConstructor(){
gameEngine = new ECS(5);
assertEquals(5, gameEngine.getMaxEntities());
}
@Test
void testAssignToNonExistentEntity(){
assertFalse(gameEngine.addComponent(1025, TestObject.class, new TestObject()));
}
@Test
void testRemoveComponent(){
int 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 = System.err;
PrintStream newErr = new PrintStream(bytes);
System.setErr(newErr);
assertTrue(gameEngine.resizeMaximum(1024));
System.setErr(orig);
newErr.flush(); // ensure the bytes are recieved
byte[] errBytes = bytes.toByteArray();
String result = new String(errBytes);
System.err.println("Captured in redirect: " + result);
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(1, TestObject.class));
}
@Test
void testAssignComponentAlreadyAssigned(){
int 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()));
}
/** /**
* Establish a simple ECS, with a single system and component * Establish a simple ECS, with a single system and component