Search Unity

  1. New Unity Live Help updates. Check them out here!

    Dismiss Notice

Best practice for running two coroutines that conflict with each other?

Discussion in 'Scripting' started by _eternal, Feb 28, 2019.

  1. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    116
    I have a few utility coroutines in a Helper class, e.g. ChangeColor or ChangeOpacity. Something like...

    Code (CSharp):
    1. float lerpTime = 0f;
    2. while (lerpTime < 1f)
    3. {
    4.     lerpTime += Time/deltaTime / duration;
    5.     img.color = Color.Lerp(oldColor, newColor, lerpTime);
    6.     yield return null;
    7. }
    This saves me the trouble of copying and pasting the code every time I want to change a new object's color.

    However, you can imagine that there are scenarios where an object would be halfway through fading out, but you want to stop the old coroutine and make the object fade back in. Otherwise, the two coroutines will clash with each other.

    Currently, I solve the problem by keeping a List<GameObject> field that tracks every object whose color is changing. If ChangeColor is called while the object is already in the list, I remove it for a couple of frames, wait for the previous coroutine to stop, and then start the new one. So the loop becomes something like...

    Code (CSharp):
    1. float lerpTime = 0f;
    2. while (lerpTime < 1f)
    3. {
    4.     lerpTime += Time/deltaTime / duration;
    5.     img.color = Color.Lerp(oldColor, newColor, lerpTime);
    6.     yield return null;
    7.  
    8.     if (!objsInChangeColor.Contains(img.gameObject)) yield break;
    9. }
    This works, but it's cumbersome and requires some copy and paste (this example is shorter than the actual code). I'm self-taught, so I'm wondering if there's a better way to do this, or if all of the solutions are just as awkward and hacky.
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    2,428
    I do this fairly often, with "conflicting" routines that Open and Close doors, Raise or Lower platforms, etc. The approach I use is to hang on to a reference to either Coroutine, depending on which is currently running, and stop that coroutine when calling either the Open or Close method. For example:

    Code (CSharp):
    1. private Coroutine _openCloseDoorsRoutine;
    2. public void OpenDoors()
    3. {
    4.     if (_openCloseDoorsRoutine != null)
    5.     {
    6.         StopCoroutine(_openCloseDoorsRoutine);
    7.         _openCloseDoorsRoutine = null;
    8.     }
    9.     _openCloseDoorsRoutine = StartCoroutine(OpenDoorsRoutine());
    10. }
    11. private IEnumerator OpenDoorsRoutine()
    12. {
    13.     throw new NotImplementedException();
    14. }
    15.  
    16. public void CloseDoors()
    17. {
    18.     if (_openCloseDoorsRoutine != null)
    19.     {
    20.         StopCoroutine(_openCloseDoorsRoutine);
    21.         _openCloseDoorsRoutine = null;
    22.     }
    23.     _openCloseDoorsRoutine = StartCoroutine(CloseDoorsRoutine());
    24. }
    25. private IEnumerator CloseDoorsRoutine()
    26. {
    27.     throw new NotImplementedException();
    28. }
    So, basically, when you called OpenDoors or CloseDoors, it checks if either Open or Close is already running, and if so, stops it. Then it starts the correct coroutine.
     
  3. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    116
    Yeah, that's the only other solution that I know of. Unfortunately it means keeping the coroutine as a field, and creating a new field every time you want to change one of the arguments. In my use case, every class that wants to use ChangeOpacity would need at least two fields (one for fade out, one for fade in), plus more if I want to change the duration or any other argument.

    I mean, I guess my current solution works fine. Every idea I've come across is wonky in some way, including my own.
     
  4. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    2,428
    Another similar approach I've used doesn't use coroutines, and it's specifically for adjusting the opacity of things. The simple idea is that the script maintains a "target" value for whatever property you're changing over time. In this case, target Alpha. Then, in Update, if the actual value is not the target value, adjust accordingly. Here's an example:

    Code (CSharp):
    1.         public CanvasGroup CanvasGroup;
    2.         public float FadeRate = 2;
    3.         private float _targetAlpha;
    4.  
    5.         void Update()
    6.         {
    7.             if (CanvasGroup.alpha < _targetAlpha)
    8.             {
    9.                 CanvasGroup.alpha = Mathf.Min(1, CanvasGroup.alpha + Time.unscaledDeltaTime * FadeRate);
    10.             }
    11.             else if (CanvasGroup.alpha > _targetAlpha)
    12.             {
    13.                 CanvasGroup.alpha = Mathf.Max(0, CanvasGroup.alpha - Time.unscaledDeltaTime * FadeRate);
    14.             }
    15.  
    16.         }
    You can then create a FadeIn() method that simply sets _targetAlpha to 0, and FadeOut that sets it to 1.
     
    Antypodish and Munchy2007 like this.
  5. AntoineDesbiens

    AntoineDesbiens

    Joined:
    Jun 9, 2014
    Posts:
    806
    Mb you could design your thing like this. Your helper class instantiates classes who's job is to actually run the coroutine. Those classes hook onto your helper class. Whenever your helper class gets a request to do a thing, it broadcasts the request, announcing the relevant object. If an instance is currently using that object, it destroys itself.

    e.g. You got a panel that fades out over 3 seconds. Your GUI manager sends the request to your ColorHelper. Your ColorHelper spawns a class that actually runs the coroutine, that class then hooks onto the ColorHelper while it's running, in case the ColorHelper broadcasts something relevant.

    2 seconds in, you decide to show that panel. your GUI manager sends another request to your ColorHelper. Your color helper broadcasts that request. The class running the coroutine sees the request, destroys itself, the new one kicks in. Does that make any sense?
     
  6. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    116
    Ah yeah, that's a good solution. I tried that for another situation and it worked well. Come to think of it, you could add multiple fields to a single script so that you don't have to make a new script for each situation... e.g. you could have targetAlpha, targetColor, targetPosition, or anything else. I'll keep that in mind.

    This is a bit more complicated... by "hook on," do you mean listening for an event? So like...
    • Instantiate a class whose sole job is to run the ChangeOpacity coroutine
    • The class subscribes to ColorHelper OnEnable so it will know if it receives another request with the same target object
    • If it receives another request, it calls StopAllCoroutines and destroys itself so that the new one will take priority
    Something like that? It's more complex than what I'm used to but maybe it can work.
     
  7. AntoineDesbiens

    AntoineDesbiens

    Joined:
    Jun 9, 2014
    Posts:
    806
    Yes, exactly. I mean, you can pool the instances, develop the system in a more generic way to allow yourself some flexibility, and so on, but you definitely understand the concept.

    The instances could also be responsible for "claiming" the call, if they're already working on the object, rather than destroying themselves. Say you're lerping from clear to white and you want to lerp back for whatever reason. If you're halfway through, maybe you'd want to lerp back to clear from the CURRENT value instead of the base value defined. This way no visual color breaks can be noticed, if that makes any sense. The instance doing the interpolation already holds the interpolation value, it's easy for it to just start from there.

    You could give them a flag so they'd know if this call can be picked up from the middle point or has to be executed from the start. Endless possibilities!
     
  8. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,728
    If you're working with colors, positions, etc, you can simply keep targetValue at class level and run one coroutine or even do it in Update function, like this:
    Code (CSharp):
    1. class Fade : MonoBehaviour {
    2.     private Color targetColor;
    3.     private Renderer targetRenderer;
    4.  
    5.     private void Update() {
    6.         if(targetRenderer.color != targetColor) {
    7.             targetRenderer.color = Color.Lerp(targetRenderer.color, targetColor, .5f);
    8.         }
    9.     }
    10.  
    11.     public void Fade(Color targetColor) {
    12.         this.targetColor = targetColor;
    13.     }
    14. }
     
unityunity