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

Question Zero-Garbage Timer solution

Discussion in 'Scripting' started by _watcher_, Sep 19, 2023.

  1. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    Hi all,

    What is your prefered solution to make timers in Unity? Start, Pause, Stop/Rewind, with triggers like OnStart, OnComplete, OnUpdate etc.

    I see some like to use Coroutines or WaitForSeconds (not garbage-free afaik), some use Update (garbage free, but hardly portable, only 1 timer per MonoBehaviour, ..), some even use various 0-gc Tweening solutions..

    Any suggestions for a reusable (instance) solution with 0-gc runtime impact you could recommend?

    Related (Coroutine caching): https://forum.unity.com/threads/c-coroutine-waitforseconds-garbage-collection-tip.224878/
     
    Last edited: Sep 19, 2023
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    I just use a small wrapper class that I can drive in whatever context it's needed. The only allocation is the initial instancing of it. I imagine that's what most folks use.
     
  3. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    What do you wrap with your class and where do you get your deltaTime (or source of tick) from and how?
     
  4. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    I once watched the Playdead prez on their game "Inside", and how they completely detached from the Unity's event system and created their own (they could plug the tick to any object type, like ScriptableObject with their new event pipeline [PreStart/PreUpdate/etc], which gave them their 0-gc footprint). But im just looking for something small, per-instance. Maybe id have to cache such instances, let's see if we can get some good suggestions here on implementation of these 'timer instances'.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    From memory, it wraps an initial starting time, threshold time, and the current time (all editable in the inspector), as well as different 'timer modes' - such as for timers that 'tick up', or 'tick down'.

    The delta is provided by the user in the main method to drive the timer. Something like
    public bool TickTimer(float delta);
    . It generally still gets driven via Update or FixedUpdate.

    If you want something detached from components then you need to hook into the player loop system: https://docs.unity3d.com/ScriptReference/LowLevel.PlayerLoop.html
     
  6. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    Ah i see, you are wrapping MonoBehaviour and using its/Unity's event system. I wanted something i could pass around and smth that doesn't sit in the displayList. Class instance or reference (cached is ok). Might need hundreds of these to be created dynamically and be running at the same time (and i dont want to create a MonoBehaviour to wrap for each). Something like a garbage-less coroutine. Except in case of coroutine, it incurs creation cost (which is fine, i can do with cached instance), but also trigger/invoke cost (which is not).

    Right now i figure the best solution for my use-case would be to use the 'Yielders' from the link i posted earlier - not ideal tho.
     
    Last edited: Sep 19, 2023
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    You don't need to make a monobehaviour to wrap each one... they're just plain C# classes so you use them in any context. But at the end of the day you need one entry point from the player-loop to drive something that drives whatever else needs to tick.

    That could just be a singular monobehaviour 'update manager' as is often used for performance reason. Though you could do this componentless with the player loop as already mentioned.

    The idea is to just wrap it all up into a reusable class than use it wherever necessary.
     
  8. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    If you only declare one timer, than yes, there would be only one. But you can have as many timers as your hearts content with using deltaTime.

    A simple example:
    Code (CSharp):
    1. float timer1;
    2. float timer2;
    3. float timer3;
    4. // etc...
    5.  
    6. void Update()
    7. {
    8.     timer1 += Time.deltaTime;
    9.     timer2 += Time.deltaTime;
    10.     timer3 += Time.deltaTime;
    11.    
    12.     if (timer1 > 2.0f) // 2secs
    13.     if (timer2 > 3.2f) // 3.2secs
    14.     if (timer3 > 5.1f) // 5.1secs
    15.     // etc...
    16. }
    Which can be used differently, especially within methods, or controlled by bools. Also don't forget to zero out the timer!

    As far as squeezing any micro-performance from the "Time.cs" some will argue to just cache your own variable of deltaTime. But I can't find any real documentation on the differences, as most things state it's a static call, so most likely caching it yourself would give no extra performance.
     
  9. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    gotcha. akin to what Playdead did. However then we have a strong coupling between manager and the instances. Aint that big of a deal i suppose, especially if there is no way to get the tick out of 'thin air'.
     
  10. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    Thanks for your post. Again, this assumes a MonoBehaviour unity event system present somewhere that drives it with tight coupling to the instances. I mean you could loosely couple them by subscribing to the manager at the end of the day, it still means the instances need to know the manager by reference. But yeah, it is possible to go 0-garbage with MonoBehaviour's Update. I have a solution like this in another project, with single Update tick and many IUpdate subscribers, even ScriptableObjects. However this is far from some type of 'fire and forget' self-contained, minimal, headache-free system id like to use now. If i could simply do something like
    Code (csharp):
    1. Timer myTimer = new Timer(myDuration);
    2. myTimer.addEventListener(TimerEvent.TIMER_COMPLETE, OnTimerComplete);
    and do that from anywhere, any time, not having to worry about managers/internal implementation/garbage etc, that would be just swell.
     
    Last edited: Sep 19, 2023
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    At some stage something has to hook into the part of Unity that runs around in circles.

    And it doesn't have to be a tight coupling. Any interface solves this.

    Like I said if you want to do this without components then use the player loop API. I've got a number of static delegates for different parts of the player-loop that I can hook into from anywhere so you could absolutely do the above.
     
    _watcher_ likes this.
  12. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    I misunderstood how the player-loop API works then. Thanks for repeating the suggestion, ill check it out! <3
     
  13. KyryloKuzyk

    KyryloKuzyk

    Joined:
    Nov 4, 2013
    Posts:
    1,070
    The simplest non-allocating approach for creating a timer is the MonoBehaviour.Invoke. As far as I remember, it was allocating in older versions of Unity, but just tested it with Unity 2022.3.10, and it doesn't allocate garbage anymore:
    Code (CSharp):
    1. public void StartTimer() {
    2.     const float duration = 0.5f;
    3.     Invoke(nameof(OnTimerComplete), duration);
    4. }
    5.  
    6. void OnTimerComplete() {
    7. }
    And to cancel the running timer, simply call
    CancelInvoke(nameof(OnTimerComplete));
     
    _watcher_ likes this.
  14. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    That still requires an instance of a monobehaviour, which may not always be present. Not to mention that it uses reflection, which can be pretty poor performance wise.
     
  15. KyryloKuzyk

    KyryloKuzyk

    Joined:
    Nov 4, 2013
    Posts:
    1,070
    I would say that in 99% of cases where a timer or delay is needed, you're doing it from the MonoBehaviour.
    Performance is very subjective, and you have to actually measure it to understand if something suits your use case. I just did the test on Macbook Pro M1, and it's 57 ms to start 100,000 Invoke() calls, and 18 ms when they are all complete at the same time. And while running, they are almost free (0.23 ms). So if your project needs a couple of hundreds of timers, using Invoke() is perfectly fine.
     
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,862
    That's dependant on personal architecture preferences and requirements. A vast majority of the time I'm operating in pure C# land without monobehaviours, and going by OP's previous posts they were looking for something capable of that too. It's quite useful be able to 'tick' stuff without having to have a monobehaviour reach all the way down and fondle a few methods just to make things turn.
     
  17. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    259
    Thanks for the suggestions guys.
     
    KyryloKuzyk likes this.