Event component data

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.

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: Tim (tertle) / Event System Ā· GitLab

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.

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 ?

A quick example could be instead of having an HealthChangedEvent entity, you could have the following (pseudo) systems:

struct LastFrameHealthSystemState : ISystemState
{
    public float Value;
}

class HealthSystem : SystemBase
{
    // before you update the Health, remember to update the LastFrameHealth
}

class HealEffectSystem : SystemBase
{
    // if Health > LastFrameHealth, play heal effect
}

class DamageEffectSystem : SystemBase
{
    // if Health < LastFrameHealth, play damage effect
}
3 Likes

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

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)

1 Like

FYI about what @brunocoimbra is saying it’s called a reactive system and I wrote a generic implementation of it here Reactive System - Generic way ?

There is also some other interesting reading by others on the same thread.

2 Likes

Thanks, everyone this was really helpful.

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

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.

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

Ok that worked like a charm, thank you

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.

An event system without structural changes and without polling could look like this:

Code

public struct DamageEvent
{
    public Entity Target;
    public float Damage;
}

public partial class DamageEventSystem : SystemBase
{
    public NativeQueue<DamageEvent> Events;

    protected override void OnCreate()
    {
        base.OnCreate();
        Events = new NativeQueue<DamageEvent>(Allocator.Persistent);
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();
        if(Events.IsCreated)
        {
            Events.Dispose();
        }
    }

    protected override void OnUpdate()
    {
        new DamageEventJob
        {
            Events = Events,
            HealthFromEntity = GetComponentDataFromEntity<Health>(false),
        }.Schedule();
    }

    [BurstCompile]
    public struct DamageEventJob : IJob
    {
        public NativeQueue<DamageEvent> Events;
        public ComponentDataFromEntity<Health> HealthFromEntity;

        public void Execute()
        {
            while(Events.TryDequeue(out DamageEvent evnt))
            {
                if(HealthFromEntity.HasComponent(evnt.Target))
                {
                    Health health = HealthFromEntity[evnt.Target];
                    health.Current -= evnt.Damage;
                    HealthFromEntity[evnt.Target] = health;
                }
            }
        }
    }
}

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

Isn’t that the same concept as tertle’s events that I mentioned?

ah yes, sorry I missed that part of your post

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.

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

4 Likes

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.

7 Likes