Unity中协程的异常处理

Exception handling of coroutines in Unity

我创建了一个脚本来更改它所附加的 GameObject 的透明度,我在一个需要取消的褪色协程中进行透明度更改(并且每次我们用新值调用 ChangeTransparency() 时取消) .我设法让它按照我想要的方式工作,但我想处理淹没我的控制台的 OperationCanceledException。我知道您 不能 yield return 语句包装在 try-catch 块中。

在 Unity 协程中处理异常的正确方法是什么?

这是我的脚本:

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);
        }
    }
}

我的临时解决方案是检查令牌的取消标志并跳出 while 循环。虽然它解决了当前的这个问题,但我仍然需要一种方法来处理在 Unity 中异步方法(协程)中抛出的这些异常。


这个答案是 “don’t do that...try this instead”.

的一个例子

编辑: 如果您不想使用 Tasks 并且对协程不太确定,为什么不使用简单的基于时间的插值函数在每个帧更新中。这是协程正在尝试做的事情的更简洁版本。


Unity 中的协程提供了一种简单的方法来对一系列帧执行冗长的操作,但是它们并非没有问题。其中之一是关于他们在整个 C# 社区中对 IEnumerator 的明显奇怪使用的激烈争论(搜索此站点)。但是,唉,我离题了。

协程的替代方案?

为什么不直接使用 async/await?自从 Unity 2018.1 以来,Unity 就 全面支持 .NET 4.5+ 并随之 基于任务的异步模式 (TAP)。一旦你这样做了,你会发现事情变得更容易了,你打开了自己的大门,接触到更广泛的代码示例,并且你避开了讨厌的 IEnumerator 辩论。

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...

示例由 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.");
    }
}

异常处理

Cancel coroutine operations in Unity

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

所以这是协程的第一个问题。

答案是完全不使用协程(假设你是运行Unity 2018.1+)并开始迁移到async/await。然后可以进行异常处理如:

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!");
    }
}

现在要处理特定情况,例如任务取消,您将:

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);
    }
}

另见

other answer 很好,但实际上并不适合您要解决的用例。

您没有任何异步代码,但需要在 Unity 主线程中执行代码,因为它使用的是 Unity API,并且大部分代码只能在主线程中使用。

A CoroutineNOT ASYNC 并且与多线程或任务无关!

协程基本上只是一个 IEnumerator 一旦它被 StartCorotuine 注册为协程然后 Unity 在其上调用 MoveNext (它将执行所有内容直到下一个 yield return 语句)就在 Update 之后,所以一旦在 Unity 主线程上出现一个帧(有一些特殊的 yield 语句,如 WaitForFixedUpdate 等,它们在不同的消息堆栈中处理,但仍然存在)。

例如

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

// 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);

如果您真的需要以某种方式对例程本身的取消做出反应,那么除了不立即抛出异常而不是仅检查 CancellationToken.IsCancellationRequested 并像

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;
    }
            
    ....

也要回答你的标题。的确,您不能在 try - catch 块内执行 yield return 并且正如一般所说,无论如何都没有真正的理由这样做。

如果你确实需要,你可以将不带 yield return 的代码的某些部分包装在 try - catch 块中,然后在 catch 中进行错误处理,然后你可以再次 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;
    }
}