Fully implemented Austin Morgan's ECS in Java

(minus generic typing)
This commit is contained in:
Brychan Dempsey 2021-04-15 16:17:04 +12:00
parent 44412dae46
commit 48a0fe2e1e
6 changed files with 249 additions and 119 deletions

View File

@ -0,0 +1,59 @@
import java.util.*;
class ComponentArray{
List<Object> componentArray = new ArrayList<>();
Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
public void entityDestroyed(int entity){
Optional<Integer> pos = entityComponentDataMap.get(entity);
if (pos.isEmpty()){
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){
Optional<Integer> pos = entityComponentDataMap.get(entity);
if (!pos.isEmpty()){
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);
}
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

@ -0,0 +1,40 @@
import java.util.BitSet;
import java.util.Map;
class ComponentManager{
Map<String, ComponentArray> componentArrays = new HashMap<>();
Map<String, Integer> componentPosIndex = new HashMap<>();
int componentPos = 0;
public void registerComponent(String name){
if (componentArrays.containsKey(name)){
System.err.println("Component " + name + " is already registered");
return;
}
componentArrays.put(name, new ComponentArray());
componentPosIndex.put(name, componentPos++);
}
public void addComponentToEntity(String componentName, Object componentData, int entity){
componentArrays.get(componentName).insertData(entity, componentData);
}
public void removeComponentFromEntity(String 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(String componentType, int entity){
return componentArrays.get(componentType).getData(entity);
}
public void entityDestroyed(int entity){
for (String key : componentArrays.keySet()) {
componentArrays.get(key).entityDestroyed(entity);
}
}
public Integer getComponentIndex(String name){
return componentPosIndex.get(name);
}
}

View File

@ -12,135 +12,48 @@
import java.util.*; import java.util.*;
public class ECS { public class ECS {
// As mentioned by Austin Morlan, a queue is a better choice of data structure for the entity list EntityManager entityManager;
// - it allows the first element to be efficiently popped from the list ComponentManager componentManager;
Queue<Integer> unusedEntities = new ArrayList<>(); SystemManager systemManager;
// As the entity subscribes (or does not) to a component, we can use a boolean state to represent the subscription public ECS(){
// If we instead store the list of indicies the component is subscribed to, we would use significantly more entityManager = new EntityManager();
// memory space due to the components being defined as a 32-bit integer. componentManager = new ComponentManager();
// In a BitSet, each bit is a flag. Therefore we can represent subscription to the first 32 components in the systemManager = new SystemManager();
// same space as one as a 32-bit int.
//
// This list therefore uses the index of the BitSet as its reference to the entity.
List<BitSet> entityRegistrations = new ArrayList<>();
// Each component can be assigned to every possible entity. There is a reasonably unlimited number of possible components;
// so therefore we get an array of arrays containing all of a component's data allocations.
// Access is in [componentType][entityNumber] format; getting the data from a subscribed entity is performed like so:
// (<datatype>)componentDataArrays.get(ComponentIndex).get(Entity);
// Unfortunately, given Java's constraints on dynamic typing (run-time type determination), the returned data must be
// explicitly cast. Therefore, all components must implement the IComponent interface - which mandates the getType()
// function
// As discussed by Austin Morlan, this array must be packed, otherwise we waste time iterating over non-subscribed entities
// This also means we cannot use the location implicitly - we must keep track of exactly which data instance associates with
// which entity.
// Therefore, we need a custom data structure that keeps reversable references to component instances and entities:
interface IComponentArray{
void entityDestroyed(int entity);
}
class ComponentArray<E> implements IComponentArray{
List<E> componentArray = new ArrayList<>();
Map<Integer, Integer> entityComponentDataMap = new HashMap<>();
Map<Integer, Integer> componentDataEntityMap = new HashMap<>();
public void entityDestroyed(int entity){
Optional<Integer> pos = entityComponentDataMap.get(entity);
if (pos.isEmpty()){
removeData(entity);
}
} }
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);
}
void insertData(int entity, E component){
Optional<Integer> pos = entityComponentDataMap.get(entity);
if (!pos.isEmpty()){
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);
}
E getData(int entity){
if (!entityComponentDataMap.containsKey(entity)){
System.err.println("Attempted to retrieve non-existent data");
return null;
}
return componentArray.get(entityComponentDataMap.get(entity));
}
}
// The actual list of data arrays
List<IComponentArray> componentDataArrays = new ArrayList<>();
// Components are either primitive types or class/struct definitions, with an additional list associating
// the entities the component is assigned to
List<Object> components = new ArrayList<>();
// Systems are user-defined functions that are called by the game engine, usually at a regular interval such
// as between every render frame. It must be able to find all instances of a specific component, in order
// to update/read its data. A component
List<Object> systems = new ArrayList<>();
int entityIndex = 0;
int componentIndex = 0;
int systemIndex = 0;
/**
* Takes the next element off the list of unassigned entities
* @return the value of the new entity
*/
Integer createEntity(){ Integer createEntity(){
int newEntity = entities.remove(0); return entityManager.addEntity();
if (componentAssociations.size() <= newEntity){
for (int i = componentAssociations.size(); i < newEntity+1 - componentAssociations.size(); i++) {
componentAssociations.add(i, new ArrayList<>());
}
}
componentAssociations.add(newEntity, element);
return newEntity;
} }
/**
* Adds the entity back to the list of unassigned entities,
* and empty all component associations
* @param entity
*/
void destroyEntity(int entity){ void destroyEntity(int entity){
entities.add(entity); entityManager.removeEntity(entity);
componentAssociations.set(entity, new ArrayList<>()); componentManager.entityDestroyed(entity);
systemManager.entityDestroyed(entity);
} }
void init(int maxEntities){ void registerComponent(String name){
for (int i = 0; i < maxEntities; i++) { componentManager.registerComponent(name);
entities.add(i);
}
} }
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));
}
void removeComponent(int entity, String componentName){
componentManager.removeComponentFromEntity(componentName, entity);
entityManager.unregisterComponent(componentManager.getComponentIndex(componentName), entity);
systemManager.entitySignatureChanged(entity, entityManager.getRegistrations(entity));
}
Object getComponentData(int entity, String componentName){
return componentManager.getComponentData(entity, componentName);
}
void setSystemSignature(String system, BitSet signature){
systemManager.setSignature(system, signature);
}
} }

View File

@ -0,0 +1,5 @@
import java.util.Set;
class ECSSystem{
Set<Integer> entities = new HashSet<>();
}

View File

@ -0,0 +1,62 @@
import java.util.BitSet;
import java.util.List;
import java.util.Queue;
import javax.swing.text.html.parser.Entity;
// 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.
*/
class EntityManager{
Queue<Integer> unusedEntities;
List<BitSet> entityRegistrations;
public EntityManager(){
unusedEntities = new ArrayList<>();
entityRegistrations = new ArrayList<>();
for (int i = 0; i < 1024; i++) {
unusedEntities.add(i);
entityRegistrations.add(new BitSet());
}
}
public EntityManager(int maxEntities){
unusedEntities = new ArrayList<>();
entityRegistrations = new ArrayList<>();
for (int i = 0; i < maxEntities; i++) {
unusedEntities.add(i);
entityRegistrations.add(new BitSet());
}
}
public Integer addEntity(){
if (unusedEntities.size() == 0){
System.err.println("No available space to create a new entity");
return -1;
}
return unusedEntities.remove();
}
public void removeEntity(int entity){
unusedEntities.add(entity);
entityRegistrations.set(entity, new BitSet());
}
public void registerComponent(int component, int entity){
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);
}
}

View File

@ -0,0 +1,51 @@
import java.util.BitSet;
import java.util.Map;
class SystemManager{
Map<String, BitSet> signatures = new HashMap<>();
Map<String, ECSSystem> 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.
// 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(String system, ECSSystem action){
if (systems.containsKey(system)){
System.err.println("System already registered");
return false;
}
systems.put(system, action);
return true;
}
public void setSignature(String system, BitSet registrations){
signatures.put(system, registrations);
}
public void entityDestroyed(int entity){
for (String key : systems.keySet()) {
systems.get(key).entities.remove(entity);
}
}
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);
}
}
}
}