Unity中的协程Coroutine

前言

相信接触过游戏开发引擎的UU们应该都知道,游戏引擎在处理每一帧时,都会在后台更新游戏数据。

在游戏进行中,逻辑更新和画面更新的时间点都是确定的。虽然多线程同样能够实现游戏对象同步的效果,但是多线程的操作十分复杂,且容易出错,需要考虑的东西太多太多。

为了避免多线程带来的一些困扰,让游戏开发更加简单,Unity引擎选择了单线程处理逻辑

我们可以理解为:对于我们编写的脚本而言,Unity其实是一种单线程编程语言,脚本们被统一放在一条主线程里面进行调度。同时,Unity也有多条渲染线程(本文不涉及讨论)

在大多数情况下,Unity采用协程(coroutine)来模拟多线程。

Unity中的协程是什么样的呢?🤔

对于一些耗费时间的操作(比如间隔某段特定时长输出特定内容),在单线程这种模式下,我们是不能等待该部分功能执行完毕后再继续执行其他操作的。

举个栗子:我想要实现在游戏中实现间隔一段时间后自动刷出怪物,我们是不能在主线程里面直接让其等待该段时间再继续执行后续刷怪物代码,这样操作的结果就是当代码执行到停顿这部分时,游戏会在此卡住对应时长才会继续更新。

“协程”这种机制能够解决在单线程中“模拟多线程”的相关问题。但Unity中的协程其实并不是多线程!从本质上来讲,Unity的协程是基于C#中的迭代器实现的,并且是基于Unity生命周期的。

Unity中使用协程的例子🤣

如图所示,我想要实现一个“每间隔1s输出一个数字”的功能,这里我就使用到了协程。

首先,该函数的返回值类型必须是IEnumerator(强制规定),因此函数内部必须要使用到yield return

附:常见协程返回值

其次,完成该函数后我们不能直接对其进行调用(如一般函数的调用方式)

而是需要使用StartCoroutine()函数来开启协程。 (相当于在Unity中注册了这个Coroutine)这样一来,当代码执行到Start()函数后,这个“每隔1s就输出一个数字”的功能就会被开启,同时不会造成主线程阻塞。

Unity中协程是如何实现的?😋

在unity中,对于单线程模式下的帧循环,其每一帧画面绘制都会执行一次生命周期

Unity中各个主要函数执行流程如下图所示:

unity生命周期图

通俗一点的概括:Unity中的“协程”其实就是“临时转让控制权”

当函数执行到“yield return”这一部分时,代码执行控制权被转交给其他部分了,也就是说coroutine函数中这一部分及其后续代码都将被挂起,而其他脚本代码继续执行。

“挂起”的时长或条件由“yield return” 后面的这一部分内容决定,比如示例中的“WaitForSeconds(1)”,也即“后续的1s内的所有帧更新,此函数内后续代码不会执行(暂时跳过),直接执行脚本的其他部分代码”,1s过后帧更新再次运行到此函数时,会执行后续代码,也就是for的第二次循环(进入i==1时的for循环).

当然,我们既然可以开启Coroutine,同样也能够提前结束Coroutine,感兴趣的小伙伴可以自行搜索相关内容。

一个实战使用案例🤣

在Unity中实现人物对话逐字输出:



using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DialogSystem : MonoBehaviour
{
    [Header("UI组件")]
    public Text textLabel;
    public Sprite faceImage1;
    public Sprite faceImage2;
    public  Image faceImage;
    [Header("文本文件")]
    public TextAsset textFile;
    public int index;
    public float textSpeed;
    bool textFinished;
    List<string> textList = new List<string>();
    
    // Start is called before the first frame update
    void Awake()
    {
        textFinished = true;
        GetTextFromFile(textFile);
    }
    // Update is called once per frame
    private void OnEnable()
    {
        //textLabel.text = textList[index];
        //index++;
        StartCoroutine(SetTextUI());
    }

    void Update()
    {
	//检测玩家是否按下R(对话键)
        if (Input.GetKeyDown(KeyCode.R))
        {
            if (index == textList.Count)
            {
                gameObject.SetActive(false);
                index = 0;
                return;
            }

            //textLabel.text = textList[index];
            //index += 1;

		    //如果前一段对话已经完成,则开启下一段
            if (textFinished)
                StartCoroutine(SetTextUI());
            
        }
    }

//获取文本内容
    void GetTextFromFile(TextAsset file)
    {
        textList.Clear();
        var data = file.text.Split('n');
        foreach (var item in data)
        {
            textList.Add(item);
        }
    }
 
//协程实现逐字输出对话内容
    IEnumerator SetTextUI()
    {
        textFinished = false;
        textLabel.text = "";
	      //切换对话框人物头像
        switch (textList[index])
        {
            case "A":
                faceImage.sprite = faceImage1;
                ++index;
                break;
            case "B":
                faceImage.sprite = faceImage2;
                ++index;
                break;
        }
	      //使用协程实现逐字输出效果
        for (int i = 0; i < textList[index].Length; i++)
        {
            textLabel.text += textList[index][i];
            yield return new WaitForSeconds(textSpeed);
            //所有在yield后面的程序都会被挂起
        }
        ++index;
        textFinished = true;
        
    }
}

参考资料

Unity协程那些事:最深入浅出的协程底层原理解析!_哔哩哔哩_bilibili

深入Unity Coroutine – 知乎 (zhihu.com)

深入剖析Unity协程 – 知乎 (zhihu.com)

Unity教程:<对话系统>#01:简介&UI制作_哔哩哔哩_bilibili

Unity协程那些事:最深入浅出的协程底层原理解析!_哔哩哔哩_bilibili