#Are there any best practices for events under the unity ecs architecture?
1 messages · Page 1 of 1 (latest)
With dots - your events is data. List of structs for example. To create event - you add to that list. Then any system can read it. Afterwards you clear it
C# events themselves won't even work
oes it mean that the event is a component?
does it mean that the event is a component?
When an event occurs, I mount the component on the entity and uninstall it from the entity when the event ends?
That what you might do in other ECS, but in dots you just keep DynamicBuffer
And you don't add/remove components, instead you add to it
And once read - remove from it
Basically, if entity is supposed to be "listening" to certain event - it just contains DynamicBuffer for it
And invoking logic adds to it
It's inverted compared to c# events
Example: DamageEvent
If entity can be hit with damage - you simply add damage event to it's buffer
And then other systems read it: apply damage to health, spawn vfsx on damage and etc
Something you can also do - modify your events before they are read by other systems
This can be used for example to calculate damage reductions for complex combat systems
So armor system just reads events, checks if they match something and reduce value in them accordingly
Next system already has it processed
Do you know how much better performance these kinds of setups gives compared to one system that calculates it all in the end? Considering the frequency of the events happening and that jobs have some overhead?
you separate every step into system with this
so you can optimize it really well
especially when you enable usage of change filters
so systems that go over those buffers will skip chunks when there is no chance of write in it
so in the end it's highly parallelizable
and very modular
and you can over optimize individual systems without affecting others
Yeah, I just figured if you split a damage calculation into 7 steps and it doesn't happen frequently, if the change filter checks adds up to more cost than just letting one system handle it. I do get all other benefits though^^
as well as add as many new ones as you want
7 steps sounds overkill
- Some system detects damage and writes it in some internal buffer
- System goes over that buffer and to each damagable entity in it - adds damage event
- As many systems as you want query damagable entity and do their thing based of damage event
- System clears damage event buffer
that's it
in the simplest example it's just 4
if you combine it into one system
you'll just save some time on main thread
that will be spent on scheduling additional jobs
on threads, it's likely that performance will be very similiar
allthough depends highly on "god" job
if you'll manage to make it parallel, it might be better
but with modular case - one very good benefit is that while jobs are small and doing atomic thing, they also access much less component types
which means that while they are not parallel themselves, they run parallel to each other
unless ofc they access same components
do you mean like this?
private partial struct Job1 : IJobEntity
{
private void Execute([WithChangeFilter(typeof(BufferElement))] DynamicBuffer<BufferElement> buffer)
{
}
}
change filter attribute goes on job struct itself
buffer should have in operator
then this job will run only when this component was accessed with RW for chunk
meaning if you write to this buffer only via lookup
then it will only trigger then
just keep in mind
it doesn't actually check if data changed
and it's per chunk
not per entity
so only use case - optimizations and that's all
hmm I see, but it is still a good improvement to keep in mind
I have tested it with IJE and IJC and it seems to be working fine, just as you said
yeah, but sadly any every frame job that uses that component with ref will ruin it (make it run every frame)
even if it doesn't write
so every time any job gets RW access our WithChangeFilter job will have to run?
yes, because change filters are updated when RW pointer is obtained, not when actual writes happen
okay that is very unfortunate then, but I can see why they did it like that
because it's cheap
just 1 int per chunk
practically free
but provides so much opportunities
for something like damage you will most likely always have some RW access, so the attribute doesn't do much then
yeah exactly
there is a way though
but WithChangeFilter most likely doesn't have any big overhead?
via IJobChunk
without IJE codegen
you can access RO pointer
do writes
and if you actually did write
update change filter
job boilerplate will grow massively
but for hot paths it's worth it
Another way to have it
but then every time you use the DynamicBuffer you would have to use IJC and not IJE
is if your ref job runs using enabled component filters
only when you write
btw, it's not just buffers
same works for any component
basically, if you have job that runs only for WithAll(typeof(IsDead)) where IsDead is IEnablableComponent
that would mean, when it's disabled
job won't run for those chunks
and won't trigger change filters
now this is a pretty good opportunity to have chains of logic execution via iteration, that fully utilizes change filters
but this is like cutting edge of data design
oh yeah I am doing that already in some parts of my code
I tried to find it, but I can't remember where it was
it's really cool, since you can even tell the System not to Update using the RequireForUpdate
not if it's enablable
enablables can only be read with a sync
so either you need a sync point to read on main thread, or you read inside of job
meaning job must be scheduled as long as at least 1 entity exists
well that's not something I expected
so having multiple IEnablableComponent on an Entity could even be counterproductive?
I don't get the context
what I said applies to RequireForUpdate
this requirments only checks for entity existance
ingoring filters
ohh only for RequireForUpdate
change filters, enablable filters
okay I misunderstood because you mentioned read 👍
I thought generally about read access
well, that's true too
if you try to read component on main thread
you need to sync
because other jobs might writing to it atm
this applies to enablable and change filters too
yep, that's a given
is there any example code that I can learn?
if you want to learn something about events in dots you can read this topic: https://forum.unity.com/threads/comparing-different-approaches-for-events-in-dots.1267775/
and it's implementations here: https://github.com/PhilSA/DOTSEventsStressTest/tree/master/Assets/StressTest/TestEvents
this is pretty old (uses foreach somewhere in project) but you can learn how to deal with "events"
Github repo: https://github.com/PhilSA/DOTSEventsStressTest
I decided I wanted to try all kinds of different approaches to DOTS events, and profile...