Search Unity

Problems with a "health regeneration" system

Discussion in 'Data Oriented Technology Stack' started by Daragorn, Dec 26, 2018.

  1. Daragorn

    Daragorn

    Joined:
    Feb 24, 2014
    Posts:
    34
    I am trying to understand ECS but i am having a lot of issues in managing to get my way around it and have it perform even the easiest of tasks without having to write a ton of redundant/duplicate code and i don't get if it is the ECS that is supposed to work that way or it is just me not knowing how to use it efficently (most likely :-D)

    So, let's say i am just trying to make a simple regeneration system that regenerates x amount of points every second.
    As of now i created a health component as this:
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public struct Health : IComponentData
    4. {
    5.     public int max;
    6.     public int maxRegen;
    7.     public int current;
    8.     public int currentRegen;
    9. }
    10. public class HealthComponent : ComponentDataWrapper<Health> { }

    A health regeneration job as this:
    Code (CSharp):
    1.  [Unity.Burst.BurstCompile]
    2.     struct HealthRegenJob : IJobProcessComponentData<Health>
    3.     {
    4.         public void Execute(ref Health stat)
    5.         {
    6.             if (stat.current >= stat.max)
    7.                 return;
    8.             stat.current += stat.currentRegen;
    9.             if (stat.current>stat.max)
    10.                 stat.current = stat.max;
    11.         }
    12.     }
    13.  
    14.     float lastTime;
    15.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    16.     {
    17.         JobHandle deps = new JobHandle();
    18.         if (Time.time > lastTime + 1)
    19.         {
    20.             lastTime = Time.time;
    21.             deps=new HealthRegenJob().Schedule(this, deps);
    22.         }
    23.         return deps;
    24.     }
    25.  
    And that's, obviously, working perfectly fine.
    Now lies the problem.
    Let's assume i want to have many other similar stats (stamina, mana, hunger, thirst, whatever). The only solution i managed to get working is just copy/paste the health/healthjob and rename it to mana/manajob, etc etc.
    It sounds to me really ineffective having to rewrite the same exact code X amount of times if i want to have X components doing the same stuff.
    Not only i will end up fast with tons of system doing the exact same thing but then, let's say i want to introduce new systems later on that can modify the regen per turn: i would have to create, likewise, X amount of "modify regen" systems each for every single stat.
    That is really clumsy and definitely a mess to keep track of. If, one day, i decide to modify the way the regen system work i would have to remember to change each and every single of those copied systems or i'd end up with stats working in different ways because i forgot to update their respective systems.

    Is there a way to "generalize" the job and have it work for whatever amount of stats?
    With OOP i could just create a generic "stat" class, implement a generic regen method and then create infinite amounts of different stats deriving from the base class and then just call the Regen() method of the base class for each of them....but how can i do that in ECS?
     
  2. e199

    e199

    Joined:
    Mar 24, 2015
    Posts:
    99
    Generic system ( <T> that stuff )
    Those won't be added into player loop automatically, until you derive from them with non generic class or add that generic system by hand into player loop
     
  3. Daragorn

    Daragorn

    Joined:
    Feb 24, 2014
    Posts:
    34
    Can you explain me a bit better how to do it in ECS? I know how to do that in OOP, but i can't get it in ECS.
    I tried to make a generic component and derive from it as this:
    Code (CSharp):
    1. [Serializable]
    2. public struct Stat: IComponentData
    3. {
    4.     public int max;
    5.     public int maxRegen;
    6.     public int current;
    7.     public int currentRegen;
    8. }
    Code (CSharp):
    1. [Serializable]
    2. public struct Health : Stat
    3. {  }
    But it doesn't work because it tells me "Stat" is not an interface (which i can't understand, since it is an IComponentData and i should be able to do that)
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    6,500
    Just brief thought, where you could.handle all in one system.

    First you would need add groups, health, stamina etc.
    System will trigger, if entity has anny of it.

    Then I OnUpdate pass.these groups, and their respective components.

    In job execute, you just need check, if given entity has a required component, and pas it through if loop. You could use in such case IJobParallelFor instead.

    Does that makes sense?
     
  5. Daragorn

    Daragorn

    Joined:
    Feb 24, 2014
    Posts:
    34
    If i understand correctly you are saying that i could just create a generic IParallelJobFor, inject in it all the "staminagroup","healthgroup", etc etc and then, inside a single job do something like if entity has component health then do stuff, etc etc?
    It would not be much different than having to write n single jobs for the n stats, so in the end it will still be a ton of copy/paste code you have to write and keep track of for doing the same stuff on just differently named components.

    But, so there is no way in ECS to have different components that have the same behaviour be dealt with a single system without having to copy/paste the same structure/job multiple times?

    The "best" solution i could came out with was to create a generic "base" struct, containing the variables and a method that does the regen, then create n "different" components for each of the stats with just a base struct as data and, finally, in the n jobs i will just call the regen method from the base struct.

    This is the base struct:
    Code (CSharp):
    1. using System;
    2.  
    3. [Serializable]
    4. public struct Stat
    5. {
    6.     public int max;
    7.     public int maxRegen;
    8.     public int current;
    9.     public int currentRegen;
    10.  
    11.     public void Regen()
    12.     {
    13.         current += currentRegen;
    14.         if (current > max) current = max;
    15.     }
    16. }
    17.  
    Each stat is just this, with a different name:
    Code (CSharp):
    1. using System;
    2. using Unity.Entities;
    3.  
    4. [Serializable]
    5. public struct Stamina : IComponentData
    6. {
    7.     public Stat Value;
    8. }
    9. public class StaminaComponent : ComponentDataWrapper<Stamina> { }
    10.  
    And then the regeneration systems looks like this:
    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.Jobs;
    3. using UnityEngine;
    4. using UnityEngine.Experimental.PlayerLoop;
    5.  
    6. [UpdateAfter(typeof(FixedUpdate))]
    7. public class RegenerationSystem : JobComponentSystem
    8. {
    9.     [Unity.Burst.BurstCompile]
    10.     struct HealthRegenJob : IJobProcessComponentData<Health>
    11.     {
    12.         public void Execute(ref Health stat)
    13.         {
    14.             stat.Value.Regen();
    15.         }
    16.     }
    17.  
    18.     [Unity.Burst.BurstCompile]
    19.     struct ManaRegenJob : IJobProcessComponentData<Mana>
    20.     {
    21.         public void Execute(ref Mana stat)
    22.         {
    23.             stat.Value.Regen();
    24.         }
    25.     }
    26.  
    27.     [Unity.Burst.BurstCompile]
    28.     struct StaminaRegenJob : IJobProcessComponentData<Stamina>
    29.     {
    30.         public void Execute(ref Stamina stat)
    31.         {
    32.             stat.Value.Regen();
    33.         }
    34.     }
    35.  
    36.     float lastTime;
    37.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    38.     {
    39.         JobHandle deps = new JobHandle();
    40.         if (Time.time > lastTime + 1)
    41.         {
    42.             lastTime = Time.time;
    43.             deps=new HealthRegenJob().Schedule(this, deps);
    44.             deps = new ManaRegenJob().Schedule(this, deps);
    45.             deps = new StaminaRegenJob().Schedule(this, deps);
    46.         }
    47.         return deps;
    48.     }
    49.    
    50.     protected override void OnStartRunning()
    51.     {
    52.         lastTime = Time.time;
    It still feels cluncky and definitely not an easier or clearer way to write code, honestly, compared to OOP....but i couldn't think of a better solution.
     
  6. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    6,500
    How many different stats are you planning to have?
    6, 10, 20?
    I think, once you have then, then you never need really look to them back.

    The main difference is, you will have all in one job. But yes you would need run through 'if' checks, in such case.
    I am not saying is most elegant, or best solution. But one of multiple options.

    Alternatively, you could store your stats data in native arrays, or buffer arrays, while given option for more flexibility. But would require bit more coding.
     
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,908
    Structs can't inherit from anything but interfaces in c#.

    Thrown together in notepad++, might have typos but you should get the idea

    Code (CSharp):
    1. public interface IModifier : IComponentData
    2. {
    3.     float GetValue();
    4. }
    5.  
    6. public interface IStat : IComponentData
    7. {
    8.     float Value { get; set; }
    9. }
    10.  
    11. SubtractModifierFromStatSystem<TM, TT> : JobComponentSystem
    12.     where TM : struct, IModifier
    13.     where TT : struct, IStat
    14. {
    15.     private struct Job : IJobProcessComponentData<TM, TT>
    16.     {
    17.         public void Execute([ReadOnly] ref TM modifier, ref TT stat)
    18.         {
    19.             stat.Value -= modifier.GetValue();
    20.         }
    21.     }
    22. }
    -edit-

    forgot IJobProcessComponentData doesn't like generics atm, probably need to do something like

    Code (CSharp):
    1.     private struct Job : IJobChunk
    2.     {
    3.         [ReadOnly]
    4.         public ArchetypeChunkComponentType<TM> ModifierType;
    5.            
    6.         public ArchetypeChunkComponentType<TT> StatType;
    7.     }
     
    Last edited: Dec 27, 2018
    e199 likes this.
  8. Daragorn

    Daragorn

    Joined:
    Feb 24, 2014
    Posts:
    34
    I don't know yet, as it is now i am still more in the process of trying to get a grasp on ECS than really planning out the game itself :)
    Even though it might be a better way to code, performance wise, it still seems to me way more intricated than OOP, resulting in a lot of copy/pasted stuff to have the same behaviour on different "copies" of similar systems.
    I have put all the regen jobs in a single regeneration system so at least they are a bit more concise and all in the same place instead of having x amount of different systems doing the same stuff with a different name. It is a bit more easier to keep track of what happens, this way, but still feels clunky.
    As far as the "once you have them you never look back", well the problem with having to have copy/pasted code is exactly that: when you decide to change something in the behaviour, you have to copy/paste it to all the copied systems using it.
    For now the best solution i have found is the one i described above, with basically making a base struct with methods and keep the logics in there so at least if i need to change something i can just change it there.

    That's what i am going to try now, i wanted to just make a single "vital" component with an array of stats so that for different entity archetypes i can have 1 or more stats and then do a for loop for the stat array lenght in a single regen job.