Unity3D协程介绍 以及 使用


作者ChevyRay ,2013年9月28日,snaker7译  原文地址:http://unitypatterns.com/introduction-to-coroutines/

在Unity中,协程(Coroutines)的形式是我最喜欢的功能之一,几乎在所有的项目中,我都会使用它来控制运动,序列,以及对象的行为。在这个教程中,我将会说明协程是如何工作的,并且会附上一些例子来介绍它的用法。

协程介绍

Unity的协程系统是基于C#的一个简单而强大的接口 ,IEnumerator,它允许你为自己的集合类型编写枚举器。这一点你不必关注太多,我们直接进入一个简单的例子来看看协程到底能干什么。首先,我们来看一下这段简单的代码...

倒计时器

这是一个简单的脚本组件,只做了倒计时,并且在到达0的时候log一个信息。

[csharp] view plain copy

  1. using Unity Engine;
  2. using System.Collections;
  3. public class Countdown : MonoBehaviour
  4. {
  5. public float timer = 3;
  6. void Update()
  7. {
  8. timer -= Time.deltaTime;
  9. if(timer <= 0)
  10. Debug.Log("Timer has finished!");
  11. }
  12. }

还不错,代码简短实用,但问题是,如果我们需要复杂的脚本组件(像一个角色或者敌人的类),拥有多个计时器呢?刚开始的时候,我们的代码也许会是这样的:

[csharp] view plain copy

  1. using UnityEngine;
  2. using System.Collections;
  3. public class MultiTimer : MonoBehaviour
  4. {
  5. public float firstTimer = 3;
  6. public float secondTimer = 2;
  7. public float thirdTimer = 1;
  8. void Update()
  9. {
  10. firstTimer -= Time.deltaTime;
  11. if(firstTimer <= 0)
  12. Debug.Log("First timer has finished!");
  13. secondTimer -= Time.deltaTime;
  14. if(secondTimer <= 0)
  15. Debug.Log("Second timer has finished!");
  16. thirdTimer -= Time.deltaTime;
  17. if(thirdTimer <= 0)
  18. Debug.Log("Third timer has finished!");
  19. }
  20. }

尽管不是太糟糕,但是我个人不是很喜欢自己的代码中充斥着这些计时器变量,它们看上去很乱,而且当我需要重新开始计时的时候还得记得去重置它们(这活我经常忘记做)。

如果我只用一个for循环来做这些,看上去是否会好很多?

[csharp] view plain copy

  1. for(float timer = 3; timer >= 0; timer -= Time.deltaTime)
  2. {
  3. //Just do nothing...
  4. }
  5. Debug.Log("This happens after 5 seconds!");

现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。

好的,你可能现在明白我的意思:协程可以做的正是这一点!

码入你的协程!

现在,这里提供了上面例子运用协程的版本!我建议你从这里开始跟着我来写一个简单的脚本组件,这样你可以在你自己的程序中看到它是如何工作的。

[csharp] view plain copy

  1. using UnityEngine;
  2. using System.Collections;
  3. public class CoroutineCountdown : MonoBehaviour
  4. {
  5. void Start()
  6. {
  7. StartCoroutine(Countdown());
  8. }
  9. IEnumerator Countdown()
  10. {
  11. for(floattimer = 3; timer >= 0; timer -= Time.deltaTime)
  12. Yield return 0;
  13. Debug.Log("This message appears after 3 seconds!");
  14. }
  15. }

这看上去有点不一样,没关系,接下来我会解释这里到底发生了什么。

[csharp] view plain copy

  1. StartCoroutine(Countdown());

这一行用来开始我们的Countdown程序,注意,我并没有给它传入参数,但是这个方法调用了它自己(这是通过传递Countdown的return返回值来实现的)。

Yield

在Countdown方法中其他的都很好理解,除了两个部分:

l IEnumerator 的返回值

l For循环中的yield return

为了能在连续的多帧中(在这个例子中,3秒钟等同于很多帧)调用该方法,Unity必须通过某种方式来存储这个方法的状态,这是通过IEnumerator 中使用yield return语句得到的返回值,当你“yield”一个方法时,你相当于说了,“现在停止这个方法,然后在下一帧中从这里重新开始!”。

注意:用0或者null来yield的意思是告诉协程等待下一帧,直到继续执行为止。当然,同样的你可以继续yield其他协程,我会在下一个教程中讲到这些。

一些例子

协程在刚开始接触的时候是非常难以理解的,无论是新手还是经验丰富的程序员我都见过他们对于协程语句一筹莫展的时候。因此我认为通过例子来理解它是最好的方法,这里有一些简单的协程例子:

多次输出“Hello”

记住,yield return是“停止执行方法,并且在下一帧从这里重新开始”,这意味着你可以这样做:

[csharp] view plain copy

  1. //This will say hello 5 times, once each frame for 5 frames
  2. IEnumerator SayHelloFiveTimes()
  3. {
  4. Yield return 0;
  5. Debug.Log("Hello");
  6. Yield return 0;
  7. Debug.Log("Hello");
  8. Yield return 0;
  9. Debug.Log("Hello");
  10. Yield return 0;
  11. Debug.Log("Hello");
  12. Yield return 0;
  13. Debug.Log("Hello");
  14. }
  15. //This will do the exact same thing as the above function!
  16. IEnumerator SayHello5Times()
  17. {
  18. for(inti = 0; i < 5; i++)
  19. {
  20. Debug.Log("Hello");
  21. Yield return 0;
  22. }
  23. }

每一帧输出“Hello”,无限循环。。。

通过在一个while循环中使用yield,你可以得到一个无限循环的协程,这几乎就跟一个Update()循环等同。。。

[csharp] view plain copy

  1. //Once started, this will run until manually stopped or the object is destroyed
  2. IEnumerator SayHelloEveryFrame()
  3. {
  4. while(true)
  5. {
  6. //1. Say hello
  7. Debug.Log("Hello");
  8. //2. Wait until next frame
  9. Yield return 0;
  10. }//3. This is a forever-loop, goto 1
  11. }

计时

...不过跟Update()不一样的是,你可以在协程中做一些更有趣的事:

[csharp] view plain copy

  1. IEnumerator CountSeconds()
  2. {
  3. int seconds = 0;
  4. while(true)
  5. {
  6. for(float timer = 0; timer < 1; timer += Time.deltaTime)
  7. Yield return 0;
  8. seconds++;
  9. Debug.Log(seconds +" seconds have passed since the Coroutine started.");
  10. }
  11. }

这个方法突出了协程一个非常酷的地方:方法的状态被存储了,这使得方法中定义的这些变量都会保存它们的值,即使是在不同的帧中。还记得这个教程开始时那些烦人的计时器变量吗?通过协程,我们再也不需要担心它们了,只需要把变量直接放到方法里面!

开始和终止协程

之前,我们已经学过了通过 StartCoroutine()方法来开始一个协程,就像这样:

[csharp] view plain copy

  1. StartCoroutine(Countdown());

如果我们想要终止所有的协程,可以通过StopAllCoroutines()方法来实现,它的所要做的就跟它的名字所表达的一样。注意,这只会终止在调用该方法的对象中(应该是指调用这个方法的类吧)开始的协程,对于其他的MonoBehavior类中运行的协程不起作用。

如果我们有以下这样两条协程语句:

[csharp] view plain copy

  1. StartCoroutine(FirstTimer());
  2. StartCoroutine(SecondTimer());

。。。那我们怎么终止其中的一个协程呢?在这个例子里,这是不可能的,如果你想要终止某一个特定的协程,那么你必须得在开始协程的时候将它的方法名作为字符串,就像这样:

[csharp] view plain copy

  1. //If you start a Coroutine by name...
  2. StartCoroutine("FirstTimer");
  3. StartCoroutine("SecondTimer");
  4. //You can stop it anytime by name!
  5. StopCoroutine("FirstTimer");

更多关于协程的学习

即将为你带来:“Scripting with Coroutines”,一个更深入的介绍,关于如何使用协程以及如何通过协程编写对象行为。

扩展链接

Coroutines – Unity Script Reference

如果你知道其他很棒的关于协程的Unity教程,或者相关的主题,请在回复中分享链接!当然,如果在教程有什么问题,比如链接无效或者其他一些问题,欢迎给我发邮件

作者ChevyRay ,2013年9月28日,snaker7译 原文地址:http://unitypatterns.com/scripting-with-coroutines/

请注意:这个关于协程的教程共有两部分,这是第二部分,如果您未曾看过第一部分——协程介绍,那么在阅读这部分内容之前建议您先了解一下。

计时器例子

第一个教程中,我们已经了解了协程如何让一个方法“暂停”下来,并且让它yield直到某些值到达我们给定的数值;并且利用它,我们还创建了一个很棒的计时器系统。协程一个很重要的内容是,它可以让普通的程序(比方说一个计时器)很容易地被抽象化并且被复用。

协程的参数

抽象化一个协程的第一个方法是给它传递参数,协程作为一个函数方法来说,它自然能够传递参数。这里有一个协程的例子,它在特定的地方输出了特定的信息。

[csharp] view plaincopy

  1. Using UnityEngine;
  2. Using System.Collections;
  3. Public class TimerExample : MonoBehaviour
  4. {
  5. Void Start()
  6. {
  7. //Log "Hello!" 5 times with 1 second between each log
  8. StartCoroutine(RepeatMessage(5, 1.0f,"Hello!"));
  9. }
  10. IEnumerator RepeatMessage(int count,float frequency,string message)
  11. {
  12. for(int i = 0; i < count; i++)
  13. {
  14. Debug.Log(message);
  15. for(float timer = 0; timer < frequency; timer += Time.deltaTime)
  16. Yield return 0;
  17. }
  18. }
  19. }

嵌套的协程

在此之前,我们yield的时候总是用0(或者null),仅仅告诉程序在继续执行前等待下一帧。协程最强大的一个功能就是它们可以通过使用yield语句来相互嵌套。

眼见为实,我们先来创建一个简单的Wait()程序,不需要它做任何事,只需要在运行的时候等待一段时间就结束。

[csharp] view plaincopy

  1. IEnumerator Wait(float duration)
  2. {
  3. for(float timer = 0; timer < duration; timer += Time.deltaTime)
  4. Yield return 0;
  5. }

接下来我们要编写另一个协程,如下:

[csharp] view plaincopy

  1. Using UnityEngine;
  2. Using System.Collections;
  3. Public class TimerExample : MonoBehaviour
  4. {
  5. voidStart()
  6. {
  7. StartCoroutine(SaySomeThings());
  8. }
  9. //Say some messages separated by time
  10. IEnumerator SaySomeThings()
  11. {
  12. Debug.Log("The routine has started");
  13. Yield return StartCoroutine(Wait(1.0f));
  14. Debug.Log("1 second has passed since the last message");
  15. Yield return StartCoroutine(Wait(2.5f));
  16. Debug.Log("2.5 seconds have passed since the last message");
  17. }
  18. //Our wait function
  19. IEnumerator Wait(float duration)
  20. {
  21. for(float timer = 0; timer < duration; timer += Time.deltaTime)
  22. Yield return 0;
  23. }
  24. }

第二个方法用了yield,但它并没有用0或者null,而是用了Wait()来yield,这相当于是说,“不再继续执行本程序,直到Wait程序结束”。

现在,协程在程序设计方面的能力要开始展现了。

控制对象行为的例子

在最后一个例子中,我们就来看看协程如何像创建方便的计时器一样来控制对象行为。协程不仅仅可以使用可计数的时间来yield,它还能很巧妙地利用任何条件。将它与嵌套结合使用,你会得到控制游戏对象状态的最强大工具。

运动到某一位置

对于下面这个简单脚本组件,我们可以在Inspector面板中给targetPosition和moveSpeed变量赋值,程序运行的时候,该对象就会在协程的作用下,以我们给定的速度运动到给定的位置。

[csharp] view plaincopy

  1. usingUnityEngine;
  2. Using System.Collections;
  3. Public class MoveExample : MonoBehaviour
  4. {
  5. ublic Vector3 targetPosition;
  6. ublic float moveSpeed;
  7. Void Start()
  8. {
  9. StartCoroutine(MoveToPosition(targetPosition));
  10. }
  11. IEnumerator MoveToPosition(Vector3 target)
  12. {
  13. while(transform.position != target)
  14. {
  15. transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
  16. Yield return 0;
  17. }
  18. }
  19. }

这样,这个程序并没有通过一个计时器或者无限循环,而是根据对象是否到达指定位置来yield。

按指定路径前进

我们可以让运动到某一位置的程序做更多,不仅仅是一个指定位置,我们还可以通过数组来给它赋值更多的位置,通过MoveToPosition() ,我们可以让它在这些点之间持续运动。

[csharp] view plaincopy

  1. Using UnityEngine;
  2. Using System.Collections;
  3. Public class MoveExample : MonoBehaviour
  4. {
  5. ublic Vector3[] path;
  6. ublic float moveSpeed;
  7. Void Start()
  8. {
  9. StartCoroutine(MoveOnPath(true));
  10. }
  11. IEnumerator MoveOnPath(bool loop)
  12. {
  13. do
  14. {
  15. foreach(var point in path)
  16. Yield return StartCoroutine(MoveToPosition(point));
  17. }
  18. while(loop);
  19. }
  20. IEnumerator MoveToPosition(Vector3 target)
  21. {
  22. while(transform.position != target)
  23. {
  24. transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
  25. Yield return 0;
  26. }
  27. }
  28. }

我还加了一个布尔变量,你可以控制在对象运动到最后一个点时是否要进行循环。

把Wait()程序加进来,这样就能让我们的对象在某个点就可以选择是否暂停下来,就像一个正在巡逻的AI守卫一样,这真是锦上添花啊!

注意:

如果你刚接触协程,我希望这两个教程能帮助你了解它们是如何工作的,以及如何来使用它们。以下是一些在使用协程时须谨记的其他注意事项:

  • l 在程序中调用StopCoroutine()方法只能终止以字符串形式启动(开始)的协程;
  • l 多个协程可以同时运行,它们会根据各自的启动顺序来更新;
  • l 协程可以嵌套任意多层(在这个例子中我们只嵌套了一层);
  • l 如果你想让多个脚本访问一个协程,那么你可以定义静态的协程;
  • l 协程不是多线程(尽管它们看上去是这样的),它们运行在同一线程中,跟普通的脚本一样;
  • l 如果你的程序需要进行大量的计算,那么可以考虑在一个随时间进行的协程中处理它们;
  • l IEnumerator类型的方法不能带ref或者out型的参数,但可以带被传递的引用;
  • l 目前在Unity中没有简便的方法来检测作用于对象的协程数量以及具体是哪些协程作用在对象上。

如果您发现教程中存在问题和错误的信息,或者有任何建议又或者您想要在这里看到其他需要的教程,可以发邮件或者在评论中留言。

时间: 2024-12-20 00:32:48

Unity3D协程介绍 以及 使用的相关文章

#协程介绍及基本示例

1 #协程介绍及基本示例 2 3 #Gevent协程(单线程,串行)在线程里启动 4 ''' 5 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程: 6 协程是一种用户态的轻量级线程. 7 8 协程拥有自己的寄存器上下文和栈.协程调度切换时, 9 将寄存器上下文和栈保存到其他地方,在切回来的时候, 10 恢复先前保存的寄存器上下文和栈.因此: 11 12 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合), 13 每次过程重入时,就相当于进入上一次调用的状态,换种

协程介绍

引子 本节的主题是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态 cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它. 操作系统将cpu的运行不断的在各个程序之中切换,以保证各个程序的并发执行.对于遇到io切换这样是能够提高效率,但是对于多个任务都是计算密集型的任务来说,不断的切换反

Python并发编程:协程介绍

一 引子 基于单线程来实现并发,即只用一个主线程(很明显可利用的CPU只有一个)情况下实现并发,先回顾一下并发的本质:切换+保存状态 CPU正在运行一个任务,会在两种情况下切走去执行其它的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它 ps:在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态 一:其中第二种情况并不能提升效率,只是让CPU能够雨露均沾,实现看起来所有任务

3-1 协程介绍

一 引子 本节的主题是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态 cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它 ps:在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态 一:其中第二种情况并不能提升效率,只是为了让cpu能够雨

lua编程之协程介绍

一,lua协程简介 协程(coroutine),意思就是协作的例程,最早由Melvin Conway在1963年提出并实现.跟主流程序语言中的线程不一样,线程属于侵入式组件,线程实现的系统称之为抢占式多任务系统,而协程实现的多任务系统成为协作式多任务系统.线程由于缺乏yield语义,所以运行过程中不可避免需要调度,休眠挂起,上下文切换等系统开销,还需要小心使用同步机制保证多线程正常运行.而协程的运行指令系列是固定的,不需要同步机制,协程之间切换也只涉及到控制权的交换,相比较线程来说是非常轻便的.

Unity3D 协程 Coroutine

协程(Coroutine)的概念存在于很多编程语言,例如Lua.ruby等.而由于Unity3D是单线程的,因此它同样实现了协程机制来实现一些类似于多线程的功能,但是要明确一点协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用. 协程类型IEnumerator 在内部方法使用 yield return 语句,直观的说语句说在位置便是协程暂停,并且将控制权移交给Unity引擎以及之后继续执行余下逻辑的地方. 例如 IEnumerator Method(){ int i=0;

Unity3D 协程 浅谈

协程 理解:协程不是线程,也不是异步执行(知道就行). 1.协程和MonoBehaviour的Update函数一样,也是在MainThread中执行的(一定得明白这句话意思). void Start () { StartCoroutine(HelloCoroutine()); } void Update () { Debug.Log("Update..."); } void LateUpdate() { Debug.Log("LateUpdate..."); } I

协程介绍前戏、协程切换手动、协程切换自动

一.协程简介.引子 ''' 协程:异步IO,队列,缓存 Nginx效率高就是用了异步IO 协程是一种用户态的轻量级线程.又称微线程,怎么理解呢?后面会说 CPU只认识线程,不会像线程一样吧上下文保存在CPU寄存器,协程是用户控制的. 协程能保留上一次调用时的状态,单线程下实现并发效果 协程的好处: 1.无需线程上下文切换的开销,用yield的时候,只是在函数之间来回切换 2.无需原子操作锁定及同步的开销,没有异步锁之类的东西,因为协程就是单线程 3.方便切换控制流,简化编程模型 4.高并发-高扩

python 并发编程 协程 协程介绍

协程:是单线程下的并发,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的 需要强调的是: 1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) 2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关) 对比操作系统控制线程的切换,用户在单线程内控制协程的切换