Coping with Change in Unity3D ECS
Back in 2018 I wrote an article titled “My wish list for Unity3D ECS”, where one of the important points for me was an introduction of reactive systems.
Today I am writing and article, how you can build a reactive system with Unity ECS framework.
But first of all, why I believe Reactive systems are such an important topic?
The idea behind a reactive system is: we get to process only changed entities. This sound like a performance benefit, but it is rather a system design benefit. Given a mechanism for reactive systems, developers tend to design components and systems differently.
The mechanism of tracking data changes is rather an expensive one. This is why Unity ECS, which is based on the premise — “Performance by default”, did not introduce it from the beginning and now there are two techniques, but they are rather sub optimal.
In this article I want to shed light on those two techniques and provide a small improvement, which IMHO will make the developer experience better.
1. FilterChanged
EntityQuery
class has an interesting method SetFilterChanged
, which has following claim:
Filters out entities in chunks for which the specified component has not changed.
This is it, isn’t it? As I described before, a reactive system is a system, which processes only changed entities, so by defining an EntityQuery
and setting the filter, we will achieve this goal 👍.
Sadly, not quite! 😔
The change flag that is used in this filter is not associated with individual entities, but rather with chunks. Chunk being a collection of entities of same Archetype. Meaning that, if a chunk is changed in some way, none of the entities in the chunk gets filtered. To make matter worse, a chunk is marked changed even if we just get a component out of it as a reference. It does not matter, if we actually change the values, because checking for actual value changes is expensive. So if you ask for a component reference for example like this:
Entities.ForEach((ref ComponentA a) =>
{
Debug.Log(a);
});
Every chunk and so every entity, which has ComponentA
is marked as changed. Just because we could mutate instance of ComponentA
inside of the closure.
This fact makes FilterChanged
technique a very performance oriented choice, but not very practical.
2. ISystemStateComponentData
ISystemStateComponentData
is a way to define a component, which has one interesting characteristic, if an entity is destroyed, the value of such component is not removed. Users need to remove such component explicitly.
Now how does it help us with building a reactive system?
Imagine you have a Position
component, which is just an IComponentData
and you created another component PreviousPosition
, which is a ISystemStateComponentData
. When we find an entity which has Position
, but does not have a PreviousPosition
. This means that the Position
was just added, when we have both, we can compare their values and check if the component was updated. If we have PreviousPosition
, but no Position
the component was removed explicitly, or through the destruction of an entity. This is cool but leaves us with some foot work, which can become cumbersome to implement.
Let me present the improvement for developer workflow:
First of all, I would like to define a much more specific and useful interface
public interface IDiffComponent<T> : ISystemStateComponentData
where T: struct, IComponentData, IEquatable<T>
{
T Value { get; set; }
}
Now our components can be defined as following:
struct Position : IComponentData, IEquatable<Position>
{
public int X;
public int Y;
// I am skiping the implementation of IEquatable here
}struct PreviousPosition : IDiffComponent<Position>
{
public Position Value { get; set; }
}
Every component which needs a history for diffing, needs to implement IEquatable
interface and the component, which will be used for diffing implements IDiffComponent
.
In my previous article “Convenience API for Unity ECS” I introduced an idea of using extension methods in order to achieve better developer experience. Let us introduce two extension methods on Entity
in order to identify component change and balance/maintain the change.
https://gist.github.com/mzaks/fbdf810109cbf4061b26b2b3d2c65d69
With this extension methods in place, we can perform following checks:
entity.Diff<Postion, PositionHistory>().IsAdded()
entity.Diff<Postion, PositionHistory>().IsUpdated()
entity.Diff<Postion, PositionHistory>().IsAddedOrUpdated()
entity.Diff<Postion, PositionHistory>().IsRemoved()
entity.Diff<Postion, PositionHistory>().IsChanged()
There is still a some what complex question of: “When to call the Balance
method”. However the possibility to filter out entities based on the value changes, becomes feasible and there for we can design our game logic with reactive system mindset.
I hope my small serious on Unity3D ECS was interesting, or even helpful.
Thank you for reading!!! You may clap, if you like 😌.