-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Systems
There are 5 different types of Systems:
-
IInitializeSystem
: Executes once, before the game begins (system.Initialize()
). -
IExecuteSystem
: Executes every frame (system.Execute()
) -
ICleanupSystem
: Executes every frame after the other systems are finished (system.Cleanup()
) -
ITearDownSystem
: Executes once, after the game ends (system.TearDown()
) -
ReactiveSystem<Entity>
: Executes when the observed group changed (system.Execute(Entity[])
)
I recommend you create systems for each single task or behaviour in your application and execute them in a defined order. This helps to keep your app deterministic.
var systems = new Systems()
.Add(new SomeInitializeSystem(contexts))
.Add(new SomeReactiveSystem(contexts))
.Add(new SomeExecuteSystem(contexts));
Note that there is no semicolon after creating Systems, each system is added in a fluent interface-style. Get inspired by Match-One GameController.cs
Initialize systems run once at the start of your program. They implement the interface IInitializeSystem
, which defines the method Initialize()
. This is where you set up your initial game state, in a similar way to Unity's Start()
method.
using Entitas;
public class MyInitSystem : IInitializeSystem {
public void Initialize() {
// Initialization code here
}
}
- Add event handlers to
Group
s orContext
s - Create global entities that will exist for the lifetime of your game (e.g. for accessing configuration data)
Execute systems run once per frame. They implement the interface IExecuteSystem
, which defines the method Execute()
. This is where you put code that needs to run every frame, similar to Unity's Update()
method.
using Entitas;
public class MyExecSystem : IExecuteSystem {
public void Execute() {
// per-frame code goes here
}
}
- Poll for events not represented as components (e.g.
UnityEngine.Input
)
Cleanup systems run at the end of each frame, after all other systems have completed their work. They implement the interface ICleanupSystem
, which defines the method Cleanup()
. These are useful if you want to create entities that only exist for one frame (see example mixed system below).
public class MyCleanupSystem : ICleanupSystem {
public void Cleanup() {
// cleanup code here
// runs after every execute and reactive system has completed
}
}
Teardown systems run once at the end of your program. They implement the interface ITearDownSystem
, which defines the method TearDown()
. This is where you can clean up all resources acquired throughout the lifetime of your game.
using Entitas;
public class MyTearDownSystem : ITearDownSystem {
public void TearDown() {
// Teardown code here
}
}
- Release all resources not managed by Unity
- Flush modified files (e.g. save data, logs) to disk
- Gracefully terminate network connections
Entitas also provides a special system called ReactiveSystem
, which is using a Group Observer under the hood. It holds changed entities of interest at your fingertips. Imagine you have 100 fighting units on the battlefield but only 10 of them changed their position. Instead of using a normal IExecuteSystem and updating all 100 views depending on the position you can use a IReactiveSystem which will only update the views of the 10 changed units. So efficient :).
Unlike the other systems, ReactiveSystems inherit from a base class ReactiveSystem<TEntity>
instead of implementing an interface. Entitas generates an entity Type for each context in your game. If your contexts are Game
, GameState
and Input
, Entitas generates three types: GameEntity
, GameStateEntity
and InputEntity
. Reactive systems require that you provide the specific context and associated entity type to which they react.
The base class defines some abstract methods you must implement. First you must create a constructor that calls the base constructor and provides it with the appropriate context. You must override 3 methods: GetTrigger()
returns a collector, this tells the system what events to react to. Filter()
performs a final check on the entities returned by the collector, ensuring they have the required components attached before Execute()
is called on each of them. Execute()
is where the bulk of your game logic resides.
Note: You should not try to combine a reactive system with an execute system - think of reactive systems as a special case of execute systems. All the other interfaces can be mixed (see example below).
using System.Collections.Generic;
using Entitas;
public class MyReactiveSystem : ReactiveSystem<MyContextEntity> {
public MyReactiveSystem (Contexts contexts) : base(contexts.MyContext) {
// pass the context of interest to the base constructor
}
protected override ICollector<MyContextEntity> GetTrigger(IContext<MyContextEntity> context) {
// specify which component you are reacting to
// return context.CreateCollector(MyContextMatcher.MyComponent);
// you can also specify which type of event you need to react to
// return context.CreateCollector(MyContextMatcher.MyComponent.Added()); // the default
// return context.CreateCollector(MyContextMatcher.MyComponent.Removed());
// return context.CreateCollector(MyContextMatcher.MyComponent.AddedOrRemoved());
// combine matchers with AnyOf and AllOf
// return context.CreateCollector(LevelMatcher.AnyOf(MyContextMatcher.Component1, MyContextMatcher.Component2));
// use multiple matchers
// return context.CreateCollector(LevelMatcher.MyContextMatcher, MyContextMatcher.Component2.Removed());
// or any combination of all the above
// return context.CreateCollector(LevelMatcher.AnyOf(MyContextMatcher.Component1, MyContextMatcher.Component2),
// LevelMatcher.Component3.Removed(),
// LevelMatcher.AllOf(MyContextMatcher.C4, MyContextMatcher.C5).Added());
}
protected override bool Filter(MyContextEntity entity) {
// check for required components
}
protected override void Execute(List<MyContextEntity> entities) {
foreach (var e in entities) {
// do stuff to the matched entities
}
}
}
To react to changes of entities from multiple contexts you will need to use multi-reactive system. First you need to declare an interface that will combine entities from multiple contexts that have the same components, and you need to implement that interface for the entity classes via partial classes.
public interface PositionViewEntity : IEntity, IPosition, IView {}
public partial class EnemyEntity : PositionViewEntity {}
public partial class ProjectileEntity : PositionViewEntity {}
Then create a system inherited from MultiReactiveSystem and pass the new interface.
public class ViewSystem : MultiReactiveSystem<PositionViewEntity, Contexts> {
public ViewSystem(Contexts contexts) : base(contexts) {}
protected override ICollector[] GetTrigger(Contexts contexts) {
return new ICollector[] {
contexts.Enemy.CreateCollector(EnemyMatcher.Position),
contexts.Projectile.CreateCollector(ProjectileMatcher.Position)
};
}
protected override bool Filter(PositionViewEntity entity) {
return entity.hasView && entity.hasPosition;
}
protected override void Execute(List<PositionViewEntity> entities) {
foreach(var e in entities) {
e.View.transform.position = e.Position.value;
}
}
}
Entitas Features provide you with some extra control over the organisation your systems. Use them to group related systems together. This has the added benefit of separating the visual debugging objects for your systems in the Unity hierarchy. Now they can be inspected in logical groups instead of all at once.
Features also help you to enforce broader paradigmatic rules in your project. The order of execution of features is determined by the order in which they're added and is always respected by Entitas. Separating your systems into InputSystems : Feature
, GameLogicSystems : Feature
and RenderingSystems : Feature
then initializing them in that order provides an easy way of ensuring that game logic doesn't interfere with rendering.
Features require that you implement a constructor. Use the Add()
method in the ctor to add systems to the feature. The order in which they are added here defines their execution order at runtime. Features can be used in your GameController to instantiate groups of systems together.
using Entitas;
public class InputSystems : Feature
{
public InputSystems(Contexts contexts) : base("Input Systems")
{
// order is respected
Add(new EmitInputSystem(contexts));
Add(new ProcessInputSystem(contexts));
}
}
Then in your GameController:
Systems createSystems(Contexts contexts) {
// order is respected
return new Feature("Systems")
// Input executes first
.Add(new InputSystems(contexts))
// Update
.Add(new GameBoardSystems(contexts))
.Add(new GameStateSystems(contexts))
// Render executes after game logic
.Add(new ViewSystems(contexts))
// Destroy executes last
.Add(new DestroySystem(contexts));
}
Below is an example Reactive system that operates in the Game
context. In this context we have defined two components: PositionComponent
which stores coordinates on a 2D integer grid and ViewComponent
which stores a unity GameObject which is responsible for visualization:
using Entitas;
using UnityEngine;
[Game]
PositionComponent : IComponent {
int x;
int y;
}
[Game]
ViewComponent : IComponent {
GameObject gameObject;
}
The example system listens for changes to Entities' PositionComponent
. The collector gathers all of the entities whose position has changed in the previous frame (via entity.ReplacePosition(x, y)
). These Entities are passed through the filter to check that they also have a View added (and therefore a GameObject to move). The entities that have both a changed position and a view have their view GameObject moved to their new position.
public class RenderPositionSystem : ReactiveSystem<GameEntity>
{
// ctor is called during GameController.CreateSystems()
// this system operates on the Game context so pass this to the base ctor
public RenderPositionSystem(Contexts contexts) : base(contexts.game) {
}
// our collector gathers entities whose Position component has changed
protected override Collector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Position);
}
// filter to ensure entity has a view component and that its position component has
// not been removed by another system since it was collected
protected override bool Filter(GameEntity entity)
{
return entity.hasView && entity.hasPosition;
}
// walk the list of entities and move their view GameObject
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
var pos = e.gridPosition.position;
e.view.gameObject.transform.position = new Vector3(pos.x, pos.y, 0);
}
}
}
This system is both an Execute and a Cleanup system. Its function is to monitor Unity's Input
class for mouse clicks and create entities with InputComponent
added. A separate system processes these components, then, in the Cleanup phase these entities are destroyed.
The advantage of this arrangement is that we could have multiple separate systems listening for InputComponent
and doing different things with them. None of these systems should be responsible for destroying the entities they process, since we may later add more systems or remove existing ones. Still the entity should be destroyed before the next frame since we will never need it again. This is where Cleanup()
comes in, allowing the system that created the entities to retain responsibility for destroying them.
This system also shows how you can use groups to easily find entities with specific components attached and keep track of them. Here we've added a constructor to set up the reference to the group of entities with InputComponent
attached.
using Entitas;
using UnityEngine;
public class EmitInputSystem : IExecuteSystem, ICleanupSystem {
readonly InputContext _context;
readonly IGroup<InputEntity> _inputs;
// get a reference to the group of entities with InputComponent attached
public EmitInputSystem(Contexts contexts) {
_context = contexts.input;
_inputs = _context.GetGroup(InputMatcher.Input);
}
// this runs early every frame (defined by its order in GameController.cs)
public void Execute() {
// check for unity mouse click
var input = Input.GetMouseButtonDown(0);
if(input) {
// perform a raycast to see if we clicked an object
var hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, 100);
if(hit.collider != null) {
// we hit an object, so this is a valid input.
// create a new entity to represent the input
// give it the position of the object we hit
var pos = hit.collider.transform.position;
_context.CreateEntity()
.AddInput((int)pos.x, (int)pos.y);
}
}
}
// ~~~~~~ OTHER SYSTEMS EXECUTE - PROCESS THE ENTITIES CREATED HERE ~~~~~ //
// all other systems are done so we can destroy the input entities we created
public void Cleanup() {
// group.GetEntities() always provides an up-to-date list
foreach(var e in _inputs.GetEntities()) {
e.Destroy();
}
}
}
Guides: Introduction - Installation - Upgrading - FAQ - Cookbook - Contributing
Need Help? Ask a question on Discord or create an issue.
- The Basics
- Concepts
- Architecture / Patterns