Unity-协程详解

article/2025/11/1 6:12:54

1. 简介

unity的**协程(Coroutine)**只是在c#的基础上做了一层封装,其实yield是C#的关键字。

unity协程是一个能够暂停协程执行,暂停后立即返回主函数,执行主函数剩余的部分,直到中断指令完成后,从中断指令的下一行继续执行协程剩余的函数。
函数体全部执行完成,协程结束。
由于中断指令的出现,使得可以将一个函数分割到多个帧里去执行。

协程不是进程,也不是线程,它就是一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行。

协程方法与普通方法的区别:

  • 普通方法
    被调用时,原来执行的部分保留现场,停止执行,然后去执行要调用的方法,并且,被调用的方法执行完之后才能返回到调用前的状态接着往下执行。
  • 协同方法
    执行不用等协同方法执行完再执行调用之前原来方法的代码,而是两者异步执行。

协程不是多线程,它与主线程同时运行,它在主线程运行的同时开启另一段逻辑处理。
类似一个子线程单独出来处理一些问题,性能开销较小。
Unity的协程会在每帧结束之后去检测yield的条件是否满足,满足则执行yield return之后的代码。

在一个MonoBehaviour提供的主线程里只能有一个处于运行状态的协程,而其他协程处于休眠状态。
协程实际上是在一个线程中,只不过每个协程对CUP进行分时,协程可以访问和使用unity的所有方法和component。

性能:
在性能上相比于一般函数没有更多的开销

协程的好处:
让原来要使用异步 + 回调方式写的非人类代码, 可以用看似同步的方式写出来。
能够分步做一个比较耗时的事情,如果需要大量的计算,将计算放到一个随时间进行的协程来处理,能分散计算压力

协程的坏处:
协程本质是迭代器,且是基于unity生命周期的,大量开启协程会引起gc
如果同时激活的协程较多,就可能会出现多个高开销的协程挤在同一帧执行导致的卡帧

协程书写时的性能优化:
常见的问题是直接new 一个中断指令,带来不必要的 GC 负担,可以复用一个全局的中断指令对象,优化掉开销;在 Yielders.cs 这个文件里,已经集中地创建了上面这些类型的静态对象
这个链接分析了一下https://blog.csdn.net/liujunjie612/article/details/70623943

协程是在什么地方执行?
协程不是线程,不是异步执行;协程和monobehaviour的update函数一样也是在主线程中执行
unity在每一帧都会处理对象上的协程,也就是说,协程跟update一样都是unity每帧会去处理的函数
经过测试,协程至少是每帧的lateUpdate后运行的。
参照unity的生命周期图

前驱知识:

  • 设计模式——迭代器模式
  • C#中的IEnumerator、IEnumerable接口

2. 协程的实现

协程的实现需要在Unity中继承MonoBehaviour并使用C#的迭代器IEnumrator,格式如下所示:

IEnumrator 函数名(形参表)  //最多只能有一个形参 
{   yield return xxx; //恢复执行条件//方法体
}

在IEnumerator类型的方法中写入需要执行的操作,遇到yield后会暂时挂起,等到yield return后的条件满足才继续执行yield语句后面的内容。

3. 协程的开启与中止

相关测试:Unity 协程的一些基本用法及测试

3.1 协程的开启

开启协程需要使用StartCoroutine()方法:

  • 开启无参数的协程:
    StartCoroutine(协程名());StartCoroutine("协程名");

  • 开启单参数的协程:
    StartCoroutine(协程名(参数));StartCoroutine("协程名",参数);

  • 开启多参数的协程:
    StartCoroutine(协程名(参数1,......));

    void StartCoroutine()//开启协程的函数
    {IEnumerator coroutine = Test(5, 6);StartCoroutine(coroutine);
    }public IEnumerator Test(int a, int b)//协程{//等待帧画面渲染结束yield return new WaitForEndOfFrame();a=2;b=3;
    }
    

    用“协程名”启动的方式不允许传入 一个以上的参数

3.2 协程的结束

结束协程有两种情况:

  • 当协程的方法体执行完毕将会自动结束

  • 调用StopCoroutine();方法中止协程执行

中止协程的几种情况:

  • 中止所有协程:
    StopAllCoroutines();

  • 使用对象实例中止指定协程

    Coroutine c;
    void Start()
    {c = StartCoroutine(CountSeconds());        
    }
    void Update()
    {if (Input.GetKeyDown(KeyCode.J)){StopCoroutine(c);}
    }
    
  • 使用字符串中止指定协程

    StartCoroutine("协程名");
    StopCoroutine("协程名");
    

    只有以协程名字符串启动的协程可以用此方法中止
    既:StartCoroutine(“协程名”);StartCoroutine(“协程名”,参数);

    允许使用**StopCoroutine(“协程名”);**中止协程

    不允许使用直接调用协程方法的方式中止指定协程
    既:**StopCoroutine(协程名([参数]));**不被允许

4. yield 协程回复条件语句

快查表:

yield语句功能
yield return null;下一帧再执行后续代码
yield return 0;下一帧再执行后续代码
yield return 6;(任意数字)下一帧再执行后续代码
yield break;直接结束该协程的后续操作
yield return asyncOperation;等异步操作结束后再执行后续代码
yield return StartCoroution(其它协程);调用执行其它协程后再执行后续代码
yield return WWW();等待WWW操作完成后再执行后续代码
yield return new WaitForEndOfFrame();等待帧结束,等待直到所有的摄像机和GUI被渲染完成后,在该帧显示在屏幕之前执行
yield return new WaitForSeconds(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间会受到Time.timeScale的影响);
yield return new WaitForSecondsRealtime(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间不受到Time.timeScale的影响);
yield return WaitForFixedUpdate();等待下一次FixedUpdate开始时再执行后续代码
yield return new WaitUntil()将协同执行直到当输入的参数(或者委托)为true的时候
yield return new WaitWhile()将协同执行直到 当输入的参数(或者委托)为false的时候

生命周期图:
在这里插入图片描述

4.1 yield return null;

从生命周期图中可以看到,在GameLogic部分对协程中挂起的条件进行了判断。

也就是说,协程顺序为:
(当前帧为第1帧)
第1帧在start中开启协程,执行协程(自上而下),遇到yield return null 将后面的内容挂起。
这时继续执行第1帧剩下的东西直到第1帧Update执行结束,这时对挂起的协程进行判断 是否满足return条件,
满足则在第2帧Update之后,在LateUpdate前执行协程中yield return 以后的代码;
不满足条件则继续执行第1帧的LateUpdate。
第2帧同第1帧相同。

测试如下:

using System.Collections;
using UnityEngine;
public class CorTest2 : MonoBehaviour
{int i = 0;//update中判断次数的变量private void Start(){Debug.Log("start 1");//开启协程1StartCoroutine(Test());Debug.Log("start 2");}private void Update(){Debug.Log("第" + ++i + "帧开始");}private void LateUpdate(){Debug.Log("第" + i + "帧结束");}IEnumerator Test(){while (true){Debug.Log("协程1第一次");//挂起时机yield return null;Debug.Log("协程1第二次");}}
}

结果如下:

在这里插入图片描述

可以看到,协程运行到一半在第一帧被挂起,第二帧Update执行完后满足条件继续执行。

4.2 yield return StartCoroutine();

测试如下:

IEnumerator Test(){while (true){Debug.Log("协程1第一次");//挂起时机yield return StartCoroutine(Test2());Debug.Log("协程1第二次");}}IEnumerator Test2(){Debug.Log("协程2第一次");yield return null;Debug.Log("协程2第二次");}

结果如下:
在这里插入图片描述

原理都是一样的,执行完yield return 后挂起(注意不是遇到就挂起,而是执行),在每一帧的update与lateupdate之间对挂起的内容进行判断,满足则继续执行被挂起的协程的剩余部分。

4.3 yield return new WaitUntil();

案例:

public int counter;
IEnumerator Start()
{counter=20;yield return new WaitUntil(TestWait);Debug.Log("Start执行完毕");
}
bool TestWait()
{return true;
}
  • 当方法TestWait的返回值为true的时候
    Start会一次性执行完。
  • 当方法TestWait的返回值为false的时候
    Start会一直等待着,只要返回值为false,那么Start的最后一句打印就不会执行。

可以使用lambda表达式

4.4 yield return new WaitWhile()

案例:

public int counter;
IEnumerator Start()
{counter=20;yield return new WaitWhile(TestWait);Debug.Log("Start执行完毕");
}
bool TestWait()
{return false;
}
  • 当方法TestWait的返回值为true的时候
    Start会一直等待着,只要返回值为true,那么Start的最后一句打印就不会执行。
  • 当方法TestWait的返回值为false的时候
    Start会一次性执行完。

可以使用lambda表达式

5. 协程的嵌套

利用yield return StartCoroution(其它协程);可以实现多个协程的嵌套使用。
例如:

IEnumerator SaySomeThings()   
{       Debug.Log("The routine has started");       yield return StartCoroutine(RepeatMessage(1, 1f, "Hello"));       Debug.Log("1 second has passed since the last message");       yield return StartCoroutine(RepeatMessage(1, 2.5f, "Hello"));       Debug.Log("2.5 seconds have passed since the last message");   
}

执行结果:

在这里插入图片描述

6. 注意

  • IEnumerator 类型的方法不能带 ref 或者 out 型的参数,但可以带被传递的引用
  • 在函数 Update 和 FixedUpdate 中不能使用 yield 语句,否则会报错, 但是可以启动协程
  • 在一个协程中,StartCoroutine()和 yield return StartCoroutine()是不一样的。
    前者仅仅是开始一个新的Coroutine,这个新的Coroutine和现有Coroutine并行执行。
    后者是返回一个新的Coroutine,是一个中断指令,当这个新的Coroutine执行完毕后,才继承执行现有Coroutine。

7. 使用案例

7.1 运动到某一位置

在Inspector面板中设置目标位置和运动速度,在游戏开始时将一个物体移动到目标位置

public Vector3 targetPosition;
public float moveSpeed=5;
void Start()
{c = StartCoroutine(MoveToPosition(targetPosition));
}
IEnumerator MoveToPosition(Vector3 target)
{while (transform.position != target){transform.position = Vector3.MoveTowards(transform.position,target,moveSpeed*Time.deltaTime);yield return 0;}
}

7.2 按指定路径前进

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

public List<Vector3> path;    
IEnumerator MoveOnPath(bool loop)
{do{foreach (var point in path)yield return StartCoroutine(MoveToPosition(point));}while (loop);
}

7.3 倒计时

IEnumerator CountDown(int time)
{for(int t = time;t >= 0;t -= 1){print(time);time -= 1; yield return new WaitForSecondsRealtime(1f); //WaitForSecondsRealtime不受时间缩放影响}
}

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

相关文章

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组密码文件/…

Linux 用户管理

目录 1. 添加用户 2. 设置用户密码 3. 切换用户 4. 删除用户 ​​Linux 中&#xff0c;用户保存在 /etc/passwd 中&#xff0c;用户密码保存在 /etc/shadow 中&#xff0c;用户组保存在 /etc/group 中。 1. 添加用户 useradd <username> 在添加 Linux 用户之前&#xf…