Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Alterantive to Coroutines for timers and cooldowns?

Discussion in 'Scripting' started by GeriB, Apr 8, 2021.

  1. GeriB

    GeriB

    Joined:
    Apr 26, 2017
    Posts:
    192
    Hi,

    Lately I've been hearing a lot of arguments against Coroutines, which is fair. But at the same time Coroutines are great, they make things super easy and intuitive.

    So I was wondering. How do you do Timers in your projects? Whats the best way to wait for a few seconds and then do something?

    I tend to use Coroutines and in some cases even the infamous Invoke. I can't really bother to do Update() logic to handle this sort of thing, it's not very flexible and it's extremely verbose.

    Is there any "best alternative" the people that complain against Coroutines use?
     
  2. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    I would ask them. If they don't have one you like, their complaints are not worth taking seriously. And, really, just what are these complaints? I love coroutines for the exact reasons you mention: easy, intuitive, flexible, and compact.

    For a delay, I just use the WaitForSeconds class:
    Code (CSharp):
    1.     StartCoroutine(DoAfterDelay(1.5f, () => Debug.Log("Done!")));
    2.  
    3. IEnumerator DoAfterDelay(float delaySeconds, Action thingToDo)
    4. {
    5.     yield return new WaitForSeconds(delaySeconds);
    6.     thingToDo();
    7. }
     
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,517
    Really? I mean REALLY!?

    Code (csharp):
    1. void Update()
    2. {
    3.    if (gunheat > 0)
    4.    {
    5.      gunheat -= Time.deltaTime;
    6.    }
    7.    if (PlayerTriesToShoot())
    8.    {
    9.       if (gunheat > 0)
    10.       {
    11.            // cooling still, no shoot
    12.       }
    13.       else
    14.       {
    15.            /// shoot!
    16.            gunheat = 1.0f;  /// cooldown time
    17.       }
    18.    }
    19. }
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    I personally use Update/Coroutines a bunch. I have yet to have a single issue with either.

    I guess you could alternatively use 'async/await'. The newer 4.x .net targeting has enabled it. Though honestly when it comes to the synchronization context and all that, I've yet to actually USE it in Unity. I primarily use async/await in my day job (business software). So as it comes to it "just working out of the box" I don't know the status of it. Early on you had to implement your own syncrhonizationcontext, then I know some asset store things popped up... word was Unity would add direct support "out of the box" but I don't know the status on that. I'm still back on an early Unity 2018 build without the 4.x .net support (just a couple more weeks and we'll be moving to projects with newer versions of unity).

    Anyways, maybe someone could give you more specifics on the status of async/await working in Unity without issues (note there can be invisible issues in the background). But once you got it working (may you have to import some asset or be on a specific version of unity) it'd be as simple as:

    Code (csharp):
    1. public async Task DoSomething()
    2. {
    3.     await Task.Delay(1000); //wait 1 second
    4.     this.transform.position = Vector3.zero; //do something, in this case move to origin
    5. }
    Though I mean... performance wise I'm unsure about it compared to Coroutines. I'm going to guess it's more efficient (again haven't used async/await in unity specifically). But it's going to have its own overheads as well for sure.

    ...

    Alternatively, you could also write some wrapper logic that uses Update, but makes it straightforward/easy to use.
     
    GeriB, Munchy2007 and Kurt-Dekker like this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Question though...

    Why are you looking to avoid Coroutines?

    You seem to like them:
    Who cares about other people's opinions?

    If they work for you, and you're getting the performance you need from Unity. Why fix what ain't broke?
     
    stain2319, Munchy2007 and Kurt-Dekker like this.
  6. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    Async Await works fine.

    I think it might actually be slower than a coroutine, due to the fact that async await is designed to automatically sync thread contexts and unwrap tasks. I think there is a cost that you pay there for that convenience even if you aren't using it to return results from other threads. I might be wrong though.

    Coroutines apparently take unity timescale into account. Async Await won't. Definitely something to keep in mind.
     
    Last edited: Apr 9, 2021
  7. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    I often use Update for simple timers. It might be a couple extra lines of code, but it is really simple to keep track of exactly what is going on each frame. If the number of timers themselves is variable, or the timer is rather complex, then I'll default to coroutines.
     
    Kurt-Dekker likes this.
  8. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    Some really interesting perspectives here! I think Kurt-Dekker's example shows how integrating cooldown into Update can be easy and sensible, particularly when "TriesToShoot()" is (I assume) a polling method that necessarily needs to be called on each Update.

    Now, if code that TriesToShoot conditions were replaced by an event-driven call to a method (which might well be the case if one were using the Input System instead of the Input class), moving the cooldown out of Update and into its own coroutine looks appealing to me. The coroutine could run until gunheat <= 0, then exit. That avoids constantly checking gunheat in Update, over and over, when it's going to be zero a lot of the time. If one is counting CPU cycles, this is all small potatoes, and one method is as good as another, really. If one has an Update method that begins to accumulate a lot of conditionals, that might be another story. I like coroutines as an alternative there, but it makes just as much sense to break the "God" class containing that Update into multiple small classes, each of which can have its own (much cleaner, smaller) Update method.

    I tell my students that Update is probably the way to go when something will need to run repeatedly, often, and for the lifetime of the object it's in (like TriesToShoot, for example), and that coroutines might be the better bet when something happens from time to time, and for only a limited time.

    I rarely use async/await, but I have done a lot of multithreaded coding. Something else I find I have to tell my students is that coroutines are not multithreaded. (The students who have done any concurrent coding tend to see the similarities between that and coroutines, and some of those students get really anxious about synchronization and cache-coherence issues that just don't exist with single-threaded coroutines.) If you can do anything to get your mutli-threaded code to run on another core than the main thread, you get some real gains there. If the async/await stuff is running on the same core, I expect it would slightly degrade performance as, albeit light, there is some overhead in switching thread contexts.

    Keep it coming! I love this kind of shop talk (when you work at home, alone, it's all there is).
     
    Opaweynch and Kurt-Dekker like this.
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,517
    I also tend to favor code with extremely clear flow paths... I just ASSUME code will fail and have bugs, so when I'm writing it I am always thinking,

    "Where can I put the breakpoints?"

    ... always... It's like a mantra. I know that I am GOING to need breakpoints, that's not even negotiable. Make it easy now, air the code out vertically, one thing at a time (which btw is all the CPU is going to be doing!), and step, step, step.

    I have used Linq but trust me, there are no chained().on().things().in().my(codebase);
     
  10. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,910
  11. GeriB

    GeriB

    Joined:
    Apr 26, 2017
    Posts:
    192
    They allocate garbage and aren't very fast. Which to be honest is ok by me. I am just really curious and willing to learn about some alternatives. Thanks for your response btw :D
     
  12. GeriB

    GeriB

    Joined:
    Apr 26, 2017
    Posts:
    192
  13. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    What do you have in mind?
    What makes you say so?
     
    Kurt-Dekker likes this.
  14. GeriB

    GeriB

    Joined:
    Apr 26, 2017
    Posts:
    192
    Doing new yield for WaitForSeconds(n); allocates memory. And StartCorutine ain't free.

    Which to be honest is ok by me. I use Coroutines all the time. I was just really curious and willing to learn about some alternatives :D

    This thing looks great and works great if you want to take a look. Is more flexible than Coroutines too: https://github.com/Cysharp/UniTask
     
  15. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    If you mean
    yield return new WaitForSeconds(n);
    , you are correct. But you can use a single WaitForSeconds object as many times as you want.
    What is?

    You still haven't answered my earlier question:
     
  16. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,517
  17. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    Man, if you ever do, please post it. I used to be a politician, which was when I learned that Einstein was wrong when he said nothing moves faster than light. All politicians know that nothing moves faster than bullshit. Gamedevs probably know this too.

    I'm a little frustrated that our OP hasn't really addressed this. Just what are these complaints against coroutines?
     
    Kurt-Dekker likes this.
  18. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    I think you should avoid starting coroutines from a semi hot loop and absolutely not from a hot loop. Thats about it.
     
  19. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    I'm not familiar with these terms. What are a "semi hot loop" and a "hot loop?"
     
  20. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    I am curious about how UniTask works. The main page at the github repo says this:
    But the code includes lots of await instructions. Await forks and creates a new thread.
     
  21. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Starting from update every frame is a hot loop.

    Semi hot could be for example in a multiplayer game. For example in our game we delay the audio for firearms to compensate for the speed of sound. Since you can't control when players fire thee guns this is a semi hot loop. Edit: though this example is best solved with PlayDelayed
     
  22. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Invoking any function which yields incurs a memory allocation, since under the hood an instance of a hidden class is created to hold the iteration state machine.

    But tasks and lambdas also allocate, so if your goal is zero allocations things are a bit harder and more restrictive.

    One practical issue with coroutines is that they stop running altogether if you set the timescale to zero, so they are out of question for stuff you need running while your game is paused.
     
  23. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Async/await does not necessarily require additional threads, even though the example may suggest it.

    The overall idea is to say "do that ... I'll wait here and proceed when you're done". The caller doesn't and most importantly shouldn't really care about how things are done internally, though you could of course configure it and/or allow the caller to pass additional options/preferences.

    Anyway, an implementation of an async methods can of course make use of threads internally, other might use different mechanisms to "run" and "complete" their work. Threading is absolutely no requirement for async stuff.

    However, since the caller doesn't know anything about what happens behind the scenes, that's why it's generally better to have an additional synchronization context (at least in environments where it matters ... like Unity). You never know who completes the task, nor where, when and how. The synchronization context ensures tasks can complete as one desires - for example, on the main thread in Unity, since continuations which access the engine's API have to be run on that particular thread.

    Without the sync context, you'd be fine when it completes on the main thread. Without it, you'd have to take care of it each time a task completes, which you generally shouldn't do as it clutters the caller's code, it's also error-prone and generally way too difficult or at least too much effort to document all the details.
     
    MDADigital likes this.
  24. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Its incredible how much misconception there is around async await
     
  25. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    By golly, you're right! This code all runs on one thread:

    Code (CSharp):
    1. void Main()
    2. {
    3.     ID.p("In");
    4.     As();
    5.     ID.p("Out");
    6. }
    7.  
    8. static class ID
    9. {
    10.     public static Action<string> p = (n) => Console.WriteLine($"                {n} {Thread.CurrentThread.ManagedThreadId}");
    11. }
    12.  
    13. async void As()
    14. {
    15.     ID.p("Before");
    16.     await new C();
    17.     ID.p("After");
    18. }
    19.  
    20. class C
    21. {
    22.     public A GetAwaiter()
    23.     {
    24.         ID.p("GetAwaiter");
    25.         return new A();
    26.     }
    27. }
    28.  
    29. class A : System.Runtime.CompilerServices.INotifyCompletion
    30. {
    31.     public void OnCompleted(Action a)
    32.     {
    33.         ID.p("OnCompleted");
    34.     }
    35.  
    36.     public void GetResult()
    37.     {
    38.         ID.p("GetResult");
    39.     }
    40.    
    41.     public bool IsCompleted
    42.     {
    43.         get
    44.         {
    45.             ID.p("Get");
    46.             return true;
    47.         }
    48.     }
    49. }
    Here's the output:


    In 16
    Before 16
    GetAwaiter 16
    Get 16
    GetResult 16
    After 16
    Out 16
     
    Suddoha likes this.
  26. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    Incredible? Why?
     
  27. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    They are not that advanced. But people even believed that threading was involved with coroutines so its not so strange misconceptions exists for async await too :)
     
    Suddoha and Kurt-Dekker like this.
  28. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    I will try harder to keep up.

    Could you point me to a good reference on resumption delegates? I'm finding that a bit murky.
     
    Last edited: Apr 10, 2021
  29. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Stephen Cleary is a good resource on anything Task related.
     
  30. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    upload_2021-4-10_11-42-56.png
    Any other ideas?
     
  31. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198

    Im just guessing here

    Code (CSharp):
    1. public async Task MyAsyncMethod()
    2. {
    3.      var x = somecode;
    4.  
    5.      await awaitSomeCode();
    6.    
    7.      var y = codeHereGoesIntoTheResumptionDelegate();
    8. }
     
  32. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    To be fair, if you had asked me some years ago, I would have said the same, just by spontaneous intuition à la "I have never actually bothered / thought about that / looked deeper into it, but if you need an answer right now, my initial guess would be this and that".

    One has to be pragmatic sometimes, and if it's not really important whether or not it's actually doing XYZ (in this case, whether it's always threaded, never threaded, or varies)... well, use it and just continue, investigate later if it's interesting enough or simply required to know about it.
    A principle that applies to so many things. It's generally not bad to care about stuff like that, but sometimes it's the typical can of worms so that it's better to skip and come back later.

    That's also what I did with coroutines in the early days of my journey with Unity, because there were far more important fundamentals to understand for me at that time...
    One day though, I actually started to care when I saw a post in the forums about issues with coroutines, and how coroutines work behind the scenes. It was actually a post by @lordofduct some 6 or 7, maybe 8 years ago... despite his lengthy post in which he sort of tried to explain half of the universe, the key information were so interesting that it caught my attention and inspired me to finally take on that beast and understand/master that concept as well. I felt so silly when I realized it's no rocket science but just a smart way of using a language feature. And suddenly, speaking about metaphoric universes, it felt like I discovered yet another dimension in the world of Unity.

    It also turns out there are way too many explanations about what async really means in general, in different technical contexts, specifically in IT terminology, in software engineering, in a particular language... just google it, and you'll be surprised about all the different views.
     
    MDADigital and lordofduct like this.
  33. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    I just feel that something as fundamental as code execution is something you as a develop need to know :)
     
    Kurt-Dekker likes this.
  34. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    Please, go on.
     
  35. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Ye, but what are the "fundamentals of code execution" (with regards to async/await or coroutines).

    I know very decent developers who have never cared about the inner workings. They know how to use these tools, they know how their coroutine executes, how the async methods execute. They do a great job.

    Sure, whether or not coroutines are threaded is something one should know early on. Whether or not async/await uses threads - it can, it doesn't have to so basically you wouldn't need to care - that's the whole point of this black-box feature. In the end that's an implementation detail of the async method in question.

    That's where you can already stop caring unless you need to. It doesn't mean someone's not a good developer. I wouldn't expect anyone to be able to explain coroutines in-depth as long as he can use that tool when it makes sense.

    And seriously, the standard use case is writing an async method or a coroutine, and await something that already exists / yield something that already exists, i.e. an async method / yield instruction that does what you want.

    That's not a big deal, that's fundamental, and that's all you need most of the time and that's what a developer should definitely know.

    But if you want to go deeper, like writing your own async methods that do not simply wrap around existing awaitables, but which actually implement the async behaviour based on your preferred async technique - that's a whole different story and no longer fundamental IMO.
     
    lordofduct likes this.
  36. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    I feel that something as fundamental as code execution (of course, I'm not 100% certain what exactly you're referring to as code execution and only gleening based on context that you mean how the underpinnings of a feature actually work once compiled) is something you learn over time as it's not a necessity to getting the job done, but is a nicety that helps develop your skills in the long run.

    Like... I learned assembly for a few different processors, which aside from machine code, is about as bare metal as one can get. And it certainly has shaped my skills as a developer. But I wouldn't say it's something anyone getting into game development necessarily "needs to know".

    This isn't to say one shouldn't learn such things. Far from.

    As Suddoha just put it:
    Knowing or not knowing such things does not stand in between you and being a good developer. Rather it just shapes how you may approach developing. Cause at the end of the day... you could spend 3 life times learning every in and out of computer science... but if you're not writing anything, what was the point? (of course an exception is made to those who are literally getting doctorates in computer science an pursuing a more academic route... but we're talking in the context of programming right now)

    TLDR:

    I don't need to know how to make paint to be a good painter. But knowing how to make paint wouldn't hurt and may alter the way I approach painting in interesting ways.
     
    Last edited: Apr 10, 2021
    Suddoha likes this.
  37. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    If you don't know how your async code executes it's just random chance if the result will be good / behave as you think it does. Sure in most cases it probebly will turn out OK.

    For same reason I don't understand developers that just use existing code without checking how it is implemented. I see this often in my day job.
     
    Kurt-Dekker likes this.
  38. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    To what extent does one need to know though?

    I would argue not knowing if my await may or may not thread or not is less consequential than not reading code one copy pasted from a stack overflow or other source.

    The implications of the 2 are vastly different.

    In the await situation... at the end of the day I know that my code after this call to await will not execute until the task I'm awaiting is completed. Say I'm awaiting a unity method that downloads data... that doesn't matter to me. It only matters that the download completes and that I get the result. May unity do that on a thread or not doesn't change how my code is written... I'm awaiting its completion. It's the entire point of the async/await design is to encapsulate the "how things are working" behind a simple interface that I interact with in a consistent and predictable manner. If down the line Unity decides to change how it downloads stuff it doesn't matter... my code continues working because all I'm doing is awaiting the result of the task. And I would hardly call that random.

    Where as copy pasting code off of stackoverflow with out reading it has all sorts of problems that could arise. Security issues, bugs, not even knowing if it actually does the thing I want it to, heck even licensing issues could be a big problem. There's a lot more that can go wrong here.

    ...

    Again this isn't to say having the knowledge of what's going on within the encapsulated task is not bad. Yeah, very useful thing to know.

    And knowing that async/await doesn't necessarily mean thread is a VERY good thing to be aware of.

    But heck... you don't know which methods do or do not use threads when you await them unless you're specifically told (or you wrote it yourself). Like I said, we don't necessarily know how any given API we access, say from Unity or others. And that doesn't stop you from using it.

    And that's what I mean about a line... where is the line change from "need to know" to "nice to know".

    And mind you... you're talking to someone who over explains things to no end. As @Suddoha mentioned, it was one of my lengthy "explain half the universe" posts that inspired them to research deeper into Coroutines.

    So I feel ya... I'm an information excavator myself.

    ...

    [edit]

    Really if anyone really wants to know what is needed to know about async/await. Read the MSDN article about it.

    https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

    And as the article states right off the bat:
    It's designed to be a black box.

    It's designed to be encapsulated.

    ...

    With all that said, continue. @Stevens-R-Miller seems very interested and want's to know more!
     
    Last edited: Apr 10, 2021
    Suddoha likes this.
  39. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    I ment code written by another team member. I have seen code that rounds trip to database being used from a hot loop becasue the dev didn't check the code first.
     
  40. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    OK... my point still stands.
     
  41. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    I have seen dev's that don't know that the code until the first await will be executed directly. All I'm saying that's info any dev using Tasks should know about :)
     
  42. Stevens-R-Miller

    Stevens-R-Miller

    Joined:
    Oct 20, 2017
    Posts:
    676
    You've said more than that.