How to build reactive systems with Unity ECS: Part 2

Table of Contents

How to detect a modified component

In part 1 I showed how you can detect whether an entity was created or destroyed or a component was added to or removed from an entity. This is sufficient for many use cases however sometimes you need to know when the values of a component have changed. In this post I will show how you can achieve this. If you’d like you can checkout a finished project from GitHub for your own experiments.

A common example when you want to know if the value of a component changed, is the health of your player character or units. In this example, let’s use a simple a Health component:

public struct Health : IComponentData {
    public int Value;
}

Some system in your game will change the health, maybe the unit got damaged:

// read the health
var health = EntityManager.GetComponentData<Health>(unit);
// apply some damage
health.Value -= damage; 
// write it back to the entity.
EntityManager.SetComponent(unit, health);

Now another system would need to update the UI for the damaged unit. It could of course update the UI every frame, but if you have a lot of units and UI updates aren’t cheap, this can cost more performance than you would like to spend. So how can you pick only the entities that have actually changed their health value without having to iterate over all of them?

Component version tracking

Tracking changes to component values is surprisingly tricky to do when you don’t want to sacrifice a lot of performance for it. Unity ECS tracks changes using a so-called component version. Using this version you can detect if a certain component has changed or not. So how does it work?

First off, there is a GlobalSystemVersion in EntityManager. It starts with 1 and is increased by one after each system has run. For example, if you have two systems DamageSystem and UISystem and let each of them run twice, GlobalSystemVersion will be 4.

Each system also has a LastSystemVersion. It starts with 0 and is set to the value of GlobalSystemVersion at the end of the system’s execution. The next picture shows the value of LastSystemVersion in parentheses after the system name:

When the systems run for the first time their LastSystemVersion is 0. When they are executed again their LastSystemVersion is the value that GlobalSystemVersion had, when the system was last executed. On the second execution of DamageSystem LastSystemVersion of DamageSystem is 1 because this was the value of GlobalSystemVersion when DamageSystem was last executed. Similarly, when UISystem is executed for the second time, its LastSystemVersion is 2 because that was the value GlobalSystemVersion when UISystem was executed before.

So how does this help us tracking component changes? Whenever a system changes the values of a component (in contrast to adding and removing a component) Unity ECS will set GlobalSystemVersion as the component version of that component. Now a system can check if the component version is greater than its own LastSystemVersion to detect if the component has changed since the last execution of the system. The next picture shows how DamageSystem modifies a Health component and UISystem can track this change. Note, that the picture is not 100% correct as Unity ECS doesn’t actually track changes down to single components (I’ll come to that in a minute). It should however illustrate the idea of how component versioning works and how you can detect changes using version numbers, so here you go:

You start with a Health component that has a version of 0. Now DamageSystem modifies the Health component, so ECS will assign the current GlobalSystemVersion to the Health component, so its version is now 1.

The next system in line is UISystem. UISystem can now compare the component version of the Health component (which is 1) with its own LastSystemVersion (which is 0). Because LastSystemVersion is smaller than the component’s version, UISystem now knows that the Health component has changed and can act on the change (e.g. update the UI).

After UISystem, DamageSystem is again executed. It again modifies the Health component, so the Health component’s version is set to 3 (because this is the current value of GlobalSystemVersion). After this UISystem runs again. UISystem’s LastSystemVersion is 2 while the Health component’s version is 3, so again UISystem knows that the Health component has changed since its last execution and can update the UI.

Using the component version for detecting changes

Now that you know how Unity tracks component versions, how can you actually read the component version of a component so you can decide whether or not the component has changed? You can do this with chunk iteration. First you need to define an EntityQuery for the type of components you are interested in.

_query = Entities
    .WithAll<Health>()
    .ToEntityQuery(); 

Inside of your OnUpdate method you can now iterate over the chunks of your EntityQuery and get the component version of a certain component:

protected override void OnUpdate()
{
  // get the chunk component type for the
  // health component.
  var chunkComponentType = 
    GetArchetypeChunkComponentType<Health>(true);

  // get the array of chunks for the query
  using (var chunks = 
    _query.CreateArchetypeChunkArray(Allocator.TempJob))
  {
    // iterate over the chunks
    foreach (var chunk in chunks)
    {
      // get the component version 
      var healthVersion = 
        chunk.GetComponentVersion(chunkComponentType);

      // compare the component version to the
      // LastSystemVersion which is a member
      // of ComponentSystemBase
      
      // this will work but don't do it this way, see below.
      if (healthVersion > LastSystemVersion) {
        Debug.Log(
          "Chunk contains entities with modified health.");
      }  
      
      // there is a helper function available
      // which takes care of the fact that LastSystemVersion may 
      // roll over at some point. You should use this function.
      if (ChangeVersionUtility
        .DidChange(healthVersion, LastSystemVersion)) {
        Debug.Log(
          "Chunk contains entities with modified health.");
      }
    }
  }
}

While the check is a bit verbose in code, it is quite straightforward to do. However you may have noticed a thing that is strange here. Did you? Yes indeed, GetComponentVersion works on a chunk level, not on a component level. A chunk may contain multiple Health components and only some of them may have changed:

This means that when you perform work based on the component version, you need to keep in mind that the chunk may also contain unchanged components and you have no way of finding out which of the components were changed and which were not. Therefore your update logic needs to work in a way that updating for a unchanged component is maybe wasted work but doesn’t break the system (the fancy term for this is “Idempotence”). For example, updating the UI is idempotent because if you updated the UI for a unit that didn’t actually change its health value, you will waste some CPU cycles but the UI will still be correct. As a counterexample, if you keep a counter (no pun intended) of units with modified health somewhere (maybe for some achievement) then this will not be idempotent. If you count all Health components in the chunk, your counter will be wrong because the chunk may contain a mix of changed and unchanged Health components.

Having the version at the chunk level is a trade-off between performance and usability. If only a few Health components have changed, you can quickly skip over large chunks of Health components without having to look at each Health component individually. This increases performance. On the flip side, you will do some wasted work when a chunk contains only a few changed components together with unchanged ones.

Streamlining the code

Chunk iteration is workable but it is very verbose. Therefore for simple cases, you can directly have an EntityQuery filter the changed components for you:

_query = Entities.WithAll<Health>().ToEntityQuery();
_query.SetFilterChanged(ComponentType.ReadOnly<Health>());

This query will return only entities in chunks that have changed since the last excution of your system. The ECS API will do some magic to inject LastSystemVersion into this EntityQuery before your system is run. You can then simply use the built-in convenience methods to iterate over the changed components:

protected override void OnUpdate()
{
  Entities
    .With(_query)
    .ForEach((Entity entity, ref Health healthItem) =>
    {
        Debug.Log($"New health: {healthItem.Value}");
    });
}

Now this is a lot less code than with the chunk iteration. Be aware though, that even if this looks like it would only give you changed Health components, it will also give you unchanged ones if they happen to live in a chunk together with the changed ones. It’s just a little less code but under the hood it uses the same chunk iteration. So you still need to keep your logic idempotent, otherwise you will run into subtle bugs.

Summary

  • Unity ECS’ EntityManager has a GlobalSystemVersion that is increased by one whenever a system has finished its execution.
  • Each system has a LastSystemVersion that is set to the value of GlobalSystemVersion at the end of the system’s execution.
  • When a system modifies a component the component’s modification version will be set to the current value of GlobalSystemVersion.

To check if a component has changed:

  • Do a chunk iteration over the chunks of an EntityQuery for your component of interest.
  • Use GetComponentVersion(chunkComponentType) to get the component version.
  • Compare the component version with LastSystemVersion using the ChangeVersionUtility.DidChange(componentVersion, LastSystemVersion) utility function.
  • You can use EntityQuery’s SetFilterChanged method to have EntityQuery do the chunk iteration for you and streamline your code.

Be aware that changes are only tracked to a chunk level, not to an individual component level, so if you detect that a chunk has changed, the chunk still may contain a mix of changed and unchanged components. Your update code therefore needs to be idempotent.

Again, you can check out an example project from GitHub and try it for yourself.