Search Unity

Event component data

Discussion in 'Entity Component System' started by RoughSpaghetti3211, Nov 15, 2020.

  1. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Is it a good idea to create empty entities holding an empty event tag component. It feels strange to me spawning an entity, attaching some event tags components, and then have a separate system run it owns
    Entities.ForEach on different data if an event entity is found.
     
  2. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    For low-frequency event: yes, totally good.
    For mid-frequency to high-frequency events: either rethink your logic outside of event-driven architecture and move on to a more data-oriented way of thinking or create/use something like that: https://gitlab.com/tertle/com.bovinelabs.event
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Agree with brunocoimbra

    You should think about alternatives to event driven architecture. I designed my event system more for tools than gameplay (think things like gizmo drawing or telemetry.)

    Not to say it can't be used for gameplay things, there are certainly use cases where it makes sense but you shouldn't design your entire game around these events.
     
  4. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Very helpful thanks you. It did get me wondering if I stick to DOD and ECS what’s the general vanilla way to communicate between system at a mid to hight rate. Is it just not a thing dod do well ?
     
  5. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    A quick example could be instead of having an HealthChangedEvent entity, you could have the following (pseudo) systems:

    Code (CSharp):
    1. struct LastFrameHealthSystemState : ISystemState
    2. {
    3.     public float Value;
    4. }
    5.  
    6. class HealthSystem : SystemBase
    7. {
    8.     // before you update the Health, remember to update the LastFrameHealth
    9. }
    10.  
    11. class HealEffectSystem : SystemBase
    12. {
    13.     // if Health > LastFrameHealth, play heal effect
    14. }
    15.  
    16. class DamageEffectSystem : SystemBase
    17. {
    18.     // if Health < LastFrameHealth, play damage effect
    19. }
     
    Sarkahn, RoughSpaghetti3211 and jdtec like this.
  6. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Ok so let’s say I have a large amount of entities
    Ive never seen ISystemState interface, thank you for sharing this. I will do some reading on ISystemState, could be the cheese needed from my cheeseburger

    Is ISystemState the same as ISystemStateComponentData, Ive been out of ECS past month so im not sure about all the changes
     
    Last edited: Nov 16, 2020
  7. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Yes, I was referring to ISystemStateComponentData, I just was too lazy to write the entire name haha

    It could even be a normal IComponentData, I just tend to write ISystemStateComponentData as it makes it more clear that the ComponentData is, well, a SystemState, not an "Entity State". Also, it is useful to do some initialization and destruction stuff (ie. an entity with Health and without LastFrameHealth is means that it was just spawned, and the opposite means that it was just destroyed, so 2 more places to play some cool effects)
     
    RoughSpaghetti3211 likes this.
  8. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
  9. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Thanks, everyone this was really helpful.
     
  10. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Got one more question

    Let's say I have a single satellite entity orbiting a planet. On the planet's surface, I have various entities like houses and cars that need to do something every 40 degrees. The satellite entity is using the ISystemState so track its degrees of rotation and update every 40 % ( essentially creating a signal every 40 degrees )

    how would a system that operates on the house and car work? Is it as simple as creating a separate query in each system and checking some value on the ISystemStateComponent data or is there something in ISystemStateComponent that should handle this? Or do I need to add the ISystemStateComponent to each house and car entity? or Maybe a SCD ? I really want to try and keep this DOD as much as possible
     
  11. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    If you have a sinlge satelite, I would make it a singleton entity. I don't think you need an ISystemState, just compute the current rotation of the satelite and update a 'Segment' IComponentData on the Singleton Entity. Then your house system can do what it has to based on the 'Segment' IComponentData value. Dont' forget to check for the actual change of the segement value before schedulong the 'house' system.
     
  12. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    I would have to be honest, last I used the entity singleton it wasn't kind to me. I guess i will try it again
     
  13. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,705
    Ok that worked like a charm, thank you
     
  14. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    761
    Isn't that a problem because you're basically polling? If you have 10,000 entities and 8 different systems listening for that "event" and you send one event (change health) every minute on average, you're going to end up with 80,000 iterations saying "did my health change?" every single frame just to handle something that happens only once per minute.

    It all kinda goes back to that age-old "tagging" vs "flagging" debate. Do you taking the hit from polling or from ugly expensive of architype changes? My first thought was that an event system based on NativeStream like tertle's seems like it fits somewhere in between the two for making something that is reactive. But maybe I'm mistaken because it seems like tertle is trying to say there are better alternatives? So far the only two alternatives I've seen so far all boil down to tagging or flagging.

    In object orientated programming events, you're just calling a lambda function so the execution just happens immediately and there is no need for polling or architype changes. It's fast and efficient (albeit single-threaded). There are a lot of cool things about ECS and it's super fast, but when it comes to events, it seems like OOP has the upper-hand. I have read about some implementations of ECS that don't have expensive architype change costs so they might be better for handling events than the Unity implementation, however, they have drawbacks in other areas.
     
    Last edited: Apr 11, 2022
  15. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    An event system without structural changes and without polling could look like this:

    Code (CSharp):
    1.  
    2. public struct DamageEvent
    3. {
    4.     public Entity Target;
    5.     public float Damage;
    6. }
    7.  
    8. public partial class DamageEventSystem : SystemBase
    9. {
    10.     public NativeQueue<DamageEvent> Events;
    11.  
    12.     protected override void OnCreate()
    13.     {
    14.         base.OnCreate();
    15.         Events = new NativeQueue<DamageEvent>(Allocator.Persistent);
    16.     }
    17.  
    18.     protected override void OnDestroy()
    19.     {
    20.         base.OnDestroy();
    21.         if(Events.IsCreated)
    22.         {
    23.             Events.Dispose();
    24.         }
    25.     }
    26.  
    27.     protected override void OnUpdate()
    28.     {
    29.         new DamageEventJob
    30.         {
    31.             Events = Events,
    32.             HealthFromEntity = GetComponentDataFromEntity<Health>(false),
    33.         }.Schedule();
    34.     }
    35.  
    36.     [BurstCompile]
    37.     public struct DamageEventJob : IJob
    38.     {
    39.         public NativeQueue<DamageEvent> Events;
    40.         public ComponentDataFromEntity<Health> HealthFromEntity;
    41.  
    42.         public void Execute()
    43.         {
    44.             while(Events.TryDequeue(out DamageEvent evnt))
    45.             {
    46.                 if(HealthFromEntity.HasComponent(evnt.Target))
    47.                 {
    48.                     Health health = HealthFromEntity[evnt.Target];
    49.                     health.Current -= evnt.Damage;
    50.                     HealthFromEntity[evnt.Target] = health;
    51.                 }
    52.             }
    53.         }
    54.     }
    55. }
    56.  

    The "Events" NativeQueue can be written to in parallel from other systems/jobs. A performance improvement would be to use NativeStream instead of NativeQueue, to allow faster parallel writing and also parallel processing of events. But in this specific case of damage events, it would be bad to process them in parallel since different events can attempt to modify the health of the same entity
     
    Last edited: Apr 11, 2022
  16. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    761
    Isn't that the same concept as tertle's events that I mentioned?
     
  17. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    ah yes, sorry I missed that part of your post
     
  18. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    761
    To me what you propose seems like a good in-game solution for events that persist for only one frame, but @tertle has cautioned several times in this thread and others that there are better "alternative" methods, but for situations where systems need to react once to a single change, I'm having trouble coming up with anything other than polling or structural changes. I guess there is that other idea of creating and destroying event entities, but in tertle's benchmark that looked comparatively slow.
     
    Last edited: Apr 11, 2022
  19. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    A lot of people hate polling, and for good reason, but in entities it's actually really not that bad.
    Seriously, go profile a job that checks the value of 1,000,000 entities and early outs.
    Burst just loves this.
    And unless you have a really really well written project, you're still probably main thread limited so still have a bit of space in your worker threads to do a bit of extra work.

    Would avoid doing this in 100s of jobs but more because of the cost of scheduling 100s of jobs

    -edit-

    also for rarely tweaked components you can use change filtering to optimize this a lot
     
    Last edited: Apr 12, 2022
  20. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    tertle is definitely right that if you a main-thread bottlenecked, polling on worker threads in Burst is usually quite effective. However, I actually have a couple projects where the main thread spends more time waiting on saturated worker threads than doing main thread things. This isn't common, but if you find yourself in a similar situation, here's a few more advanced techniques I've used.

    First off, there really is a place for something like tertle's event system for gameplay. Often times it happens when a parallel chunk iteration job needs to conditionally push data to something else that can't receive that data in a thread-safe manner without an intermediate thread-safe container. EntityCommandBuffer is an example of this. ParentSystem also does this for updating the Child buffer when children's parents change using hashmaps. Oddly enough, my own use cases for this sort of thing parallel one of those two.

    If you are willing to use IJobEntityBatch, change filtering is incredibly powerful, and the critical bugs have all been fixed in 0.50. But beware of using change filtering inside of EntityQueries because those can force jobs to complete for reasons I don't believe are logically sound. IJobEntityBatch lets you check versions for more than two components anyways. If you are polling using ComponentDataFromEntity, it can still be advantageous to check change versions as there are far fewer change version numbers than there are components, and that increases the chances of those version numbers being temporarily retained in cache enough to get some more cache hits and consequently skip fetching the actual components. That really depends on your event frequency though.

    If you have a really expensive operation per change event, then ISystemStateComponentData can be used on top of change filters to get entity-level granularity. Copying components into system state components whenever they change is pretty cheap using chunk iteration. And so is memory comparing for differences. ParentSystem uses this to detect entities with changed parents before adding them to their intermediate hashmap.

    So far I have talked about two kinds of events, events with large generated payloads, and change events on components. But there's a third type, which I will refer to as signals. Signals don't have data. They just indicate that something happened or that something is in a current state. Usually there's an emitter and a listener. The relationship between these two dictate what strategy to use. EnabledComponents may offer new ways to do these sorts of things in the future, but the current approaches should be sufficient for the few people that need to employ these tactics.

    In a pure 1-to-1 dual entity relationship, you can of course use a byte-sized component and write to the destination using ComponentDataFromEntity with [NativeDisableParallelForRestriction].

    But if your source and destination are the same entity (albeit you want the signal generation and response to be in separate systems), bitfields in chunk components are incredibly powerful. Critical bugfixes for chunk components also arrived in 0.50, so they are pretty reliable now. You can use them to skip across chunks and only evaluate flagged indices. And you can also do extremely efficient complex queries, such as only wanting to process entities with signals A and B set but not C. It is also possible to query a chunk component on an arbitrary entity using StorageInfoFromEntity.

    Lastly, if you have a bidirectional relationship established between the source and destination such that they can agree upon an index, you can use a bit array. This is effectively the thread-safe container approach, but way faster. 65,000 channels can be represented in a mere 8kB, which is a quarter of L1 cache on most devices. This is especially powerful in one-to-many and many-to-many relationships where you need "any" or "all" style signaling and the random accesses of other techniques bring performance to a crawl.

    There's likely even more techniques in the DoD world that I haven't discovered yet. But so far I have found a highly-performant approach to every situation where I may have used a general-purpose event system in the past. I sometimes recoil at the mentioning of "event system" now because it is generally the wrong mindset.
     
    Occuros, DrBoum, lclemens and 4 others like this.
  21. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    761
    @tertle - I guess it still just seems weird... in that situation I described, 80,000 iterations checking a comparison every frame just to service something that happens once or twice a minute. Even though burst is crazy fast, it's tough to swallow when an OOP event in the same situation would simply execute 8 lambdas and complete in a couple of nanoseconds. I think I read somewhere in these forums that as a general rule you typically use flagging for things that last less than a couple of seconds and tagging for things that last longer. Is that still true?

    @DreamingImLatios - Wow, lots of great info! I'll take a look at change filtering again in 0.50. I have to admit, some of your info went over my head (the signals bit).
     
  22. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm curious about what was mentioned earlier in the thread, about potential reasons not to use a NativeQueue/NativeStream-based events system. It's hard for me to think of any big disadvantage at the moment, but the advantages are clear

    I suppose one reason would be the inability to use composition, but for typical "events" I think it's somewhat rare to require composition. Another reason would be that these events must end up doing some ComponentDataFromEntity and so if they modify values on other entities it's not safe to process them in parallel. But I feel like you'd need a LOT of very frequent events for this to become less efficient than a polling approach
     
    Last edited: Apr 13, 2022
    lclemens likes this.
  23. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    These are actually the main reasons. Most of the time, there's a better approach. Containers can eat up a lot more memory bandwidth than is otherwise necessary, and there's threading concerns which can cause latency issues if your systems are ordered wrong. A third reason is that every time you use a container, you end up having to pay the cost of an extra JobHandle.CombineDependencies().

    It is important to remember that a lot of people are coming from an OOP background where 90% of their game logic is event-based. And trying to use a container event pattern for everything is going to defeat the ECS data layout. But you are more experienced with DOTS than most so I imagine you are already exercising a good amount of caution before deciding to use events, and are using them in far fewer places than the naïve developer would.
     
    lclemens and PhilSA like this.
  24. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    for events that need to change values on entities, like "Damage events", I feel like we often have to choose between:
    1. Parallel event creation, but singlethread event processing (this is the NativeStream approach)
    2. Singlethread event creation but parallel event processing (in this approach, we write events in dynamicbuffers on the entities they need to modify, and we poll those events every frame)
    But is it possible to have both? I suppose if we can only choose one, parallel event processing will be more valuable for performance than parallel event creation most of the time. But maybe we could have a 3-step approach:
    1. Parallel event creation by writing them to nativestream
    2. Singlethreaded job that takes those events from the nativestream and add them to buffers on their target entities
    3. Parallel event processing by polling events on entities
    It could be worth it if the jobs that must create those events are also pretty heavy & could benefit from parallelism

    --------

    But now that I'm thinking about it, in the case of damage events, doing a "polling" events approach would have a big advantage over the nativestream approach: being able to have multiple event consumers. So for each damage event, one consumer would reduce health, one consumer would play a VFX, one consumer would spawn some damage numbers over the hit unit, etc... I suppose that would be another reason to favor those over nativestream events
     
    Last edited: Apr 14, 2022
    lclemens likes this.
  25. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Admittedly it might be a little unfair to talk about this particular example, because it is one of the reasons I wrote Psyshock. I have thread-safe write access to both entities in a pair in parallel. So I can just write to a damage component on the target entity. Then in a separate system or two I poll for damage using change filters to calculate health and whatnot.

    Now your hypothetical problem isn't specific enough for me to suggest something, so I'm going to make some assumptions which may or may not help you. I'm assuming you are doing raycasts in a parallel job, where you want to affect the hit target of which you have an entity reference from Unity Physics but no write access to its components.

    Well if it were me, I would say "screw that" and write to the component anyways using StorageInfoFromEntity, ComponentTypeHandle, and Interlocked.Add. After that, it would be the same situation as above where damage can be polled for using change filters. If I needed stats I might still end up writing shooter/target pairs into a blocklist and then have a stats system tally things up in a job while the main thread does structural changes.
     
  26. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    That is actually what I do in my ability system.
    I write to a native stream in parallel then the consumer system uses a native multi hash m map to remap each effect (damage) to it's target entity (also in parallel) and last I use a IjobChunk to process events in parallel (per entity). All event on the same entity are processed at the same time so it should not be bad in term of memory layout. And if nothing is to do I see the stream is not created and don't execute the consumer at all.
     
  27. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm trying to test this approach, but not sure if I'm doing things right. Here's what I have:
    Code (CSharp):
    1.  
    2. [BurstCompile]
    3. public unsafe struct ParallelApplyStreamEventsToEntitiesJob : IJobParallelFor
    4. {
    5.     public NativeStream.Reader StreamDamageEvents;
    6.     public StorageInfoFromEntity StorageInfoFromEntity;
    7.     public ComponentTypeHandle<Health> HealthType;
    8.  
    9.     public void Execute(int index)
    10.     {
    11.         StreamDamageEvents.BeginForEachIndex(index);
    12.         while (StreamDamageEvents.RemainingItemCount > 0)
    13.         {
    14.             StreamDamageEvent damageEvent = StreamDamageEvents.Read<StreamDamageEvent>();
    15.             if (StorageInfoFromEntity.Exists(damageEvent.Target))
    16.             {
    17.                 EntityStorageInfo storageInfo = StorageInfoFromEntity[damageEvent.Target];
    18.                 ArchetypeChunk chunk = storageInfo.Chunk;
    19.                 if (chunk.Has(HealthType))
    20.                 {
    21.                     NativeArray<Health> chunkHealth = chunk.GetNativeArray(HealthType);
    22.                     int healthLocation = UnsafeUtility.AsRef<int>(chunkHealth.GetUnsafePtr()) + (storageInfo.IndexInChunk * UnsafeUtility.SizeOf<Health>());
    23.                     Interlocked.Add(ref healthLocation, -1);
    24.                 }
    25.             }
    26.         }
    27.         StreamDamageEvents.EndForEachIndex();
    28.     }
    29. }
    My code does reach the point where I do "Interlocked.Add(ref healthLocation, -1);", but I don't see my health value changing in the DOTS inspector
     
  28. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    I think you need to do:
    ref int healthLocation = ref UnsafeUtility.AsRef...