RedZero9 发表于 2022-9-28 15:35

Unity手册 协程(翻)

Unity手册–协程 原文

协程允许您跨越多帧执行任务,在Unity中,协程允许暂停执行中的函数并将控制权交还Unity,被暂停的函数将在接下来的帧中恢复暂停时的状态继续运行。
在大多数场景中,当函数被调用时,它只会在完整地执行结束后将控制权返回给调用它的函数。这意味着这次调用所产生的任何结果都局限于一帧当中。
但当您需要调用一个包含动画或是随时间变化的一系列动作的函数时(这通常需要跨越多帧执行),您可以使用携程。
不过一定需要记住的一点是——协程与线程是不同的。在协程运行同步操作仍在主线程上执行。如果您想减少花费在主线程上的CPU时间,那么在协程中避免阻塞操作就像在任何其他脚本代码中一样重要。如果您想在unity中使用多线程,请参考:C# 作业系统 - Unity 手册
如果您需要处理长时间的异步操作,例如等待HTTP传输、资产加载或文件I/O完成,那么使用协程是最好的选择。
例子

考虑以下代码——逐渐减少一个物体的Alpha值,直到它变得完全不可见:
void Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
      c.a = alpha;
      renderer.material.color = c;
    }
}
在此例中,Fade()函数并不能实现预期的效果。为了使渐隐效果可见,您必须在一系列帧渲染时逐渐减少物体的Alpha值。但是在上面的例子中仅仅是在一帧中完成了所有对Alpha值的修改。
为了解决这种情况,您可以在Update()函数中添加渐隐代码以逐帧执行。然而,对于这类任务使用协程会更方便。
在C#代码中,您可以像这样使用协程:
IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
      c.a = alpha;
      renderer.material.color = c;
      yield return null;
    }
}
协程是使用IEnumerator作为返回值,同时在函数体中使用yield关键字。yield return null 表明函数将在此处暂停并在下一帧中从此处开始继续执行。为了使协程运行起来,您需要使用StartCoroutine()函数。
void Update()
{
    if (Input.GetKeyDown("f"))
    {
      StartCoroutine(Fade());
    }
}
在协程的生命周期内,在yield之间所有的局部变量或是参数都会被保存,Fade()函数中循环计数器始终会保持正确的值。
延时执行

默认情况下,yield之后的协程会在下一帧中被Unity唤醒。如果您想引入一个时间延迟,请使用WaitForSeconds():
IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
      c.a = alpha;
      renderer.material.color = c;
      yield return new WaitForSeconds(.1f);
    }
}
您可以使用WaitForSeconds()实现持续一段时间的效果,同时您可以把它当做在Update()函数中实现功能的替代方法。Unity会在一秒内多次调用Update()函数,如果您不需要一项任务如此频繁被调用,您可以把它放进一个协程中已获得定期更新而不是每帧都被调用。
举个例子,您可能需要在您的游戏中设计一个在敌人接近时提醒玩家的警报器,代码如下:
bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
      if (Vector3.Distance(transform.position, enemies.transform.position) < dangerDistance) {
                return true;
      }
    }

    return false;
}
如果有很多敌人,那么每帧都调用此函数可能会带来很大开销。但是,可以使用协程,每十分之一秒调用一次:
IEnumerator DoCheck()
{
    for(;;)
    {
      if (ProximityCheck())
      {
            // Perform some action here
      }
      yield return new WaitForSeconds(.1f);
    }
}
这减少了Unity执行的检查次数,而不会对游戏玩法产生任何明显的影响。
停止协程

使用StopCoroutine()和StopAllCoroutines()来停止协程。协程同样可以通过调用与协程相关的GameObject的SetActive()为false来结束。调用Destroy(example) (example为MonoBehavior的实例)立即触发OnDisable(),此时Unity会销毁协程从而高效地停止它,之后,接下来OnDestroy()会在在帧的末尾被调用。
注意:如果您通过将enabled设置为false来禁用MonoBehaviour, Unity不会停止协程。
性能分析

协程的运行状况与其他脚本代码不同,大多数Unity的脚本代码性能跟踪内只会出现一次,那就是在特定的回调调用之下出现。协程的CPU代码总是在性能跟踪中出现在两个地方。
Coroutines execute differently from other script code. Most script code in Unity appears within a performance trace in a single location, beneath a specific callback invocation. However, the CPU code of coroutines always appears in two places in a trace. //原文,这地方给我整不会翻了所有协程的初始代码,也就是携程函数开始到第一个yield ,出现在跟踪中Unity启动协程的位置。初始代码最常出现在StartCoroutine()被调用时。Unity回调生成的协程(比如返回IEnumerator的Start()回调)首先出现在它们各自的Unity回调中。
协程中的其他代码(从第一次被恢复直到协程结束执行)会出现在Unity的主循环(main loop)中的DelayedCallManager行中。
之所以会这样是因为Unity调用协程的方式,c#编译器(C# compiler)会自动生成一个支持协程的类的实例。然后Unity就会通过这个实例跟踪多次被执行的单个协程函数的状态。因为协程中的局部作用域变量必须跨yield调用持续存在。Unity将局部作用域的变量提升到生成的类中,在协程持续期间他们被分配在堆上。这个对象会跟踪协程的内部状态:它记住协程在代码中的哪个点必须在yield后恢复。
因此,协程启动时的内存压力等于固定的分配开销加上其局部变量的大小相关的开销。
启动协程的代码会构建并调用一个对象,然后Unity的DelayedCallManager 会在协程yield后的条件被满足时再次调用它。因为协程通常会在协程的外部被启动,这将它们的执行开销在yield调用和DelayedCallManager之间分割。
您可以使用Unity Profiler来查看和理解Unity是如何在您的程序中调动协程的。为了做到这一点应在Profiler中启动深度分析(Deep Profiling),这会使Profiler记录并显示所有的函数调用。然后您可以通过CPU Usage Profiler module查看您的程序中的协程。


被delayedCall调用的协程
最佳实践是将一系列操作压缩到尽可能少的协程。嵌套的协程可使代码清晰且易于维护。但是由于会创建协程跟踪对象相对来说增加了更多的内存开销。
如果一个协程在每一帧都被调用且在执行耗时操作时没有调用yield不如将协程替换为Update()或LateUpdate()这样开销更小。如果您有长时间运行或无限循环的协程,这很有用。

stonstad 发表于 2022-9-28 15:44

望勘误
页: [1]
查看完整版本: Unity手册 协程(翻)