问题描述
我了解协程的原理.我知道如何让标准的 StartCoroutine
/yield return
模式在 Unity 中的 C# 中工作,例如通过 StartCoroutine
调用返回 IEnumerator
的方法并在该方法中执行某些操作,执行 yield return new WaitForSeconds(1);
等待一秒钟,然后做点别的.
我的问题是:幕后到底发生了什么?StartCoroutine
到底做了什么?WaitForSeconds
返回的是什么 IEnumerator
?StartCoroutine
如何将控制权返回给被调用方法的其他"部分?所有这些如何与 Unity 的并发模型(其中许多事情同时进行而不使用协程)交互?
经常引用的Unity3D 协程详解 链接已失效.由于在评论和答案中提到了它,我将在此处发布文章的内容.本内容来自本镜像.
Unity3D 协程详解
游戏中的许多过程发生在多个帧的过程中.你有密集"的过程,比如寻路,它在每一帧都努力工作,但被分割成多个帧,以免对帧率产生太大影响.你有稀疏"的进程,比如游戏触发器,在大多数帧中什么都不做,但偶尔会被要求做关键的工作.并且您在两者之间有各种各样的流程.
每当您创建一个将在多个帧上进行的进程时(没有多线程),您都需要找到某种方法将工作分解为可以每帧运行一个的块.对于任何具有中心循环的算法,这是相当明显的:例如,A* 探路者可以构造成半永久性地维护其节点列表,每帧仅处理开放列表中的少数节点,而不是尝试一次性完成所有工作.需要做一些平衡来管理延迟——毕竟,如果你将帧速率锁定在每秒 60 或 30 帧,那么你的过程将只需要每秒 60 或 30 步,这可能会导致过程只需要整体太长了.简洁的设计可能会在一个级别上提供尽可能小的工作单元——例如处理单个 A* 节点——并在顶部分层,将工作组合成更大的块——例如继续处理 A* 节点 X 毫秒.(有人称之为时间切片",但我不这么认为).
不过,允许以这种方式分解工作意味着您必须将状态从一帧转移到下一帧.如果您正在分解迭代算法,那么您必须保留所有迭代共享的状态,以及跟踪下一次要执行的迭代的方法.这通常不算太糟糕——A* 探路者类"的设计相当明显——但也有其他情况不太令人愉快.有时您会面临长时间的计算,这些计算会逐帧执行不同类型的工作;捕获其状态的对象最终可能会产生一大堆半有用的本地",用于将数据从一帧传递到下一帧.如果您正在处理一个稀疏流程,您通常最终不得不实现一个小型状态机来跟踪何时应该完成工作.
如果您不必跨多个帧显式跟踪所有这些状态,并且不必多线程并管理同步和锁定等,而是将您的函数编写为单个块,这不是很好吗?代码,并标记函数应该暂停"并在稍后继续执行的特定位置?
Unity - 连同许多其他环境和语言 - 以协程的形式提供.
他们看起来怎么样?在Unityscript"(Javascript)中:
function LongComputation(){而(一些条件){/* 做一大块工作 *///在这里暂停并继续下一帧屈服;}}
在 C# 中:
IEnumerator LongComputation(){而(一些条件){/* 做一大块工作 *///在这里暂停并继续下一帧收益率返回空;}}
它们是如何工作的?让我快速说一下,我不为 Unity Technologies 工作.我没有看到Unity源代码.我从未见过 Unity 协程引擎的胆量.但是,如果他们以与我将要描述的完全不同的方式实施它,那么我会感到非常惊讶.如果 UT 的任何人想插话并谈论它的实际运作方式,那就太好了.
主要线索在 C# 版本中.首先,请注意该函数的返回类型是 IEnumerator.其次,请注意其中一个语句是 yield返回.这意味着 yield 必须是关键字,并且由于 Unity 的 C# 支持是 vanilla C# 3.5,所以它必须是 vanilla C# 3.5 关键字.确实,这是在 MSDN 中 – 谈论所谓的迭代器块.'那么发生了什么?
首先,有这个 IEnumerator 类型.IEnumerator 类型就像一个序列上的光标,提供两个重要的成员:Current,它是一个属性,为您提供光标当前所在的元素,以及 MoveNext(),一个移动到序列中下一个元素的函数.因为 IEnumerator 是一个接口,它没有具体说明这些成员是如何实现的;MoveNext() 可以只添加一个 toCurrent,或者它可以从文件中加载新值,或者它可以从 Internet 下载图像并将其散列并将新散列存储在 Current 中……或者它甚至可以为第一件事做一件事序列中的元素,而第二个元素则完全不同.如果您愿意,您甚至可以使用它来生成无限序列.MoveNext() 计算序列中的下一个值(如果没有更多值则返回 false),Current 检索它计算出的值.
通常,如果你想实现一个接口,你必须写一个类,实现成员等等.迭代器块是实现 IEnumerator 的一种便捷方式,没有那么麻烦——您只需遵循一些规则,IEnumerator 实现就会由编译器自动生成.
迭代器块是一个常规函数,它 (a) 返回 IEnumerator,并且 (b) 使用 yield 关键字.那么 yield 关键字实际上是做什么的呢?它声明序列中的下一个值是什么——或者没有更多的值.代码遇到 yield 的点return X 或 yield break 是 IEnumerator.MoveNext() 应该停止的点;yield return X 导致 MoveNext() 返回 true 并为 Current 分配值 X,而 yieldbreak 导致 MoveNext() 返回 false.
现在,诀窍就在这里.序列返回的实际值是什么并不重要.可以重复调用MoveNext(),忽略Current;计算仍将执行.每次调用 MoveNext() 时,您的迭代器块都会运行到下一个yield"语句,而不管它实际产生什么表达式.所以你可以这样写:
IEnumerator TellMeASecret(){PlayAnimation("LeanInConspiringly");同时(播放动画)收益率返回空;Say("我从饼干罐里偷了饼干!");同时(说)收益率返回空;PlayAnimation("LeanOutRelieved");同时(播放动画)收益率返回空;}
你实际编写的是一个迭代器块,它生成一长串空值,但重要的是它计算它们的工作的副作用.你可以使用一个简单的循环来运行这个协程:
IEnumerator e = TellMeASecret();while(e.MoveNext()) { }
或者,更有用的是,您可以将其与其他工作混合使用:
IEnumerator e = TellMeASecret();while(e.MoveNext()){//如果他们按下Escape",则跳过过场动画if(Input.GetKeyDown(KeyCode.Escape)) { break;}}
一切都在时机如您所见,每个 yield return 语句都必须提供一个表达式(如 null),以便迭代器块可以实际分配给 IEnumerator.Current.一长串空值并不是很有用,但我们对副作用更感兴趣.我们不是吗?
实际上,我们可以用这个表达式做一些方便的事情.如果,而不是仅仅产生 null并忽略它,我们产生了一些表明我们何时需要做更多工作的东西?通常,我们需要直接进行下一帧,当然,但并非总是如此:在动画或声音播放完毕后,或经过特定时间后,我们有很多次想要继续进行.那些同时(播放动画)收益率返回空;构造有点乏味,你不觉得吗?
Unity 声明了 YieldInstruction 基类型,并提供了一些指示特定等待类型的具体派生类型.你有 WaitForSeconds,它在指定的时间后恢复协程.你有WaitForEndOfFrame,它在同一帧稍后的特定点恢复协程.你有 Coroutine 类型本身,当协程 A 产生协程 B 时,它会暂停协程 A,直到协程 B 完成.
从运行时的角度来看,这是什么样子的?正如我所说,我不在 Unity 工作,所以我从未见过他们的代码;但我想它可能看起来有点像这样:
ListunblockedCoroutines;列表shouldRunNextFrame;列表shouldRunAtEndOfFrame;SortedListshouldRunAfterTimes;foreach(unblockedCoroutines 中的 IEnumerator 协程){if(!coroutine.MoveNext())//这个协程已经完成继续;if(!coroutine.Current 是 YieldInstruction){//这个协程产生了 null,或者其他一些我们不明白的值;下一帧运行它.shouldRunNextFrame.Add(协程);继续;}if(coroutine.Current 是 WaitForSeconds){WaitForSeconds 等待 = (WaitForSeconds)coroutine.Current;shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);}else if(coroutine.Current 是 WaitForEndOfFrame){shouldRunAtEndOfFrame.Add(协程);}else/* 其他 YieldInstruction 子类型的类似内容 */}unblockedCoroutines = shouldRunNextFrame;
不难想象可以添加更多的 YieldInstruction 子类型来处理其他情况——例如,可以添加对信号的引擎级支持,并使用 WaitForSignal("SignalName")YieldInstruction 来支持它.通过添加更多的 YieldInstructions,协程本身可以变得更具表现力——yieldreturn new WaitForSignal("GameOver") 比 while(!Signals.HasFired("GameOver")) 更好读yield return null,如果你问我的话,除了在引擎中执行它可能比在脚本中执行更快这一事实之外.
一些不明显的后果关于这一切,人们有时会忽略一些有用的东西,我认为我应该指出.
首先,yield return 只是产生一个表达式——任何表达式——而 YieldInstruction 是一个正则类型.这意味着您可以执行以下操作:
YieldInstruction y;如果(某事)y = 空;否则如果(别的东西)y = new WaitForEndOfFrame();别的y = new WaitForSeconds(1.0f);收益回报 y;
具体行 yield return new WaitForSeconds(), yieldreturn new WaitForEndOfFrame() 等很常见,但实际上它们本身并不是特殊形式.
第二,因为这些协程只是迭代器块,如果你愿意,你可以自己迭代它们——你不必让引擎为你做.我以前用它来向协程添加中断条件:
IEnumerator DoSomething(){/* ... */}IEnumerator DoSomethingUnlessInterrupted(){IEnumerator e = DoSomething();布尔中断 = 假;而(!中断){e.MoveNext();收益回报 e.Current;中断 = HasBeenInterrupted();}}
第三,您可以在其他协程上让步这一事实可以让您实现自己的 YieldInstructions,尽管它们的性能不如引擎实现的那么好.例如:
IEnumerator 直到TrueCoroutine(Func fn){while(!fn()) yield return null;}协程直到真(Func fn){返回 StartCoroutine(UntilTrueCoroutine(fn));}IEnumerator SomeTask(){/* ... */收益率返回直到真(()=> _lives< 3);/* ... */}
但是,我真的不推荐这样做 - 启动协程的成本对我来说有点沉重.
结论我希望这能澄清一些在 Unity 中使用协程时真正发生的事情.C# 的迭代器块是一个非常棒的小构造,即使你没有使用 Unity,也许你会发现以同样的方式利用它们很有用.
I understand the principle of coroutines. I know how to get the standard StartCoroutine
/ yield return
pattern to work in C# in Unity, e.g. invoke a method returning IEnumerator
via StartCoroutine
and in that method do something, do yield return new WaitForSeconds(1);
to wait a second, then do something else.
My question is: what's really going on behind the scenes? What does StartCoroutine
really do? What IEnumerator
is WaitForSeconds
returning? How does StartCoroutine
return control to the "something else" part of the called method? How does all this interact with Unity's concurrency model (where lots of things are going on at the same time without use of coroutines)?
The oft referenced Unity3D coroutines in detail link is dead. Since it is mentioned in the comments and the answers I am going to post the contents of the article here. This content comes from this mirror.
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
IEnumerator TellMeASecret()
{
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */
}
unblockedCoroutines = shouldRunNextFrame;
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
这篇关于StartCoroutine/yield 返回模式如何在 Unity 中真正起作用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!