Home > Blockchain >  Cancel coroutine operations in Unity
Cancel coroutine operations in Unity

Time:12-15

I created a script that changes the transparency of the GameObject it's attached to and I do the transparency change in a fading coroutine which needs to be cancellable (and canceled everytime we call ChangeTransparency() with a new value). I managed to get it working the way I want it but I want to handle the OperationCanceledException that is flooding my Console. I know that you cannot wrap a yield return statement inside a try-catch block.

What is a proper way to handle Exceptions inside Unity coroutines?

Here is my script:

using System;
using System.Collections;
using System.Threading;
using UnityEngine;

public class Seethrough : MonoBehaviour
{
    private bool isTransparent = false;
    private Renderer componentRenderer;
    private CancellationTokenSource cts;
    private const string materialTransparencyProperty = "_Fade";


    private void Start()
    {
        cts = new CancellationTokenSource();

        componentRenderer = GetComponent<Renderer>();
    }

    public void ChangeTransparency(bool transparent)
    {
        //Avoid to set the same transparency twice
        if (this.isTransparent == transparent) return;

        //Set the new configuration
        this.isTransparent = transparent;

        cts?.Cancel();
        cts = new CancellationTokenSource();

        if (transparent)
        {
            StartCoroutine(FadeTransparency(0.4f, 0.6f, cts.Token));
        }
        else
        {
            StartCoroutine(FadeTransparency(1f, 0.5f, cts.Token));
        }
    }

    private IEnumerator FadeTransparency(float targetValue, float duration, CancellationToken token)
    {
        Material[] materials = componentRenderer.materials;
        foreach (Material material in materials)
        {
            float startValue = material.GetFloat(materialTransparencyProperty);
            float time = 0;

            while (time < duration)
            {
                token.ThrowIfCancellationRequested();  // I would like to handle this exception somehow while still canceling the coroutine

                material.SetFloat(materialTransparencyProperty, Mathf.Lerp(startValue, targetValue, time / duration));
                time  = Time.deltaTime;
                yield return null;
            }

            material.SetFloat(materialTransparencyProperty, targetValue);
        }
    }
}

My temporary solution was to check for the token's cancellation flag and break out of the while loop. While it solved this current problem, I am still in need of a way to handle these asynchronous exceptions in Unity.

CodePudding user response:


This answer is an example of “don’t do that...try this instead”.


EDIT: If you don't want to use Tasks and aren't too sure about coroutines, why not use a simple time-based interpolation function in each frame update. It's a cleaner version of what coroutines are trying to do.


Coroutines in Unity provide an easy way to perform a lengthy operation over a series of frames however they are not without their problems. One of which is the heated debate over their apparent odd use of IEnumerator from the C# communality as a whole (search this site). But alas I digress.

Alternatives to coroutines?

Why not just use async/await? Ever since Unity 2018.1, Unity has had full support for .NET 4.5 and with it the Task-based Asynchronous Pattern (TAP). Once you do you will find that things become easier, you open yourself up to a much wider of code samples and you dodge that pesky IEnumerator debate.

MSDN:

In Unity, asynchronous programming is typically accomplished with coroutines. However, since C# 5, the preferred method of asynchronous programming in .NET development has been the Task-based Asynchronous Pattern (TAP) using the async and await keywords with System.Threading.Task. In summary, in an async function you can await a task's completion without blocking the rest of your application from updating... More...

Sample courtesy MSDN:

// .NET 4.x async-await
using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log("Wait.");
        await WaitOneSecondAsync();
        DoMoreStuff(); // Will not execute until WaitOneSecond has completed
    }
    private async Task WaitOneSecondAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Finished waiting.");
    }
}

Exception handling

Cancel coroutine operations in Unity

What is a proper way to handle Exceptions inside Unity coroutines?

So this is one of the first problems with coroutines.

The answer is not to use coroutines at all (assuming you are running Unity 2018.1 ) and start to migrate to async/await. Then exception handling can be performed such as:

using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
    CancellationTokenSource cts;

    private async void Start()
    {
        cts = new CancellationTokenSource ();
        Debug.Log("Wait.");

        try
        {
            await DoSomethingRisky(cts.Token);
        }
        catch (ApplicationException ex)
        {
            Debug.LogError("Terribly sorry but something bad happened");
        }

        DoMoreStuff(); // Will not execute until DoSomethingRisky has completed/faulted
    }

    private async Task DoSomethingRisky()
    {
        // ...something risky here

        throw new ApplicationException("Ouch!");
    }
}

Now to handle specic cases such as Task cancellation you would:

public class AsyncAwaitExample : MonoBehaviour
{
    CancellationTokenSource cts;

    private async void Start()
    {
        cts = new CancellationTokenSource ();
        Debug.Log("Wait.");

        try
        {
            await PerformLengthyOperationAsync(cts.Token);
        }
        catch (OperationCanceledException ex) 
        {
            // task was cancelled
        }
        catch (ApplicationException ex)
        {
            Debug.LogError("Terribly sorry but something bad happened");
        }
    }

    private void Update ()
    {
        try
        {
            if (someCondition)
               cts.Cancel();
        }
        catch (OperationCanceledException ex) 
        {
            // Yup I know, I cancelled it above
        }
    }

    private async Task PerformLengthyOperationAsync(CancellationToken token)
    {
        var ok = true;

        do
        {
            token.ThrowIfCancellationRequested ();

            // continue to do something lengthy...
            // ok = still-more-work-to-do?
        }
        while (ok);
    }
}

See also

CodePudding user response:

The other answer is good and all but actually not really on point for the use case you are trying to solve.

You don't have any async code but need the code to be executed in the Unity main thread since it is using Unity API and most of it can only be used on the main thread.

A Coroutine is NOT ASYNC and has nothing todo with multithreading or Tasks!

A Coroutine is basically just an IEnumerator as soon as it is registered as a Coroutine by StartCorotuine then Unity calls MoveNext on it (which will execute everything until the next yield return statement) right after Update so once a frame (there are some special yield statements like WaitForFixedUpdate etc which are handled in different message stacks but still) on the Unity main thread.

So e.g.

// Tells Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;

or

// Will once a frame check if the provided time in seconds has already exceeded
// if so continues in the next frame, if not checks again in the next frame
yield return new WaitForSeconds(3f);

If you really need to somehow react to the cancelation within the routine itself there speaks nothing against your approach except, instead of immediately throw an exception rather only check the CancellationToken.IsCancellationRequested and react to it like

while (time < duration)
{
    if(token.IsCancellationRequested)
    {
        // Handle the cancelation

        // exits the Coroutine / IEnumerator regardless of how deep it is nested in loops
        // not to confuse with only "break" which would only exit the while loop
        yield break;
    }
            
    ....

To answer also on your title. It is true you can't do yield return inside a try - catch block and as said in general there isn't really a reason to do so anyway.

If you really need to you can wrap some parts of your code without the yield return in a try - catch block and in the catch you do your error handling and there you can again yield break.

private IEnumerator SomeRoutine()
{
    while(true)
    {
        try
        {
            if(Random.Range(0, 10) == 9) throw new Exception ("Plop!");
        }
        catch(Exception e)
        {
            Debug.LogException(e);
            yield break;
        }

        yield return null;
    }
}
  • Related