【Unity】Unity协程(Coroutine)的原理与应用

article/2025/11/1 6:04:52

文章目录

  • 前言
  • 一、什么是协程
  • 二、应用场景
    • 1.异步加载资源
    • 2.将一个复杂程序分帧执行
    • 3.定时器
  • 三、协程的使用
      • 注意事项
  • 四、Unity协程的底层原理
    • 1. 协程本体:C#的迭代器函数
    • 2. 协程调度:MonoBehaviour生命周期中实现
  • 五、参考资料


前言

本文是作者在学习Unity过程中对协程相关知识的汇总,以方便以后查阅。大部分内容转载自不同文章,原文链接可在最后一部分查看,如果对文章内容有任何困惑或者疑问,建议阅读原文相关部分。


一、什么是协程

首先看一下Unity官方对协程的定义:

A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.

Unity中的协程是一种返回值为IEnumerator的特殊函数,它可以主动的请求暂停自身并提交一个唤醒条件,Unity会在唤醒条件满足的时候去重新唤醒协程,所以协程还是运行在主线程上。

二、应用场景

1.异步加载资源

资源加载指的是通过IO操作,将磁盘或服务器上的数据加载成内存中的对象。资源加载一般是一个比较耗时的操作,如果直接放在主线程中会导致游戏卡顿,通常会放到异步线程中去执行。

举个例子,当你需要从服务器上加载一个图片并显示给用户,你需要做两件事情:

  1. 通过IO操作从服务器上加载图片数据到内存中。
  2. 当加载完成后,将图片显示在屏幕上。

其中,2操作必须等待1操作执行完毕后才能开始执行。

//伪代码
​
IEnumerator ShowImageFromUrl(string url)
{Image image = null;yield return LoadImageAsync(url, image); //异步加载图像,加载完成后唤醒协程Show(image);
}

使用协程来进行异步加载在Unity中是一个很常用的写法。异步资源加载是一个较为深奥的话题,有兴趣的话可以通过下面两个参考链接进行研究:
Unity官方的异步加载场景的示例
倩女幽魂手游中的资源加载与更新方案

2.将一个复杂程序分帧执行

如果一个复杂的函数对于一帧的性能需求很大,我们就可以通过yield return null将步骤拆除,从而将性能压力分摊开来,最终获取一个流畅的过程,这就是一个简单的应用。

举一个案例,如果某一时刻需要使用Update读取一个列表,这样一般需要一个循环去遍历列表,这样每帧的代码执行量就比较大,就可以将这样的执行放置到协程中来处理:

public class Test : MonoBehaviour
{public List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6 };private void Update(){if(Input.GetKeyDown(KeyCode.Space)){StartCoroutine(PrintNum(nums));}}//通过协程分帧处理IEnumerator PrintNum(List<int> nums){foreach(int i in nums){Debug.Log(i);yield return null;}}
}

3.定时器

当你需要延时执行一个方法或者是每隔一段时间就执行某项操作时,可以使用协程。当然这种应用场景很少,如果我们需要计时器有很多其他更好用的方式,下面是官方一个案例。

游戏中的许多任务需要定期执行,最容易想到的方法是将任务包含在 Update 函数中。但是,通常情况下,每秒将多次调用该函数。不需要以这样的频繁程度重复任务时,可以将其放在协程中来进行定期更新,而不是每一帧都更新。这方面的一个示例可能是在附近有敌人时向玩家发出的警报。此代码可能如下所示:官方手册链接

bool ProximityCheck()
{for (int i = 0; i < enemies.Length; i++){if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {return true;}}return false;
}

如果有很多敌人,那么每帧都调用此函数可能会带来很大开销。但是,可以使用协程,每十分之一秒调用一次:

IEnumerator DoCheck()
{for(;;){if (ProximityCheck()){// Perform some action here}yield return new WaitForSeconds(.1f);}
}

这将大大减少所进行的检查次数,而不会对游戏运行过程产生任何明显影响。

三、协程的使用

MonoBehaviour.StartCoroutine()方法可以开启一个协程,这个协程会挂在该MonoBehaviour下。

要想使用协程,只需要以IEnumerator为返回值,并且在函数体里面用yield return语句来暂停协程并提交一个唤醒条件。然后使用StartCoroutine来开启协程。

下面这个实例展示了协程的用法。

IEnumerator Demo(int arg1)
{Debug.Log($"协程A被开启了");yield return null;Debug.Log("刚刚协程被暂停了一帧");yield return new WaitForSeconds(1.0f);Debug.Log("刚刚协程被暂停了一秒");yield return StartCoroutine(CoroutineB(arg1, arg2));Debug.Log("CoroutineB运行结束后协程A才被唤醒");yield return new WaitForEndOfFrame();Debug.Log("在这一帧的最后,协程被唤醒");Debug.Log("协程A运行结束");
}//在程序种调用协程public void Test(){//第一种与第二种调用方式,通过方法名与参数调用StartCoroutine("Demo", 1);//第三种调用方式, 通过调用方法直接调用StartCoroutine(Demo(1));}

在一个协程开始后,同样会有结束协程的方法StopCoroutineStopAllCoroutines两种方式,需要注意的是,两者的使用需要遵循一定的规则。在此之前,先介绍一下关于StopCoroutine重载:

StopCoroutine(string methodName):通过方法名(字符串)来进行
StopCoroutine(IEnumerator routine):通过方法形式来调用
StopCoroutine(Coroutine routine):通过指定的协程来关闭

刚刚说到两种结束协程方法的使用有一定的规则,那么规则是什么呢,答案是前两种结束协程方法的使用上,如果我们是使用StartCoroutine(string methodName)来开启一个协程的,那么结束协程就只能使用StopCoroutine(string methodName)StopCoroutine(Coroutine routine)来结束协程

注意事项

  1. 协程是挂在MonoBehaviour上的,必须要通过一个MonoBehaviour才能开启协程。
  2. 通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。也就说协程虽然是在MonoBehvaviour启动的StartCoroutine,但是协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制,也应该是和MonoBehaviour脚本一样每帧“轮询” yield 的条件是否满足。
  3. 协程看起来有点像是轻量级线程,但是本质上协程还是运行在主线程上,不是异步执行的。协程更类似于Update()方法,Unity会每一帧去检测协程需不需要被唤醒。一旦你在协程中执行了一个耗时操作,很可能会堵塞主线程。这里提供两个解决思路:(1) 在耗时算法的循环体中加入yield return null来将算法分到很多帧里面执行;(2) 如果耗时操作里面没有使用Unity API,那么可以考虑在异步线程中执行耗时操作,完成后唤醒主线程中的协程。
  4. 经过测试验证,协程至少是每帧的LateUpdate()后去运行。这里贴上实验链接和测试结果,下面补一张Monobehaviour的函数执行顺序图

在这里插入图片描述

四、Unity协程的底层原理

协程分为两部分,协程与协程调度器:协程仅仅是一个能够中间暂停返回的函数,而协程调度是在MonoBehaviour的生命周期中实现的。 准确的说,Unity只实现了协程调度部分,而协程本身其实就是用了C#原生的”迭代器方法“。

1. 协程本体:C#的迭代器函数

许多语言都有迭代器的概念,使用迭代器我们可以很轻松的遍历一个容器。 但是C#里面的迭代器要屌一点,它可以“遍历函数”。

C#中的迭代器方法其实就是一个协程,你可以使用yield来暂停,使用MoveNext()来继续执行。 当一个方法的返回值写成了IEnumerator类型,他就会自动被解析成迭代器方法(后文直接称之为协程),你调用此方法的时候不会真的运行,而是会返回一个迭代器,需要用MoveNext()来真正的运行。看例子:

static void Main(string[] args)
{IEnumerator it = Test();//仅仅返回一个指向Test的迭代器,不会真的执行。Console.ReadKey();it.MoveNext();//执行Test直到遇到第一个yieldSystem.Console.WriteLine(it.Current);//输出1Console.ReadKey();it.MoveNext();//执行Test直到遇到第二个yieldSystem.Console.WriteLine(it.Current);//输出2Console.ReadKey();it.MoveNext();//执行Test直到遇到第三个yieldSystem.Console.WriteLine(it.Current);//输出test3Console.ReadKey();
}static IEnumerator Test()
{System.Console.WriteLine("第一次执行");yield return 1;System.Console.WriteLine("第二次执行");yield return 2;System.Console.WriteLine("第三次执行");yield return "test3";
}
  • 执行Test()不会运行函数体,会直接返回一个IEnumerator

  • 调用IEnumerator的MoveNext()成员,会执行协程直到遇到第一个yield return或者执行完毕。

  • 调用IEnumerator的Current成员,可以获得yield return后面接的返回值,该返回值可以是任何类型的对象。

这里有两个要注意的地方:

  1. IEnumerator中的yield return可以返回任意类型的对象,事实上它还有泛型版本IEnumerator,泛型类型的迭代器中只能返回T类型的对象。Unity原生协程使用普通版本的IEnumerator,但是有些项目(比如倩女幽魂)自己造的协程轮子可能会使用泛型版本的IEnumerator

  2. 函数调用的本质是压栈,协程的唤醒也一样,调用IEnumerator.MoveNext()时会把协程方法体压入当前的函数调用栈中执行,运行到yield return后再弹栈。这点和有些语言中的协程不大一样,有些语言的协程会维护一个自己的函数调用栈,在唤醒的时候会把整个函数调用栈给替换,这类协程被称为有栈协程,而像C#中这样直接在当前函数调用栈中压入栈帧的协程我们称之为无栈协程。Unity中的协程是无栈协程,它不会维护整个函数调用栈,仅仅是保存一个栈帧。

2. 协程调度:MonoBehaviour生命周期中实现

仔细翻阅Unity官方文档中介绍MonoBehaviour生命周期的部分,会发现有很多yield阶段,在这些阶段中,Unity会检查MonoBehaviour中是否挂载了可以被唤醒的协程,如果有则唤醒它。

通过对C#迭代器的了解,我们可以模仿Unity自己实现一个简单的协程调度。这里以YieldWaitForSeconds为例

// 伪代码
void YieldWaitForSeconds()
{//定义一个移除列表,当一个协程执行完毕或者唤醒条件的类型改变时,应该从当前协程列表中移除。List<WaitForSeconds> removeList = new List<WaitForSeconds>();foreach(IEnumerator w in m_WaitForSeconds) //遍历所有唤醒条件为WaitForSeconds的协程{if(Time.time >= w.beginTime() + w.interval) //检查是否满足了唤醒条件{//尝试唤醒协程,如果唤醒失败,则证明协程已经执行完毕if(it.MoveNext();){//应用新的唤醒条件if(!(it.Current is WaitForSeconds)){removeList.Add(it);//在这里写一些代码,将it移到其它的协程队列里面去}}else {removeList.Add(it);}}}m_WaitForSeconds.RemoveAll(removeList);
}

原文还较为详细的描述了如何扩展Unity的协程和不同框架下协程的共同点,建议去阅读一下原文,这里贴上链接:
Unity协程的原理与应用

虽然本文内容来源于对其他文章的整理,但技术类文章一般都有时效性,本人习惯不定期对自己的博文进行修正和更新,因此请访问出处以查看本文的最新版本。欢迎转载,请注明文章出处


五、参考资料

  1. 【UNITY3D 游戏开发之六】UNITY 协程COROUTINE与INVOKE
  2. Unity 协程(Coroutine)原理与用法详解
  3. Unity协程的原理与应用

http://chatgpt.dhexx.cn/article/Pm22qlMv.shtml

相关文章

Unity-协程详解

1. 简介 unity的**协程&#xff08;Coroutine&#xff09;**只是在c#的基础上做了一层封装&#xff0c;其实yield是C#的关键字。 unity协程是一个能够暂停协程执行&#xff0c;暂停后立即返回主函数&#xff0c;执行主函数剩余的部分&#xff0c;直到中断指令完成后&#xff…

Unity 协程(Coroutine)原理与用法详解

前言&#xff1a; 协程在Unity中是一个很重要的概念&#xff0c;我们知道&#xff0c;在使用Unity进行游戏开发时&#xff0c;一般&#xff08;注意是一般&#xff09;不考虑多线程&#xff0c;那么如何处理一些在主任务之外的需求呢&#xff0c;Unity给我们提供了协程这种方式…

linux的用户管理

1.linux的用户管理 linux的用户管理和组管理&#xff0c; 每个用户都必须要有一个且仅有一个初始组&#xff0c;可以有多个附加组&#xff0c;使用useradd命令创建用户时&#xff0c;如果没有指定初始组&#xff0c;系统默认会创建一个于其同名的组。 用户和组信息保存在4个文…

Linux用户管理练习

Linux下用户分为3类&#xff1a;超级用户&#xff08;root&#xff09;、系统用户、普通用户。 超级用户的用户名为root&#xff0c;它具有一切操作权力&#xff0c;因此为安全起见&#xff0c;建议不要轻易的在root账户下面对文件进行操作。在Linux操作系统的字符界面&#xf…

Linux用户管理工具

Linux用户管理工具 1. 用户 1.1创建用户 useradd -m username该命令为用户创建相应的帐号和用户目录/home/username&#xff1b; passwd username该命令为用户设置密码 1.2 删除用户 userdel -r username不带选项使用 userdel&#xff0c;只会删除用户。用户的家目录将…

Linux上的用户管理

Linux是一个多用户多任务的系统&#xff0c;任何人想要访问系统资源&#xff0c;必须通过登录账号来访问系统资源 添加用户 useradd&#xff1a;添加用户的命令&#xff08;root才能添加用户&#xff09; 用法&#xff1a;useradd 用户名 例&#xff1a;useradd water 就创建…

实现Linux用户管理

1.添加用户组&#xff1a;groupadd 用户组名称 2.创建用户 &#xff08;一&#xff09;仅创建用户&#xff1a;useradd 用户名 &#xff08;二&#xff09;指定用户所属的用户组&#xff1a; useradd -g 用户组 3.设置用户密码 &#xff08;一&#xff09;passwd 普通用户名 …

linux用户管理及操作指令

1、首先了解下linux是一个多用户多任务的操作系统。任何一个需要使用资源的用户都需要从linux系统中分配一个用户角色&#xff0c;比如&#xff1a;root、user、、然后使用对应账号进入系统。一个root用户下面能创建多个用户&#xff0c;每个用户下面对应一个目录 2、添加用户…

浅谈Linux用户管理

Linux用户管理 准备工作&#xff1a; 在管理用户时&#xff0c;执行命令后是无法看到效果的&#xff0c;最开始我们可以通过系统监视命令来对用户信息进行监控&#xff0c;使操作步骤可视。 watch -n 1 tail -n3 /etc/passwd;ls -l /home/ 注释&#xff1a; 部分含义watch …

Linux用户管理(Centos7)

用户管理 用户命令 添加登录用户&#xff1a; 例&#xff1a;添加一个名为harry的用户&#xff0c;并使用bash作为登录的shell [rootaws ~]# useradd harry [rootaws ~]# tail -1 /etc/passwd harry:x:1002:1002::/home/harry:/bin/bashharry:x:1001:1001::/home/harry:/bin/…

Linux用户管理机制

Linux系统中的用户管理涉及用户账号文件 /etc/passwd、用户密码文件 /etc/shadow、用户组文件 /etc/group。 一、用户账号文件 /etc/passwd 该文件为纯文本文件&#xff0c;可以使用cat、head等命令查看。该文件记录了每个用户的必要信息&#xff0c;文件中的每一行对应一个用…

【Linux用户管理】

目录 前言 用户管理的基本命令 前言 Linux是一个多用户、多任务的操作系统&#xff0c;具有很好的稳定性与安全性&#xff0c;在linux系统中&#xff0c;root用户具有最高的权限&#xff0c;但该身份不当使用会带来一些不必要的麻烦和潜在的风险&#xff0c;故需要添加一些普通…

Linux之用户管理

一、用户的增删改查 1.增加用户 语法&#xff1a;useradd用户名 在终端输入 useradd 用户名 在linux系统home文件夹下面会出现新建用户的文件夹 2.修改密码 语法&#xff1a;passwd用户名 输入passwd 用户名&#xff0c;输入新的密码之后就可以使用新建的用户登录 3.删除用…

Linux用户管理-相关命令及配置文件-超详细-概念详解-初学全

前言 Linux系统作为多用户多任务的操作系统&#xff0c;可以在同一时间内允许多个用户登录、操作及配置计算机&#xff0c;随着需求的增加&#xff0c;用户的增加&#xff0c;我们也就需要对用户进行管理&#xff0c;以至于更有效地开展项目&#xff0c;改善工作&#xff0c;更…

Ubuntu/Linux用户管理与权限管理(超详细解析)

由于实验室几个老师的学生要共同使用一台服务器&#xff0c;所以需要规范一下服务器的使用&#xff0c;并且给各位学生配置相关的用户和权限&#xff0c;之前一直都是自己用&#xff0c;所以借此机会学习和总结一下Linux服务器的用户管理与权限管理。 Ubuntu/Linux用户管理与权…

轻松搞懂Linux中的用户管理

文章目录 概念用户账户用户组用户权限用户管理工具 概念 用户管理是Linux系统管理员必须掌握的重要技能之一。Linux系统是一个多用户操作系统&#xff0c;可以支持多个用户同时使用&#xff0c;每个用户拥有自己的账户和权限&#xff0c;因此管理员需要了解如何创建、管理和删…

Linux用户管理相关命令(全)

1、Linux用户(账号)管理 查询用户(账号)信息&#xff08;判断用户(账号)是否存在&#xff09; id account新增用户(账号) useradd account设置用户(账号)密码 方式1&#xff1a; passwd account 方式2&#xff1a; echo 123|passwd --stdin account; #密码为123删除用户(账…

Linux用户管理、组管理及权限管理

文章目录 Linux用户管理whoami指令who指令useradd指令userdel指令passwd指令usermod指令 - 修改已有账号自身的信息id指令 - 查询用户信息 Linux用户组管理groupadd 指令groupdel指令groupmod - 修改用户组自身的属性groups指令 - 显示用户所属组 Linux权限的概念su指令sudo指令…

【Linux】用户管理命令

往期内容&#xff1a; Linux常用指令合集 Linux文本编辑器 Linux软件包管理 Linux用户管理 Linux权限管理 文章目录 用户配置文件用户信息文件路径&#xff1a; /etc/passwd 影子文件路径&#xff1a;/etc/shadow 组信息文件和组密码文件组信息文件/etc/group组密码文件/…