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

时间:2012-10-17 10:30:48

标签: c# unity3d coroutine

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

我的问题是:幕后真的发生了什么? StartCoroutine真正做了什么? IEnumerator WaitForSeconds返回的是什么? StartCoroutine如何将控制权返回到被调用方法的“其他”部分?所有这些如何与Unity的并发模型相互作用(在不使用协同程序的情况下,许多事情同时发生)?

6 个答案:

答案 0 :(得分:92)

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

可能无聊的Coroutines实施细节

Wikipedia和其他地方解释了协同程序。在这里,我将从实际的角度提供一些细节。 IEnumeratoryieldC# language features是在Unity中用于某种不同目的的This link

简单地说,IEnumerator声称拥有一组值,您可以逐个请求,有点像List。在C#中,具有签名以返回IEnumerator的函数不必实际创建并返回一个函数,但可以让C#提供隐式IEnumerator。然后,该函数可以通过IEnumerator语句以懒惰的方式提供将来返回的yield return的内容。每次调用者从该隐式IEnumerator请求另一个值时,该函数将执行,直到下一个yield return语句,该语句提供下一个值。作为此副产品,函数暂停,直到请求下一个值。

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

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

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

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

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

基本上,当您拨打StartCoroutine(MyCoroutine())时,它就像对MyCoroutine()的常规函数​​调用一样,直到第一个yield return X,其中X类似于null },new WaitForSeconds(3)StartCoroutine(AnotherCoroutine())break等。这是它开始与函数不同的时候。 Unity“暂停”该功能正好在yield return X行,继续与其他业务和一些帧通过,当它再次时,Unity在该行之后立即恢复该功能。它会记住函数中所有局部变量的值。这样,您可以拥有一个for循环,例如每两秒循环一次。

当Unity恢复你的协程取决于你Xyield return X的内容。例如,如果您使用yield return new WaitForSeconds(3);,则会在3秒后恢复。如果您使用yield return StartCoroutine(AnotherCoroutine()),则会在AnotherCoroutine()完成后恢复,这样您就可以及时嵌套行为。如果您刚刚使用yield return null;,则会在下一帧恢复正常。

答案 1 :(得分:9)

它不会更简单:

Unity(以及所有游戏引擎)基于框架

整个观点,Unity的整个存在理由是,它是基于框架的。 发动机做的事情"每个框架"对你而言。(动画,渲染物体,做物理等等。)

你可能会问......"哦,那太好了。如果我希望引擎每帧都为我做一些事情怎么办?如何告诉引擎在框架中执行此类操作?"

答案是......

这正是" coroutine"是为了。

就这么简单。

考虑一下......

你知道"更新"功能。很简单,你放在那里的任何东西都是每一帧。它与coroutine-yield语法完全相同,完全没有区别。

ListViewController

绝对没有区别。

脚注:正如大家所指出的那样,Unity只是 没有线程 。 "帧"在Unity或任何游戏引擎中完全没有任何与线程的连接。

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

答案 2 :(得分:6)

最近深入研究,在这里写了一篇文章 - http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ - 揭示了内部(密集的代码示例),底层IEnumerator接口,以及它如何用于协同程序

  

为此目的使用集合枚举器对我来说似乎有点奇怪。这是调查员设计的反面。枚举数点是每次访问时返回的值,但Coroutines的点是值返回值之间的代码。在这种情况下,实际返回的值毫无意义。

答案 3 :(得分:0)

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# 中执行异步操作的流行技巧。

示例 - 异步下载功能:

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

此外,社区似乎更支持协程而不是异步:

  • 开发人员熟悉协程;
  • 协程与 Unity 集成;
  • 以及其他;

我推荐 2019 年的 Unity 讲座“最佳实践:异步与协程 - Unite Copenhagen 2019”:https://youtu.be/7eKi6NKri6I


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

答案 4 :(得分:-1)

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

  void func()

您写

  IEnumerator func()

用于协程。 同样,您可以使用代码行

来控制普通功能中的时间
  Time.deltaTime

协程在控制时间的方式上有特定的处理方式。

  yield return new WaitForSeconds();

尽管这不是在IEnumerator /协程内部唯一可行的方法,但它是协程用于的有用功能之一。您必须研究Unity的脚本API才能了解协同程序的其他特定用途。

答案 5 :(得分:-1)

StartCoroutine是一种调用IEnumerator函数的方法。它类似于只调用一个简单的void函数,不同之处在于您在IEnumerator函数上使用了它。这种类型的函数是唯一的,因为它可以允许您使用特殊的 yield 函数,请注意,您必须返回某些内容。就我所知。 在这里,我统一编写了一个简单的闪烁游戏文本上方方法

    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()方法。 希望我能有所帮助。我本人是一个初学者,因此,如果您纠正我或赞赏我,任何形式的反馈都将是很棒的。