如何在Unity中使用StartCoroutine / yield return模式?

155

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

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


3
C#编译器会转化返回IEnumerator/IEnumerable(或其泛型版本)且包含yield关键字的方法。请查询迭代器相关内容。 - Damien_The_Unbeliever
4
迭代器是一种非常方便的“状态机”抽象。如果你先了解这个,那么你也会理解Unity的协程。http://en.wikipedia.org/wiki/State_machine - Hans Passant
3
“unity”标签是由微软Unity保留的。请不要滥用它。 - Lex Li
12
我觉得这篇文章很有启发性:详解Unity3D协程 - Kay
5
@Kay - 我希望我能请你喝一杯啤酒。那篇文章正是我需要的。我开始质疑自己的理智,因为我的问题似乎没有意义,但那篇文章直接回答了我比我想象中更好的问题。也许你可以附上这个链接的答案,这样未来的SO用户也可以受益? - Ghopper21
显示剩余8条评论
8个回答

125

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



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

在C#中:
IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

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

它们是如何工作的? 让我简单地说一下,我不为Unity Technologies工作。我没有看过Unity源代码。我从未见过Unity协程引擎的内部结构。然而,如果他们实现的方式与我即将描述的方式截然不同,那么我会感到非常惊讶。如果任何UT的人想加入并谈论它的实际工作原理,那就太好了。 大线索在于C#版本。首先,请注意函数的返回类型为IEnumerator。其次,请注意其中一个语句是yield return。这意味着yield必须是一个关键字,并且由于Unity的C#支持是普通的C# 3.5,因此它必须是普通的C# 3.5关键字。确实,在MSDN中有这里——谈论一些称为“迭代器块”的东西。那么到底发生了什么呢? 首先,有这个IEnumerator类型。IEnumerator类型就像是一个序列上的光标,提供了两个重要的成员:Current,它是一个属性,给出光标当前所在的元素;和MoveNext(),它是一个函数,用于移动到序列中的下一个元素。因为IEnumerator是一个接口,它不指定这些成员的具体实现方式;MoveNext()可能只是将Current加1,或者它可能从文件中加载新值,或者它可能从互联网下载图像并对其进行哈希,并将新哈希存储在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,而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;
}

你实际上编写了一个迭代器块,它生成一长串的空值,但重要的是它计算这些值时产生的副作用。你可以使用简单的循环来运行这个协程,如下所示:
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; }
}

“时间非常重要。正如您所见,每个yield return语句都必须提供一个表达式(例如null),以便迭代器块实际上有东西可以分配给IEnumerator.Current。一长串的null并不是非常有用,但我们更感兴趣的是副作用。不是吗?实际上,我们可以利用这个表达式做一些方便的事情。如果我们不仅仅是产生null并忽略它,而是产生一些指示我们需要何时做更多工作的东西会怎样呢?通常我们需要在下一帧继续进行,当然,但并不总是:在动画或声音播放完成后,或者经过一定的时间后,我们将有很多时候想要继续进行。那些while(playingAnimation)yield return null;构造有点乏味,不是吗?Unity声明了YieldInstruction基类型,并提供了一些具体的派生类型,表示特定种类的等待。你有WaitForSeconds,它在指定的时间后恢复协程。你有WaitForEndOfFrame,在同一帧的稍后某个点恢复协程。你还有协程类型本身,当协程A yield协程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. For example, engine-level support for signals 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 than 'while(!Signals.HasFired('GameOver')) yield return null', if you ask me. Additionally, doing it in the engine could be faster than doing it in script. There are a couple of useful things about all this that people sometimes miss. Firstly, yield return is just yielding an expression - any expression - and YieldInstruction is a regular type. This means you can do things like:"
YieldInstruction y;

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

yield return y;

具体的代码行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();
  }
}

第三,你可以在其他协程上使用yield,这样就可以实现自己的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);
  /* ... */
}

然而,我不太建议这样做——启动协程的成本有点高,不符合我的喜好。

结论 我希望这能澄清一些当你在Unity中使用协程时实际上正在发生的事情。C#的迭代器块是一个很棒的构造,即使你不使用Unity,也可能会发现以同样的方式利用它们很有用。


2
感谢您在此处复制。这非常棒,对我有很大帮助。 - Naikrovek
我不理解最后一部分。为什么你需要为此启动一个新的协程?我认为你可以放弃当前的“UntilTrue”方法(它返回协程而不是IEnumerator),并直接执行IEnumerator SomeTask() { yield return UntilTrueCoroutine(() => _lives < 3); },尽管你可能不会将该方法命名为“UntilTrueCoroutine”,而是使用“UntilTrue”来代替你当前命名为“UntilTrueCoroutine”的方法。 - matthias_buehlmann
很抱歉,我真的无法支持这个答案。(提到的文章是垃圾。事实上,它是我写那篇“关于协程的真正著名文章”的原因,那篇文章曾经在SO的“专家”网站上发布,但后来关闭了。)理解协程只是一个IEnumerator非常简单(还能是什么?它只是一次又一次的返回)。我在下面用几句话完全解释了整个过程。 - Fattie
我不是要无礼或唠叨,但引用的文章是那些由一个对专业主题一无所知、在查找资料并组织“文章”的人写的文章之一(恐怕是这样)。基本上每个句子都是完全错误的、误导性的,并且并不真正理解(极其简单的)系统的运作方式。很遗憾它在互联网上的一个拥有10万次浏览量的问答中被传播了 :/ - Fattie

108

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

协程的可能乏味实现细节

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

简单来说,IEnumerator 声明拥有一组值,你可以一个接一个地请求这些值,有点像 List。在 C# 中,一个带有返回类型为 IEnumerator 的函数不需要实际创建并返回一个 IEnumerator,而是可以让 C# 提供一个隐式的 IEnumerator。然后,该函数可以通过 yield return 语句以延迟的方式提供将要返回的 IEnumerator 的内容。每当调用方从该隐式的 IEnumerator 请求另一个值时,该函数执行到下一个 yield return 语句,即提供下一个值。作为副产品,该函数暂停直到请求下一个值。
在Unity中,我们不使用这些来提供未来的值,而是利用函数暂停的事实。由于这种利用,关于Unity中协程的很多东西都没有意义(IEnumerator与任何东西有什么关系?yield是什么?为什么要使用new WaitForSeconds(3)等)。在“幕后”,你通过IEnumerator提供的值被StartCoroutine()用于决定何时请求下一个值,从而确定你的协程何时会再次暂停。

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

协程不是线程。Unity有一个主循环,你编写的所有函数都是由同一主线程按顺序调用的。你可以通过在任何一个函数或协程中放置while(true);来验证这一点。它会冻结整个东西,甚至连Unity编辑器也会冻结。这证明了所有内容都在一个主线程中运行。Kay在上面评论中提到的链接也是一个很好的资源。
(*) Unity从一个线程调用你的函数。因此,除非你自己创建一个线程,否则你编写的代码是单线程的。当然,Unity确实使用其他线程,如果你愿意,你也可以自己创建线程。

游戏程序员的协程实用描述

基本上,当你调用StartCoroutine(MyCoroutine())时,它就像对MyCoroutine()的普通函数调用一样,直到第一个yield return X,其中X是像nullnew WaitForSeconds(3)StartCoroutine(AnotherCoroutine())break等东西。这时它开始与一个函数不同。Unity在那个yield return X行暂停该函数,继续其他业务并经过一些帧,再次到达时间时,Unity恢复该函数,恢复在该行后面的所有局部变量的值。这样,您就可以拥有一个每两秒循环一次的for循环,例如。

Unity会根据你在yield return X中使用的X来决定何时恢复你的协程。例如,如果你使用了yield return new WaitForSeconds(3);,它会在3秒后恢复。如果你使用了yield return StartCoroutine(AnotherCoroutine()),它会在AnotherCoroutine()完成后恢复,这使你可以嵌套行为和时间。如果你只是使用了yield return null;,它会在下一帧立即恢复。


2
很遗憾,UnityGems似乎已经挂了一段时间了。 Reddit上的一些人设法获得了存档的最后一个版本:https://web.archive.org/web/20140702051454/http://unitygems.com/coroutines/ - ForceMagic
3
这段内容非常模糊,可能是不正确的。这是代码实际编译并正常工作的方式,也解决不了问题。https://dev59.com/hnA75IYBdhLWcg3wKluM - Louis Hong
4
我同意使用"yield return false",我加上它是因为有人批评我的答案没有它,并且我赶时间去审核它是否有用,所以只是添加了链接。我现在已经将它删除了。然而,我认为Unity是单线程的,以及协程如何与之配合并不是对每个人都很明显的。我和很多初学者的Unity程序员交谈过,他们对整个过程的理解非常模糊,因此从这样的解释中受益匪浅。我编辑了我的答案来提供一个实际的答案来回答这个问题。欢迎提出建议。 - Gazihan Alankus
2
Unity并不是单线程的,它有一个主线程,MonoBehaviour生命周期方法在其中运行,但它也有其他线程。你甚至可以自由地创建自己的线程。 - benthehutt
1
Unity宝石又可以使用了!https://unitygem.wordpress.com/2016/01/27/coroutine/ - Onat Korucu
显示剩余4条评论

8

再简单不过了:

Unity(以及所有游戏引擎)是基于帧进行处理的。

整个Unity的意义和目的就在于它是基于帧的。 引擎会在每一帧为您完成各种任务。 (动画制作、渲染对象、物理运算等等。)

你可能会问,“哦,太好了。如果我想让引擎在每一帧为我做点什么,我该如何告诉引擎执行某些操作?”

答案就是...

这正是“协程”的用途。

就是这么简单。

关于“更新”函数的说明...

简而言之,您放入“更新”中的任何内容都会在每一帧上执行。 这与协程-暂停语法完全相同,没有任何区别。

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

完全没有区别。

线程与帧/协程完全没有关系,没有任何联系。两者之间没有任何关联。

游戏引擎中的帧与线程完全没有关系,两者是完全、彻底、绝对无关的问题。

(你经常听到“Unity是单线程的!”请注意,即使这种说法也非常混乱。 帧/协程与线程根本就没有任何关系。如果Unity是多线程的、超线程的或在量子计算机上运行的!!! ……它与帧/协程完全没有关系。 这是一个完全、彻底、绝对无关的问题。)

如果Unity是多线程的、超线程的或在量子计算机上运行的!!! ……它与帧/协程完全没有关系。 这是一个完全、彻底、绝对无关的问题。

所以总结一下...

因此,协程/yield只是访问Unity中帧的方式。就是这样。

(实际上,它与Unity提供的Update()函数完全相同。)

就是这些,很简单。

为什么使用IEnumerator?

再简单不过了:IEnumerator返回“一遍又一遍”的东西。

(那些东西的列表可以有一个特定的长度,比如“10个东西”,或者列表可以永远继续下去。)

因此,显然,IEnumerator就是你要使用的。

在.Net中,任何想要“一遍遍地返回”的地方都可以使用IEnumerator。

当然,所有基于帧的计算都使用IEnumerator来返回每个帧。还能用什么?

(如果您是C#的新手,请注意,IEnumerator也用于逐个返回“普通”的东西,例如数组中的项等。)


1
谢谢!但是你的回答解释了如何使用协程——而不是它们在幕后的工作原理。 - Ghopper21
2
你说“完全没有区别”。那么为什么Unity会创建协程,当它们已经有一个像Update()这样的精确工作实现呢?我的意思是这两种实现和它们的用例之间应该至少存在一些微小的差异,这是相当明显的。 - MamiBe
@LeandroGecozo - Update只是为初学者添加的一个愚蠢的简化,没有什么神秘的。然而,一个区别是你知道Update按特定顺序运行(可以轻松查找各个帧调用发生的模糊顺序)。 - Fattie
2
这个答案有多个问题。协程比这更复杂,并且有更多需要注意的地方。如果你只是用它们做简单的事情 - 太好了!干得好!我很高兴!- 但你错过了它们的作用和工作原理。 - Adam
1
亚当,你知道这个问题是关于Unity的吗? - Fattie
显示剩余3条评论

5
最近我深入探究了这个问题,并在这里写了一篇文章 - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - 它阐述了内部细节(包括密集的代码示例)、底层IEnumerator接口,以及如何将其用于协程。

对我来说,仍然觉得使用集合枚举器有点奇怪。这与枚举器旨在设计的相反。枚举器的重点是每次访问返回的值,但是协程的重点是值之间的代码。在此情况下,实际返回的值是无意义的。


3
在Unity 2017+中,您可以使用本地C# async/await关键字来实现异步代码,但是在那之前,C#没有本地方法来实现异步代码
Unity必须使用一种解决异步代码的方法。他们通过利用C#迭代器来实现这一点,这是当时流行的异步技术。 看一下C#迭代器:

假设您有以下代码:

IEnumerable SomeNumbers() {
  yield return 3;
  yield return 5;
  yield return 8;
}

如果您通过循环调用它,就像调用数组一样,您将得到3 5 8:
// Output: 3 5 8
foreach (int number in SomeNumbers()) {
  Console.Write(number);
}

如果您不熟悉迭代器(大多数语言都有它们来实现列表和集合),它们的工作方式类似于数组。区别在于回调函数生成值。
它们如何工作?
在C#中循环迭代器时,我们使用MoveNext转到下一个值。
在此示例中,我们使用foreach,在幕后调用此方法。
当我们调用MoveNext时,迭代器执行所有内容,直到其下一个yield。父调用者获得yield返回的值。然后,迭代器代码暂停,等待下一个MoveNext调用。
由于它们的“惰性”功能,C#程序员使用迭代器运行异步代码。
使用迭代器进行C#异步编程
在2012年之前,使用迭代器是在C#中执行异步操作的一种流行的hack方法。
示例-异步下载功能:
IEnumerable DownloadAsync(string URL) {
  WebRequest  req      = HttpWebRequest.Create(url);
  WebResponse response = req.GetResponseAsync();
  yield return response;

  Stream resp = response.Result.GetResponseStream();
  string html = resp.ReadToEndAsync().ExecuteAsync();
  yield return html;

  Console.WriteLine(html.Result);
}

PS:上面的代码来自于这篇优秀但有点陈旧的文章,介绍了使用迭代器进行异步编程:http://tomasp.net/blog/csharp-async.aspx/

我应该使用async还是StartCoroutine

截至2021年,官方Unity文档在示例中使用协程而不是async

此外,社区似乎更倾向于使用协程而不是async

  • 开发人员熟悉协程;
  • 协程与Unity集成;
  • 等等。

我推荐观看2019年的Unity讲座,标题为"最佳实践:Async vs. coroutines - Unite Copenhagen 2019":https://youtu.be/7eKi6NKri6I


PS: 这是一个2012年的旧问题,但我回答它是因为在2021年仍然相关。


0
协程就像独立的函数。它们可以独立运行,因此您可以包含等待和其他酷炫的功能。

1
这似乎更像是一条评论而不是一个答案。 - undefined

-2

StartCoroutine是一种调用IEnumerator函数的方法。它类似于调用简单的void函数,只是不同之处在于您将其用于IEnumerator函数。这种类型的函数是独特的,因为它可以允许您使用特殊的yield函数,注意您必须返回一些东西。就我所知,就这些了。 这里我在Unity中编写了一个简单的闪烁游戏结束文本方法。

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

然后我从IEnumerator本身调用它

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

正如你所看到的,我是如何使用StartCoroutine()方法的。 希望我能在某种程度上帮助到你。我自己也是一个初学者,如果你能纠正我或者赞赏我,任何形式的反馈都将不胜感激。


-2
Unity中自动获取的基本函数是Start()函数和Update()函数,因此Coroutine实际上就像Start()和Update()函数一样。任何旧函数func()都可以像调用Coroutine一样被调用。Unity显然为Coroutines设定了某些边界,使它们与常规函数不同。
其中一个区别是,Coroutine不会立即执行完毕,而是可以在执行过程中暂停并在稍后继续执行。这使得它们非常适合处理需要等待时间的任务,例如动画或网络请求。
  void func()

你写

  IEnumerator func()

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

  Time.deltaTime

协程具有特定的处理方式,可以控制时间。
  yield return new WaitForSeconds();

虽然在IEnumerator / Coroutine中执行的操作不仅限于此,但这是Coroutine用于的一种有用方式。您需要研究Unity的脚本API以了解Coroutine的其他特定用途。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接