junkdog / artemis-odb

A continuation of the popular Artemis ECS framework
BSD 2-Clause "Simplified" License
779 stars 113 forks source link

[FEATURE] ReactiveSystem for runtime operations #613

Open genaray opened 4 years ago

genaray commented 4 years ago

Here i am again... this time with a mechanic that is called "ReactiveSystem". Well actually artemis ODB already has one : EntitySubscription-Listeners.

But one major issue i occured was the lack of runtime... sure callbacks are great ! But what if we wanna iterate with a normal system over those newly added/removed entities ? Or if we wanna run over all newly added/removed entities of a certain aspect since the last frame ? This is why i have implemented a addition to the reactive system :-)

It takes multiple frames to "complete" and works like this ( same for removed )

  1. Entity of aspect was added
  2. Entity gets "Added" marker component attached, on start of next frame
  3. Entity with "Added" exists one frame
  4. Entity gets "Added" marker removed, on end of the current frame

A system which should only exists once in the system hierarchy. You can easily register a aspect, a component that should get added to that component if it was newly inserted and a component that gets added once it exited the aspect.

Furthermore it keeps track of those registered aspects to automaticly add/remove those "marker" component and as an addition it will store all of them since the last frame.

Its not perfect yet... and i havent found a good implementation of attaching the component to destroyed entitites, because they dont exist anymore at that point. But it should work for the most use cases :) ( In Unity this is managed by something called StateComponent )

It makes use of the BufferSystem i developed a week ago :) You simply need to extend that system to inject your own BufferSystems, one running at the start of the frame and one on the end.


/**
 * A system which marks {@link com.artemis.Entity} once those are created and destroyed based on a {@link Aspect}.
 * It also stores lists of created or destroyed entities for each registered {@link Aspect} to ease the iteration
 * over such entities during the frame itself.
 */
public abstract class ReactiveSystem extends BaseSystem {

    /** Simply stores a composition of a aspect we are using **/
    private class Composition{
        public Aspect.Builder aspect;
        public Class<? extends Component> toRemove;
        public Class<? extends Component> toAdd;
    }

    protected BufferSystem startBufferSystem;
    protected BufferSystem endBufferSystem;

    protected Map<Aspect.Builder, Composition> compositions = new ConcurrentHashMap<>();

    protected Map<Aspect.Builder, IntBag> addedEntities = new ConcurrentHashMap<>();
    protected Map<Aspect.Builder, IntBag> removedEntities = new ConcurrentHashMap<>();

    protected Map<Aspect.Builder, EntitySubscription> removeAddedQuery = new ConcurrentHashMap<>();
    protected Map<Aspect.Builder, EntitySubscription> removeRemovedQuery = new ConcurrentHashMap<>();

    @Override
    protected abstract void initialize();

    @Override
    protected void processSystem() {

        // Remove the "add" marker component from entities that already had one whole iteration.
        for(var entry : removeAddedQuery.entrySet()){

            var composition = compositions.get(entry.getKey());
            var entities = entry.getValue().getEntities();

            for(var index = 0; index < entities.size(); index++){

                var entityID = entities.get(index);
                endBufferSystem.remove(entityID, composition.toAdd);
            }
        }

        // Remove the "remove" marker component from entities that already had one whole iteration.
        for(var entry : removeRemovedQuery.entrySet()){

            var composition = compositions.get(entry.getKey());
            var entities = entry.getValue().getEntities();

            for(var index = 0; index < entities.size(); index++){

                var entityID = entities.get(index);
                endBufferSystem.remove(entityID, composition.toRemove);
            }
        }

        // Process all added entities per aspect in order to mark them with the "add" component
        for(var entry : addedEntities.entrySet()){

            var composition = compositions.get(entry.getKey());
            for(var index = 0; index < entry.getValue().size(); index++){

                var entityID = entry.getValue().get(index);
                startBufferSystem.create(entityID, composition.toAdd);
            }
        }

        // Process all added entities per aspect in order to mark them with the "add" component
        for(var entry : removedEntities.entrySet()){

            var composition = compositions.get(entry.getKey());
            for(var index = 0; index < entry.getValue().size(); index++){

                var entityID = entry.getValue().get(index);
                if(world.getEntityManager().isActive(entityID)) continue;

                startBufferSystem.create(entityID, composition.toRemove);
            }
        }
    }

    @Override
    protected void end() {
        super.end();

        addedEntities.clear();
        removedEntities.clear();
    }

    /**
     * Registers all entities of a {@link Aspect} for being marked with a special component once they entered or
     * exited the {@link Aspect} aswell as being tracked in the internal {@link IntBag} each frame.
     * @param builder The aspect we choose our entities with
     * @param toAdd The component that gets added to each of them on the start of the frame, once they enter the aspect.
     * @param toRemove The component that gets added to each of them on the start of the frame, once they exit the aspect.
     */
    public void register(Aspect.Builder builder, Class<? extends Component> toAdd, Class<? extends Component> toRemove){

        if(compositions.containsKey(builder)) return;
        var subscribtion = world.getAspectSubscriptionManager().get(builder);

        // Store composition
        var composition = new Composition();
        composition.aspect = builder;
        composition.toAdd = toAdd;
        composition.toRemove = toRemove;
        compositions.put(builder, composition);

        // Create subscribtion for tracking the entities
        subscribtion.addSubscriptionListener(new EntitySubscription.SubscriptionListener() {

            @Override
            public void inserted(IntBag intBag) {

                if(addedEntities.containsKey(builder)) addedEntities.get(builder).addAll(intBag);
                else{
                    var newBag = new IntBag();
                    newBag.addAll(intBag);
                    addedEntities.put(builder, newBag);
                }
            }

            @Override
            public void removed(IntBag intBag) {

                if(removedEntities.containsKey(builder)) removedEntities.get(builder).addAll(intBag);
                else {
                    var newBag = new IntBag();
                    newBag.addAll(intBag);
                    removedEntities.put(builder, newBag);
                }
            }
        });

        // Query used for removing the "Add" after one frame :)
        removeAddedQuery.put(builder, world.getAspectSubscriptionManager().get(builder.copy().all(toAdd)));
        removeRemovedQuery.put(builder, world.getAspectSubscriptionManager().get(builder.copy().all(toRemove)));
    }

    /**
     * Returns an {@link IntBag} of all newly added entities to the {@link Aspect} since last frame.
     * Always one frame delayed, but who cares in an asyn. environment ?
     * @param builder The aspect we wanna search all new entities for
     * @return The {@link IntBag} filled with those entity ids
     */
    public IntBag getAdded(Aspect.Builder builder){ return addedEntities.get(builder); }

    /**
     * A little helper method to execute logic for each entity added to the certain aspect since last frame.
     * The passed entities do not have the "added" marker component attached yet, that one is getting attached one
     * frame later.
     * @param builder The aspect we wanna search all new entities for
     * @param action The action getting executed for all of them
     */
    public void forEachAdded(Aspect.Builder builder, Consumer<Integer> action){

        var bag = getAdded(builder);
        if(bag == null) return;

        for(var index = 0; index < bag.size(); index++) action.accept(bag.get(index));
    }

    /**
     * Returns an {@link IntBag} of all newly removed entities to the {@link Aspect} since last frame.
     * Always one frame delayed, but who cares in an asyn. environment ?
     * @param builder The aspect we wanna search all removed entities for
     * @return The {@link IntBag} filled with those entity ids
     */
    public IntBag getRemoved(Aspect.Builder builder){ return removedEntities.get(builder); }

    /**
     * A little helper method to execute logic for each entity added to the certain aspect since last frame.
     *  The passed entities do not have the "removed" marker component attached yet, that one is getting attached one
     *  frame later.
     * @param builder The aspect we wanna search all new entities for
     * @param action The action getting executed for all of them
     */
    public void forEachRemoved(Aspect.Builder builder, Consumer<Integer> action){

        var bag = getRemoved(builder);
        if(bag == null) return;

        for(var index = 0; index < bag.size(); index++) action.accept(bag.get(index));
    }
}

Useage is pretty simple... you need to inject your own buffer systems

public class WiredReactiveSystem extends ReactiveSystem{

    StartBufferSystem startBufferSystem;  // A buffer system in MY project running on the start of a frame, the first one
    EndBufferSystem endBufferSystem;    // Last one, at the end of the frame

    @Override
    protected void initialize() {

        super.startBufferSystem = startBufferSystem; // Inject
        super.endBufferSystem = endBufferSystem;
    }
}

And then you simply start using it by acessing your extended ReactiveSystem like...


        // Somewhere in initialize, main or some constructor

        // All entities with Player.class should get the "OnPlayerCreated" attached if newly inserted to the aspect
        // And OnPlayerDestroyed once they are out of the aspect.
        myReactiveSystem.register(Aspect.all(Player.class), OnPlayerCreated.class, OnPlayerDestroyed.class);  

        // Somewhere else 
        myReactiveSystem.forEachAdded(Aspect.all(Player.class), id -> {
            // Do something for all added aspects of this type since the last frame
        });

       // Or iterate over them without any need to care of tagging them
       @All({Player.class, OnPlayerCreated.class})
       public class TestSystem extends IteratingSystem {

             @Override
             protected void process(int i) {
                  // I only run one frame wuuuu
             }
        }
DaanVanYperen commented 3 years ago

.. sure callbacks are great ! But what if we wanna iterate with a normal system over those newly added/removed entities ? Or if we wanna run over all newly added/removed entities of a certain aspect since the last frame ? This is why i have implemented a addition to the reactive system.

Cool! Do you have concrete examples of use?

As a feature request, this is fairly specialized and less core material (artemis-odb-contrib), unless there are extension points missing in artemis-odb.

Its not perfect yet... and i havent found a good implementation of attaching the component to destroyed entitites, because they dont exist anymore at that point. But it should work for the most use cases :) ( In Unity this is managed by something called StateComponent )

Non-typical lifecycle behavior could be done by adding a 'Deleted' component to signifiydeletion, instead of calling .deleteFromWorld(), and adding an exclusion filter on spots you don't want affected by the deleted entity.

genaray commented 3 years ago

Well i actually use those systems quite heavy. I often mark my entities ( Composition > Inheritance ) with components to signalize certain entity specific events. Thats where my reactive systems come in quite nicely.

Its just an alternative to the inserted and removed callbacks for components which allows us to iterate over them for those who prefer a nice flow. It should be better for multithreading than those added/removed callbacks ^^