Search Unity

Is that good approach to implement timer?

Discussion in 'Entity Component System' started by eterlan, Apr 4, 2019.

  1. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Hello, I want to implement a timer in ECS. What I find out is that I can use struct in the component, so for the reason of code reuse, I write a time system, which update some values in a timer struct in a timers component. Then I can use this timer in other system. The reason why I am doing this is I guess I need more than one timer on an entity?

    Code (CSharp):
    1. public struct Timer
    2. {
    3.     public float elapsedTime;
    4.     private float duration;
    5.     private ByteBool running;
    6.     private ByteBool started;
    7.    
    8.     public ByteBool Started
    9.     {
    10.         set { started = value; }
    11.         get { return started; }
    12.     }
    13.     public float Duration
    14.     {
    15.         get { return duration; }
    16.         set
    17.         {
    18.             if ( value > 0 && !Running )
    19.             {
    20.                 duration = value;                
    21.             }
    22.         }
    23.     }
    24.     public ByteBool Finished
    25.     {
    26.         get { return Started && !Running; }
    27.     }
    28.  
    29.     public ByteBool Running
    30.     {
    31.         get { return running; }
    32.         set { running = value; }
    33.     }
    34.    
    35.     public void Run()
    36.     {
    37.         elapsedTime = 0;
    38.         Started = true;
    39.         Running = true;
    40.     }
    41. }
    Code (CSharp):
    1. public struct Timers : IComponentData
    2. {
    3.     public Timer timer;
    4.     // other Timer can be added if required.
    5. }
    What confused me is this looks like an OOP approach and there is some logic in the properties in the timer struct in component. It works, but I want to know whether that's "correct"?

    I first use an straight forward way to deal with time, and that's not reusable at all...
    Code (CSharp):
    1. if (sth happen)
    2. {
    3.     if (isInitialize)
    4.     {
    5.         //record Time
    6.         isInitialize = false;
    7.     }
    8.     if (Time - recordTime > duration)
    9.     {
    10.         //do sth
    11.         isInitialize = false
    12.     }
    13. }
    I'm not an experienced developer, so any suggestion would be appreciated! Also I am wondering is there any better implementation? :p
     
  2. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    Hi well there speaks nothing against this approach.
    I'ts valid to provide some methods that deal with members of this struct. Just pay attention to dependencies to other classes / structs...

    There are different ways to handle multiple timers.

    Here are some others:

    Use of Dynamic buffers
    Code (CSharp):
    1.  
    2. struct Timer : IBufferElementData {
    3. //...
    4. }
    5.  
    6. struct TimerJob : IJobChunk {
    7.             [ReadOnly] public EntityArchetype EntityArchetype;
    8.             [ReadOnly] public ArchetypeChunkBufferType<Time> TimeBufferType;
    9.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    10.  
    11.                 var bufferAccessor = chunk.GetBufferAccessor(TimeBufferType);
    12.                 for (int i = 0; i < chunk.Count; i++) {
    13.                     var timerBuffer = bufferAccessor[i];
    14.                     for (int j = 0; j < timerBuffer.Length; j++) {
    15.                         var timer = timerBuffer[j];
    16.                         timerBuffer[j] = //...
    17.                     }
    18.                 }
    19.             }
    20.         }
    21.  
    22. Or use:  GetBufferFromEntity<Timer>()
    23.  
    Generic approach
    Build a generic timer class and a generic system.
    Than create specific systems and components for that.


    Code (CSharp):
    1.  
    2.     struct Timer<T> : IComponentData {
    3.         public float elapsedTime;
    4.         private float duration;
    5.         private bool running;
    6.         private bool started;
    7.         //...
    8.     }
    9.  
    10.     public class TimerUpdateGroup : ComponentSystemGroup { }
    11.  
    12.     [UpdateInGroup(typeof(TimerUpdateGroup))]
    13.     public abstract class TimerSystem<T> : JobComponentSystem  where T: struct {
    14.  
    15.        struct TimerUpdateJob : IJobProcessComponentData<Timer<T>> {
    16.             public void Execute(ref Timer<T> c0) {
    17.                 // do timer update...
    18.             }
    19.         }
    20.  
    21.         protected override JobHandle OnUpdate(JobHandle inputDeps) {
    22.             return new TimerUpdateJob().Schedule(this, inputDeps);
    23.         }
    24.     }
    25.  
    26.  
    27.     //somewhere...
    28.     EntityManager.AddComponentData(entity, new Timer<MySpecificTimer>());
    29.  
    30.     public class MySpecificTimerSystem : TimerSystem<MySpecificTimer> { }

    I would prefere the generic approach. Because I can define Timers for specific components and I only need to create a specifc TimerSystem<MySpecificTimer> for that class. It also gives me full flexebility to extend my timer system and I know which timer is for which data.

    EDIT:
    Added TimerUpdateGroup System for a better topology

    EDIT:
    Fixed typo in the TimerUpdateJob...
    Use Timer<T> instead of T in Job definition...
     
    Last edited: Apr 4, 2019
    Neil-Corre and eterlan like this.
  3. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Thanks for your help!:D It take me a while for thinking the generic approach, because I thought one system should take care all similar things. It's good to know generic is valid, never thought of that but it looks very interesting!
     
  4. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    The generic approach won't violate the the "One system should take care all similar things" principle.
    Indeed it supports it.

    Because, you can define specific timers for specific tasks, components, ... like a timer for Animations, Actions or Delays...
    Therefor you can easly create systems for that. And the best thing you won't have duplicate code.

    You can also extend the behaviour of your timers. Just override OnUpdate and call base.OnUpdate somewhere in the function.

    It's also better for debugging and extend things...

    Imagen you would use the DynamicBuffer approach. Then you have an array of timers.
    So you should be aware of the position in that array, when adding, removing, debugging it...


    With the generic approach:
    If you want to stop a specific timer system. Just disable that system e.g. AnimationTimerSystem. -> You only disabled the Timer of animation and not the ones for delays or what ever else you have.


    EDIT:
    And one more thing... You can group all timers easly.
    Create a TimerGroupSystem and attach the UpdateInGroup(typeof(TimerGroupSystem)) attribute to the base class.
    Now everything stick together and it's clearly ordered
     
    Last edited: Apr 4, 2019
    Neil-Corre likes this.
  5. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Thanks! It's very nice of you teaching me so much! Learn a lot from your reply, wish you have a nice day!;)
     
    Neil-Corre, PanMadzior and Spy-Shifty like this.