Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Comparing different approaches for Events in DOTS

Discussion in 'Entity Component System' started by PhilSA, Apr 14, 2022.

  1. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm ready to believe you, but I'd like to try to come up with use cases to discuss first:

    1- Improving performance (Parallel changes to another entity)
    What if a parallel bullets job must apply damage & status effects to hit entities? It would be a parallel job attempting to add StatusEffect buffer elements to potentially the same entity. We'd need to create some form of request (event) to add StatusEffect buffer elements later in a single-thread job. It could be done with ECB.AppendToBuffer(), but ECB is essentially an events system. You could limit those jobs to be single-threaded and run one after the other, OR, you can use event systems to allow them to all run in parallel and apply the change later. The latter approach will perform better in most cases

    2- Schedule a change to be applied later, at the correct time
    What if damage has to be applied at a rather specific point in the frame where all defense stats have been properly recalculated from buffs, but a Bullets job in the FixedStepSimulationGroup needs to detect hits and apply damage? By using events, you can "schedule" that damage to be applied later in the frame at the correct time (after buffs have been recomputed).

    3- Improving code maintainability and ease of use
    What if applying damage requires getting over 5 different component types on the hit entity, in order to compute the final damage value? (Health, various kinds of Defenses, BodyPart, Team, CharacterState, DamageMultiplier, EquippedItemsBuffer, etc....). Having to pass all 5+ ComponentDataFromEntity to our jobs every single time something has to deal damage seems like it wouldn't be as good of a solution as simply creating a damage event that hides all those requirements. Without events, one day you might need a change in data required for damage events, and all of a sudden you'd have to make that change in 30+ different places in the code.

    What if the hit entity has a special power that does "return x% of received damage to the entity that caused it"? Should this logic be processed inside every 30+ job that can apply damage, or would it be better to store damage events so another system can take care of this? Without events, every single special damage-reactive logic in the game must be implemented directly inside a static DoDamage() function, which might require dozens of ComponentDataFromEntity in total. And every single job in your game that could apply damage needs to be passed those dozens of CDFE. And every time you add a new damage-reactive logic, you need to modify all damage jobs in order to pass a new CDFE

    Damage-reactive logic will often not be implementable only with change filters. Each individual damage event has unique data associated to it (instigator, type, etc...), and that data will end up being used in reactive logic

    4- Apply a change that should be processed differently based on archetype of target
    What if damage must be processed differently based on the archetype of the hit entity? By using events, we can add a damage event to a dynamicBuffer on the target entity (we can add this in parallel), so we can then process those events with various different jobs later (different jobs for different archetypes)


    I could imagine maybe finding alternatives that don't require events... but would we have a good reason to avoid events in these cases, or would it be just because we've arbitrarily decided to avoid events?
     
    Last edited: Apr 17, 2022
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    I try not to get too much into this topic because I really don't have a reason for my opinion. I wrote my event system over 2.5 years ago and used it quite extensively for gameplay events for a while and something just didn't sit right with me. I can't explain it except to say the code base just felt wrong.

    I do intend to explore the concept of stats in the coming months when I finish with saving. I wrote a proof of concept a few months ago that tackled the problem in a completely different way I'd like to investigate further. However until I've spent considerable time investigating and have an alternate solution that I consider production ready I'd rather not speculate on solutions. Could be a complete flop.
     
  3. ElliotB

    ElliotB

    Joined:
    Aug 11, 2013
    Posts:
    215
    This is an excellent thread and thank you for putting it together.

    I always favoured the 'damage event as entity approach', which seems slowest of the ecs approaches above. However, the flexibility given by behaviour by composition is awesome and I wouldn't want to give that up. I'd be interested to hear how others would extend the other approaches to more complex behaviour, e.g. something that scales certain attacks with distance, which is straight forward with the entity approach https://github.com/ElliotB256/ECSCo...iveRange/CalculateRangeEffectivenessSystem.cs

    Cheers!
     
    lclemens likes this.
  4. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    I totally disagree on this. Just this week I made an integration for another dots ecs based plugin called Flock box.
    The only thing I needed to do is to give option to read from Local to world. The performance is undeniable and access to millions of devs that don't even need to know how dots works to be able to do games like a tower defense, is a real game maker. Its totally possible to keep ecs as the back end for 3d and let people rapidly prototype ideas. Devs don't even need to access ecs, if you gave the proper oop styled interface to do things they need for the game.

    Its propbaly a turn off for ecs programmers, then again I only care about the games devs create
     
  5. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,068
    it feels nice to see that DOTS forum starts to get interesting threads again after the 0.50 release :)

    From my personal point of view, event systems are not anti-patterns in DoD, as events themselves are represented by data.

    the EntityCommandBuffer which is a mainstay in the entities package is a kind of event system that at some point in the frame playsback write events.
    I don't think any pure ECS project can survive without it.
     
  6. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    Now that the @TheOtherMonarch mentioned its harder to justify events.

    Just trying to understand why would I need this.

    Basically when you add component to an entity you are creating an event basically, right?
    Every system that has that component as requirement will now start processing that event. After that component is removed the event is stopped. Creating a logic that will allow you to just one shot the event is simple.

    Basically this is the default DOD way of doing the thing. Why would I need the events?

    "This makes it more tedious to code anything that can create damage, and makes the codebase more difficult to maintain (if damage data access changes, you need to change it everywhere)"

    Isn't this like the way you design things. If you are applying a poison damage you really don't need physical damage resist or defense component. The only things you need is actually magic damage resist, and poison resist, right?

    All of the point that you made I think there is a way of doing it inside the DOTS framework.

    Also
    "if damage data access changes, you need to change it everywhere"
    You can always encapsulate logic in functions and and call these in multiple places. That way you can avoid changing things in multiple places.

    Like what do you mean exactly by damage data access changes. You mean like the way the damage is calculated for poison changes so it stacks, so it becomes exponential and this creates a snow ball effect where you need to change all the jobs that modify this data? Could you not solve this by designing things in DOTS better? Maybe there is a point a case I don't understand. I have not worked in a project that has complicated damage system. Like thousands of effects that react to each other.
     
  7. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,131
    I also enjoy the thread as it is content rich and has code examples for people interested. Thanks Phil for making the effort. Good debate and it’s normal that opinions differ. In particular as generalizations are usually a compromise (take transform system / hybrid renderer) and depending if something is in the hot path or not we have the optimization vs. convenience trade off…
     
    lclemens likes this.
  8. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Adding a component (or using change filtering) lets you know about one cummulative change to the entity. What if you need to know about multiple changes, and get passed some data specific to each individual change? (who caused the change, what type was the change, etc...)

    By "data access", I mean all the ComponentDataFromEntity that has to get passed to your jobs by systems. If you make a static function for applying damage, that doesn't solve the problem of having to:
    • Make system get all the CDFE with proper read/write access
    • Declare all the CDFE with the proper attributes in your job ([ReadOnly], [NativeDisableParallelFor...])
    • Pass all the CDFE to the static DoDamage() function
    • (you have to do this for all jobs that could want to apply damage, and there is no way to "encapsulate" it other than events)
    See this bit of my last post (that post also gives plenty of other examples of why I think events are a great solution to several problems):
     
    Last edited: Apr 17, 2022
    lclemens and mikaelK like this.
  9. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    "What if you need to know about multiple changes, and get passed some data specific to each individual change?"

    ok good points :), this might sounds stupid and overly simple, but why not just add multiple components and pass data through them?

    For example somebody casts poison component on the player and the priest applies poison resistance component.
    Now you would have a system that processes poison damage. In this system you would have Entities for each loops for these events.

    This would go nicely with singe responsibility principle and would be manageable. Got a new poison damage type? Add a new entities for each or component to an existing for each.

    You would then have physical damage system and poison damage system. If you need to order them you can specify order in the system update before and update in group attributes.

    Not trying to diss events or anything. I really like the idea in object oriented programming, since they are easy for example in cases where game is paused or started etc etc.
     
  10. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    The problem arises if you need two different people casting two poison spells (of different strengths) on the same entity. And those two poison spells will each have a different lifetime, so they must be individually-identifiable in order to be removed individually. Each instance of a poison spell on a given entity has to keep track of its own lifetime counter

    If we want to add "multiple components", we could add an element to a "PoisonEffects" DynamicBuffer instead of adding a component. And since you can't safely add to a buffer on another entity in parallel (could cause different threads trying to add at the same time on same entity), you need some kind of event system to request those changes for a single-threaded job later. This is Approach A.
     
    JesOb, mikaelK and RaL like this.
  11. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Added a Why use events section to the top post, right before the various Approaches. I think if we really want to settle the debate of "Are events a DOTS anti-pattern", we'd need to look at all these points and come up with alternatives that are clearly better
     
  12. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    In that case. Agreed.

    As an alternative pure DOD approach the system could be designed differently. For example 1 type of poison has some strength and half time. Now a player wants to put more poison -> increase poison duration and add 1 to poison stack. If the player 2 has more levels and increased intelligence then add 2 stacks more (calculated from the stats). Since physically the same kind of poison has its half time. So in real world you wouldn't be able to add the same poison with different lifetimes, so adding the same kind of poison would increase its potency and duration.

    So how would you know who killed the enemy with the poison? realistically it would be impossible to know whose poison killed someone in the end, if they are the same kind of poison. Unless it is injected directly to heart or brain for example or if the initial poison was really low in potency.

    In that case, alternative would be to add damage source component to enemy which contains player ids and total damage. Score system could process these and decide who get the most points.

    How to add these player ids. Would probably need an dynamic buffer of fixed array.
    If player counts are low and system relatively simple you can use bit mask. Using fixed array and bitmask is as I understand and have experimented really fast.
     
  13. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    If you need to change a fixedarray/bitmask on another entity in a parallel job, you still need an events system.

    Are we certain we have a valid reason for wanting to avoid event systems at all costs here, or is it just a baseless limitation we impose on ourselves? We can try to find alternatives, but are the alternatives really better? In this case, an event system would most likely provide:
    • Better ease of use (no need to pass all CDFE around and no need to know exactly when damage needs to be processed)
    • Better flexibility of the system (no need for pre-defined poison strengths)
    • Better performance (allows damage/statusEffects/poison application to be done in parallel)
    Why not use it? If an events system gives you best performance, then event systems are DoD
     
    Last edited: Apr 17, 2022
  14. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    You make good points

    For this I have to disagree. Just because something is fast doesn't mean it s data oriented design. Tried to do few google searches what is the consensus, is events oop design pattern or something else. Event programming doesn't seem to fall in any of the categories and is its own paradigm. I hope I got it right.

    "Better ease of use (no need to pass all CDFE around and no need to know exactly when damage needs to be processed)"
    Does this work in multiplayer where you need to know everything, so the game is cheat safe?

    "If you need to change a fixedarray/bitmask on another entity in a parallel job, you still need an events system"

    Ok this I didnt' get since I have been accessing fixed array full of bit masks in entities for each and jobs. And I would not want to multithread fixed array full of uints anyways. Seems like overkill and possible overhead.

    "Better performance (allows damage/statusEffects/poison application to be done in parallel)"
    I don't understand why you would want to do such small task in parallel. The dod way as I understand is to process each entity enemy in parallel where each thread would process 1 damage type for example. This is also more performant, yes? The calculation is so simple that processing it multi threaded is slower.

    Few of the points is good in cases where you don't need to know something so you can just do it and not think about it, but I can see that this kind of thinking can easily go and back stab you.

    "Are we certain we have a valid reason for wanting to avoid event systems at all costs here, or is it just a baseless limitation we impose on ourselves?"
    The following few years I have been actually thinking how to work things that are normally done in oop environment in ecs. Having events never crossed my mind, if this would be something that is needed the why didn't unity make it in the first place?

    I could ask you the same question. Are we looking to use events at all costs and trying to figure out reason to use events when there might not be one?

    Again I'm just trying to understand the problem and if it could be solved with the tools Unity gave. Mostly because relying on tool given by the producer is more safe. If I adopt someone elses event pattern and base my game around it, and Unity decides to change something that breaks the event framework that I have no idea or time to check how it actually works. Don't think nobody wants to be in that position
     
  15. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Well I have to admit I don't know the official definition of DoD. But if the official definition of DoD is not "do whatever is best", then DoD is not what I want to do

    Makes no difference really. Instead of doing the implementation in JobA, you do the implementation in JobB. You're still in control of how you implement it

    If your bullets job is heavy (typically they will all be doing raycasts, so they will be heavy), then your bullet jobs should be done in parallel. Since a bullet can apply a status effect upon a hit, you need events to schedule those effects to be applied in single-thread later

    ECBs are event systems, and they are a core part of DOTS
     
    Krajca and JesOb like this.
  16. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    While normally I would provide suggestions anyways, for the sake of this conversation, I am going to point out that your scenarios are severely underspecified. Here's an incomplete list of questions I am asking:

    What is the environment of the problem? What are the bottlenecks? What are the dependencies? What is "free"?
    What is the frequency of the transformation?
    What is the latency tolerance of the transformation? Per frame? Multiple frames?
    Aside from the source-destination relationship, are entity transformations independent of each other?
    What are the lifecycles of the entities involved?
    What is the persistence of the relationship between the source and destination entities?
    Is the transformation linear time-invariant? If not, does it have any other useful properties?
    What information can be discarded during the transformation?

    (1) Specific:
    Are the number of effects determined at compile time?
    How many are there max?
    How many can happen simultaneously?
    What additional fields do their transformations require?

    (2 - 4) Specific:
    I'm just going to say this right now, you may be able to use intermediate components to do the same thing, which is usually faster and requires less juggling of containers.

    There's a difference between "events" and a "generalized event system". Event processing is a necessary aspect of games. But in a DoD mindset, you should think of them as transforms involving conditional temporary data. ECB is already specialized to structural changes, yet even that is too generalized. By specializing instantiation and initialization, I have achieved up to a 4X speedup on those operations.
     
    Krajca and PhilSA like this.
  17. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,081
    DOD is design where you have one simple Functions that can fit in cache and array of data that will be processed by function.

    We think about data because it is important thing for current generation CPUs and about minimum cache misses because this is how CPUs work. May be in far future we will have new computers that work like our brains and current DOD will not work best for it.

    Based on this I can say that everything that make logic process data in linear fashion with minimum side effects and high cache hit (minimum cache miss) is DOD. So writing event stream (linear array write) and then apply it to entities in linear fashion is cache friendly DOD style programming

    Dont hesitate that we call this thing event.
    This absolutely not the same thing like C# Event based on delegates.
    It Is just serve the same purpose.
     
  18. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    Well its getting more confusing now. :D

    "ECBs are event systems, and they are a core part of DOTS"
    This is the thing I'm thinking about, if ecb is event system for dots, then why should I use extra effort to use someone elses?

    "Since a bullet can apply a status effect upon a hit, you need events to schedule those effects to be applied in single-thread later"
    But if I can do those without these events more easily with the tools given by unity and without extra costs, then why would I want?

    But anyways thanks for the thread it was a good thinking and read. Especially the poison ability system example use case and talk around it.
     
  19. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,081
    You are right. If for your use case it is enough to use Unity systems that you dont need any 3rd part solutions.
    You need to use it only in case you need more performance that Unity ECS give you OOTB
     
  20. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    ECBs are an event system for main thread changes, but there will be cases where you could benefit from events that cause no sync points

    How would you safely add elements to a buffer on a target entity, from a parallel job?
     
    lclemens and Opeth001 like this.
  21. JesOb

    JesOb

    Joined:
    Sep 3, 2012
    Posts:
    1,081
    Good example is UniTask.
    It is 3rd part tool for Unity but some studios go for it in full because it is better than Unity coroutines and now (few years later) Unity work with author of UniTask to create their own OOTB solution.

    May be the same will be with DOTS event
     
    Walter_Hulsebos and mikaelK like this.
  22. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I think part of the problem is that half of my posts in this thread have been huge walls of texts that make you want to take a whole nap before reading it, so I'm always worried about adding EVEN MORE details :D

    It's also pretty tricky and time consuming to clearly explain the entire problem here. Giving you an accurate explanation could require explaining the implementation of almost the entire game, because everything could be inter-related. And often you can't make too many assumptions about specific requirements, because game design almost always changes during development. Too much rigidity for the sake of performance will work against reaching the only goal that matters: to ship a good game in a reasonable amount of time

    So I think an easier way to settle this would be to ask: can you think of any situations at all where an events approach would be desirable, all things considered? That means: performance, usability within a large team, time constraints, etc...

    This thread is not about a specific problem, or about a generic events system to use everywhere; it's about providing implementation ideas for when you do end up in those situations. There's value in sharing these sorts of strategies with the community, because it might help people out.

    Worst case scenario; it makes us have these discussions, which I really appreciate
     
    Last edited: Apr 17, 2022
  23. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    This is from a super old project.
    Code (CSharp):
    1.    
    2.     public struct DataValue: IAnimationValue
    3.     {
    4.         public Vector3 CenterPointCoordinates { get; set; }
    5.         public int TimeInSeconds { get; }
    6.         public half RotationInDegrees { get; set; }
    7.  
    8.         public unsafe fixed int ArrayOfValues[8];
    9.         public byte State { get; set; }
    10.     }
    You can access the fixed array in a job. Accessing the fixed array in parallel from many jobs obviously doesn't work, but as I said multi-threading an array of ints is nonsense, right? Overhed of threading it is larger than processing it on a single thread. If you want array inside of arrays. For example booleans. Then you can fill this fixed array with uints where 1 is true and 0 is false.

    1011 = true false true true

    or maybe in a game of 10 players use number from 0-9

    edit, and this is burst compatible code since the size of the array is predetermined
     
    Last edited: Apr 17, 2022
  24. hippocoder

    hippocoder

    Digital Ape Moderator

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Read everything from before then, and was struck early on by "why would I need this?" syndrome since I felt it was an anti-pattern to DOTS doing it this way. I didn't want to talk because I am still learning DOTS.

    The other remark somewhere was that asset store is not suited to DOTS based assets, which is of course absolute nonsense because you can be in the market for a polished character controller, which has it's own challenges, regardless if DOTS is used or not, and things like that are a great fit.

    Not really convinced by events, but suspect this will be part of a greater asset you've got in mind?
     
    Krajca likes this.
  25. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    Yes the same with neuc reactive extensions, which I have been using. Its good, but after the namespace changes unity made it stopped working as a plugin inside my plugin. used few days of rewriting code and now planning to get rid of it completely.
     
  26. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    No plans for other assets at the moment, but event systems are something that helped me efficiently solve problems i encountered while making a prototype game that involved projectiles, damage, spells, abilities, buffs, stats that depend on other stats, lots of "reactive" behaviour, etc... A bit like a roguelike/RPG shooter where all character/weapon/spell stats can scale with buffs & other factors, and where all kinds of skills can have complex OnReceivedDamage effects.

    I'm not sure I'd have the energy to explain the problems in detail, but what I can say is that there were several cases where I've compared an "events" approach to other "classic ecs" approaches, and the events approach was a clear winner for both performance and ease of use

    Most of the time it's because it unlocks possibilities to do more stuff in parallel
     
    Last edited: Apr 17, 2022
  27. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    The most important thing is that if it makes you more productive and makes your life better then its already the best solution. As long as it satisfies other specs
     
  28. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @DreamingImLatios

    I'll try to give you specific answers based on one prototype project I made, which involves the kind of RPG Attributes/Abilities systems you'd find in MOBAs, RPGs, Roguelikes, RTS, etc....

    • Depends what we mean by "effect". I suppose the individual "building blocks" of effects will have a set number of different types (FireDamage, IceDamage, Confusion, AddToAttribute, ClampAttribute, Poison, Heal, etc..), but we can mix n match those procedurally during gameplay to create effects that have more than one of those building blocks.
      • Each instance of an effect on an entity can have unique values that can change throughout its lifetime
      • There can also be multiple different instances of the same effect type affecting the same entity (one weak poison effect + one strong poison effect + one poison whose strength scales with your current move speed).
      • The stacking rules is up to each effect type and could depend on character Attributes.
      • The effect instances must be identifiable based on some sort of unique ID, so they can be individually removed based on certain rules (duration, instigator decides when to remove, disappears when damaged, etc...)
      • ....So almost none of this can be pre-determined
    • Undefined (because of point above)
    • Unlimited. We want to have crazy roguelike scenarios where your character has >100 different class/item/ability effects affecting it
    • Typically that would be the Instigator Entity that created the change, as well as "DamageType" so we can apply the correct defense reduction to it, and trigger any reactive damage behaviours that are specific to that damage type. And then we need to make sure it'll be easy to add any other data we might end up needing later as the design progresses

    Intermediate components don't solve the problem when we need to record data specific to each individual change on an Entity during a single frame (Instigator, DamageType). Unless that intermediate component is a DynamicBuffer, in which case that's basically an events system (Approach A or B)

    And that DynamicBuffer cannot be kept on the Projectile Entity or in a global hashMap if we want to do archetype-specific effect processing; it has to be on the hit Entity.

    ______________________________

    Overall it's hard for me to find a better solution to this than events that add effects to buffers. And this sort of Attributes/Ability system is something we find very frequently in games
     
    Last edited: Apr 18, 2022
    lclemens and mikaelK like this.
  29. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Perhaps it may be time to start a new thread or PM focused on complex yet real problems. I guess anyone is welcome to start a thread on a real design problem they are facing and mention me to get my attention. Just be prepared because I will ask lots of questions.

    Edit: Wrote the previous part and then you responded just before I hit "post reply".

    People spend a lot more time designing for hypothetical use cases that aren't ever realized than they do redesigning systems because of changed requirements. This is especially true in DoD and with ECS architectures. When designing, you are going to draw specific interface boundaries somewhere. With DOTS, those are usually components on entities and particular system ordering constraints. Learning how to come up with good interfaces is something that comes with practice and experience. But once you have these interfaces in place, designing the details for a particular problem within those interfaces is something most people struggle when trying to learn DOTS. They no longer have all their design patterns and other OOP tools at their disposal. Instead, there's a whole different set of patterns and tools with no formal names or books or anything like that. I happen to know quite a few of them, but I find it hard to teach them without real concrete use cases.

    If by "events" you mean stuffing stuff into a container to be processed later, then yes. I do that sometimes. Specifically, I will do that if it has the least cost towards the primary bottleneck of project. Or if that cost is negligible, requires the least amount of effort to implement. But "events" are a fundamentally independent domain. I use the term "event" as a semantic for a piece of data to describe the role that data plays in the solution. Other types of semantic terms include "settings", "caches", "references", "state", "stats", "tags", ect.. The transformation is independent to the concept. So there may be "events" which may be components, or they may be special structs stored in a container, but there is no "event system" in my code.

    So, if I am understanding this correctly, you have a damage type, which has a nearly infinite set of possibilities. So it could be a string identifier, but more likely it is an int identifier. And a damgee during a single update identifies a damagee and attempt to apply damage of a specialized type. However, at that point, the system does not want the responsibility of calculating the effects of health. That should be handled by separate systems which require write access to the damagee's components.

    So right now, you are storing "damage events" into containers because you don't have write access to the damagees when you are identifying "damage events". But so far, you have not specified anything requiring write access to the damagers. Yet it seems you are iterating over damagers to identify damagees. What if we flipped that logic such that damagees discover damagers instead? (Or to reference an old thread some might remember, what if trees feed the animals rather than animals eat the trees?)

    There might be reasons why that isn't possible, but you only answered a fraction of my questions, so I'm still left making tons of assumptions about your problem.

    Edit: It was stupid for me to participate in this thread. A lot of what I have said is being misinterpreted and isn't helping anyone, which means I need to stay out of this discussion.
     
    Last edited: Apr 18, 2022
  30. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Sorry about that, I was editing my post while you answered. I've tried to add more details
     
    Last edited: Apr 18, 2022
  31. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    New attempt at explaining. I kept re-editing my previous post again so I think it's safer to just post a new reply instead

    Let's consider the "adding effects to hit entities" scenario;
    • On top of damage, bullets can also add effects to their hit targets
    • An effect can have an update (ex: a poison effect that constantly applies damage), but some effects don't (ex: an effect that boosts Strength attribute)
    • I want effect updates on their targets to be chunk-iterated (because effect updates will have to access potentially a lot of component data on the entity they are associated to), and to be able to benefit from change filtering (because non-updating effects might need to recalculate things when their values change)
    • I need each effect instance to be individually-identifiable, because we could remove them individually based on all kinds of rules,
    • An entity can have 2+ effects of the same type affecting it, and they can have different values despite being of the same "type"
    • because of the previous points, I store effects in a dynamicBuffer on their target entity and have them keep a unique Id. I also give them an [InternalBufferCapacity(0)] so as to not bloat size in chunks
      • Effects could be a good candidate for PolymorphicStructs, so they'd be held in a single buffer. Especially useful if there is a need to apply effects in the order they were added. I'd have to experiment with it and see if it sticks. Downside is not having a job per effect, so all Component Data access must be merged into one job
    • In order to add those effects to buffers from all the different parallel jobs that could add them, I need "events" like Approach A
    • Why not create/Destroy Entities for effects? Well, that is a story too long to tell. It mostly has to do with efficient Attribute recalculation from attribute-modifying effects. But also, the performance hit could be bad in certain scenarios (imagine an effect-applying trigger zone moving through an army, or an effect-applying research in an RTS game that simultaneously applies an effect to all units you possess)
    But we could also not care about effects and only consider damage:
    • I have an actor that needs to return a % of all received damage to the instigator of that damage
    • That % can depend on all sorts of data held in components on that actor as well as the damage instigator actor (attributes, current health, Team, etc...)
    • That % also depends on the "type" of damage (ex: return physical damage but don't return magic damage)
    • So every time we apply damage, we must have access to data that's specific to that damage instance (Instigator, and DamageType)
    • Many different jobs can create damage
    • Let's consider what this could look like if we didn't use events. We could be using a static ApplyDamage() function that damager jobs would call for instant damage processing:
      • In a game with about 20 different damage-reactive behaviours, this function would have to get passed probably over 20 CDFE as parameters, and contain the implementations of 20 damage-reactive behaviours in a switch statement, all in one place.
      • In order to understand which damage-reactive behaviour(s) we have to execute, we'd have to iterate on a DynamicBuffer<DamageBehaviour> on the hit entity, and run that switch statement for each DamageBehaviour in that buffer, getting all sorts of ComponentDataFromEntity on the target entity along the way.
      • As soon as at least one of our damage-reaction behaviours can't be processed in parallel (very likely to happen), all of our damager jobs, as well as all damage-reactive logic, would be restricted to being single-threaded and all execute one after the other.
      • If damage has to be applied at a specific point in the frame (ex: when defense attributes have been properly recalculated, after all effects have been applied, etc...), then you need to make sure all your damaging jobs update at the correct point in the frame. Not only is this likely to be the cause of many hard-to-figure-out bugs due to human error, but it could also be impossible, since certain damaging jobs would be constrained to the FixedStepSimulation group, which would most likely update before the point where it's safe to apply damage. The only way to defer damage without using an "events system" in those cases would be to store all individual damage requests in a buffer on the damager entities and execute them later with ApplyDamage(), but there is a performance price to pay for this. It's almost the same price we'd pay for adding "damage events" to buffers on target entities (remember; this is all likely to be constrained to happen in single-thread now because of the previous point), except this has the disadvantage of not being able to execute damage reactions in parallel.
      • Overall, this approach is rather ugly, user-unfriendly, poorly-maintainable, and filled with limitations & pitfalls. Performance-wise, it might be equal to an "events" approach at best, but likely worse in many cases due to lack of parallel damage reaction logic. We can do much better
    • By using damage "events" like Approach A, we can make sure all damage events affecting the same entity will end up in the same place (dynamicBuffer)
    • Then, each damage-reaction behaviour can have its own chunk-iterated archetype-specific job, all executed in parallel
    • Some of these damage reaction jobs might need to be single-thread (like the "return damage to sender" behaviour), but since each behaviour has its own job, it doesn't prevent other behaviour jobs from being executed in parallel
    • If using events Approach D instead, we'd have much of the same limitations as the ApplyDamage() approach: switch statement over every possible behaviour, lots of CDFE, forced to be single-threaded, etc... but at least we'd be solving the systems ordering problem, and we'd be able to split damage logic into several jobs
    Not only do events give us better performance potential, but it also gives us a much easier & more elegant solution. If your game doesn't have damage happening that often and you don't care much about damage performance (which, to be fair, should be the case for most games), events still have the usability advantage over the "big ApplyDamage() switch statement with 20 CDFE" approach
     
    Last edited: Apr 22, 2022
    JesOb likes this.
  32. TheOtherMonarch

    TheOtherMonarch

    Joined:
    Jul 28, 2012
    Posts:
    791
    Code (CSharp):
    1. public struct unitHealth : IComponentData
    2. {
    3.     public float isBleedingTimer; // start with 0
    4.     public float isPoisonedTimer; // start with 0
    5.     public int health; // start with  100
    6. }
     
  33. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Wouldn't satisfy these requirements:
     
    Last edited: Apr 18, 2022
    Walter_Hulsebos likes this.
  34. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    908
    For anyone who says there is no reason for an event system or anything related: The goal is not to write an event system, it's just a pattern/word we, as devs, are known how to associate. A unit damaging another unit, is an event, if you like it or not or jump up and down, it won't change it how we would express this in the English language.

    The goal is to write the fastest way to apply damage from one group of entities to another entities health in the fastest way possible, in parallel and with no physics. This is clearly stated in OPs post. Either this or something very similar to it. I can think, on the spot of many game mechanics that are similar to this.

    So, to anyone who has a problem with this now, for whatever reason, and especially to @DreamingImLatios, I beg of you, write some code and stop posting about philosophies and theory. It's honestly quite annoying to read sooo much vagueness and "problems" about it, with little code. You're coders, express yourself in code. I don't want to irritate anyone, it just feel like it's more helpful to talk in code than in long walls of texts.

    I understand some of you are not fans of event systems, I agree, pretty much fully, but the alternatives have to be better than using Interlocked or CDFE in the systems where the damage is calculated.
    If there are no better solutions I think it's quite unfair to belittle PhilSAs works and disrespectful to engage in these long, are event systems now helpful and valid in DOD or not. It derails the thread in very uninteresting ways.

    You're turning the problem on its head, expecting that it would get easier. If you have a spatial system and triggers that is a valid solution but in this case there are no triggers, nor spatial system.
    So, the answer is, transform the DamageEvent data in a NativeMultiHashMap with the target as key. Iterate over the damagers, lookup if there's a key, iterate over the available DamageEvents and apply.

    This has several problems. First, NMHM writes in parallel are slow. Iterating over multiple entries for a key is slow. Transforming a linear array of DamageEvents into a NMHM is slow. Allocating the correct size of NMHM is impossible to know in advance.
    Iterating over 500k damagers with 2 DamageEvents makes no sense.

    Maybe you are on the right track and my implementations just never were good enough. Or, turning the problem on its head doesn't solve anything really.

    I attached timings and code for this implementation

    upload_2022-4-18_6-12-17.png
     

    Attached Files:

    Last edited: Apr 18, 2022
  35. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    I made a new event system based on the I. So basically all I did was to make the hashmap writer parallel.
    This is the I on my machine.
    upload_2022-4-18_14-15-21.png

    This is the new I

    upload_2022-4-18_14-15-54.png

    So it looks faster, but I'm unsure if ther results are correct since I just modified the job to work as parallel writer and call oncomplete afterwards. Its all the time I can spare atm
    Code (csharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using Unity.Transforms;
    8.  
    9. public partial class ParallelWriteToStream_ParallelPollHashMapAsParallelWriter_System : SystemBase
    10. {
    11.     public NativeStream PendingStream;
    12.     public NativeMultiHashMap<Entity, DamageEvent> DamageEventsMap;
    13.  
    14.     protected override void OnCreate()
    15.     {
    16.         base.OnCreate();
    17.         DamageEventsMap = new NativeMultiHashMap<Entity, DamageEvent>(500000, Allocator.Persistent);
    18.     }
    19.  
    20.     protected override void OnDestroy()
    21.     {
    22.         base.OnDestroy();
    23.         if (PendingStream.IsCreated)
    24.         {
    25.             PendingStream.Dispose();
    26.         }
    27.         if (DamageEventsMap.IsCreated)
    28.         {
    29.             DamageEventsMap.Dispose();
    30.         }
    31.     }
    32.  
    33.     protected override void OnUpdate()
    34.     {
    35.         if (!HasSingleton<EventStressTest>())
    36.             return;
    37.  
    38.         if (GetSingleton<EventStressTest>().EventType != EventType.ParallelWriteToStream_ParallelPollHashMap2)
    39.             return;
    40.  
    41.         EntityQuery damagersQuery = GetEntityQuery(typeof(Damager));
    42.  
    43.         if (PendingStream.IsCreated)
    44.         {
    45.             PendingStream.Dispose();
    46.         }
    47.         PendingStream = new NativeStream(damagersQuery.CalculateChunkCount(), Allocator.TempJob);
    48.  
    49.         Dependency = new DamagersWriteToStreamJob
    50.         {
    51.             EntityType = GetEntityTypeHandle(),
    52.             DamagerType = GetComponentTypeHandle<Damager>(true),
    53.             StreamDamageEvents = PendingStream.AsWriter(),
    54.         }.ScheduleParallel(damagersQuery, Dependency);
    55.         Dependency.Complete();
    56.         Dependency = new WriteStreamEventsToHashMapParallelJob
    57.         {
    58.             StreamDamageEvents = PendingStream.AsReader(),
    59.             DamageEventsMap = DamageEventsMap.AsParallelWriter(),
    60.         }.Schedule(PendingStream.ForEachCount, 8, Dependency);
    61.  
    62.         int threadCount = 16;
    63.         Dependency = new ParallelPollDamageEventHashMapJob
    64.         {
    65.             DamageEventsMap = DamageEventsMap,
    66.             HealthFromEntity = GetComponentDataFromEntity<Health>(false),
    67.             ThreadCount = threadCount,
    68.         }.Schedule(threadCount, 1, Dependency);
    69.  
    70.         Dependency = new ClearDamageEventHashMapJob
    71.         {
    72.             DamageEventsMap = DamageEventsMap,
    73.         }.Schedule(Dependency);
    74.     }
    75. }
    76.  
    77.  

    The Jobs.

    Code (csharp):
    1.  
    2. [BurstCompile]
    3. public struct WriteStreamEventsToHashMapJob : IJob
    4. {
    5.     public NativeStream.Reader StreamDamageEvents;
    6.     public NativeMultiHashMap<Entity, DamageEvent> DamageEventsMap;
    7.  
    8.     public void Execute()
    9.     {
    10.         for (int i = 0; i < StreamDamageEvents.ForEachCount; i++)
    11.         {
    12.             StreamDamageEvents.BeginForEachIndex(i);
    13.             while (StreamDamageEvents.RemainingItemCount > 0)
    14.             {
    15.                 StreamDamageEvent damageEvent = StreamDamageEvents.Read<StreamDamageEvent>();
    16.                 DamageEventsMap.Add(damageEvent.Target, damageEvent.DamageEvent);
    17.             }
    18.             StreamDamageEvents.EndForEachIndex();
    19.         }
    20.     }
    21. }
    22.  
    23. [BurstCompile]
    24. public struct WriteStreamEventsToHashMapParallelJob : IJobParallelFor
    25. {
    26.     public NativeStream.Reader StreamDamageEvents;
    27.     public NativeMultiHashMap<Entity, DamageEvent>.ParallelWriter DamageEventsMap;
    28.  
    29.     public void Execute(int index)
    30.     {
    31.         StreamDamageEvents.BeginForEachIndex(index);
    32.         while (StreamDamageEvents.RemainingItemCount > 0)
    33.         {
    34.             StreamDamageEvent damageEvent = StreamDamageEvents.Read<StreamDamageEvent>();
    35.             DamageEventsMap.Add(damageEvent.Target, damageEvent.DamageEvent);
    36.         }
    37.         StreamDamageEvents.EndForEachIndex();
    38.     }
    39. }
    40.  
    41.  
     

    Attached Files:

    Last edited: Apr 18, 2022
  36. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    I agree with this, whit the exception of deliverable assets. If you get a product from asset store or a game you are only interested that it works as fast as possible or fast enough. There are a lot of code that the developer will never touch, if the product works well, such as math utilities and other low level checks.

    To save myself those situations I have adopted this model, where and when I need to optimize something to as far as possible, I just copy the readable version of the function, so I have return point when I need to change something.

    For example, if you purchase my targeting system plugin. Do you buy it because you want to develop it or do you buy it as a service?
     
    PublicEnumE likes this.
  37. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    Yea its getting little off track, I actually would have liked for someone to comment on the results that I god. My profiler showed huge difference and I'm interested if @PhilSA can verify the results and hopefully update the post.

    "I'd say that buying someone else's code, and using it in your project without understanding exactly how it works is dangerous. In my experience, that's asking for bugs and surprise production problems late in development."

    Yes, unless you have integrations tests for the plugin. Using few days or even few weeks on writing those is better than using few years on developing a tool that you need to make your game come true. Most people I guess just drop the game idea as impossible to implement.

    "I used to be more open to using AssetStore assets. But over time I've one a 180 on that front, based on bad experiences. I'm still happy to buy the assets, and I've very glad such talented people are producing them. But these days we will typically only use them as reference, to figure out how to solve a problem ourselves."

    The most successful games actually have a hug list of assets they use. You can see them on the splash/ending screen.
    Do you really want to use your time on developing terrain shaders, asset hunters, debuggers, tree system or my targeting system for your game or just focus on building the game itself.
     
  38. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    908
    The problem with this test is that you know how much you need to allocate for the NMHM.

    Add just one more DamageEvent and the hashmap will be full in parallel. In parallel there's no resizing.
    I think it's a difficult problem to solve. How do we know, before writing in a loop how much to allocate?
    There could be a solution but right now I think it' a deal breaker because every solution adds more overhead and it's not that fast to begin with.

    Also, if you post screenshots, expand the Jobs and let us see beautiful green lines.
    In the first screenshot, it's hard to see but I can make out some blue lines which would indicate that burst is not enabled.
     
    mikaelK likes this.
  39. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    ah yes my bad, I'm used to the hierarchy view. I found it much easier to read. It shows more relevant information.

    new images,

    Old no parallel hash write:
    upload_2022-4-18_17-23-48.png
    new with parallel write:
    upload_2022-4-18_17-24-27.png

    Still there is a massive difference in speed.

    Also

    "I think it's a difficult problem to solve. How do we know, before writing in a loop how much to allocate?"

    You can allocate some memory based on the games scale.
    Also if the event buffer is huge there is a memory impact, but yo don't need to go trought the fole buffer. You can keep index of how many events are in the buffer and then just process those.

    Also, if the index goes over the buffer, then you can resize the buffer based on prediction (works at least with native arrays and lists).
     

    Attached Files:

    Last edited: Apr 18, 2022
  40. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    My bad; I posted the results of the final parallel hashmap approach in this thread, but forgot to actually push the real approach code to the repo. The code that was there was just WIP stuff that didn't really work

    Here's the final implementation:
    System
    WriteToHashMapParallelJob
    ReadHashMapParallelJob

    It does this;
    1. single-thread job ensures capacity of hashmap
    2. parallel job writes stream events to hashmap
    3. parallel job reads events from hashmap, using a custom job type for iterating hashmaps in parallel in a way where all the same entities will end up on same thread. There used to be a similar job type in older entities versions, but @tertle provided a new version here
    So there are no sync points required here
     
    Last edited: Apr 22, 2022
    mikaelK and hippocoder like this.
  41. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    Weird. It looks like the version what I posted is still a lot faster.
    Player loop 4ms vs 8 ms
    upload_2022-4-18_19-45-7.png
     
  42. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I think I'd have to see what your version of "ParallelPollDamageEventHashMapJob" does. If it's the one I had before I just pushed, that was WIP code that didn't really work (I think.... can't really remember at this point)
     
  43. mikaelK

    mikaelK

    Joined:
    Oct 2, 2013
    Posts:
    281
    It's little different. The new one has Ensure hash map capacity.
    Code (CSharp):
    1.         if (!HasSingleton<EventStressTest>())
    2.             return;
    3.  
    4.         if (GetSingleton<EventStressTest>().EventType != EventType.ParallelWriteToStream_ParallelPollHashMap2)
    5.             return;
    6.  
    7.         EntityQuery damagersQuery = GetEntityQuery(typeof(Damager));
    8.  
    9.         if (PendingStream.IsCreated)
    10.         {
    11.             PendingStream.Dispose();
    12.         }
    13.         PendingStream = new NativeStream(damagersQuery.CalculateChunkCount(), Allocator.TempJob);
    14.  
    15.         Dependency = new DamagersWriteToStreamJob
    16.         {
    17.             EntityType = GetEntityTypeHandle(),
    18.             DamagerType = GetComponentTypeHandle<Damager>(true),
    19.             StreamDamageEvents = PendingStream.AsWriter(),
    20.         }.ScheduleParallel(damagersQuery, Dependency);
    21.         Dependency.Complete();
    22.  
    23.         Dependency = new WriteStreamEventsToHashMapParallelJob
    24.         {
    25.             StreamDamageEvents = PendingStream.AsReader(),
    26.             DamageEventsMap = DamageEventsMap.AsParallelWriter(),
    27.         }.Schedule(PendingStream.ForEachCount, 8, Dependency);
    28.         int threadCount = 16;
    29.         Dependency = new ParallelPollDamageEventHashMapJob
    30.         {
    31.             DamageEventsMap = DamageEventsMap,
    32.             HealthFromEntity = GetComponentDataFromEntity<Health>(false),
    33.             ThreadCount = threadCount,
    34.         }.Schedule(threadCount, 1, Dependency);
    35.  
    36.         Dependency = new ClearDamageEventHashMapJob
    37.         {
    38.             DamageEventsMap = DamageEventsMap,
    39.         }.Schedule(Dependency);
     
  44. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    Pushed another update:
    • Added a sync point at the end of the frame, so we better see the jobs in order in the profiler (no jobs done over multiple frames), and so the tests are more representative of a typical project
    • Renamed the enums to include the approach letter
    • Added two new bonus approaches:
      • Direct single-threaded modification: this is a simple job that directly modifies health from entity for each damager. It serves as a point of reference for comparison, but since the use case in the test is too simplistic to really let event systems shine, it's not really a fair comparison
      • Manual Monobehaviour Update: this is an approach similar to what @gentmo suggested earlier. I knew Monobehaviour Update() was terribly slow, but I never expected it to be THAT slow compared to a manual update
    I've reprofiled everything and re-took cleaner profiler snapshots, and I think the sync point changed things. Maybe some of the approaches were secretly benefiting from the lack of sync points, but I'm still trying to figure it out. Most notably: Approach H and I are slower than they used to be

    EDIT: The slower H and I were actually because I replaced single-thread hashmap write with parallel hashmap write. In this case, the parallel write is slower (my version of it at least). It'll be something to investigate further. I've restored the single-threaded job now

    The two new approaches also serve as a comparison between bursted non-linear ComponentDataFromEntity modification VS OOP data modification through reference. We often say ComponentDataFromEntity is slow, but compared to the OOP version of doing the same thing, it's pretty comparable. In fact, if you look at the profiler snapshot of each, the actual OOP update cost is 3.53ms, and the actual DOTS update cost is 3.0ms, so in this case the ComponentDataFromEntity approach does things faster than OOP (thanks to burst I suppose)

    I really should be measuring things more precisely instead of measuring total frame time, but not sure I have the time to do all that work

    EDIT: I've updated the post with more accurate measurements that only measure the actual time of the approach in the frame; not total frame time
     
    Last edited: Apr 19, 2022
    gentmo, mikaelK and DreamingImLatios like this.
  45. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I've let the discussions of the last few days simmer in my mind a little bit, and I think I now have much simpler conclusions to draw out of all of this:

    I really think the most useful "event systems" are when you end up in a situation where you want "call a function" on a target entity, but that function must be processed differently based on the archetype of that target entity. That function can also be called multiple times per frame, and have parameters specific to each call. So that would be Approach A or B. Buffer elements on the target entity represent the various "function calls with parameters" it received, and the processing of these functions can be done by one or more archetype-specific jobs. It's a bit like calling a polymorphic function in OOP, or like invoking a delegate with multiple listeners.
    • You want an entity to execute all of its unique assigned OnDamaged behaviours when damaged (the behaviours of each entity can change during gameplay)
    • Each OnDamaged behaviour type holds different kinds of instance data (some have just a float, some have a prefab reference and 3 floats, etc...)
    • An entity can have multiple OnDamaged behaviours of the same type, each with different instance data (ex: a "AddTimedEffectToSelf" for an effect A for 1s, and a "AddTimedEffectToSelf" for an effect B for 4s)
    • None of the instance data values of the OnDamaged behaviours can be pre-determined, since individual OnDamaged behaviours can be "upgraded" and customized during gameplay, and they also depend on Attributes of the character they are associated to (Level, Intelligence, etc...)
    • Each OnDamaged behaviour needs to react to individual damage events; not just the cummulative damage on this frame (ex: for a "ReturnFireDamageToSender" behaviour, we need to know the Source entity, DamageAmount, and DamgeType of each individual damage event)
    • Each OnDamaged behaviour type potentially needs access to different kinds components to execute its logic, and there will be dozens of these behaviours
    • Creating new OnDamaged behaviours needs to be an easy and hassle-free process for the development team

    Another reason is if you want to modify data on a target entity from parallel jobs, but those modifications could be really heavy and complex, and so they would benefit from being done in parallel. Approach A or I allow you to "sort" those events per entity to make this possible in another job.

    Then there's event systems that can help make APIs of common tasks simpler to execute (no need to get any CDFE or create sync points or worry about update order), but I think that's pretty much it
     
    Last edited: Apr 20, 2022
    mikaelK likes this.
  46. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    939
    There are also things you want to happen once if hit (play hit animation/particule effect) and other you want per hit (increase aggro of attacker).
    An event can "spawn" other events.
     
  47. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    For the record... I learned a ton from this thread - you all have some great insights!

    I just remembered that last year tertle talked about the concept of event patterns in his event system thread post. https://forum.unity.com/threads/event-system.779711/page-5#post-7603249 . I basically replied that tagging is too heavy, to which tertle replied and described something that I believe closely matches Approach C, as a better performing alternative to his NativeStream based event system. I replied back that I didn't like that fact that "C" is doing so much polling, and then we got into a discussion about change filters. What I didn't know at the time, and it is probably obvious to most of you, is that the NativeStream based approaches are also doing polling. Haha stupid 2021 me!

    Another interesting thing of note: DapperDino uses Approach C in his damage tutorial from back in the day:
    . I'm probably not the only one whos first exposure to damage application systems was that video ;)

    When I first read PhilSA's post here: https://forum.unity.com/threads/com...or-events-in-dots.1267775/page-2#post-8054318 I was assuming he was only talking about the native-stream versions and thinking to myself that he's advocating an "Anti-DOD" pattern. However, after re-reading it, his definition of "events" also includes the built-in-unity approaches such as Approaches C and F. I now realize that he was referring to the abstract concept of a "many-to-many-trigger" and not necessarily an exact event implementation, so the post is not as controversial as I had first thought.

    When it came to writing an ability/spell system, my first thought was to use tertle's events, but because I respect tertle so much and he nudged me to avoid overuse of his event system for gameplay, I went with Approach C. All the great info in this thread was not available back then. Damage/stat application accumulation/finalization uses dynamic event buffers and I have separate entity instances for each spell/ability. Each spell/ability has a dynamic buffer of impacts so I can send impact events from any thread or system group. I got a lot of inspiration from these two reddit threads:
    Not everything in those two threads was a good idea so my first attempt was pretty crappy, but I ended up rewriting it and just finished the second rewrite today! Overall I'm very happy with how it turned out and I don't have any regrets for using approach C. One thing I like about the approach is not having to use CDFE, which in my humble opinion, keeps it clean. I'm disappointed that it's obviously not the fastest one on the block, but if there is a future need, it can be swapped out later. So now I have plasma beams that follow targets, physics projectiles that lob fireballs and set enemies on fire with area splash damage, fast linear non-physics bullets, abilities that spawn other abilities, slow-effects, elemental resistances, particle effects, sound effects, and a bunch of other goodies that I can mix and match to create new and interesting ability combinations. Adding new mechanisms is pretty straightforward - usually it's just one new system and an options component. I doubt it's as cool or advanced as PhilSA's, but hey, it's a start!

    P.S. It will be interesting to see what sort of stuff we can come up with if/when the "Enabled state" filtering makes it into a release. https://forum.unity.com/threads/dots-skill-system-repo-available.894007/#post-5875804 .
     
    Last edited: Apr 22, 2022
    Walter_Hulsebos, PhilSA and Opeth001 like this.
  48. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    I'd like to point out for 99% of use cases and users this solution is way faster than what you'll need it. Very few games are running around firing 100k events/frame.

    Usability and maintainability can not be underestimated.
     
    Last edited: Apr 22, 2022
  49. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,068
    I would be very interested to see the Ability/System you use, as in the next sprint I'll start writing my own.
    I already have an idea of how I'll implement it but reading multiple approaches always gives more options and tips.
    Obviously if the package you are using is open source :)
     
    lclemens likes this.
  50. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    In fact, if we think about damage events, the very large majority of games won't have more than a dozen of these in a single frame tops (like when an explosion happens in a group of enemies). Even a game like Vampire Surviors, which gives the impression of having a crazy amount of damage events, probably reaches 200-300 damages in a frame tops. By DOTS standards that's a very tiny amount and it would most likely be a waste to do them in parallel

    Discussions in this forum can often have an almost cult-like devotion to theorizing about the best-performing thing in the worst-case scenario (I am very guilty of that), and I imagine it can falsely lead a lot of people to think that every problem in DOTS needs a ton of thinking & analysis in order to be solved. But really that's not the case at all, and most approaches here would be totally fine for most games.

    The level of thinking we're doing here would only be necessary for a Diablo-like game that has armies of enemies the size of what you'd find in a "Total War" game. Or something like the 2017 DOTS Nordeus demo perhaps, if every enemy can have a wide variety of skills that react to damage
     
    Last edited: Apr 22, 2022
    carl010010, lclemens, Krajca and 2 others like this.