Systems
Systems are where all the logic lives, it takes Entities from the collection and executes logic on each one. The way Systems are designed there is an orchestration layer which wraps all systems and handles the communication between the pools and the execution/reaction/setup methods known as ISystemExecutor (Which can be read about on other pages).
This means your systems don't need to worry about the logistics of getting entities and dealing with them, you just express how you want to interact with entities and let the SystemExecutor handle the heavy lifting and pass you the entities for processing. This can easily be seen when you look at all the available system interfaces which all process individual entities not groups of them.
If you havent already its recommended you look at the SystemsR3 Systems Docs, which cover some system paradigms we build on top of here or can still be used in conjunction with the ECS ones.
System Types
All ECS (Not SystemsR3) systems have the notion of a Group which describes what entities to target out of the pool, so you don't need to do much other than setup the right group contracts and implement the methods for the interfaces.
BatchedSystem
BatchedSystemThis is by far the quickest system type and should probably be your default system to use for all entity execution, it simply aligns all the entities and their related component allocations up in memory then provide them directly for you to process.
This means you do not need to do any entityComponentAccessor.GetComponent<SomeComponent>() for each thing you want in the Group, you just get given all the required components via the method call.
public class BatchedExampleSystem : BatchedSystem<SomeComponentA, SomeComponentB>
{
// We dont need to provide an explicit Group as it can infer them from the generics provided, but you can override it for ExcludedComponent scenarios
protected override Observable<Unit> ReactWhen()
{ return Observable.EveryUpdate(); }
protected override void Process(Entity entity, SomeComponentA componentA, SomeComponentB componentB)
{
// Do something with the components and/or entity
}
}You can use both
classandstructcomponents with this system, but the structs will be passed by value so are read only (there is a performance benefit to not having to deal withrefs)
BatchedRefSystem
BatchedRefSystemThis is the same as the BatchedSystem for all intents and purposes but it is meant for struct based Components and provides them as refs so they can be updated within the system.
This
Systemis fine if all yourstructcomponents are going to need to be written to, but if you only need to write to some of them and the others are just for reading from you should look at theBatchedMixedSystembelow.
BatchedMixedSystem
BatchedMixedSystemThis is a hybrid of both previous systems, it allows you to mix ref and non ref components.
This can be handy if you have a mix of
classandstructcomponents but only a couple of thestructcomponents need to be updated, the rest of read only, or the others are classes.
Due to the MANY permutations of this that you can have its recommended that if you have very specific scenarios you just copy the code for the current
BatchedMixedSystemand alter the signatures however you need.
MultiplexingSystem + IMultiplexedJob
MultiplexingSystem + IMultiplexedJobThis is very much like a batched system, but it acts as a sort of multiplexer to allow you to run multiple jobs within one update, a job is basically the same as a batched systems Process method but standalone.
This is really useful if you have multiple systems which all require same components and execute at same time, this ensures that it only needs to lookup the batches once and then runs all jobs back to back with the batch data, this can allow for better utilisation of CPU/Memory.
On one hand this may seem slightly more complex but in a way it also makes things a bit simpler as your Jobs are lightweight objects that can just be scheduled together and you have a smaller logic footprint.
Remember you dont need to have 100% overlap on required components etc, there will be a tipping point but if you have several systems which all use 75% of the same components you can possibly get a decent performance bonus making them into jobs and giving them all the same components, even if a few of the jobs ignore a component or two it may still end up being more efficient than running them as fully fledged systems.
MultiplexingBatchedRefSystem + IMultiplexedRefJob
MultiplexingBatchedRefSystem + IMultiplexedRefJobSame as above but it lets you pass the components to jobs with ref keyword, mainly meant for struct scenarios.
There is currently no mixed one but it may be added in the future, there is so many varieties of approaches to mix
refit is currently left to you to implement your own variants if you need them.
IBasicEntitySystem
IBasicEntitySystemThis system is like a IBasicSystem allowing you to process each entity within the group on every update cycle.
ISetupSystem
ISetupSystemThis interface implies that you want to setup entities, so it will match all entities via the group and will run a Setup method once for each of the entities.
This is primarily there for doing one off setup methods on entities, such as setting up view related data (i.e
ViewResolvers) or other one time things.
ITeardownSystem
ITeardownSystemThis is similar to ISetupSystem, but is used when a matched entity's group is ABOUT to be removed.
This distinction is important because it means that all components should still be on the entity when this system gets triggered, if we triggered it after the component was removed you wouldnt be able to access it on the entity.
IReactToEntitySystem
IReactToEntitySystemThis interface implies that you want to react to individual changes in an entity. It will pass each entity to the ReactToEntity method to setup the observable you want, such as Health changing, input occurring, random intervals etc. This only happens once per matched entity, here is an example of the sort of thing you would do here:
Once you have setup your reactions the Process method is triggered every time the subscription from the reaction phase is triggered, so this way your system reacts to data rather than polling for changes each frame, this makes system logic for succinct and direct, it also can make quite complex scenarios quite simple as you can use the power of R3 to daisy chain together your observables to trigger whatever you want.
IReactToGroupSystem
IReactToGroupSystemThis is like the IReactToEntitySystem but rather than reacting to each entity matched, it instead just schedules at the at the group level. The ReactToGroup is generally used as a way to process all entities every frame using Observable.EveryUpdate() and selecting the group, however you can do many other things such as reacting to events at a group level or some other observable notion, here is a simple example:
While this system lets you do whatever you want on entities, in almost every scenario you would use this system, you would be better off using a
BatchedSystem, i.eBatchedSystem<SimpleReadComponent, SimpleWriteComponent>which would function the same but perform FAR faster.
IReactToDataSystem<T>
IReactToDataSystem<T>So this is the more complicated and lesser used flavour of system. It is basically the same as the IReactToEntitySystem however it reacts to data rather than an entity.
For example lets say you wanted to react to a collision event and your system wanted to know about the entity as normal, but also the collision event that occurred. This system is the way you would do that, as its subscription passes back some data rather than an entity, here is an example:
So this offers a bit more power as the Process method takes both the entity in the pool and the returned data from the subscription allowing you to work with external data when processing.
System Loading Order
So by default (with the default implementation of ISystemExecutor) systems will load in the order of:
Implementations of
ISetupSystemImplementations of
IReactToEntitySystemOther Systems
However within those groupings it will load the systems in whatever order Zenject/Extenject (assuming you are using it) provides them, however there is a way to enforce some level of priority by applying the [Priority(1)] attribute, this allows you to specify the priority of how systems should be loaded. The ordering will be from lowest to highest so if you have a priority of 1 it will load before a system with a priority of 10.
Performance Implications
While there are a myriad of out the box conventional systems to use, its worth saying that in almost all scenarios your first go to system type should be BatchedSystem, its by far faster than the others and has enough flexibility to cope with most scenarios.
The IReactToEntity system is generally the slowest, so should only really be used when you NEED to react to specific per entity situations rather than reacting to the group as a whole.
Last updated