【问题标题】:Exception handling of coroutines in UnityUnity中协程的异常处理
【发布时间】:2022-01-18 04:03:25
【问题描述】:

我创建了一个脚本来更改它所附加的 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 中的异步方法(协程)中引发的异常。

【问题讨论】:

  • 为什么不在新的ChangeTransparency 调用中停止之前的协程?
  • 当您将token.ThrowIfCancellationRequest() 替换为if (token.IsCancellationRequested){ yield break; } 时,您看到了什么异常?你得到了什么“异步异常”?
  • 我编辑了我的最后一句话。我指的是异步运行的方法(协程)中抛出的异常。检查令牌的取消标志时,我没有收到任何异常,但我仍然希望能够处理异常。

标签: c# unity3d coroutine cancellation


【解决方案1】:

这个答案是“don’t do that...try this instead”的一个例子。


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


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

协同程序的替代方案?

为什么不直接使用async/await?自 Unity 2018.1 以来,Unity 已完全支持 .NET 4.5+ 以及 基于任务的异步模式 (TAP )。一旦你这样做了,你会发现事情变得更容易了,你可以向更广泛的代码示例敞开心扉,并避开讨厌的 IEnumerator 辩论。

MSDN:

在 Unity 中,异步编程通常使用协程完成。但是,从 C# 5 开始,在 .NET 开发中首选的异步编程方法一直是基于任务的异步模式 (TAP),它在 System.Threading.Task 中使用 async 和 await 关键字。总之,在异步函数中,您可以等待任务完成,而不会阻止应用程序的其余部分更新...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.");
    }
}

异常处理

在 Unity 中取消协程操作

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

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

答案是根本不使用协程(假设您运行的是 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);
    }
}

另见

【讨论】:

  • 感谢您的详细解答!我不知道 Unity 支持 TAP 库。
  • 这是一个非常广泛的答案!但是,在 OP 的原始用例中,我会说协程确实绝对是您想要使用的! OPs 代码应该在 Unity 主线程中每帧执行一次,完全没有异步!
  • @derHugo OP 的问题是关于 “Unity 中协程的异常处理” - 这是我回答的第二部分。 OP 是否执行 interpolation 是偶然的。
  • @Zserbinator 如果您不想使用Task,请尝试让自己远离协程。我所有的基于时间的、帧更新的插值函数都只是一个函数。也不需要Tasks。我看到的绝大多数协程代码都可以不用它来完成。查看我对问题的更新
  • @MickyD time-based interpolation function in each frame update 正是协程中发生的事情,只是它只有在真正需要并且协程正在运行时才会发生。 Exception handling of coroutines in Unity 是标题,是的.. XY 问题的一个例子。但是从代码和描述中,您可以很好地看到 OP 实际上试图实现的目标:在令牌被取消的那一刻结束协程,并在此之前可选地做出一些反应。这与插值无关,而是Material.SetFloat 只能在主线程中完成
【解决方案2】:

other answer 很好,但实际上对于您要解决的用例来说并不是很重要。

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

CoroutineNOT ASYNC,与多线程或任务无关!

Coroutine 基本上只是一个 IEnumerator,只要它被 StartCorotuine 注册为 Coroutine,然后 Unity 在其上调用 MoveNext(它将执行所有操作,直到下一个 yield return 语句)在 @987654328 之后@ 所以在 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;
    }
}

【讨论】:

  • 协程不异步是什么意思?协程中的方法是否实际在主线程上执行,而枚举模式的 yield return 负责执行状态?
  • 哦,我看到您刚刚编辑了答案并添加了指向 Unity 文档的链接。谢谢!
  • @Zserbinator 一个 Coroutine 基本上只是一个 IEnumerator,只要它被 StartCorotuine 注册为 Coroutine,然后 Unity 就会在其上调用 MoveNext(它将执行所有操作,直到下一个 @987654344 @ 语句)就在 Update 之后,因此在 Unity 主线程上的一帧(有一些特殊的 yield 语句,如 WaitForFixedUpdate 等,它们在不同的消息堆栈中处理,但仍然如此)
  • @MickyD 这是在 Unity 中实现协程的方式。这就是OP所要问的。 IEnumerator(顺便说一句不是IEnumerable)上的一些随机开发人员意见(那里的论点非常值得怀疑)并不算数。协程不是,也不打算替换异步代码!退出协程直到 Unity 主线程上的一帧内的下一个 yield return,这对于能够与未准备好多线程的 Unity API 进行交互至关重要!
  • @MickyD 正如所说的make a simple frame-updated-time-based interpolator function 正是 Unity 中的协程的用途。如果您愿意,它们基本上是小的临时 Update 方法,但它们可以打开和关闭,不需要依赖外部类字段,但可以存储肯定不会被其他任何东西更改/访问的局部变量。
猜你喜欢
  • 2019-10-17
  • 1970-01-01
  • 2020-11-09
  • 2019-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-16
相关资源
最近更新 更多