unity协程coroutine 简明教程

本篇内容基于 https://gamedevbeginner.com/coroutines-in-unity-when-and-how-to-use-them/
以及官方教程

为什么使用协程

协程非常适合设置需要随时间发生变化的游戏逻辑。很自然我们会想到update,update里指出每一帧unity会执行什么操作。协程则可以将代码从update中解放出来,至于为什么要这样做,请看例子:

假设我们有一辆坦克,当点击地面时,我希望坦克转向我点击的位置,朝该位置移动,到达后,等待一秒再开火。像这样:
请添加图片描述
那么我们可以给坦克列一个行动清单:

  • 转向特定角度
  • 移动到特定点
  • 等待一秒后开火

这样一个看起来简单的逻辑,如果要再update中执行,将变得非常混乱且难以理解,因为我们要想这三个步骤按照顺序(而非同时)发生,我们要设置很多额外的变量,代码如下:

// Please don't do this
bool tankMoving;
bool facingRightWay; 
bool tankInPosition;
float timer;
void Update()
{
     if(Input.GetMouseButtonDown(0))
        {
            tankMoving = true;
        }
     if (tankMoving)
        {
            MoveTank();
        }
}
void MoveTank()
    {
        if (facingRightWay == false)
        {
            if (angleIsWrong)
            {
                TurnTank();
            }
            else if (angleIsCorrect)
            {
                facingRightWay = true;
            }
        }
        if (facingRightWay && tankInPosition == false)
        {
            if (positionIsWrong)
            {
                MoveToPosition();
            }
            else if (positionIsCorrect)
            {
                tankInPosition = true;
            }
        }
        if (facingRightWay && tankInPosition && timer < 1) 
        {
            timer += Time.deltaTime;
        }
        else if (facingRightWay && tankInPosition && timer > 1)
        {
           FireTank();
           facingRightWay = false;
           tankInPosition = false;
           tankMoving = false;
           timer = 0;
        }
    }

可以看到,要判断哪些事情已经发生了,当前帧可以做哪些事情,非常混乱且易出错,如果用协程?

void Update()
{
    if(Input.GetMouseButtonDown(0))
    {
        StartCoroutine(MoveTank());
    }
}
IEnumerator MoveTank()
    {
        while(facingWrongWay)
        {
            TurnTank();
            yield return null;
        }
        while (notInPosition)
        {
            MoveToPosition();
            yield return null;
        }
        yield return new WaitForSeconds(1);
        Fire();    
    }

这次,代码更像一个代办事项清单,每一个动作都在最后一个动作完成后执行。Unity处理每一个while循环时,直到其条件不为真,然后处理下一个。
实际上这样与unity执行常规函数的方式并没有不同,只是这样逻辑是在多帧而不是一帧上执行的。
因为yield关键字,它告诉unity:“这一帧就执行到这里停下!下一帧再从这儿开始!”
如果理解不了,举一个例子,把每一帧想象成你打了一天的游戏,晚上这个yield出现了,它会叫你睡觉,存下进度,明天接着这里再玩。之前呢,我们是一天从早到晚不吃不喝地玩(所有的操作都在一帧里执行)现在呢,我们是每天玩一点,分多天去玩(操作分在多帧里)人的负担是不是小了很多。

何时在unity中使用协程

当你想要创建需要暂停的动作、按顺序执行一系列步骤、或者想要运行长时间任务(这个任务所要花费的时间比一帧长,举例就是这个游戏你一天通不了关,,)这些个时候,你就要考虑用协程。举例包括:

  • 将对象移动到某个位置
  • 为对象提供要执行的任务列表(比如前面的坦克)
  • 淡入淡出的视觉效果或音频
  • 等待资源加载

那么,如何编写协程?

如何编写协程

协程的结构和常规函数基本一致,但有几个关键区别。

首先,协程中的返回类型是IEnumerator,like this:

IEnumerator MyCoroutine()
{
    // Code goes here!
}

先不用管这个IE什么的,现在只要知道,这是unity将函数执行拆分到多个帧的实现方式。
就像一般函数一样,我们可以将参数传递给协程:

IEnumerator MyCoroutine(int number)
{
    number++;
    // Code goes here.
}

只要协程没有被终止,在协程中的变量都会保持值(即变量的值会带到下一帧去,没有人每天都会从头开始打游戏吧?)
此外,和常规函数不同的是,协程允许我们在代码执行当中,用yield语句暂停代码。

如何在unity中暂停协程(yield)

在我们希望函数终端的时候,使用关键字yield return。
yield表面方法是一个迭代器,并且它将执行超过一帧;而return与常规函数一样,会在该点终止执行,并将控制权交给调用它的方法。
yield return之后的内容,将指定unity在继续执行钱,等待多久,我们有哪些选择呢?

yield return null (等到下一帧再执行)

yield return null告诉unity等到下一帧再执行,将它与while结合起来:

IEnumerator MyCoroutine()
    {
        int i = 0;
        while (i < 10)
        {
            // Count to Ten
            i++;
            yield return null;
        }
        while (i > 0)
        {
            // Count back to Zero
            i--;
            yield return null;
        }
        // All done!
    }

unity将完成第一个循环,每一帧累加一个数字,然后是第二个循环,每帧减少一个数字,直到代码块结束。

如果没有 yield return null,所有代码将立即执行,和常规函数一样。

wait for seconds(等待一段时间)

Wait For Seconds或Wait For Seconds Real Time(使用未缩放的时间)允许我们指定确切的等待时间。它只能在 Coroutine 中使用(即它在 Update 中不起作用)。

就像之前一样,我们需要将 Wait for Seconds 与yield return语句一起使用,在这种情况下,需要使用new关键字才能使其工作。

IEnumerator WaitFiveSeconds()
    {
        print("Start waiting");
        yield return new WaitForSeconds(5);
        print("5 seconds has passed");
    }

这样的写法适合一次性等待。如果要重复延迟,先缓存一下WaitForSeconds对象好一些(不用每次都new)

WaitForSeconds delay = new WaitForSeconds(1);
    Coroutine coroutine;
    void Start()
    {
        StartCoroutine("MyCoroutine");
    }
    IEnumerator MyCoroutine()
    {
        int i= 100;
        while (i>0)
        {
            // Do something 100 times
            i--;
            yield return delay;
        }
        // All Done!
    }

Wait for Seconds Real Time执行完全相同的功能,但使用未缩放的时间。这意味着即使更改时间刻度,它仍然可以工作,例如暂停游戏时。

IEnumerator WaitFiveSeconds()
    {
        // Pause the game
        Time.timeScale = 0;
        yield return new WaitForSecondsRealtime(5);
        print("You can't stop me");
    }

Yield Return Wait Until / Wait While (等待委托)

Wait Until暂停执行,直到委托评估为真,而Wait While等待它为假后再继续。

以下是它在脚本中的使用:

int fuel=500;
    void Start()
    {
        StartCoroutine(CheckFuel());
    }
    private void Update()
    {
        fuel--;
    }
    IEnumerator CheckFuel()
    {
        yield return new WaitUntil(IsEmpty);
        print("tank is empty");
    }
    bool IsEmpty()
    {
        if (fuel > 0)
        {
            return false;
        }
        else 
        {
            return true;
        }
    }

等待帧结束

此特定指令会等到 Unity 渲染完每个 Camera 和 UI 元素,然后才实际显示帧。一个典型的用途是截屏。

IEnumerator TakeScreeshot()
    {
        // Waits until the frame is ready
        yield return new WaitForEndOfFrame();
        CaptureScreen();
    }

等待另一个协程

最后,可以让 yield 直到另一个由 yield 语句触发的协程完成执行。

只需在 yield return 之后使用 Start Coroutine 方法,如下所示:

void Start()
    {
        StartCoroutine(MyCoroutine());
    }
    IEnumerator MyCoroutine()
    {
        print("Coroutine has started");
        yield return StartCoroutine(MyOtherCoroutine());
        print("Coroutine has ended");
    }
    IEnumerator MyOtherCoroutine()
    {
        int i = 3;
        while (i>0)
        {
            // Do something 3 times
            i--;
            yield return new WaitForSeconds(1);
        }
        print("All Done!");
    }

在启动的协程完成后,代码将继续执行

如何启动协程

启动协程有两种方法,可以用函数名的字符串启动:

void Start()
    {
        StartCoroutine("MyCoroutine");
    }
    IEnumerator MyCoroutine()
    {
        // Coroutine business...
    }

也可以捅过引用方法名称来启动协程(和常规函数一样)

void Start()
    {
        StartCoroutine(MyCoroutine());
    }
    IEnumerator MyCoroutine()
    {
        // Do things...
    }

这两种技术都是 Start Coroutine 的重载方法。多数情况下,它们非常相似,但有几个关键区别。

首先,使用字符串方法而不是名称方法会对性能造成轻微影响。

另外,使用字符串启动协程时,只能传入一个参数,如下所示:

void Start()
    {
        StartCoroutine("MyCoroutine", 1);
    }
    IEnumerator MyCoroutine(int value)
    {
        // Code goes here...
    }

然而,当停止协程时,会注意到使用一种方法与另一种方法之间的最大区别。

如何结束协程

协程在其代码执行后自动结束。您不需要显式结束协程。然而,我们可能希望在协程完成之前手动结束它。这可以通过几种不同的方式来完成。

从协程内部(使用yield break)

添加 yield break 语句将在协程完成之前结束它。这对于作为条件语句的结果退出协程很有用。

IEnumerator MyCoroutine(float value)
    {
        if (value > 10)
        {
            // Do one thing
            yield break;
        }
        // Do another
    }

这允许您创建可以退出协程的条件代码路径。

但是如果你想意外地停止一个协程怎么办。例如,想完全取消协程正在执行的操作。幸运的是,协程也可以在外部停止。

从协程外部结束(使用停止协程)

使用其字符串停止协程

如果是使用其字符串启动协程,则可以使用相同的字符串再次停止它。像这样

StopCoroutine("MyCoroutine");

但是,如果使用相同的字符串启动了多个协程,则在使用此方法时所有协程都将停止。

那么,如果想停止一个特定的协程实例怎么办?

通过引用停止协程

如果您在启动时存储对该 Coroutine 的引用,则可以停止特定的 Coroutine 实例。

bool stopCoroutine;
Coroutine runningCoroutine;
    void Start()
    {
        runningCoroutine = StartCoroutine(MyCoroutine());
    }
    void Update()
    {
        if (stopCoroutine == true)
        {
            StopCoroutine(runningCoroutine);
            stopCoroutine = false;
        }
    }
    IEnumerator MyCoroutine()
    {
        // Coroutine stuff...
    }

停止 MonoBehaviour 上的所有协程

停止协程最简单、最可靠的方法是调用Stop All Coroutines。

像这样:

StopAllCoroutines();

这将停止由调用它的脚本启动的所有协程,因此它不会影响在其他地方运行的其他协程。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值