I am looking for some best practice advices when dealing with “Events” in ECS.
In my game, Events are simple entities that have an EventData component which contains some information about which entity is targeted by the event, some event data, etc. So an Event is an independant entity, it is not attached to the entity that triggered the event, or that is targeted by the event. This allows to have multiple events of the same type targetting the same entity.
I then have several systems that can handle these events and perform some actions depending on which components has the entity targetted by the event.
For instance, let’s say I have a CollisionEvent which contains the Entity that was collided.
We could for instance want to destroy the entity that collided, or make it bounce on collision, etc.
So I have a bunch of tag components like DestroyOnCollision, BounceOnCollision, etc.
This means that when I process a CollisionEvent, I need to check if the target entity has a DestroyOnCollision component, or a BounceOnCollision component, maybe both, etc.
What is the best way (in terms of best practice) to do this ?
Check for the component using EntityManager.HasComponent(entity). And if we need to get access to some data, use GetComponentData.
Inject ComponentDataFromEntity and check bounce.Exists(entity). If we need to get access to some data, directly use that of the injected data.
In particular, what are the differences in term of performance ?
Our recommendation is to add components to the entities that receive the event. We will optimize specifically for that and we believe this will be the most efficient approach.
I am working on the same thing and have the same questions. Have you done some tests, do you know what performance is like?
First thing I did for my input system is attaching events to their consumer, but then as you said it doesn’t allow for multiple events of the same time per entity, besides (even more likely) multiple unspecified entities might need to respond to the same event. There are ways to facilitate that but the best looking approach is to have events separately and to use ComponentDataFromEntity. And in the case of network events, I need to find target entities anyway.
Have you found some nice solution for your event system?
Also having events on the consumer doesn’t allow me to treat them as an easily disposable component or entity. How am I supposed to clean up these? Do I have to have a special case for every single event component type in a cleanup system that would individually remove them?
My event system is similar to yours and it relies on HasComponent as well.
For performance CDFE.Exists equals HasComponent but only a bit faster because type information is already there. EntityManager’s Exists have to process the type in generic a bit. Then both must iterate through types in that entity’s archetype. The more types you have attached to an entity the slower.
For data retrieval you can compare the source code.
The code is almost the same, but GetCDA again requires processing the type, has job completion, and use the same method but without the lookup cache. Without a lookup cache means it has to do an equivalent of HasComponent every time you try to get data from entity. CDFE has a lookup cache because the type is already fixed. Only the first time that it must search for the type.
CDFE is faster in both case but I am not sure if it will be significant or not
And what about the cost of those in comparison to having the event on the consumer entity? I was about to make a benchmark, but maybe you have already done that?
I’m also thinking that there are not that many events… ~100 per frame, which is probably nothing in terms of performance overhead of CompDataFromEntity and cache misses. My code might end up on a bigger server though and there might be much more events and performance would be critical.
But then things like Force are additive. And it’s much nicer to have them as separate entities instead of messing with existing Force on an entity, In which case I need to check if it already exists, or if it always exists, the system that applies accumulated force has to go through ALL entities with force even zero force, to apply them.
I suspect that even if I have thousands of Force components and I check every single one of them every frame to see if force is >0 and needs to be applied that might still be a better performance than directly randomly accessing ComponentData.
I gotta benchmark it.
EDIT: Posting benchmark results.
No Jobs, no Burst.
10,000 entities exist with Health and 1000 (10%) Damage instances per frame.
Separate damage entities: 1.85ms
Damage added directly to Damage component on Health entity: 0.53ms
100,000 entities exist with Health and 1000 (1%) Damage instances per frame.
Separate damage entities: 2.60ms
Damage added directly to Damage component on Health entity: 3.75ms
So the conclusion is that in the simplest scenario separate entities as events give better performance in cases where less than ~2 % of the entities each frame have the event applied to them. At 10% the direct application is already about 4x faster.
The results depend HEAVILY on many other details.
The results should be more in favor of direct application with Burst enabled. (Will test tomorrow)
The “event attached on the target” way of course suffer from only one kind per entity problem and require some design around it, but in the recent update zero-sized component is becoming a special case in the source code. I think this is what Joachim saying about this will be the most efficient approach. (So, you cannot put any data in the event in order to be counted as this special case) I don’t know the details of this special case optimization and it is still changing every recent version. A benchmark today might be slower than some time in the future. (It is good to on board on this design perhaps?)
But from my guess they will make those tag component not interrupting the archetype, making things stay in the same chunk even if each one has a different tag component. Kind of like mini-ISharedComponentData without changing archetype? That should speed up iteration speed when you have variety of tags in the game.
By the way, in the separate events benchmark, way more time is spent on creating the events (1.64) than on applying them using ComponentDataFromEntity (0.31).
public class CreateDamageSeparatelySystem : ComponentSystem
{
public struct Data
{
public ComponentDataArray<Health> Health;
public EntityArray Entities;
public readonly int Length;
}
[Inject] Data _data;
protected override void OnUpdate()
{
for (int i = 0; i < 1000; i++)
{
PostUpdateCommands.CreateEntity();
PostUpdateCommands.AddComponent<SeparateDamage>(new SeparateDamage() { value = 1, target = _data.Entities[i] });
}
}
}
[UpdateAfter(typeof(CreateDamageSeparatelySystem))]
public class ApplyDamageSeparatelySystem : ComponentSystem
{
public struct Data
{
public ComponentDataArray<SeparateDamage> Damage;
public readonly int Length;
}
[Inject] Data _damage;
[Inject] ComponentDataFromEntity<Health> _health;
protected override void OnUpdate()
{
for (int i = 0; i < _damage.Length; i++)
{
var h = _health[_damage.Damage[i].target].value;
_health[_damage.Damage[i].target] = new Health { value = h - _damage.Damage[i].value };
}
}
}
If they optimize flag components than it might become more efficient to use something like the DamageTaken flag in this case in addition to the direct damage so that damage application system can iterate only through the flagged entities. Currently this approach is almost as slow as creating separate entities. (AddComponent is expensive)
Time for the Direct damage application went from 4.7 ms on the main thread to 0.3 on a worker thread.
While the performance of the Damage on a separate event entity stayed the same. Most of the time is still spent on the main thread creating entities and components.
Even with 1,000,000 Health entities to check and 1,000 Damage instances (0.1%) the direct brute force Damage approach takes 1.5ms on 3 worker threads and separate Damage takes 2.0ms on the main thread.