How to build reactive systems with Unity ECS: Part 1

Table of Contents

Why do you need reactive systems?

Your systems don’t live alone all by themselves. What one system changes, may be of interest for another system.  For example, if you build a strategy game you will most likely need to know where your units are in the game world. If your game world is a 2D grid, you could realize this with a GridPosition component that looks like this:

public struct GridPosition {
    public int2 Value;
}

For many use cases you will need to answer this question: “Is there a unit at Position X?”. To answer this question, you could iterate all the units in your game and find out by looking at their GridPosition components. However, if you do this every frame, you may get performance problems if you have a lot of units. You can solve this by creating a cache where you can look up a position and see if there is a unit there. But you need to keep this cache up to date whenever a unit changes position. How could you do this?

Approach 1: Send messages

One way of solving this, is to send a message whenever you change the position of a unit. Your cache system could then process this message and update the cache. This has its drawbacks though:

  • You need to remember to send the message every time you change the position of a unit. There may be a lot of different use cases where you do this (e.g. simple movement, special abilities like teleports, portals in your game world, etc.). So it’s easy to forget sending the message. This will introduce subtle bugs as your cache becomes stale and you may not even realize it until a few minutes later. Then it is very hard to trace back why your cache has incorrect data in it.
  • When you update the positions of a lot of units, you will send out a lot of events. This makes batch-handling of the changes difficult as you never know how many change events you get and you may do more work than necessary.

Approach 2: Use reactive systems

A better way of solving this problem is to use reactive systems. Such systems allow you to react on changes to entities while avoiding the problems you get with an event-based approach. That being said, events have their place, but this is a topic for another time. So how can you detect and react on changes to your entities? Unity ECS allows you to detect several conditions:

  • something created an entity
  • something destroyed an entity
  • something added a component to an entity
  • something has removed a component from an entity
  • something changed the values of a component

The first four cases are all done in a very similar and easy (though not exactly straightforward) way, while the last one is more complicated. All the first four cases boil down to detection of an added or removed component and this part will focus on these cases. The fifth case needs some more explanation, I will cover it in part 2.

Detecting created entities

To detect created entities, you will use an EntityQuery.  In this example you want to add all units to the cache that have both, a GridPosition and a Unit component. You can write an entity query for this:

_unitsToAdd = Entities
    .WithAll<Unit,GridPosition>()
    .ToEntityQuery();

Look at this for a second and you realize that this query will not cut it. While it gives you all the entities you are interested in, it will also give you entities you have already in your cache. But you are only interested in the newly created ones. The EntityQuery doesn’t seem to have a facility for achieving this. When you think about this a while you may come up with the question “What does ‘newly created’ mean?”. Does it mean entities created in this frame? Or in the last frame? What if the definition is not useful for my use case? For the cache use case you really don’t care who created the entity in which frame, you only care about if it is in the cache or not.

You can add a marker component to your entity once you add it to the cache. Let’s call this component IsInCache. It could look like this:

public struct IsInCache : IComponentData {}

In the code where you fill the cache, you add the marker component to the entity:

AddToCache(entity);
PostUpdateCommands.AddComponent(entity, new IsInCache());     

With this marker component in place, you can now search for units that are not yet in your cache:

_unitsToAdd = Entities
    .WithAll<Unit,GridPosition>()
    // all units without this component
    .WithNone<IsInCache>() 
    .ToEntityQuery();

Detecting destroyed entities

In strategy games units usually get blown up, so the entities representing them will get destroyed. When this happens, you will want to remove the destroyed entities from the cache. But how can an EntityQuery find an entity that does no longer exist? The answer is: it can’t. When you destroy an entity, no EntityQuery will find it anymore.

So you may think about this for a while and then come up with an IsDestroyed marker component that marks an entity as destroyed. Then your cache could search for entities with this IsDestroyed marker and clean up accordingly. But how do you finally destroy the entity once you have updated your cache? Will the cache destroy it? What if there are other caches? Which of them will destroy the entity? These questions are easier asked than answered, even more so when your game grows in size and complexity. Also, adding such a marker is the “sending events” approach in disguise. Whenever you want to destroy an entity, you would need to remember to add the marker instead of calling EntityManager.DestroyEntity.

You can solve this problem by using ISystemStateComponentData instead of IComponentData for your IsInCache marker component.

private struct IsInCache : ISystemStateComponentData {}

When you have a component derived from ISystemStateComponentData this component will prevent the destruction of the entity until you remove it. You start with an entity with Unit, GridPosition and IsInCache components.

Now you destroy the entity:

EntityManager.DestroyEntity(entity);

Because IsInCache is derived from ISystemStateComponentData you will end up with this:

Unity ECS will remove all components which are not derived from ISystemStateComponentData but the entity itself will stay alive until you remove all  ISystemStateComponentData-based components from it. Now you can find destroyed entities using this entity query:

_destroyedUnits = Entities
    .WithAll<IsInCache>()
    .WithNone<Unit,GridPosition>()
    .ToEntityQuery();

You can iterate over these, remove their entries from the cache and finally remove the IsInCache component.

RemoveFromCache(entity);
PostUpdateCommands.RemoveComponent<IsInCache>(entity);

Removing the IsInCache component will automatically destroy the entity.

Detecting added and removed components

You already know how to detect created and destroyed entities and with this knowledge you can also detect added and removed components, because it works the same way. Maybe your units can get poisoned and you want to show and hide an icon that tells the player whether a unit is currently poisoned. So you create an IsPoisoned component that marks poisoned units.

public struct IsPoisoned : IComponentData {}

Now you can build a system that reacts when some other system adds or removes the IsPoisoned component. The implementation is very similar to the implementation of the cache - you will use a marker component so you can easily filter the relevant entities. In this case a HasPoisonIndicator marker component would be appropriate:

private struct HasPoisonIndicator : ISystemStateComponentData {}

...
// find all units which are poisoned but have no
// poison indicator displayed

_poisonedUnitsWithoutIndicator = Entities
    .WithAll<IsPoisoned>()
    .WithNone<HasPoisonIndicator>()
    .ToEntityQuery();
    
// find all units which have a poison indicator displayed
// but are no longer poisoned
_nonPoisonedUnitsWithIndicator = Entities
    .WithNone<IsPoisoned>()
    .WithAll<HasPoisonIndicator>()
    .ToEntityQuery();

Now you can use these entity queries to show and hide the UI icon:

ForEach( entity => {
    AddPoisonedUIIndicator(entity);
    PostUpdateCommands
        .AddComponent(entity, new HasPoisonIndicator());

}, _poisonedUnitsWithoutIndicator);

ForEach( entity => {
    RemovePoisonUIIndicator(entity);
    PostUpdateCommands
        .RemoveComponent<HasPoisonIndicator>(entity);

}, _nonPoisonedUnitsWithIndicator);

Because you derived HasPoisonIndicator from ISystemStateComponentData your system will also remove the UI indicator when a poisoned unit gets destroyed.

Summary

To detect when something adds or removes a component from an entity or if an entity was created or destroyed, you can use a marker component. The marker component should be derived from ISystemStateComponentData. To detect an added component/created entity:

  • find entities with the interesting component but without your marker component
  • process those entities, then add your marker component to them

To detect a removed component/destroyed entity:

  • find entities with your marker component but without the component that was removed
  • process those entities, then remove your marker component from them

In part 2 I’ll dive into how you can detect and handle changes to the values of components, which are a bit more complicated to process.