You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Those are used to "buffer" the creation, destruction and composition of entities, great if you wanna create a bunch of entities at the start of the frame for example. Or if you have multiple threads working on entities.
As i did for the Job-System Feature Request, i came along my own implementation aswell. It would be great if artemis ODB would provide such "simply" but "essential" tools itself :) Its honestly a pain to take care of that, but i hope that my "idea" of the buffer system is a good point to start.
Currently im using it in my own game, there probably still some little bugs.
I havent found a nice solution for buffering changes to component attributes... but the current implementation should do the trick for the first.
Dont get confused by all the maps, buffered maps contains all buffered elements for entites not existant yet. And the other maps ( without buffered in their name ) contain buffered elements for already existing entities. The system either uses the artemis entity id for applying buffered changes to existing entities or generates a internal id for the buffered creation of entities, that one is also used to modify the buffered entities.
packagecom.parallelorigin.extension.code.ecs.systems.lifecycle.buffer;
importcom.artemis.Archetype;
importcom.artemis.BaseSystem;
importcom.artemis.Component;
importcom.artemis.Entity;
importcom.parallelorigin.extension.base.classes.pattern.typeobject.IDManager;
importcom.parallelorigin.extension.base.classes.structure.Tuple;
importcom.parallelorigin.extension.base.interfaces.structure.ITuple;
importjava.util.Map;
importjava.util.Queue;
importjava.util.concurrent.ConcurrentHashMap;
importjava.util.concurrent.ConcurrentLinkedQueue;
importjava.util.function.Consumer;
/** * A system that is used to buffer {@link Entity} creation, destruction and modification calls. */publicabstractclassBufferSystemextendsBaseSystem {
/** A simply class that holds values for a buffered change of a component **/publicclassBufferedChange {
publicClass<? extendsComponent> target;
publicConsumer<Component> change;
/** * Creates the buffered change. * @param target The target component class * @param change A callback that modifies the passed object */publicBufferedChange(Class<? extendsComponent> target, Consumer<Component> change) {
this.target = target;
this.change = change;
}
}
protectedQueue<Runnable> onProcess = newConcurrentLinkedQueue<>();
// Creation & DestructionprotectedQueue<Integer> createRawQueue = newConcurrentLinkedQueue<>();
protectedQueue<ITuple<Integer, Archetype>> createArcheTypeQueue = newConcurrentLinkedQueue<>();
protectedQueue<Integer> destroyQueue = newConcurrentLinkedQueue<>();
// Structural changesprotectedMap<Integer, Queue<Class<? extendsComponent>>> createToBufferedQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<Component>> addToBufferedQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<Class<? extendsComponent>>> removeFromBufferedQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<Class<? extendsComponent>>> createQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<Component>> addQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<Class<? extendsComponent>>> removeQueue = newConcurrentHashMap<>();
// Value changesprotectedMap<Integer, Queue<BufferedChange>> bufferedChangesQueue = newConcurrentHashMap<>();
protectedMap<Integer, Queue<BufferedChange>> changesQueue = newConcurrentHashMap<>();
// Internal ID to real IDprotectedMap<Integer, Integer> bufferIdToEntity = newConcurrentHashMap<>();
@OverrideprotectedvoidprocessSystem() {
while(!onProcess.isEmpty()) onProcess.poll().run();
// Create entities and put them into the listwhile(!createRawQueue.isEmpty()){
intbufferID = createRawQueue.poll();
varentity = world.create();
bufferIdToEntity.put(bufferID, entity);
}
// Create entities by their archetype and put them into the listwhile(!createArcheTypeQueue.isEmpty()){
vartuple = createArcheTypeQueue.poll();
intbufferID = tuple.getKey();
varentity = world.create(tuple.getValue());
bufferIdToEntity.put(bufferID, entity);
}
// Create buffered components to the newly created buffered entitiesfor(varentry : createToBufferedQueue.entrySet()){
intbufferID = entry.getKey();
intentity = bufferIdToEntity.get(bufferID);
varcreateComponentsQueue = entry.getValue();
while(!createComponentsQueue.isEmpty()){ world.edit(entity).create(createComponentsQueue.poll()); }
}
// Add buffered components to the newly created buffered entitiesfor(varentry : addToBufferedQueue.entrySet()){
intbufferID = entry.getKey();
intentity = bufferIdToEntity.get(bufferID);
varaddComponentsQueue = entry.getValue();
while(!addComponentsQueue.isEmpty()){ world.edit(entity).add(addComponentsQueue.poll()); }
}
// Remove buffered components from the newly created buffered entitiesfor(varentry : removeFromBufferedQueue.entrySet()){
intbufferID = entry.getKey();
intentity = bufferIdToEntity.get(bufferID);
varaddComponentsQueue = entry.getValue();
while(!addComponentsQueue.isEmpty()){ world.edit(entity).remove(addComponentsQueue.poll()); }
}
// Remove buffered components from the newly created buffered entitiesfor(varentry : bufferedChangesQueue.entrySet()){
intbufferID = entry.getKey();
intentity = bufferIdToEntity.get(bufferID);
varchangesQueue = entry.getValue();
while(!changesQueue.isEmpty()){
varbufferedReplace = changesQueue.poll();
varcmp = world.getEntity(entity).getComponent(bufferedReplace.target);
bufferedReplace.change.accept(cmp);
}
}
// Create buffered components to the existing entitiesfor(varentry : createQueue.entrySet()){
intentity = entry.getKey();
varcreateComponentsQueue = entry.getValue();
while(!createComponentsQueue.isEmpty()){ world.edit(entity).create(createComponentsQueue.poll()); }
}
// Add buffered components to the existing entitiesfor(varentry : addQueue.entrySet()){
intentity = entry.getKey();
varaddComponentsQueue = entry.getValue();
while(!addComponentsQueue.isEmpty()){ world.edit(entity).add(addComponentsQueue.poll()); }
}
// Remove buffered components from the existing entitiesfor(varentry : removeQueue.entrySet()){
intentity = entry.getKey();
varaddComponentsQueue = entry.getValue();
while(!addComponentsQueue.isEmpty()){ world.edit(entity).remove(addComponentsQueue.poll()); }
}
// Change component value from the existing entitiesfor(varentry : changesQueue.entrySet()){
intentity = entry.getKey();
varchangesQueue = entry.getValue();
while(!changesQueue.isEmpty()){
varbufferedChange = changesQueue.poll();
varcmp = world.getEntity(entity).getComponent(bufferedChange.target);
bufferedChange.change.accept(cmp);
}
}
// Clearing, prevents that we write the same buffered components multiple times into the same entityaddToBufferedQueue.clear();
removeFromBufferedQueue.clear();
bufferedChangesQueue.clear();
addQueue.clear();
removeQueue.clear();
changesQueue.clear();
bufferIdToEntity.clear();
// Destroy entitieswhile(!destroyQueue.isEmpty()) world.delete(destroyQueue.poll());
}
/** * Enlists a {@link Runnable} Callback getting executed while this system processes. * @param execute The runnable callback we wanna execute during this frame. */publicvoidbuffer(Runnableexecute){ onProcess.add(execute); }
/** * Creates a buffer {@link Entity} and returns its internal ID which can be used to modify that buffered {@link Entity} * @return The internal id of the buffered entity. */publicintcreate(){
varid = (int)IDManager.getID(BufferSystem.class);;
createRawQueue.add(id);
bufferIdToEntity.put(id, 0);
returnid;
}
/** * Creates a buffer {@link Entity} by its {@link Archetype} and returns its internal ID which can be used to * modify that buffered {@link Entity} * @return The internal id of the buffered entity. */publicintcreate(Archetypearchetype){
varid = (int)IDManager.getID(BufferSystem.class);;
createArcheTypeQueue.add(newTuple<>(id, archetype));
bufferIdToEntity.put(id, 0);
returnid;
}
/** * Buffers a new component for being created, to a already buffered {@link Entity} or existing {@link Entity}which * gets added once this system was processed. * @param id The unique internal id of the buffered {@link Entity} or the global id of a {@link Entity} * @param component The component to add to the {@link Entity} */publicvoidcreate(intid, Class<? extendsComponent> component){
// Either put the modification into the queue that works on non existent entities or into the queue that// works with existing entities.if(bufferIdToEntity.containsKey(id)){
if(createToBufferedQueue.containsKey(id)) createToBufferedQueue.get(id).add(component);
elsecreateToBufferedQueue.put(id, newConcurrentLinkedQueue<>(){{add(component);}});
}
else {
if(createQueue.containsKey(id)) createQueue.get(id).add(component);
elsecreateQueue.put(id, newConcurrentLinkedQueue<>(){{add(component);}});
}
}
/** * Buffers a component to add or to set, to a already buffered {@link Entity} or existing {@link Entity} which * gets added once this system was processed. * @param id The unique internal id of the buffered {@link Entity} or the global id of a {@link Entity} * @param component The component to add to the {@link Entity} */publicvoidadd(intid, Componentcomponent){
// Either put the modification into the queue that works on non existent entities or into the queue that// works with existing entities.if(bufferIdToEntity.containsKey(id)){
if(addToBufferedQueue.containsKey(id)) addToBufferedQueue.get(id).add(component);
elseaddToBufferedQueue.put(id, newConcurrentLinkedQueue<Component>(){{add(component);}});
}
else {
if(addQueue.containsKey(id)) addQueue.get(id).add(component);
elseaddQueue.put(id, newConcurrentLinkedQueue<Component>(){{add(component);}});
}
}
/** * Enlist a {@link BufferedChange} to modify a {@link Component} in the buffer * @param id The unique internal id of the buffered {@link Entity} or the global id of a {@link Entity} * @param component The component we wanna modify a attribute in * @param changeCallback The callback that is used to change the component and its attributes */public <TextendsComponent> voidchange(intid, Class<T> component, Consumer<T> changeCallback){
if(bufferIdToEntity.containsKey(id)){
if(bufferedChangesQueue.containsKey(id)) bufferedChangesQueue.get(id).add(newBufferedChange(component, (Consumer<Component>) changeCallback));
elsebufferedChangesQueue.put(id, newConcurrentLinkedQueue<>(){{add(newBufferedChange(component, (Consumer<Component>) changeCallback));}});
}
else {
if(changesQueue.containsKey(id)) changesQueue.get(id).add(newBufferedChange(component, (Consumer<Component>) changeCallback));
elsechangesQueue.put(id, newConcurrentLinkedQueue<>(){{add(newBufferedChange(component, (Consumer<Component>) changeCallback));}});
}
}
/** * Buffers a component removal to a already buffered {@link Entity} or existing one which gets removed as soon as * this system was processed. * @param id The unique internal id of the buffered {@link Entity} or the id of the global entity. * @param component The component to remove to the buffered {@link Entity} */publicvoidremove(intid, Class<? extendsComponent> component){
if(bufferIdToEntity.containsKey(id)){
if(removeFromBufferedQueue.containsKey(id)) removeFromBufferedQueue.get(id).add(component);
elseremoveFromBufferedQueue.put(id,newConcurrentLinkedQueue<Class<? extendsComponent>>(){{add(component);}});
}
else {
if(removeQueue.containsKey(id)) removeQueue.get(id).add(component);
elseremoveQueue.put(id,newConcurrentLinkedQueue<Class<? extendsComponent>>(){{ add(component);}});
}
}
/** * Enlists a entity for being destroyed at the end of the frame. * @param entityID The entity-id */publicvoiddelete(intentityID){ destroyQueue.add(entityID); }
}
/** * This class is used to generate unique id's for different class types. <p> * It reuses old ID's which arent used anymore. */publicclassIDManager {
privatestaticConcurrentHashMap<Class<?>, Queue<Long>> freed = newConcurrentHashMap<>();
/** * This method returns a unique long id for each class type. * @param object * @return */publicstaticsynchronizedlonggetID(Class<?> object){
// If theres a free ID... reuse it !if(freed.containsKey(object) && !freed.get(object).isEmpty())
returnfreed.get(object).poll();
// If no free ID's... create a new one !returnUUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
}
/** * Removes a generated ID for a specifc class type. <p> * This id gets reused later on. * @param id * @param object */publicstaticsynchronizedvoidremoveID(longid, Class<?> object){
if(freed.containsKey(object))
freed.get(object).add(id);
elsefreed.put(object, newArrayDeque<Long>(){{add(id);}});
}
}
/** * Represents a tuple... containing two values. */publicclassTuple<T,E> implementsITuple<T, E> {
privateTkey;
privateEvalue;
publicTuple(Tkey, Evalue){
this.key = key;
this.value = value;
}
@OverridepublicTgetKey() { returnkey; }
@OverridepublicEgetValue() { returnvalue; }
@OverridepublicStringtoString() { return"("+key+","+value+")"; }
}
/** * A interface for a class which is able to store a key and a value as a single tuple. * @param <K> The key * @param <V> The Value */publicinterfaceITuple<K, V> {
/** * Returns the key. * @return The key */KgetKey();
/** * Returns the value. * @return The value */VgetValue();
}
Here a little example of how to use this buffer system ;)
// Creating the entity once the buffer system is processingvarplayerEntityID = myBufferSystem.create(myPlayerArcheType);
// Adding stuff to the not existent yet entitymyBufferSystem.add(playerEntityID , newInvincible(10.0f));
// Change some stuffmyBufferSystem.change(playerEntityID , Player.class, (player) -> player.name = "God");
myBufferSystem.change(playerEntityID , Velocity.class, (velo) -> velo.speed = 10000.0f);
// Remove some componentsmyBufferSystem.remove(playerEntityID, Collider.class);
// Congratulations, you just buffered a entity which will get created and modified once the used buffer system is processing // :)
The text was updated successfully, but these errors were encountered:
Man you are an idea machine. Appreciate the posts. I'm reading it in backwards order.
Do you have any usecases of ReactiveSystem? I'm looking for a reason why everyone should be using this (and it should be integrated in core instead of a plugin). It feels fairly specialized.
A command pattern might improve the design to preserve order of operations, otherwise you get strange behavior when somebody adds, deletes and adds again. Something like what the operations plugin does (functionally very close to this but currently no callbacks). https://github.com/DaanVanYperen/artemis-odb-contrib/wiki/Operations-Plugin
Well its me again... yeah i know, but i recently used Unitys ECS and came across [CommandBufferSystems] (https://docs.unity3d.com/Packages/[email protected]/manual/entity_command_buffer.html).
Those are used to "buffer" the creation, destruction and composition of entities, great if you wanna create a bunch of entities at the start of the frame for example. Or if you have multiple threads working on entities.
As i did for the Job-System Feature Request, i came along my own implementation aswell. It would be great if artemis ODB would provide such "simply" but "essential" tools itself :) Its honestly a pain to take care of that, but i hope that my "idea" of the buffer system is a good point to start.
Currently im using it in my own game, there probably still some little bugs.
I havent found a nice solution for buffering changes to component attributes... but the current implementation should do the trick for the first.
Dont get confused by all the maps, buffered maps contains all buffered elements for entites not existant yet. And the other maps ( without buffered in their name ) contain buffered elements for already existing entities. The system either uses the artemis entity id for applying buffered changes to existing entities or generates a internal id for the buffered creation of entities, that one is also used to modify the buffered entities.
Here a little example of how to use this buffer system ;)
The text was updated successfully, but these errors were encountered: