Search Unity

Communication Between Systems

Discussion in 'Entity Component System' started by Zajoman, Mar 14, 2019.

  1. Zajoman

    Zajoman

    Joined:
    May 31, 2014
    Posts:
    15
    Hey, guys!

    First of all, I want to commend Unity on undertaking this huge task (DOTS) and on the impressive results so far. Great work!

    I think ECS is a good fit for writing games, but there is one concept that's pretty unclear to me. From what I gathered in this forum and Unity examples, the current plan to make systems communicate between each other is by adding / removing components to / from entities, so that the system that is expected to react to the change (or "event") will pick it up later and do its job, quite possibly raising another event by adding another component so that yet another system can do its job later on.

    This approach might work fine for simple things. But what about an RPG system with all kinds of active and passive skills, perks, buffs, and whatnot. These systems tend to be quite complex, and it's quite common for a single event to cause a chain of 3-4 more events down the line. Which, with this approach, might cause 3 issues:
    1. Changes in the footprint of entities (change of component setup), which I believe is not cheap.
    2. Possibly delaying the execution by quite a few frames, if the chain is long enough.
    3. Be quite cumbersome.
    Just to be clear, I'm not talking about C# events or Unity events for that matter. I'm talking about a logical in-game event. It might be implemented in many ways (direct function call for instance).

    Let's go through an example. You have a passive skill that says, "Return 50% of incoming damage back to the attacker." An orc attacks you. Therefore some AttackSystem needs to tell your player entity to take damage, so it adds a TakeDamage component to your entity. Depending on the order of the systems, this might get picked up the next frame by a DamageSystem. Okay, so the DamageSystem does a number on you, but it also needs to tell the SkillSystem that you just took damage, so the skills can possibly react to that. So let's remove the TakeDamage component from the player entity and add a DidTakeDamage component to it. The next time the SkillSystem goes through its entities, it will pick up that component and realize that, hey, we just took damage and I have one skill here that would like to react to that by retaliation damage of its own. Right, so let's remove the DidTakeDamage component from the player entity and add a TakeDamage component to the orc entity. Now, again, the DamageSystem will pick that up, quite possibly the next frame, and will act on it. Suppose the orc also has a passive skill that says, "Whenever you take damage, become enraged, dealing 50% more damage for 5 seconds."

    You get the point. And that's just a simple example. Some scenarios involve many more "events".

    I think you can mitigate this by putting your systems in an order that most RPG "events" flow, but because skills can be very different from one another, and sometimes very unique, you can hardly guarantee the order.

    So, is this the plan, to go the "component is an event" route? Or do you plan to allow some messaging between systems?

    I realize I'd probably be able to implement the RPG system in a more traditional way and have it live along the ECS if need be.

    Thank you!
     
  2. herdinstinct

    herdinstinct

    Joined:
    Jan 29, 2016
    Posts:
    7
    Very interesting question! Hoping this gets answered by someone sooner rather than later/never.
     
  3. Arnold_2013

    Arnold_2013

    Joined:
    Nov 24, 2013
    Posts:
    284
    I don't think there is an easy answer to this. It all depends on the game you are designing and how it works.

    If you need a specific order => schedule + set dependencies very strictly => potentially have some worker threads idle

    If you want a lot of data influencing a lot of other data => use a big component with all the data or use a lot of small components => lose performance on add/remove component/tag or lose performance because a system blocks the big component so other systems cant write to it.

    You can store data in components 'events' or you could have a DynamicBuffer with entities where each entity being an Event holding some data. Or store data in NativeArrays in systems and access these from other systems. There are a lot of possibilities and they all have pro's and con's. Personally I go for the 'easiest to code' solution and when the profiler shows me its slow I try to find a better solution.

    For events maybe this post will help, since it goes over a lot op possibilities and their performance impact. https://forum.unity.com/threads/comparing-different-approaches-for-events-in-dots.1267775/

    When comparing ECS/DOTS to OOP in terms of what is the best way to do something. Its good to realize OOP probably uses a bad performance version under the hood. So even if the OOP code looks simpel it will bounce around memory to pickup all the data, so if you use a lot of entity lookups in ECS the performance might still be better compared to OOP.
     
  4. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    This sort of thing is the main kind of use case that motivated me to create this tool:
    https://forum.unity.com/threads/sou...m-in-dots-now-with-source-generators.1264616/

    You'll sometimes encounter situations in DOTS where you're stuck between all kinds of solutions that all have big downsides:
    • Too many structural changes (when you add components as events)
    • Too many jobs that have to check for changes every frame (when your events are stored in DynamicBuffers)
    • Things can't all happen in the same frame (gameplay quality/responsiveness is sacrificed, and it might be a source of potential bugs if you're not careful)
    • Too many jobs required to implement something that should be simple (adds a constant performance cost, and makes it tedious to implement certain features)
    • etc.....
    PolymorphicStructs will often be the key to solving that sort of problem. For example, in this case, you could implement things like this:
    • Every actor has a DynamicBuffer<OnDamagedBehaviour>
    • OnDamagedBehaviour is a PolymorphicStruct that can represent any OnDamaged behaviour in the game. The "Return 50% of damage to sender" behaviour would be an example of that
    • When your DamageSystem wants to apply damage to something, it simply gets the DynamicBuffer<OnDamagedBehaviour> on the target entity, and executes all of them
     
    Last edited: May 9, 2022
    hippocoder likes this.
  5. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    I believe one of the things addressing this will be the ability to enable or disable a component in 1.0, which will lead to being able to keep all the components you need and use some as event triggers without structural changes. Any thoughts on what might improve through that?
     
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    There are cases where it'll be an improvement, but generally it replaces one problem with another: the punctual cost of structural changes is replaced with the constant cost of change polling on those enabled components

    I've done some tests with a prototype Ability/Effect system where each kind of effect in the game has its own change-polling job on an "enabled byte" on an effect component, and the constant cost of running all of these change-polling jobs really adds up fast. Especially in the context of network prediction where that cost is multiplied by x5-x10. With 50 different effects in your game, that's 50 jobs that must be scheduled every frame, or 500 in a network game. And just like that, a significant % (close to 1ms on my machine) of your frame budget is gone just for specifically checking if effects need to be processed.

    I think it would be a pretty good idea to combine "PolymorphicStructs" and "enabled components" though. Your Effects would be a polymorphic struct, but they would be processed by a change polling job instead of being directly executed by the damageSystem. So for 50 effects in your game, you now have 1 change-polling job instead of 50
     
    Last edited: May 9, 2022
    hippocoder likes this.
  7. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    959
    Sadly not. As someone who was a big fan of the idea of enabled/disabled components, mostly because it was suggested as a solution for what I was doing, the more experience I got, the less important it got, to the point where I'm thinking, why is this even built?
    IMO it will be even more abused than adding/removing components for events. Archetypes count will explode because everything is an entity, an arbitrary limit of 128 entities per chunk will be enforced and because it "works" and is "easy" devs will encourage the practice when the data should really live inside NativeContainers or Blob data.

    On topic of communication between systems. Systems shouldn't have the need to communicate at all, at least not more than sharing job handles or native containers.
    The idea of an AttackSystem, DidDamageSystem, TakeDamageSystem, etc... stems very much of OOP design and when the design changes, the need for system communication changes with it. More to the point, it vanishes.
     
    hippocoder likes this.
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,255
    While I do agree that the cost of scheduling jobs and updating systems hurts the viability of enabled components, I haven't seen anyone benchmark the concept of enabled components using tzcnt iteration (which is what enabled components almost certainly will be using). That's less operations than polling and can be done prior to loading in the actual component values into cache, making it much faster.
     
    hippocoder likes this.