Fully implemented Austin Morgan's ECS in Java
(minus generic typing)
This commit is contained in:
parent
44412dae46
commit
48a0fe2e1e
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class ECSSystem{
|
||||||
|
Set<Integer> entities = new HashSet<>();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user