junkdog / artemis-odb

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

Separate systems, processing strategies and subscription lists #159

Closed DaanVanYperen closed 9 years ago

DaanVanYperen commented 9 years ago

Moved from #149. Cleaned up a bit.

You've seen the matrices for my games; systems discrete to a fault, and everything done inside the ECS. Right now there's a bit of a functionality gap between the simplest games, and having several full fledged specialized external systems hooked into Artemis.

Things like Deferred System fill this gap, but it's a bit of a hack.

I see some possibility of breaking things up and filling this gap without breaking things for casual users. Just thinking out loud though, I'm mainly interested in your thoughts about this. Code examples are merely illustrative.

Breaking up systems

Before breakup

So at its core EntitySystems are a discrete unit of work to keep things conceptually clean, a unary subscription list, and an iterative process, all in the same class. (sidestepping any performance benefits that I do not understand).

IntervalEntityProcessingSystem, DelayedEntityProcessingSystem mainly provide an iteration strategy Odb-contrib's Deferred Systems is an extension of this, where the iteration strategy covers multiple systems.

VoidEntitySystems and Managers take care of usecases where the unary relationship between Systems and a subscription list just doesn't make sense.

VoidEntitySystem is treated as an EntitySystem, while it isn't really. it's just a generic system that needs to be fired somewhere alongside entity systems.

Managers area mainly glorified event listeners, providing some utility along the way..

There's also a few use cases that do not fit in the provide System, for example a collision system where you need to offset multiple groups of entities against each other. The current unary subscription list is not much use here. I'd want to fetch multiple aspects, filtered by Aspect, and throw them at eachother.

After breakup

So how to translate all this into something flexible under the hood that still allows for the same experience above the sheets.

One thing would be pulling Aspect entity lists out of systems and into a cache to freely (or on subscription basis) pull sets out of. Preferably with support for groups and tags. So underwater the entities would track family (WINK Ashley) membership instead of system membership. Systems would then underwater be subscribed to the N families they are interested in.

Second could be stripping out the iteration strategy from EntitySystems, and treating them more as a discrete operation. This opens up the ability to reuse and nest iteration strategies, and to process systems more like a pipeline if needed. This'll also allow us to provide Nullary, Unary and Binary variants of systems.

All this could make the process methods an interface implementation as well, not sure if that matters for multimorphingpowerrangers.

Implementation details

Some hoops here, mainly required so we can define systems in a single class.

Refactoring system superclasses

So on most legacy classes this could be totally invisible. The only exception is a direct subclass of EntitySystem, where people would need to now implement using our new toolkit. Makes most sense anyway, since this is usually done to introduce new processing strategies.

Examples

public abstract class EntityProcessingSystem implements UnarySystem, EntityObserver { 
{
            public EntityProcessingSystem( Aspect aspect ) { .. } 
            ProcessingStrategy getStrategy() { return new UnaryIterator(aspect); } 
            protected void process ( Entity a ) {}
            protected void added ( Entity a ) {}
            etc
}

public abstract class VoidEntitySystem implements NullarySystem { 
{
            protected void process () {} 
}

interface NullarySystem extends System { .. }
interface UnarySystem extends System { .. }
interface BinarySystem extends System { .. }

UnarySystem would provide the process method, EntityObserver all the listening methods. Upon system registration we'd do some magic to make sure our entity manager registers with all the aspects used in the iterator and the processing strategy. The ProcessingStrategy is responsible for calling the system.

So now say you'd want to use the new toys to make a superclass to deal with collisions:

@Wire
public abstract class CollisionEntitySystem implements BinarySystem {
            public CollisionEntitySystem( Aspect a, Aspect b )
            ProcessingStrategy getStrategy() { new BinaryComparitiveIterator(a,b) } 
}

or what about deferred calls or subsystems that share a strategy, Just brew up a strategy with a common backing object.

public abstract class RenderSubSystem implements UnarySystem {
          public RenderSubSystem( ProcessingStrategy masterRenderStrategy )  { .. }
            ProcessingStrategy getStrategy() { return new SubStrategy(masterStrategy, aspect); } 
            protected abstract process( Entity a, Entity b) { .. }
}

or maybe we want to combine both the UnaryIterator, sorted, with a cooldown?

public SuperSortedCooldownKermitTheFrogSystem implements EntityProcessingSystem { 
{
            public EntitySystem( Aspect aspect ) { .. } 
            ProcessingStrategy getStrategy() { return new CooldownStrategy(new SortStrategy(super.getStrategy(), .. )); } 
             ..
}

Just since I haven't driveled enough crap, small part of the puzzle,

ProcessingStrategy

interface ProcessingStrategy implements EntityObserver {
           /** Process entities */
           void process(); 
           /** Register system relationship with this strategy. */
           void setSystem( System system );
           /** Strategy will register all subscriptions with the family manager */
           void initialize();
}

Initialization

World.initialize() would fetch all strategies from all systems, register the system with the strategy, then initialize the strategies to subscribe all needed families with the family manager for events, and to make sure the family manager is aware what families to track.

After all this systems are initialized. ProcessingStrategy receive family events and pass them on to their registered system. Or you could just register the systems directly for events with the family manager, if that saves cycles.

World#process

Basically, step through each system, fetch the ProcessingStrategy, and process() it. Which calls the system. Since it matches the arity it'll know how to step through the system without needing reflection.

Passive systems we can give a null strategy, or a PassiveStrategy(). ;)

Overarching strategies are a bit more complex, but that'll come down to signalling a new processing cycle to the strategies and having only the first one of a set fire based on a shared backing object.

well of course a raw idea, but you get the picture.

Pitfalls

Fragmented operations

Potential pitfall in all this is causing people to fragment the operation itself. For some strategies it makes sense externalizing them, as it's more what and when you process than part of the operation itself. When you talk about highly optimized collision detection, it comes pretty close to the operation to be part of the system, externalizing it into a strategy seems less appropriate. We should provide for this usecase.

Binary operations, O(N^2)

Junkdog: Some things look like a possible O(N^2) scenario though, but specialized strategies could possibly circumvent it, achieving O(log(N)) or something.

In practice the binary operations will typically be asymmetrical enough to perform, say all Enemies versus all Friendlies (players). Though it certainly wouldn't compete with say a box2d system.

Afterthoughts

DaanVanYperen commented 9 years ago

After playing around with this I feel processing strategies will just create a lot of abstraction and makes order of operations hard to understand.

Perhaps better (and a lot cheaper!) to just sanitize the EntitySystem class hierarchy a bit to open it up.

Main step would be introducing a superclass/interface to EntitySystem (basically just a process() method) and without EntityObserver.

Adding system is such a tiny tweak but will make the whole setup a lot less presuming and a lot more flexible.

DaanVanYperen commented 9 years ago

Backlog grooming. Too broad. Need concrete tickets!