From 2192a4e93b3337ea5d5eb4a313535ebf27432b59 Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Tue, 27 Apr 2021 13:50:59 +1200 Subject: [PATCH] Fully implemented the ECS Basic example implementation also included, with a single entity that is under the effect of gravity. Various debug systems included --- .../App.java | 72 +++++++++++++++- .../ComponentArray.java | 27 ++++-- .../ComponentManager.java | 12 ++- .../ECS.java | 85 +++++++++++++++++-- .../ECSSystem.java | 15 ++++ .../EntityManager.java | 21 ++++- .../FrameRateSystem.java | 9 ++ .../Gravity.java | 23 +++++ .../LogVec2DSystem.java | 17 ++++ .../PhysicsSystem.java | 33 +++++++ .../RidgidBody.java | 18 ++++ .../SystemManager.java | 33 ++++--- .../Vec2D.java | 13 +++ 13 files changed, 349 insertions(+), 29 deletions(-) create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/FrameRateSystem.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Gravity.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/LogVec2DSystem.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/PhysicsSystem.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/RidgidBody.java create mode 100644 javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Vec2D.java diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/App.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/App.java index e8a5b96..9fc537e 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/App.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/App.java @@ -1,5 +1,7 @@ package nz.ac.massey.programming_project_159333_s1_2021; +import java.util.BitSet; + /** * Hello world! */ @@ -10,8 +12,76 @@ public final class App { /** * Says hello to the world. * @param args The arguments of the program. + * @throws InterruptedException */ - public static void main(String[] args) { + public static void main(String[] args) throws InterruptedException { + ECS gameEngine = new ECS(); System.out.println("Hello World!"); + gameEngine.registerComponent(Vec2D.class.getName()); + gameEngine.registerComponent(RidgidBody.class.getName()); + gameEngine.registerComponent(Gravity.class.getName()); + + PhysicsSystem physicsSystem = new PhysicsSystem(gameEngine); + gameEngine.registerSystem(PhysicsSystem.class.getName(), physicsSystem); + { + BitSet signature = new BitSet(); + signature.set(gameEngine.getComponentIndex(Gravity.class.getName())); + signature.set(gameEngine.getComponentIndex(Vec2D.class.getName())); + signature.set(gameEngine.getComponentIndex(RidgidBody.class.getName())); + gameEngine.setSystemSignature(PhysicsSystem.class.getName(), signature); + } + + physicsSystem.init(); + + LogVec2DSystem logVec2DSystem = new LogVec2DSystem(gameEngine); + gameEngine.registerSystem(LogVec2DSystem.class.getName(), logVec2DSystem); + { + BitSet signature = new BitSet(); + signature.set(gameEngine.getComponentIndex(Gravity.class.getName())); + signature.set(gameEngine.getComponentIndex(Vec2D.class.getName())); + signature.set(gameEngine.getComponentIndex(RidgidBody.class.getName())); + gameEngine.setSystemSignature(LogVec2DSystem.class.getName(), signature); + } + + logVec2DSystem.init(); + + FrameRateSystem frameRateSystem = new FrameRateSystem(); + gameEngine.registerSystem(FrameRateSystem.class.getName(), frameRateSystem); + + // Create components: + + int entity = gameEngine.createEntity(); + + gameEngine.addComponent(entity, Gravity.class.getName(), new Gravity(0.0, -9.80665, -1.0, 100.0)); + gameEngine.addComponent(entity, Vec2D.class.getName(), new Vec2D(1.5, 0.0, 1)); + gameEngine.addComponent(entity, RidgidBody.class.getName(), new RidgidBody()); + + + + double dt = 0.0; + + double frameRate = 1.0 / 20; // 1 second divided by target number of frames + + boolean quit = false; + long loopStart = System.currentTimeMillis(); + while (!quit){ + // Get the time between frames for accurate time-based calculations (i.e. physics systems etc) + long startTime = System.nanoTime(); + + frameRateSystem.update(dt); + physicsSystem.update(dt); + logVec2DSystem.update(dt); + dt = (System.nanoTime() - startTime) / 1e9; // convert nanoseconds to seconds + + // Limit frame rate (approximately) to minimum of 16 ms + if (dt < frameRate){ + Thread.sleep((int)((frameRate-dt)*1000)); + dt = (System.nanoTime() - startTime) / 1e9; + } + if (System.currentTimeMillis() - loopStart >= 1000){ + loopStart = System.currentTimeMillis(); + System.out.println("Second Elapsed"); + } + } } } diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java index 886a07e..2cc52fc 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentArray.java @@ -1,4 +1,23 @@ -import java.util.*; +package nz.ac.massey.programming_project_159333_s1_2021; +/** + * 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{ @@ -8,8 +27,7 @@ class ComponentArray{ Map componentDataEntityMap = new HashMap<>(); public void entityDestroyed(int entity){ - Optional pos = entityComponentDataMap.get(entity); - if (pos.isEmpty()){ + if (entityComponentDataMap.containsKey(entity)){ removeData(entity); } } @@ -36,8 +54,7 @@ class ComponentArray{ } public void insertData(int entity, Object component){ - Optional pos = entityComponentDataMap.get(entity); - if (!pos.isEmpty()){ + if (entityComponentDataMap.containsKey(entity)){ System.err.println("Entity is already subscribed to the component"); return; } diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java index 071e493..4b69a41 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ComponentManager.java @@ -1,5 +1,15 @@ -import java.util.BitSet; +package nz.ac.massey.programming_project_159333_s1_2021; +/** + * Component Manager + * This class manages component registrations, and adding, + * removing, and retrieving data from the component arrays. + * + * Contributors: + * Brychan Dempsey - brychand@hotmail.com + * + */ import java.util.Map; +import java.util.HashMap; class ComponentManager{ Map componentArrays = new HashMap<>(); diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java index 36554a4..25924e5 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECS.java @@ -1,58 +1,127 @@ +package nz.ac.massey.programming_project_159333_s1_2021; /** - * ECS root file + * 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.util.*; - +import java.util.BitSet; +/** + * The ECS manager. + *

+ * See https://git.software.kauripeak.co.nz/BrychanD/JavaECS + * for documentation and more information. + */ public class ECS { - EntityManager entityManager; - ComponentManager componentManager; - SystemManager systemManager; - + protected EntityManager entityManager; + protected ComponentManager componentManager; + protected SystemManager systemManager; + /** + * Initialises the ECS with default values + *

+ * 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 id of the new entity + */ Integer createEntity(){ return entityManager.addEntity(); } + /** + * 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(String name){ componentManager.registerComponent(name); } + Integer getComponentIndex(String name){ + return componentManager.getComponentIndex(name); + } + + /** + * 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, String 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, String componentName){ componentManager.removeComponentFromEntity(componentName, entity); entityManager.unregisterComponent(componentManager.getComponentIndex(componentName), 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 componentName the class name of the component + * @return the component data Object associated with the entity + */ Object getComponentData(int entity, String componentName){ - return componentManager.getComponentData(entity, componentName); + return componentManager.getComponent(componentName, entity); } + /** + * + * @param systemName + * @param action + */ + void registerSystem(String systemName, ECSSystem action){ + systemManager.registerSystem(systemName, 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(String system, BitSet signature){ systemManager.setSignature(system, signature); } diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java index f3a01c0..93fa7c8 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/ECSSystem.java @@ -1,4 +1,19 @@ +package nz.ac.massey.programming_project_159333_s1_2021; +/** + * ECS System class + * This class stores entity-system registrations. These are + * updated by SystemManager, but need a type to store the + * data under. + * + * All systems should inherit from this + * + * Contributors: + * Brychan Dempsey - brychand@hotmail.com + * + */ + import java.util.Set; +import java.util.HashSet; class ECSSystem{ Set entities = new HashSet<>(); diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java index e4d7255..c74808d 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/EntityManager.java @@ -1,8 +1,21 @@ +package nz.ac.massey.programming_project_159333_s1_2021; +/** + * 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. + * + * Contributors: + * Brychan Dempsey - brychand@hotmail.com + * + */ + import java.util.BitSet; +import java.util.LinkedList; import java.util.List; import java.util.Queue; - -import javax.swing.text.html.parser.Entity; +import java.util.ArrayList; // Define the manager classes internally - should be moved to seperate source files as appropriate /** @@ -15,7 +28,7 @@ class EntityManager{ List entityRegistrations; public EntityManager(){ - unusedEntities = new ArrayList<>(); + unusedEntities = new LinkedList<>(); entityRegistrations = new ArrayList<>(); for (int i = 0; i < 1024; i++) { @@ -25,7 +38,7 @@ class EntityManager{ } public EntityManager(int maxEntities){ - unusedEntities = new ArrayList<>(); + unusedEntities = new LinkedList<>(); entityRegistrations = new ArrayList<>(); for (int i = 0; i < maxEntities; i++) { diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/FrameRateSystem.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/FrameRateSystem.java new file mode 100644 index 0000000..900f5fe --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/FrameRateSystem.java @@ -0,0 +1,9 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class FrameRateSystem extends ECSSystem{ + void init() {} + + void update(double dt){ + System.out.print("dt: " + dt + " "); + } +} diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Gravity.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Gravity.java new file mode 100644 index 0000000..d09503c --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Gravity.java @@ -0,0 +1,23 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class Gravity { + public Gravity(){ + + } + public Gravity(double x, double y){ + this.x = x; + this.y = y; + } + public Gravity(double x, double y, double terminalX, double terminalY){ + this.x = x; + this.y = y; + this.terminalX = terminalX; + this.terminalY = terminalY; + } + double x = 0.0; + double y = -9.80665; + // Gravity won't be considered if the velocity exceeds this (absolute value). Negative values are ignored (not limited) + double terminalX = -1.0; + double terminalY = -1.0; + +} diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/LogVec2DSystem.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/LogVec2DSystem.java new file mode 100644 index 0000000..1c0a47a --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/LogVec2DSystem.java @@ -0,0 +1,17 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class LogVec2DSystem extends ECSSystem{ + ECS gameEngine; + public LogVec2DSystem(ECS gameEngine){ + this.gameEngine = gameEngine; + } + + void init() {} + + void update(double dt){ + for (Integer entity : entities) { + Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class.getName()); + System.out.println("X: " + pos.x + ", Y:" + pos.y); + } + } +} diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/PhysicsSystem.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/PhysicsSystem.java new file mode 100644 index 0000000..a560d63 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/PhysicsSystem.java @@ -0,0 +1,33 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class PhysicsSystem extends ECSSystem{ + ECS gameEngine; + public PhysicsSystem(ECS gameEngine){ + this.gameEngine = gameEngine; + } + + void init() {} + + void update(double dt){ + for (Integer entity : entities) { + Vec2D pos = (Vec2D)gameEngine.getComponentData(entity, Vec2D.class.getName()); + RidgidBody ridgidBody = (RidgidBody)gameEngine.getComponentData(entity, RidgidBody.class.getName()); + Gravity gravity = (Gravity)gameEngine.getComponentData(entity, Gravity.class.getName()); + // Firstly, add the result of the accelerative forces to the ridgidbody + ridgidBody.xdot += ridgidBody.xAcc * dt; + ridgidBody.ydot += ridgidBody.yAcc * dt; + + // Special case of gravity + if (gravity.terminalX < 0 || Math.abs(ridgidBody.xdot) < gravity.terminalX){ + ridgidBody.xdot += gravity.x * dt; + } + if (gravity.terminalY < 0 || Math.abs(ridgidBody.ydot) < gravity.terminalY){ + ridgidBody.ydot += gravity.y * dt; + } + + // Finally, move the vec2d by the new velocity + pos.x += ridgidBody.xdot * dt; + pos.y += ridgidBody.ydot * dt; + } + } +} diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/RidgidBody.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/RidgidBody.java new file mode 100644 index 0000000..10fb1f4 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/RidgidBody.java @@ -0,0 +1,18 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class RidgidBody { + public RidgidBody(){} + public RidgidBody(double xdot, double ydot, double xAcc, double yAcc){ + this.xdot = xdot; + this.ydot = ydot; + this.xAcc = xAcc; + this.yAcc = yAcc; + } + + // Velocities + double xdot = 0.0; + double ydot = 0.0; + // accelerations + double xAcc = 0.0; + double yAcc = 0.0; +} diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java index 5984685..db2603c 100644 --- a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/SystemManager.java @@ -1,15 +1,22 @@ +package nz.ac.massey.programming_project_159333_s1_2021; +/** + * System Manager + * This class manages systems registrations, and keeps a current list + * of all entities the system should operate on + * + * Contributors: + * Brychan Dempsey - brychand@hotmail.com + * + */ + import java.util.BitSet; import java.util.Map; +import java.util.HashMap; class SystemManager{ Map signatures = new HashMap<>(); Map systems = new HashMap<>(); - - public SystemManager(ECS baseECS){ - this.baseECS = baseECS; - systemIndex = 0; - } // 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. @@ -25,6 +32,7 @@ class SystemManager{ return false; } systems.put(system, action); + signatures.put(system, new BitSet()); return true; } @@ -40,11 +48,16 @@ class SystemManager{ public void entitySignatureChanged(int entity, BitSet entitySignature){ for (String key : systems.keySet()) { - if (entitySignature.and(signatures.get(key)) == signatures.get(key)){ // Bitwise check if the entity is subscribed to this system - systems.get(key).entities.add(entity); - } - else{ - systems.get(key).entities.remove(entity); + // 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); + } + else{ + systems.get(key).entities.remove(entity); + } } } } diff --git a/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Vec2D.java b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Vec2D.java new file mode 100644 index 0000000..518f746 --- /dev/null +++ b/javaecs/src/main/java/nz/ac/massey/programming_project_159333_s1_2021/Vec2D.java @@ -0,0 +1,13 @@ +package nz.ac.massey.programming_project_159333_s1_2021; + +public class Vec2D { + public Vec2D(){} + public Vec2D(double x, double y, int z){ + this.x = x; + this.y = y; + this.z = z; + } + double x = 0.0; + double y = 0.0; + int z = 0; +}