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

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

协程介绍

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

倒计时器

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

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

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

  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循环来做这些,看上去是否会好很多?

  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循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。

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

码入你的协程!

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

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

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

  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是“停止执行方法,并且在下一帧从这里重新开始”,这意味着你可以这样做:

  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()循环等同。。。

  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()不一样的是,你可以在协程中做一些更有趣的事:

  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()方法来开始一个协程,就像这样:

  1. StartCoroutine(Countdown());

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

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

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

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

  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直到某些值到达我们给定的数值;并且利用它,我们还创建了一个很棒的计时器系统。协程一个很重要的内容是,它可以让普通的程序(比方说一个计时器)很容易地被抽象化并且被复用。

协程的参数

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

  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()程序,不需要它做任何事,只需要在运行的时候等待一段时间就结束。

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

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

  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变量赋值,程序运行的时候,该对象就会在协程的作用下,以我们给定的速度运动到给定的位置。

  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() ,我们可以让它在这些点之间持续运动。

  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中没有简便的方法来检测作用于对象的协程数量以及具体是哪些协程作用在对象上。

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

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

  1. [转]Unity3D协程介绍 以及 使用

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

  2. Unity3D协程yield的理解

    Unity3D的协程概括地将就是:对于一段程序,你可以加上yield标明哪里需要暂停,然后在下一帧或者一段时间后,系统会继续执行这段代码.协程的作用:①延迟一段时间执行代码.②等某个操作完成之后再执行 ...

  3. python 全栈开发,Day43(引子,协程介绍,Greenlet模块,Gevent模块,Gevent之同步与异步)

    昨日内容回顾 I/O模型,面试会问到I/O操作,不占用CPU.它内部有一个专门的处理I/O模块.print和写log 属于I/O操作,它不占用CPU 线程GIL保证一个进程中的多个线程在同一时刻只有一 ...

  4. {python之协程}一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二

    python之协程 阅读目录 一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二 一 引子 本 ...

  5. Unity3D协程

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

  6. 【python】-- 协程介绍及基本示例、协程遇到IO操作自动切换、协程(gevent)并发爬网页

    协程介绍及基本示例 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是协程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他 ...

  7. Unity3D协程(转)

    这篇文章转自:http://blog.csdn.net/huang9012/article/details/38492937 协程介绍 在Unity中,协程(Coroutines)的形式是我最喜欢的功 ...

  8. 协程介绍, Greenlet模块,Gevent模块,Genvent之同步与异步

    昨日内容回顾 I/O模型,面试会问到I/O操作,不占用CPU.它内部有一个专门的处理I/O模块.print和写log 属于I/O操作,它不占用CPU 线程GIL保证一个进程中的多个线程在同一时刻只有一 ...

  9. python全栈开发,Day43(引子,协程介绍,Greenlet模块,Gevent模块,Gevent之同步与异步)

    昨日内容回顾 I/O模型,面试会问道 I/O操作,不占用CPU,它内部有一个专门的处理I/O模块 print和写log属于I/O操作,它不占用CPU 线程 GIL保证一个进程中的多个线程在同一时刻只有 ...

随机推荐

  1. 基于AQS实现的Java并发工具类

    本文主要介绍一下基于AQS实现的Java并发工具类的作用,然后简单谈一下该工具类的实现原理.其实都是AQS的相关知识,只不过在AQS上包装了一下而已.本文也是基于您在有AQS的相关知识基础上,进行讲解 ...

  2. [Spring Data JPA问题]Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException

    JPQL如下: @Modifying(clearAutomatically = true) @Query("UPDATE SyncTestFromTKDO SET stuAns = '' w ...

  3. JQuery 分割字符串

    JQuery 分割字符串 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  4. Socket编程(一):建立与客户端的连接并接受数据

    我们这里利用Socket在模拟一个客户端与服务器通信,其实客户端与服务端通信就像人与人打电话一样,想要给一个人打电话,我们首先必须要有手机,必须知道对方的手机号码,这里Socket就好比一部手机,而短 ...

  5. Python之路【第七篇】:常用模块

    一. 模块介绍 1. 什么是模块 在前面的几个章节中我们基本上是用 python 解释器来编程,如果你从 Python 解释器退出再进入,那么你定义的所有的方法和变量就都消失了. 为此 Python ...

  6. pfring破解DNA限制

    最近因工作需要,对pf_ring进行反调试.官方下载的pf_ring转发数据包的过程中,对程序做了五分钟的限制.那么如何突破此限制.此篇博客记录一下过程,已备后用. 下载源码后进行编译,此处我们利用源 ...

  7. 【BZOJ 1880】 [Sdoi2009]Elaxia的路线 (最短路树)

    1880: [Sdoi2009]Elaxia的路线 Description 最近,Elaxia和w**的关系特别好,他们很想整天在一起,但是大学的学习太紧张了,他们 必须合理地安排两个人在一起的时间. ...

  8. 网络流24题之最长k可重区间集问题

    对于每个点向后一个点连流量为k费用为0的边 对每一区间连l到r流量为1费用为r-l的边 然后最小费用最大流,输出取反 一开始写的r-l+1错了半天... By:大奕哥 #include<bits ...

  9. Spring整合Mybatis案例,献给初学的朋友

    今天我们来学习Spring整合Mybatis. 开发环境:Ide:MyEclipse 2017 CI JDK:1.8 首先我们简单的认识下这两个框架 1.Mybatis MyBatis是一个支持普通S ...

  10. Codeforces Beta Round #10 A. Power Consumption Calculation 水题

    A. Power Consumption Calculation 题目连接: http://www.codeforces.com/contest/10/problem/A Description To ...