Unity协程是如何工作的?

来源:互联网 发布:大数据标准化 编辑:程序博客网 时间:2024/06/04 18:07

原文地址:http://stackoverflow.com/questions/12932306/how-does-startcoroutine-yield-return-pattern-really-work-in-unity

Unity3D coroutines in detail

Many processes in games take place over the course of multiple frames. You’ve got ‘dense’ processes, like pathfinding, which work hard each frame but get split across multiple frames so as not to impact the framerate too heavily. You’ve got ‘sparse’ processes, like gameplay triggers, that do nothing most frames, but occasionally are called upon to do critical work. And you’ve got assorted processes between the two.

Unity3D 协程 详细解释
很多进程在游戏中都需要横跨多个帧运行。你将会遇到密集进程,例如寻路,每帧都要进行高强度运算,但是它会被分散在多个帧之中以降低帧率。你会遇到松散进程,例如触发器,在大多数帧中没有动作,但是偶尔会被运行以实现重要功能。还有更多的进程介于这两者之间。

Whenever you’re creating a process that will take place over multiple frames – without multithreading – you need to find some way of breaking the work up into chunks that can be run one-per-frame. For any algorithm with a central loop, it’s fairly obvious: an A* pathfinder, for example, can be structured such that it maintains its node lists semi-permanently, processing only a handful of nodes from the open list each frame, instead of trying to do all the work in one go. There’s some balancing to be done to manage latency – after all, if you’re locking your framerate at 60 or 30 frames per second, then your process will only take 60 or 30 steps per second, and that might cause the process to just take too long overall. A neat design might offer the smallest possible unit of work at one level – e.g. process a single A* node – and layer on top a way of grouping work together into larger chunks – e.g. keep processing A* nodes for X milliseconds. (Some people call this ‘timeslicing’, though I don’t).

当你创建会跨越多个帧的进程时,如果不使用多线程你需要想一个办法把它分为很多个小块,以使每帧只运行其中一块。对于只有一个核心循环的算法,这种方法很容易理解:例如A星寻路,可以设计让它在一定时间内保存住所有的节点,在每帧中只遍历Open列表中的一部分节点,而不是让它尝试一次性做完所有工作。这里还需要为了潜在问题做一些平衡处理,如果你把帧率锁定在60或30帧每秒,那么你的进程每秒只会进行60或30步,这会导致进程运行过慢。一个干净利落的设计应该有一个最小步骤(运行一个最小单位的工作),例如遍历一个单独的A星节点,并可以控制这些最小步骤们如何一个接一个运行以完成更大的工作量,例如在X秒内连续遍历A星节点。(有些人称这为时间片,但我不这样认为。)

Still, allowing the work to be broken up in this way means you have to transfer state from one frame to the next. If you’re breaking an iterative algorithm up, then you’ve got to preserve all the state shared across iterations, as well as a means of tracking which iteration is to be performed next. That’s not usually too bad – the design of an ‘A* pathfinder class’ is fairly obvious – but there are other cases, too, that are less pleasant. Sometimes you’ll be facing long computations that are doing different kinds of work from frame to frame; the object capturing their state can end up with a big mess of semi-useful ‘locals,’ kept for passing data from one frame to the next. And if you’re dealing with a sparse process, you often end up having to implement a small state machine just to track when work should be done at all.

但是,允许进程工作以这种方式被打断意味着你需要把状态从一帧之中传到另一帧之中。如果你打断了一个迭代算法,你就需要保存整个迭代过程中的所有状态,以及如何寻找下一个要进行的迭代的方法。大部分情况下这可以接受,例如对于A星寻路中的寻路类来讲,但是在有些情况中会出问题。有时你需要面对长时间,做不同工作的计算,横跨多个帧。尝试截取它们的状态的对象将会有一大堆有用无用的局部信息,并还需把这些信息从一个帧传到另一个帧。如果你处理的是松散进程,你经常需要使用一个小的状态机去追踪何时工作已全部已完成。

Wouldn’t it be neat if, instead of having to explicitly track all this state across multiple frames, and instead of having to multithread and manage synchronization and locking and so on, you could just write your function as a single chunk of code, and mark particular places where the function should ‘pause’ and carry on at a later time?

与其这样显式的追踪这些跨越多帧的所有状态,或是通过多线程管理它们的同步与锁定,是否还有更简洁的方法?你可以把你的方法写成一个单独的代码块,然后在特定的地方标明暂停,在之后继续运行。

Unity – along with a number of other environments and languages – provides this in the form of Coroutines.
How do they look? In “Unityscript” (Javascript):

Unity - 以及其他的一些IDE以及编程语言,都提供这种功能的方法,称为协程。

它们看起来是如何的呢?在Unity脚本中他是这样的(Javascript):

function LongComputation(){    while(someCondition)    {        /* 做一些工作 */        // 在这里暂停,下一帧继续        yield;    }}

C#版本

IEnumerator LongComputation(){    while(someCondition)    {        /* 做一些工作 */        // 这里暂停下一帧继续        yield return null;    }}

How do they work? Let me just say, quickly, that I don’t work for Unity Technologies. I’ve not seen the Unity source code. I’ve never seen the guts of Unity’s coroutine engine. However, if they’ve implemented it in a way that is radically different from what I’m about to describe, then I’ll be quite surprised. If anyone from UT wants to chime in and talk about how it actually works, then that’d be great.

他们是如何工作的,简单明了的说,我不是Unity Technologies的员工,我也没见过Unity的源码,我从来没见过Unity协程的内部引擎。但是,如果他们采用的方法与我下面猜测的内容完全不同,我将会相当震惊。如果跟帖的人有人是在Unity工作并想聊一聊协程的工作原理那就再完美不过了。

The big clues are in the C# version. Firstly, note that the return type for the function is IEnumerator. And secondly, note that one of the statements is yield return. This means that yield must be a keyword, and as Unity’s C# support is vanilla C# 3.5, it must be a vanilla C# 3.5 keyword. Indeed, here it is in MSDN – talking about something called ‘iterator blocks.’ So what’s going on?

最大的线索是在上面的C#版本代码中。首先,注意方法的反悔类型是IEnumerator。第二,注意有一个语句是 yield return。这就意味着yield是一个关键字,并且Unity采用的C#3.5,它一定是一个vanilla C#3.5关键字。的确,它在MSDN这里–这里讲的是一些关于 ‘iterator blocks’ 迭代器块的内容。那么这是怎么回事呢?

Firstly, there’s this IEnumerator type. The IEnumerator type acts like a cursor over a sequence, providing two significant members: Current, which is a property giving you the element the cursor is presently over, and MoveNext(), a function that moves to the next element in the sequence. Because IEnumerator is an interface, it doesn’t specify exactly how these members are implemented; MoveNext() could just add one toCurrent, or it could load the new value from a file, or it could download an image from the Internet and hash it and store the new hash in Current… or it could even do one thing for the first element in the sequence, and something entirely different for the second. You could even use it to generate an infinite sequence if you so desired. MoveNext() calculates the next value in the sequence (returning false if there are no more values), and Current retrieves the value it calculated.

首先,先了解一下IEnumerator type,枚举器类。IEnumerator就好像一个在代码行序列上的鼠标箭头,它有两个重要的成员:Current,鼠标箭头当前所在位置的元素,和MoveNext(),一个可以使箭头移到此序列下一个元素的方法。由于IEnumerator是一个接口,它没有具体写出这些成员是如何运作的。MoveNext()可能是给Current加一,可能是从一个文件中读取一个新的值,或则它可能是从网络上下载一张图片并给它赋予哈希值并把哈希值存到Current处,或者它可以给序列中的第一个元素做一些工作,并给第二个元素做完全不同的事情。你甚至可以用它生产一个无限的序列。MoveNext()计算序列中的下一个值(返回false如果没有下一个值),而Current取回这个值。

Ordinarily, if you wanted to implement an interface, you’d have to write a class, implement the members, and so on. Iterator blocks are a convenient way of implementing IEnumerator without all that hassle – you just follow a few rules, and the IEnumerator implementation is generated automatically by the compiler.

一般情况下,如果你想使用一个接口,你需要写一个类,成员,以及其他。‘Iterator blocks’ 迭代器块是一个可以方便的操作IEnumerator的方法,他不需要上面那些繁琐的操作,只需要服从一些简单的规则,编译器会自动生成IEnumerator。

An iterator block is a regular function that (a) returns IEnumerator, and (b) uses the yield keyword. So what does the yield keyword actually do? It declares what the next value in the sequence is – or that there are no more values. The point at which the code encounters a yield return X or yield break is the point at which IEnumerator.MoveNext() should stop; a yield return X causes MoveNext() to return true andCurrent to be assigned the value X, while a yield break causes MoveNext() to return false.

一个‘iterator block’迭代器块是一个普通方法,它有两个特点,1,返回IEnumerator。2,使用yield关键字。yield关键字是做什么的呢?它声明序列中的下一个值是什么,或者没有任何一下个值。当代码遇到yield return X或者yield break,IEnumerator.MoveNext()将会停止。yield return X将会导致MoveNext()返回true,Current被赋值为X。yield break导致MoveNext()返回false。

Now, here’s the trick. It doesn’t have to matter what the actual values returned by the sequence are. You can call MoveNext() repeatly, and ignore Current; the computations will still be performed. Each time MoveNext() is called, your iterator block runs to the next ‘yield’ statement, regardless of what expression it actually yields. So you can write something like:

现在是关键部分。返回值并不是很重要,你可以反复调用MoveNext()并且忽视Current,计算仍然会继续。每次调用MoveNext(),迭代器块将运行至下一个有yield的代码行,不论它返回的是什么。你可以这样写:

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

and what you’ve actually written is an iterator block that generates a long sequence of null values, but what’s significant is the side-effects of the work it does to calculate them. You could run this coroutine using a simple loop like this:

上面的代码就是一个会生成一个很长的连续的null值的迭代器块,其实最重要的是如何在其他方面利用它生成的这些null值。你可以运行这个协程:

IEnumerator e = TellMeASecret();while(e.MoveNext()) { }

Or, more usefully, you could mix it in with other work:

或者,更有用的,你可以将它与其他代码混合。

IEnumerator e = TellMeASecret();while(e.MoveNext()) {   // If they press 'Escape', skip the cutscene  if(Input.GetKeyDown(KeyCode.Escape)) { break; }}

It’s all in the timing As you’ve seen, each yield return statement must provide an expression (like null) so that the iterator block has something to actually assign to IEnumerator.Current. A long sequence of nulls isn’t exactly useful, but we’re more interested in the side-effects. Aren’t we?

这是一个关于控制时间的功能,每个yield return语句必须提供一个表达式(例如null),所以枚举器块可以给IEnumerator.Current赋值。一段昂长的null并不是很有用,但我们更感兴趣的是他的其他附加价值。

There’s something handy we can do with that expression, actually. What if, instead of just yielding null and ignoring it, we yielded something that indicated when we expect to need to do more work? Often we’ll need to carry straight on the next frame, sure, but not always: there will be plenty of times where we want to carry on after an animation or sound has finished playing, or after a particular amount of time has passed. Those while(playingAnimation) yield return null; constructs are bit tedious, don’t you think?

实际上我们可以利用yield的返回值做一些方便的事情。如果我们并不是yield null并无视它,而是yield可以表明我们何时想运行下一个命令的指示会怎么样呢?通常我们需要延迟命令至下一帧,但很多时候我们想延迟命令至一段动画或一段语音播放完之后,或者过了特定的一段时间。如果都写成while(playingAnimation) yield return null;这种格式不是太繁琐了吗?

Unity declares the YieldInstruction base type, and provides a few concrete derived types that indicate particular kinds of wait. You’ve got WaitForSeconds, which resumes the coroutine after the designated amount of time has passed. You’ve got WaitForEndOfFrame, which resumes the coroutine at a particular point later in the same frame. You’ve got the Coroutine type itself, which, when coroutine A yields coroutine B, pauses coroutine A until after coroutine B has finished.

Unity声明了YieldInstruction基类,并提供了一些具体的派生类指明特定的等待形式。例如WaitForSeconds,在指定的时间后恢复协程运行;WiarForEndOfFrame,在此帧的尾部恢复;Coroutine本身,当 A yields Coroutine B,A将会暂停到B结束之后。

What does this look like from a runtime point of view? As I said, I don’t work for Unity, so I’ve never seen their code; but I’d imagine it might look a little bit like this:

从runtime的角度来看,我猜想它应该是这样的:

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;

It’s not difficult to imagine how more YieldInstruction subtypes could be added to handle other cases – engine-level support for signals, for example, could be added, with a WaitForSignal(“SignalName”)YieldInstruction supporting it. By adding more YieldInstructions, the coroutines themselves can become more expressive – yield return new WaitForSignal(“GameOver”) is nicer to read thanwhile(!Signals.HasFired(“GameOver”)) yield return null, if you ask me, quite apart from the fact that doing it in the engine could be faster than doing it in script.

不难想象,还有很多YieldInstruction的派生类处理其他的情况。例如在引擎层面的对信号的支持,可以采用WaitForSignal(“SignalName”)YieldInstruction。通过增加YieldInstruction,协程本身可以变得可读性更强,yield return new WaitForSignal(“GameOver”)要比(!Signals.HasFired(“GameOver”))yield return null更加清楚明了,并且在引擎级别进行操作要快过脚本层面。

A couple of non-obvious ramifications There’s a couple of useful things about all this that people sometimes miss that I thought I should point out.

还有一些不明显的小问题。人们可能忽视了,我觉得应该指出来。

Firstly, yield return is just yielding an expression – any expression – and YieldInstruction is a regular type. This means you can do things like:

首先,yield return 只是产生一个表达式,任何表达式,而YieldInstruction是一个普通的类。所以你可以这样:

YieldInstruction y;if(something) y = null;else if(somethingElse) y = new WaitForEndOfFrame();else y = new WaitForSeconds(1.0f);yield return y;

The specific lines yield return new WaitForSeconds(), yield return new WaitForEndOfFrame(), etc, are common, but they’re not actually special forms in their own right.

yield return new WaitForSeconds(),yield return new WaitForEndOfFrame()之类是正常写法,但不代表它们必须这样写出来。

Secondly, because these coroutines are just iterator blocks, you can iterate over them yourself if you want – you don’t have to have the engine do it for you. I’ve used this for adding interrupt conditions to a coroutine before:

第二,因为协程只是枚举器块,你可以自己迭代它们,你不一定必须要引擎来迭代它们,我曾经这样写过用来出来处理协程中出现打断的情况:

IEnumerator DoSomething(){  /* ... */}IEnumerator DoSomethingUnlessInterrupted(){  IEnumerator e = DoSomething();  bool interrupted = false;  while(!interrupted)  {    e.MoveNext();    yield return e.Current;    interrupted = HasBeenInterrupted();  }}

Thirdly, the fact that you can yield on other coroutines can sort of allow you to implement your own YieldInstructions, albeit not as performantly as if they were implemented by the engine. For example:

第三,因为你可以yield其他协程,所以你也可以使用自己写的 YieldInstruction,虽然性能会比在引擎级别运行的默认YieldInstruction要差。例如:

IEnumerator UntilTrueCoroutine(Func fn){   while(!fn()) yield return null;}Coroutine UntilTrue(Func fn){  return StartCoroutine(UntilTrueCoroutine(fn));}IEnumerator SomeTask(){  /* ... */  yield return UntilTrue(() => _lives < 3);  /* ... */}

however, I wouldn’t really recommend this – the cost of starting a Coroutine is a little heavy for my liking.
Conclusion I hope this clarifies a little some of what’s really happening when you use a Coroutine in Unity. C#’s iterator blocks are a groovy little construct, and even if you’re not using Unity, maybe you’ll find it useful to take advantage of them in the same way.

但是我不推荐上面这种自己写的,过于损耗性能。
结论,我希望以上内容可以让你更加清楚关于Unity协程的运行原理。C#的枚举器块是一个很精妙的设计,就算你不使用Unity,有一天你也有可能会用上它。

作者:James McMahon

0 0