Search Unity

HasComponent vs ComponentDataFromEntity for events

Discussion in 'Entity Component System' started by floboc, Apr 18, 2018.

  1. floboc

    floboc

    Joined:
    Oct 31, 2017
    Posts:
    91
    Hello,

    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 ?
    1) Check for the component using EntityManager.HasComponent<BounceOnCollision>(entity). And if we need to get access to some data, use GetComponentData.
    2) Inject ComponentDataFromEntity<BounceOnCollision> 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 ?


    Thanks !
     
    illinar likes this.
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    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.
     
    rigidbuddy and FROS7 like this.
  3. floboc

    floboc

    Joined:
    Oct 31, 2017
    Posts:
    91
    But then we cannot have multiple events of the same type (but probably with different data) targetting the same entity.

    What is the performance overhead of EntityManager.HasComponent with regards to that of ComponentDataFromEntity ?
     
  4. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    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?
     
    Last edited: Oct 4, 2018
  5. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    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.

    Screenshot 2018-10-05 00.43.04.png

    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
     
    The5, julian-moschuering and illinar like this.
  6. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    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)
     
    Last edited: Oct 5, 2018
    NotaNaN and 5argon like this.
  7. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    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.
     
  8. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    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).

    Code (CSharp):
    1. public class CreateDamageSeparatelySystem : ComponentSystem
    2. {
    3.     public struct Data
    4.     {
    5.         public ComponentDataArray<Health> Health;
    6.         public EntityArray Entities;
    7.         public readonly int Length;
    8.     }
    9.     [Inject] Data _data;
    10.  
    11.     protected override void OnUpdate()
    12.     {
    13.         for (int i = 0; i < 1000; i++)
    14.         {
    15.             PostUpdateCommands.CreateEntity();
    16.             PostUpdateCommands.AddComponent<SeparateDamage>(new SeparateDamage() { value = 1, target = _data.Entities[i] });
    17.         }
    18.     }
    19. }
    20.  
    21. [UpdateAfter(typeof(CreateDamageSeparatelySystem))]
    22. public class ApplyDamageSeparatelySystem : ComponentSystem
    23. {
    24.     public struct Data
    25.     {
    26.         public ComponentDataArray<SeparateDamage> Damage;
    27.         public readonly int Length;
    28.     }
    29.     [Inject] Data _damage;
    30.     [Inject] ComponentDataFromEntity<Health> _health;
    31.  
    32.     protected override void OnUpdate()
    33.     {
    34.         for (int i = 0; i < _damage.Length; i++)
    35.         {
    36.             var h = _health[_damage.Damage[i].target].value;
    37.             _health[_damage.Damage[i].target] = new Health { value = h - _damage.Damage[i].value };
    38.         }
    39.     }
    40. }
    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)
     
    Last edited: Oct 5, 2018
    NotaNaN and iam2bam like this.
  9. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    I did a jobified benchmark.

    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.
     
    Last edited: Oct 5, 2018
    NotaNaN likes this.
  10. Deleted User

    Deleted User

    Guest

    Use cached archetypes and after setcomponent instead of addcomponent. You would save ton of time.
     
    NotaNaN and daserra like this.
  11. pcysl5edgo

    pcysl5edgo

    Joined:
    Jun 3, 2018
    Posts:
    65
    NotaNaN likes this.
  12. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    863
    Nice. This saved me 70% execution time.

    I will check out creating entities in other worlds on other threads later if there will be the need.

    Thanks a lot, guys.

    So far an event system based on dedicated entities seems absolutely valid for a client on PC and 200 - 500 events per frame are really not a big deal.
     
    NotaNaN likes this.