Search Unity

Question ECS Buff/Debuff system

Discussion in 'Entity Component System' started by Chillu, May 30, 2020.

  1. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    Modifier = Buff/Debuff

    I'm currently jumping ship with my project from OOP to ECS. And want to change the old abstract decorate pattern to a new faster ECS one.

    How the current system works: Base abstract class Modifier has an Update function that the modifiers use to update their timers/apply over time effects, etc.
    And there's simple modifier classes like AttackSpeedModifier that implements Modifier and only cares about the values they use & how they use them. Like this:

    Code (CSharp):
    1. protected override void ApplyEffect()
    2. {
    3.     Target.AttackSpeed += Value;
    4. }

    I'm struggeling to find a proper ECS buff/debuff implementation with only using structs (value types). My current thoughts/approach are such:

    1. Make a seperate component for each buff:

    Code (CSharp):
    1. public struct ExampleFireAttackBuff : IComponentData
    2. {
    3.     public float Damage;
    4.     public float Cooldown;
    5. }
    The problem with this approach is that every modifier would need its own seperate system, & that would produce a lot of code duplication. And removing component every time the effect is done is also less than ideal.

    2. Or make a buffer of buff/debuffs and have a "master" element:

    Code (CSharp):
    1. public struct ModifierElement : IBufferElementData
    2. {
    3.     public ModifierType ModifierType;
    4.     public float Value1;
    5.     public float Value2;
    6.     public float Value3;
    7.     public float Duration;
    8.     public bool IsStackable;
    9.     etc...
    10. }
    The problem with the second approach is that I would need to implement functionality based on "ModifierType" or some other way. Instead of overriding base class. And there would be a lot of redundant data. A simple permanent speed buff would only use 3-4 variables, while the close struct would have much more because all other modifiers need them.

    Am I missing something, and/or is there a better way of making such an elaborate system? I'm not really happy with either of them currently.
    There's also a "ComboModifier" that triggers on specific sets of modifiers, but that's a topic for another day/another thought.
     
  2. Bivens32

    Bivens32

    Joined:
    Jul 8, 2013
    Posts:
    36
    I've recently designed a buff/debuff system and it pretty much uses the second approach. Yes, you have to write a system for each different ModifierType but it's really not bad once you just do it. After all, you just write one and then copy paste for all the other ones and change a few lines. I'm confused as to why you need a Value1, Value2, Value3, etc. My system just has 1 float value per modifier element.
     
  3. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    I think you may be overcomplicating things. You could put all your buffs into a single component, and put your buff applying code into the system responsible. Within your FireCasterSystem code, you can have an Entities.WithAny<FireCaster, BuffDebuff >().ForEach() that applies the appropriate buff/debuffs into your FireCaster component.

    Likewise you can also have a single BuffDebuffSystem for setting Buffs and Debuff values in the BuffDebuff Component.

    This makes things very reusable, as all you need to do to buff/debuff a new Component that you create is to add another Entities.WithAll<>.ForEach query to your system.
    For example Entities.WithAny<IceCaster, BuffDebuff>().ForEach().
    Coming from the OO world you may think you would want to have multiple buff/debuff components each matching to a system, but I actually suspect that you do not need this at all.

    Buff/debuffs are simply data, and the overhead for attaching a buff debuff component with many values set to 0 is minimal
     
  4. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    My problem with that approach is that the majority of buffs/debuffs would have a lot of variables they don't use. And use up space for no reason, which I find pretty bad in a dynamicBuffer.

    My almost full fledged variable struct would look like this:


    Code (CSharp):
    1. public struct ModifierElement : IBufferElementData
    2. {
    3.     public ModifierType ModifierType;
    4.     //"Tough" modifier values:
    5.     public float Value1;//Speed decrease on hit
    6.     public float Value2;//Added armor
    7.     public float Value3;//Slow on owner
    8.     public float Duration;
    9.     public float CurrentDuration;
    10.     public float IsStackable;
    11.     public bool IsEffectStackable;
    12.     public bool IsDurationStackable;
    13.     public ElementalType ElementalType;
    14. }
    Because I need all the functionality in the element. There's also modifiers that I want to end on the same time, that do multiple things with values. Let's say a "tough" modifier, that decreases speed on hit by Value1, ads armor to the owner by Value2, and decreases speed on owner by Value3.


    That's also the thing, I would much rather do the decorative approach. So I only have to override a OnEffect() function, and tell it what to do there. Instead of messing around, and defining every single effect based on a Enum.
     
  5. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    Ah, I see. But wouldn't that be worse than the buffer approach? Because removing components on/off is more troublesome & expensive than removing bufferElements. And that's the thing, I would like to not make a system for each type of buff, because it's not as elegant as the decorative approach. And I still wouldn't like for the components/bufferElements to have variables stored that they won't need/use.

    I haven't researched enough here, but isn't that pretty big? It would double my archetype size right now, if I had 4-5 buffs/debuffs on an entity.

    I guess You're suggesting to have a tagComponent here called FireCaster, and that would be an interesting approach to define what the buffs/debuffs do. But it would still make for a lot of redundancy & wouldnt be as clear as OOP approach mentioned above, and the component would have variables that aren't used, something that I would like to avoid.
     
  6. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    A very complicated, unsafe but maybe extremely flexible way is probably fake inheritance like Physics use.

    Won't go too much into detail as if you don't know what I'm talking about you probably should not even consider attempting it, but you would have a buffer that holds a base component, Buff, then using fake inheritance (reinterpreting) give each buff it's own behavior.

    Is it a good implementation? I don't know as I haven't attempted it, though i might at some point, but it's definitely unsafe.
     
    Last edited: May 31, 2020
  7. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    *Chunk, can't edit the comment because its "spam/inappropriate"


    Hmm. I will look into it, thanks, I remember seeing some reinterpreting docs before. Obviously would rather not mess with memory managment, but it might be worth it in the end.
     
  8. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Perhaps someone from Unity can clarify this for us, but when you use a DynamicBuffer, you have to specify a capacity. Presumably, you are allocating all the memory of that capacity size anyways in your ECS chunk. So I suppose simply putting all your buffs/debuffs into a (lets say there are 32 such buff/debuffs) into single component is about equally efficient as having a Dynamic Buffer of capacity 32 (or maybe even more efficient)?

    Not only that, but to retrieve data from a dynamic buffer, you have to loop through every item I believe. The only advantage a dynamic buffer has is that it can resize beyond its own capacity size, but once this happens, you can get cache misses which is something ECS was created to avoid in the first place.

    I think the appropriate way to think about this is treating your buffs as data, and its ok to store all this data into one place (your sometimes sparse Buff/Debuff "table"). The logic for what to do with this data should be left to your systems. This way you can create systems that might only be affected by one buff/debuff (perhaps the FireCasterSystem only uses the ImmuneToFire buff), but there are other systems that use multiple buff/debuffs (the FinalBossSystem is affected by all 32 buffs/debuffs)
     
  9. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    I'm not sure I'm following this right. With a single buff/debuff component it's impossible to have multiple of them on a single entity. That's why I would need to make a seperate component for each buff/debuff, or just use a dynamicBuffer.


    Isn't this 100% avoided with how Unity buffers work though?

    > To specify an “internal capacity" for a dynamic buffer component, use the InternalBufferCapacity attribute. The internal capacity defines the number of elements the dynamic buffer stores in the ArchetypeChunk along with the other components of an entity. If you increase the size of a buffer beyond the internal capacity, the buffer allocates a heap memory block outside the current chunk and moves all existing elements.

    Obviously not very performance nice, but that's why InternalBufferCapacity exists in the first place.

    I don't think I understand Your idea on how to make it into one component with each system doing logic to that set master buff component. Or rather, my biggest problem with that approach if I understand it, is that the approach would be very limited when You want an entity to have more than 1-2 functional buffs/debuffs. If I would want to have a Attack,AttacSpeed,Speed,Armor buffs all on the entity at the same time, with different values. I would need at least 4 value types, 4 different durations (or they all have the same duration), etc etc. I reaaallly might be understanding this wrong though
     
  10. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    I think I may be confused as to what you are trying to accomplish, but what I am suggesting is to put every single possible buff/debuff you can have in the game into a single component. You would not tack on buffs and debuffs as separate components, rather your BuffDebuff component would be the sole component needed to specify this type of information.

    To combine buffs/debuffs, you simply have systems that add/subtract/zero out values to Buff/Debuff component fields
     
  11. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    Oh yeah no, yeah, that would be a buff as well.
    But the current system is more "advanced" than that. It also allows for special types of attacks, aoe, buffing up a specific variable on set conditions, buffing specific allies every X seconds, etc. So I think it would need a more sophisticated than that.
     
  12. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    110
    I have, relative recently, started rewriting my code base to ECS, though it was really small one, but I came to a conclusion that if you look at your old code base, and try to map it to ECS approach, you gonna feel a lot more pain trying to figure out how to do it. Forgetting about old code base and thinking about features turned out to be more efficient in my case.

    Regarding your problem, have you thought about implementing something similar to what transform system group does in Entities? That way, you are basically building very few "output" components from that process that you latter use from various systems. Transforms systems create LocalToWorld and LocalToParent and nothing more I think. The single "massive" component that @larrydu88 proposed can be one of such "output" components. Or you can encode your data on a bit level flags, if feasible, so you maintain memory consumption.
     
    Chillu likes this.
  13. Chillu

    Chillu

    Joined:
    Aug 28, 2017
    Posts:
    7
    Yeah, I guess it's true. To not try to bring old different ways & systems to a new one.

    Hmm, but wouldn't those components mess up with the base chunk? Because if I add a new type of component here and there, the entities would get seperated. Unless I use dynamic buffers to store the data for those components that every entity will have, but then it might be just redundant redundancy and it might be better to just store everything in the buffer anyway.

    I'm fine with having a buffer of modifiers, might biggest problem would be that a lot of modifiers would have values they don't need and don't use.
     
  14. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    110
    Yeah, good point. I don't see a way to avoid structural changes if you take the approach where each "effect" adds/removes a new component in order to perform certain action, on health in this case.

    So I guess it all comes to a specific use case, frequency of usage etc. so you can decide what to use.