Closed DaanVanYperen closed 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.
System
- interface
EntitySystem
(extract actives logic into separate class so it becomes reusable for BinarySystem)EntityProcessingSystem
@deprecated VoidEntitySystem
- deprecated, replaced with SystemSuperSystem
/ SubSystem
- transform DeferredEntitySystem
from contrib into an easier to understand setup. This'll be the first step towards render pipelines, time slicing and such.BinaryEntitySystem
EventHandlingSystem
- PollingTagEntitySystem
GroupEntitySystem
etcAdding system is such a tiny tweak but will make the whole setup a lot less presuming and a lot more flexible.
Backlog grooming. Too broad. Need concrete tickets!
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
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:
or what about deferred calls or subsystems that share a strategy, Just brew up a strategy with a common backing object.
or maybe we want to combine both the UnaryIterator, sorted, with a cooldown?
Just since I haven't driveled enough crap, small part of the puzzle,
ProcessingStrategy
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)
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