C# StartCoroutine / yield 返回模式如何在 Unity 中真正起作用?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/12932306/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 00:59:30  来源:igfitidea点击:

How does StartCoroutine / yield return pattern really work in Unity?

c#unity3dcoroutine

提问by Ghopper21

I understand the principle of coroutines. I know how to get the standard StartCoroutine/ yield returnpattern to work in C# in Unity, e.g. invoke a method returning IEnumeratorvia StartCoroutineand in that method do something, do yield return new WaitForSeconds(1);to wait a second, then do something else.

我了解协程的原理。我知道如何让标准StartCoroutine/yield return模式在 Unity 中的 C# 中工作,例如调用IEnumerator通过该方法返回的方法StartCoroutine并在该方法中执行某些操作yield return new WaitForSeconds(1);,等待一秒钟,然后执行其他操作。

My question is: what's really going on behind the scenes? What does StartCoroutinereally do? What IEnumeratoris WaitForSecondsreturning? How does StartCoroutinereturn 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)?

我的问题是:幕后到底发生了什么?什么是StartCoroutine真的?什么IEnumeratorWaitForSeconds回归?如何StartCoroutine将控制权返回给被调用方法的“其他”部分?所有这些如何与 Unity 的并发模型(其中许多事情同时进行而不使用协程)进行交互?

采纳答案by James McMahon

The oft referenced Unity3D coroutines in detaillink 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.

经常引用的Unity3D 协程详细链接已失效。由于在评论和答案中提到了它,我将在这里发布文章的内容。此内容来自此镜像



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.

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

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.

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):

Unity3D协程详解

游戏中的许多过程发生在多个帧的过程中。你有“密集”的过程,比如寻路,它在每一帧都努力工作,但被分成多个帧,以免对帧率产生太大影响。你有“稀疏”的进程,比如游戏触发器,它们在大多数帧中什么都不做,但偶尔会被要求做关键工作。并且您在两者之间有各种各样的流程。

每当您创建一个将在多个帧上发生的进程时——没有多线程——您需要找到某种方法将工作分解成可以每帧运行一个的块。对于任何具有中央循环的算法,这是相当明显的:例如,A* 探路者可以构造成半永久性地维护其节点列表,每帧仅处理开放列表中的少数节点,而不是尝试一口气完成所有工作。需要做一些平衡来管理延迟——毕竟,如果你将帧速率锁定在每秒 60 或 30 帧,那么你的过程将只需要每秒 60 或 30 步,这可能会导致过程只需要整体太长了。一个整洁的设计可能会在一个级别上提供尽可能小的工作单元——例如 处理单个 A* 节点——并在顶部分层,将工作组合成更大的块——例如,继续处理 A* 节点 X 毫秒。(有些人称之为“时间切片”,但我不这么认为)。

尽管如此,允许以这种方式分解工作意味着您必须将状态从一帧转移到下一帧。如果您正在分解迭代算法,那么您必须保留所有迭代共享的状态,以及跟踪下一次执行哪个迭代的方法。这通常不算太糟糕——“A* 探路者类”的设计相当明显——但也有其他情况不太令人愉快。有时您会面临长时间的计算,这些计算会逐帧执行不同类型的工作;捕获其状态的对象最终可能会产生一大堆半有用的“本地”,用于将数据从一帧传递到下一帧。如果你正在处理一个稀疏的过程,

如果您不必跨多个帧显式跟踪所有这些状态,并且不必多线程并管理同步和锁定等,而是将您的函数编写为单个代码块,并且标记函数应该“暂停”并在以后继续执行的特定位置?

Unity——连同许多其他环境和语言——以协程的形式提供了这一点。

他们看起来怎么样?在“Unityscript”(Javascript)中:

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

In C#:

在 C# 中:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        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.

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?

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.

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.

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.

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:

它们是如何工作的?让我快速说一下,我不为 Unity Technologies 工作。我还没有看到 Unity 源代码。我从未见过 Unity 协程引擎的胆量。但是,如果他们以与我将要描述的完全不同的方式实施它,那么我会感到非常惊讶。如果来自 UT 的任何人想插话并谈论它的实际工作原理,那就太好了。

重要的线索在 C# 版本中。首先,请注意该函数的返回类型是 IEnumerator。其次,请注意其中一个语句是 yield return。这意味着 yield 必须是关键字,并且由于 Unity 的 C# 支持是 vanilla C# 3.5,所以它必须是 vanilla C# 3.5 关键字。确实,它在 MSDN 中– 谈论称为“迭代器块”的东西。发生什么了?

首先,有这个 IEnumerator 类型。IEnumerator 类型就像一个序列上的光标,提供两个重要成员:Current,它是一个属性,为您提供光标当前所在的元素,以及 MoveNext(),一个移动到序列中下一个元素的函数。因为 IEnumerator 是一个接口,它没有具体说明这些成员是如何实现的;MoveNext() 可以只添加一个 toCurrent,或者它可以从文件加载新值,或者它可以从 Internet 下载图像并将其散列并将新散列存储在 Current 中……或者它甚至可以为第一件事做一件事序列中的元素,而第二个则完全不同。如果您愿意,您甚至可以使用它来生成无限序列。

通常,如果你想实现一个接口,你必须编写一个类,实现成员等等。迭代器块是实现 IEnumerator 的一种便捷方式,没有那么麻烦——您只需遵循一些规则,IEnumerator 实现是由编译器自动生成的。

迭代器块是一个常规函数,它 (a) 返回 IEnumerator,并且 (b) 使用 yield 关键字。那么 yield 关键字实际上是做什么的呢?它声明序列中的下一个值是什么——或者没有更多的值。代码遇到 yield return X 或 yield break 的点是 IEnumerator.MoveNext() 应该停止的点;yield return X 导致 MoveNext() 返回 true 并为 Current 分配值 X,而 yield break 导致 MoveNext() 返回 false。

现在,诀窍就在这里。序列返回的实际值是什么并不重要。可以重复调用 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:

并且您实际编写的是一个迭代器块,它生成一长串空值,但重要的是它为计算它们所做的工作的副作用。你可以使用一个简单的循环来运行这个协程,如下所示:

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?

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?

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.

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:

一切都在计时中 正如您所见,每个 yield return 语句都必须提供一个表达式(如 null),以便迭代器块可以实际分配给 IEnumerator.Current 的内容。一长串空值并不完全有用,但我们对副作用更感兴趣。我们不是吗?

事实上,我们可以用这个表达式来做一些方便的事情。如果,而不是仅仅产生 null 并忽略它,我们产生了一些表明我们期望需要做更多工作的东西怎么办?通常,我们需要直接进行下一帧,当然,但并非总是如此:在动画或声音播放完毕后,或者经过特定时间后,我们有很多次想要继续进行。那些 while(playingAnimation) yield return null; 构造有点乏味,你不觉得吗?

Unity 声明了 YieldInstruction 基类型,并提供了一些指示特定等待类型的具体派生类型。你有 WaitForSeconds,它在指定的时间量过后恢复协程。你有WaitForEndOfFrame,它在同一帧稍后的特定点恢复协程。你有 Coroutine 类型本身,当协程 A 产生协程 B 时,它会暂停协程 A 直到协程 B 完成。

从运行时的角度来看,这是什么样子的?正如我所说,我不为 Unity 工作,所以我从未见过他们的代码;但我想它可能看起来有点像这样:

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.

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:

不难想象可以添加更多的 YieldInstruction 子类型来处理其他情况——例如,可以添加对信号的引擎级支持,并使用 WaitForSignal("SignalName")YieldInstruction 支持它。通过添加更多的 YieldInstructions,协程本身可以变得更具表现力—— yield return new WaitForSignal("GameOver") 比 while(!Signals.HasFired("GameOver")) yield return null 更好读,如果你问我,事实上,在引擎中执行它可能比在脚本中执行更快。

一些不明显的后果 关于这一切,人们有时会忽略一些有用的事情,我认为我应该指出。

首先,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.

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:

特定的行 yield return new WaitForSeconds()、 yield return new WaitForEndOfFrame() 等很常见,但实际上它们本身并不是特殊形式。

其次,因为这些协程只是迭代器块,如果你愿意,你可以自己迭代它们——你不必让引擎为你做。我以前用它来向协程添加中断条件:

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:

第三,您可以在其他协程上让步这一事实可以让您实现自己的 YieldInstructions,尽管它们的性能不如引擎实现的那么好。例如:

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,也许您会发现以同样的方式利用它们会很有用。

回答by Gazihan Alankus

The first heading below is a straight answer to the question. The two headings after are more useful for the everyday programmer.

下面的第一个标题是对问题的直接回答。后面的两个标题对日常程序员更有用。

Possibly Boring Implementation Details of Coroutines

协程可能无聊的实现细节

Coroutines are explained in Wikipediaand elsewhere. Here I'll just provide some details from a practical point of view. IEnumerator, yield, etc. are C# language featuresthat are used for somewhat of a different purpose in Unity.

协程在维基百科和其他地方有解释。这里我只从实际的角度提供一些细节。IEnumeratoryield等是C# 语言功能,在 Unity 中用于某些不同的目的。

To put it very simply, an IEnumeratorclaims to have a collection of values that you can request one by one, kind of like a List. In C#, a function with a signature to return an IEnumeratordoes not have to actually create and return one, but can let C# provide an implicit IEnumerator. The function then can provide the contents of that returned IEnumeratorin the future in a lazy fashion, through yield returnstatements. Every time the caller asks for another value from that implicit IEnumerator, the function executes till the next yield returnstatement, which provides the next value. As a byproduct of this, the function pauses until the next value is requested.

简而言之,aIEnumerator声称拥有一组您可以逐个请求的值,有点像List. 在 C# 中,一个带有返回一个签名的函数IEnumerator不必实际创建和返回一个,但可以让 C# 提供一个隐式的IEnumerator. 然后,该函数可以IEnumerator通过yield return语句以惰性方式提供将来返回的内容。每次调用者从该隐式中请求另一个值时IEnumerator,该函数都会执行到下一个yield return语句,该语句提供下一个值。作为这种情况的副产品,函数会暂停,直到请求下一个值。

In Unity, we don't use these to provide future values, we exploit the fact that the function pauses. Because of this exploitation, a lot of things about coroutines in Unity do not make sense (What does IEnumeratorhave to do with anything? What is yield? Why new WaitForSeconds(3)? etc.). What happens "under the hood" is, the values you provide through the IEnumerator are used by StartCoroutine()to decide when to ask for the next value, which determines when your coroutine will unpause again.

在 Unity 中,我们不使用这些来提供未来值,我们利用函数暂停的事实。由于这种利用,Unity 中关于协程的很多事情都没有意义(IEnumerator与任何事情有什么关系?是什么yield?为什么new WaitForSeconds(3)?等等)。“幕后”发生的事情是,您通过 IEnumerator 提供的值用于StartCoroutine()决定何时请求下一个值,这决定了您的协程何时再次取消暂停。

Your Unity Game is Single Threaded (*)

您的 Unity 游戏是单线程的 (*)

Coroutines are notthreads. There is one main loop of Unity and all those functions that you write are being called by the same main thread in order. You can verify this by placing a while(true);in any of your functions or coroutines. It will freeze the whole thing, even the Unity editor. This is evidence that everything runs in one main thread. This linkthat Kay mentioned in his above comment is also a great resource.

协程不是线程。Unity 有一个主循环,您编写的所有函数都由同一个主线程按顺序调用。您可以通过while(true);在任何函数或协程中放置 a 来验证这一点。它会冻结整个内容,甚至是 Unity 编辑器。这证明一切都在一个主线程中运行。Kay 在上面的评论中提到的这个链接也是一个很好的资源。

(*) Unity calls your functions from one thread. So, unless you create a thread yourself, the code that you wrote is single threaded. Of course Unity does employ other threads and you can create threads yourself if you like.

(*) Unity 从一个线程调用您的函数。因此,除非您自己创建线程,否则您编写的代码是单线程的。当然,Unity 确实使用了其他线程,如果您愿意,您可以自己创建线程。

A Practical Description of Coroutines for Game Programmers

游戏程序员协程实用说明

Basically, when you call StartCoroutine(MyCoroutine()), it's exactly like a regular function call to MyCoroutine(), until the first yield return X, where Xis something like null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), break, etc. This is when it starts differing from a function. Unity "pauses" that function right at that yield return Xline, goes on with other business and some frames pass, and when it's time again, Unity resumes that function right after that line. It remembers the values for all the local variables in the function. This way, you can have a forloop that loops every two seconds, for example.

基本上,当你调用StartCoroutine(MyCoroutine()),这也正是像一个普通的函数调用MyCoroutine(),直到第一个yield return X,在那里X是一样的东西nullnew WaitForSeconds(3)StartCoroutine(AnotherCoroutine())break,等,这是当它开始从功能不同。Unity 在该yield return X行“暂停”该功能,继续处理其他业务并通过一些帧,当时间再次到来时,Unity 会在该行之后立即恢复该功能。它记住函数中所有局部变量的值。例如,通过这种方式,您可以创建一个for每两秒循环一次的循环。

When Unity will resume your coroutine depends on what Xwas in your yield return X. For example, if you used yield return new WaitForSeconds(3);, it resumes after 3 seconds have passed. If you used yield return StartCoroutine(AnotherCoroutine()), it resumes after AnotherCoroutine()is completely done, which enables you to nest behaviors in time. If you just used a yield return null;, it resumes right at the next frame.

Unity 何时恢复您的协程取决于X您的yield return X. 例如,如果您使用yield return new WaitForSeconds(3);,它会在 3 秒后恢复。如果您使用yield return StartCoroutine(AnotherCoroutine()),它会在AnotherCoroutine()完全完成后恢复,这使您可以及时嵌套行为。如果您刚刚使用了yield return null;,它会在下一帧恢复。

回答by Geri Borbás

Have dig into this lately, wrote a post here - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/- that shed a light on the internals (with dense code examples), the underlying IEnumeratorinterface, and how it is used for coroutines.

最近对此进行了深入研究,在这里写了一篇文章 - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/- 阐明了内部结构(带有密集的代码示例),底层IEnumerator接口,以及它如何用于协程。

Using collection enumerators for this purpose still seems a bit weird for me. It is the inverse of what enumerators feels designed for. The point of enumerators is the returned value on every access, but the point of Coroutines is the code in-between the value returns. The actual returned value is pointless in this context.

为此目的使用集合枚举器对我来说仍然有点奇怪。它与枚举器的设计目的相反。枚举器的要点是每次访问时的返回值,而协程的要点是值返回之间的代码。在这种情况下,实际返回的值毫无意义。

回答by Fattie

It couldn't be simpler:

再简单不过了:

Unity (and all game engines) are frame based.

Unity(和所有游戏引擎)都是基于框架的

The whole entire point, the whole raison d'etre of Unity, is that it is frame based. The engine does things "each frame" for you.(Animates, renders objects, does physics, and so on.)

整点,Unity 的全部存在理由,就是它是基于框架的。引擎为你做“每一帧”的事情。(动画、渲染对象、处理物理等等。)

You might ask .. "Oh, that's great. What if I want the engine to do something for me each frame? How do I tell the engine to do such-and-such in a frame?"

你可能会问..“哦,那太好了。如果我想让引擎在每一帧为我做一些事情怎么办?我如何告诉引擎在一个帧中做某事?”

The answer is ...

答案是 ...

That's exactly what a "coroutine" is for.

这正是“协程”的用途。

It's just that simple.

就是这么简单。

And consider this....

并考虑这个......

You know the "Update" function. Quite simply, anything you put in there is done every frame. It's literally exactly the same, no difference at all, from the coroutine-yield syntax.

您知道“更新”功能。很简单,您放入的任何内容都会在每一帧中完成。从字面上看,它与 coroutine-yield 语法完全相同,完全没有区别。

void Update()
 {
 this happens every frame,
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 }

...in a coroutine...
 while(true)
 {
 this happens every frame.
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 yield return null;
 }

There is absolutely no difference.

绝对没有区别。

Footnote: as everyone has pointed out, Unity simply has no threads. The "frames" in Unity or in any game engine have utterly no connection to threads in any way.

脚注:正如大家所指出的,Unity 根本没有线程。Unity 或任何游戏引擎中的“框架”与线程完全没有任何联系。

Coroutines/yield are simply how you access the frames in Unity. That's it. (And indeed, it's absolutely the same as the Update() function provided by Unity.) That's all there is to it, it's that simple.

Coroutines/yield 只是您在 Unity 中访问帧的方式。就是这样。(实际上,它与 Unity 提供的 Update() 函数完全相同。)这就是它的全部内容,就这么简单。

回答by Alexhawkburr

The basis functions in Unity that you get automatically are the Start() function and the Update() function, so Coroutine's are essentially functions just like the Start() and Update() function. Any old function func() can be called the same way a Coroutine can be called. Unity has obviously set certain boundaries for Coroutines that make them different than regular functions. One difference is instead of

Unity 中自动获得的基本函数是 Start() 函数和 Update() 函数,因此 Coroutine 本质上是与 Start() 和 Update() 函数一样的函数。任何旧函数 func() 都可以像调用协程一样被调用。Unity 显然为协程设定了某些界限,使它们与常规函数不同。一个区别是而不是

  void func()

You write

你写

  IEnumerator func()

for coroutines. And the same way you can control the time in normal functions with code lines like

对于协程。同样,您可以使用代码行控制普通函数中的时间,例如

  Time.deltaTime

A coroutine has a specific handle on the way time can be controlled.

协程在控制时间的方式上有一个特定的句柄。

  yield return new WaitForSeconds();

Although this is not the only thing possible to do inside of an IEnumerator/Coroutine, it is one of the useful things that Coroutines are used for. You would have to research Unity's scripting API to learn other specific uses of Coroutines.

虽然这不是 IEnumerator/Coroutine 中唯一可以做的事情,但它是 Coroutines 的有用的事情之一。您必须研究 Unity 的脚本 API 才能了解协程的其他特定用途。

回答by Deepanshu Mishra

StartCoroutine is a method to call a IEnumerator function. It is similar to just calling a simple void function, just the difference is that you use it on IEnumerator functions. This type of function is unique as it can allow you to use a special yieldfunction, note that you must return something. Thats as far as I know. Here I wrote a simple flicker game Over textmethod in unity

StartCoroutine 是一种调用 IEnumerator 函数的方法。它类似于只是调用一个简单的 void 函数,不同的是你在 IEnumerator 函数上使用它。这种类型的函数是独一无二的,因为它可以让你使用特殊的yield函数,注意你必须返回一些东西。据我所知。这里我统一写了一个简单的闪烁游戏Over text方法

    public IEnumerator GameOver()
{
    while (true)
    {
        _gameOver.text = "GAME OVER";
        yield return new WaitForSeconds(Random.Range(1.0f, 3.5f));
        _gameOver.text = "";
        yield return new WaitForSeconds(Random.Range(0.1f, 0.8f));
    }
}

I then called it out of the IEnumerator itself

然后我从 IEnumerator 本身中调用它

    public void UpdateLives(int currentlives)
{
    if (currentlives < 1)
    {
        _gameOver.gameObject.SetActive(true);
        StartCoroutine(GameOver());
    }
}

As you can see how I used the StartCoroutine() method. Hope I helped somehow. I am a begainner myself, so if you correct me, or apprecite me, any type of a feedback would be great.

如您所见,我是如何使用 StartCoroutine() 方法的。希望我以某种方式有所帮助。我自己也是初学者,所以如果你纠正我或欣赏我,任何类型的反馈都会很棒。