Search Unity

Question Issue using coroutines across scripts

Discussion in 'Scripting' started by lemmons, Jan 21, 2021.

  1. lemmons

    lemmons

    Joined:
    Jun 20, 2016
    Posts:
    68
    Making some helpers specific to my game and I'd love to implement something like a timer with some callbacks that I could use in various scenarios, but I keep running into an error where it says the coroutine must be static then the class must be static then everything gets jammed up.

    I guess my real question is: how do I make a coroutine that I can call from other scripts? Is there a way to instantiate coroutines without having to attach another script to your objects?

    Ex: I'd love to be able to call something like
    MyHelpers.Timer(onStartCallback, onEndCallback);
    from any other script where "Timer" is a coroutine.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Most solutions I've seen for this use a Singleton MonoBehaviour which handles running all of the coroutines. Coroutines are inextricably linked to MonoBehaviours and their lifecycles so there's no way around having one involved.
     
    lemmons likes this.
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,736
    Here's an el-cheapo one-shot "do something later" script:

    https://gist.github.com/kurtdekker/0da9a9721c15bd3af1d2ced0a367e24e

    You can hack it to just call you repeatedly instead of destroying itself after one call: just wrap the code in Start() inside a while(true) block, or else do your own timers up and down.

    But everything that Praetor suggests above is pretty common. If you wanna simple singleton framework with the minimum amount of moving parts, here you go:

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://pastebin.com/SuvBWCpJ

    Unity3D Singleton with Prefab used for predefined data:

    https://pastebin.com/cv1vtS6G

    These are pure-code solutions, do not put anything into any scene, just access it via .Instance!
     
    lemmons likes this.
  4. lemmons

    lemmons

    Joined:
    Jun 20, 2016
    Posts:
    68
    Thanks for the advice! So it looks like I'll need to instantiate a timer prefab anytime I want to use a timer? That feels excessive, but if that's the blessed path then... ok!

    Just to clarify my goal, I don't want to affect some global timer—I want many game objects to be able to trigger a timer coroutine at any point in time, usually at the same time. In the most basic scenario, I want to trigger a sprite flash and fade in my shaders when the unit takes damage using a simple timer. There are dozens of objects that would need to trigger their own damage/flash timers totally independent of each other.

    Right now I've got the timer code in the script that controls shaders and animations but I wanted to extract it to a helper to clean up the code and make that same kind of logic usable for other things like ability cooldowns, status durations, etc.

    I don't believe the Singleton approach would solve this issue (though I could be wrong!) and it almost feels more complicated to instantiate a prefab to run the timer vs. just keeping the timer code in each script that needs it (though, again, I could be wrong).
     
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    There's no reason you would need to Instantiate a new GameObject every time you want to start a timer. You can certainly just use a singleton. There is no limit on the number of simultaneous, independent coroutines a single MonoBehaviour can run. You can run all of your timers from one object without issue.
     
    Last edited: Jan 21, 2021
  6. lemmons

    lemmons

    Joined:
    Jun 20, 2016
    Posts:
    68
    Oh, I misunderstood! I can't quite get this to work yet but this is the direction I'm moving in... if you see anything obviously broken I'd appreciate the feedback :). Mainly the whole `percentRemaining` thing would be useful for calculating fades and such. I'll keep playing around and post back if I get it working—thank you!

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class CoroutineManager : MonoSingleton<CoroutineManager>
    6. {
    7.     public void CountdownTimer(float duration, Action onStart = null, Func<float> percentRemaining = null, Action onComplete = null)
    8.     {
    9.         if (onStart != null)
    10.             onStart();
    11.  
    12.         StartCoroutine(Timer(duration, percentRemaining));
    13.  
    14.         if (onComplete!= null)
    15.             onComplete();
    16.     }
    17.  
    18.     IEnumerator Timer(float duration, Func<float> percentRemaining)
    19.     {
    20.         float remainingTime = duration;
    21.  
    22.         if (percentRemaining != null)
    23.         {
    24.             while (remainingTime >= 0f)
    25.             {
    26.                 while (remainingTime >= 0f)
    27.                 {
    28.                     //percentRemaining(remainingTime/duration); // How to return the %remaining to the script that is calling CountdownTimer?
    29.                     duration -= Time.deltaTime;
    30.                     yield return null;
    31.                 }
    32.             }
    33.         }
    34.  
    35.         else
    36.         {
    37.             while (remainingTime >= 0f)
    38.             {
    39.                 duration -= Time.deltaTime;
    40.                 yield return null;
    41.             }
    42.         }
    43.  
    44.         yield return null;
    45.     }
    46. }
    47.  
     
  7. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Try this:
    Code (CSharp):
    1. public class CoroutineManager : MonoSingleton<CoroutineManager> {
    2.     public void CountdownTimer(float duration, Action onStart = null, Action onComplete = null, Action<float> percentRemainingCallback = null) {
    3.         StartCoroutine(Timer(duration, onStart, onComplete, percentRemainingCallback));
    4.     }
    5.  
    6.     IEnumerator Timer(float duration, Action onStart, Action onComplete, Action<float> percentRemainingCallback) {
    7.         onStart?.Invoke();
    8.         float remainingTime = duration;
    9.         while (remainingTime >= 0f) {
    10.             remainingTime -= Time.deltaTime;
    11.             percentRemainingCallback?.Invoke(remainingTime / duration);
    12.             yield return null;
    13.         }
    14.          
    15.         percentRemainingCallback?.Invoke(0f);
    16.         onComplete?.Invoke();
    17.     }
    18. }
     
    lemmons likes this.
  8. lemmons

    lemmons

    Joined:
    Jun 20, 2016
    Posts:
    68
    Holy smokes, that's exactly what I was looking for. You are a lifesaver—thanks so much!