目录
一、什么是协同程序
二、协程和线程的区别
三、协程怎么使用
四、yield return的不同含义(以交通信号灯示例)
1. 基础等待指令
2. 条件等待指令
3. 时间控制(抗干扰版)
4. 协程流程控制
5. 异步操作等待
6. 自定义等待(高级)
五、协程受对象和组件失活销毁的影响
Coroutine 的本质
六、注意事项
协程就像你边煮泡面边看剧的机智操作:
当你需要同时做多件事时,协程允许你把一个长任务拆分成多个小步骤
每次执行一步后可以暂停,让主线程继续处理其他事情(如画面渲染)
下次再从暂停的位置继续执行,像自动书签一样记录执行进度
协程 | 线程 |
---|---|
单车道上的交替通行 | 多条并行的独立车道 |
在主线程"夹缝"中执行(Update之间) | 完全独立的新车道 |
可以直接操作Unity对象 | 不能直接操作Unity组件 |
适合处理需要分帧的轻量任务 | 适合复杂计算/网络通信 |
1. 基本语法框架:核心原理:协程本质是一个分步执行器,IEnumerator(迭代器接口)就像给代码装上了自动书签
// 函数返回类型必须是 IEnumerator
IEnumerator 协程函数名() {
// 步骤1代码
yield return 暂停条件; // 在此处插入书签
// 步骤2代码(恢复后执行)
}
yield return:相当于对Unity说:"先执行到这里,满足条件再翻页继续"
2.举例示意:
步骤① 声明协程函数
IEnumerator AttackAction()
{
// 阶段一:攻击前摇(比如举刀动作)
PlayAnimation("SwordRaise");
yield return new WaitForSeconds(0.3f); // 等待0.3秒
// 阶段二:实际攻击判定
CheckEnemyInRange(); // 检测敌人是否在攻击范围
DealDamage(); // 造成伤害
PlaySound("SwordHit");
// 阶段三:攻击后摇(收刀动作)
PlayAnimation("SwordReturn");
yield return new WaitForSeconds(0.2f);
isAttacking = false; // 标记攻击结束
}
步骤② 启动协程
// 在角色攻击时调用
void StartAttack()
{
if(!isAttacking)
{
isAttacking = true;
StartCoroutine(AttackAction()); // 点火启动协程
}
}
注意:不可以在Update中开启协程
四、关于不同的yield return 的不同含义
指令 | 等效信号 | 执行时机 | 现实生活场景 |
---|---|---|---|
yield return null |
绿灯 | 下一帧立即继续(在Update之后) | 每天早高峰的常规放行 |
yield return new WaitForSeconds(2) |
红灯倒计时2秒 | 等待指定真实时间后继续(受 Time.timeScale 影响) |
路口红灯倒计时 |
yield return new WaitForFixedUpdate() |
物理黄灯 | 等待物理引擎更新后继续(在 FixedUpdate 之后) |
货车专用通道放行(物理相关操作) |
yield return new WaitForEndOfFrame() |
终极大黄灯 | 当前帧完全渲染完成后继续(画面渲染完毕时) | 末班地铁后的清场检查 |
yield break |
道路封闭 | 立即终止协程 | 突发封路,所有车辆强制调头 |
yield return new WaitUntil(条件) |
安检闸机 | 每帧检查条件,直到条件为true时继续 | 必须出示健康码才能通行 |
yield return new WaitWhile(条件) |
临时管制 | 每帧检查条件,只要条件为true就卡住 | 前方有事故,持续堵车时禁止通行 |
yield return new WaitForSecondsRealtime(秒) |
防干扰红灯 | 等待指定真实时间(不受 Time.timeScale 影响) |
现实世界钟表倒计时(游戏暂停时依然有效) |
yield return StartCoroutine(其他协程) |
子协程专用道 | 等待另一个协程完全执行完毕后再继续 | 先让VIP车队通过,再放行普通车辆 |
yield return new WWW(url) |
物流等待(旧版) | 等待网络资源加载完成(已过时,Unity推荐用 UnityWebRequest ) |
等快递到达才能拆箱使用 |
yield return new AsyncOperation |
异步收费站 | 等待异步操作完成(如场景加载) | 排队进高速公路收费站 |
yield return CustomYieldInstruction |
自定义信号灯 | 继承 CustomYieldInstruction 类自定义条件(需实现 keepWaiting 属性) |
交警手动指挥交通 |
来,咱们逐个来解释说明!
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield return null |
无 | - | 暂停协程,在下一帧的Update 后继续执行 |
每天早高峰按固定频率放行 |
yield return new WaitForSeconds(n) |
float |
2.5f |
等待n 秒(受游戏时间缩放影响)后继续 |
红灯倒计时n 秒 |
yield return new WaitForFixedUpdate() |
无 | - | 等待物理引擎更新(FixedUpdate 周期)后继续 |
物理货车专用通道放行 |
yield return new WaitForEndOfFrame() |
无 | - | 等待当前帧完全渲染后继续 | 末班车后的安全检查 |
IEnumerator FlashEffect() {
while(true) {
sprite.color = Color.red;
yield return null; // 等一帧
sprite.color = Color.white;
yield return null; // 再等一帧
}
}
就是执行完当前操作 然后等一会 然后往下继续执行,这里只是简单的用等一帧进行实例;其余的你看API名字应该都能理解。
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield return new WaitUntil(条件) |
Func (返回bool 的委托) |
() => playerHealth > 50 |
每帧检查条件,条件为true 时继续 |
安检闸机:必须满足条件才放行 |
yield return new WaitWhile(条件) |
Func (返回bool 的委托) |
() => isGamePaused |
每帧检查条件,条件为false 时继续 |
临时管制:条件解除后恢复通行 |
示例:
// Lambda表达式形式(最常用)
yield return new WaitUntil(() => player.isAlive);
// 方法名形式(需定义返回bool的方法)
bool CheckPlayerReady() => player.isReady;
yield return new WaitUntil(CheckPlayerReady);
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield return new WaitForSecondsRealtime(n) |
float |
3f |
等待n 秒(不受Time.timeScale 影响)后继续 |
例如现实世界钟表倒计时 |
例如:
// 参数示例:等待3秒真实时间(即使游戏暂停也计时)
IEnumerator ShowPauseTip()
{
// 游戏暂停时弹出提示
ShowPopup("游戏已暂停,3秒后显示帮助信息");
// 等待3秒真实时间(不受Time.timeScale影响)
yield return new WaitForSecondsRealtime(3f);
// 3秒后显示帮助(比如玩家按暂停时依然会倒计时)
ShowHelpMessage("按ESC键恢复游戏");
}
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield break |
无 | - | 立即终止协程,后续代码不执行 | 突发封路,强制终止 |
yield return StartCoroutine(协程) |
IEnumerator |
StartCoroutine(PlayAnimation()) |
等待传入的协程完全执行完毕后继续 | VIP车队优先通行 |
//指令1:yield break 无参数,直接终止
IEnumerator PatrolEnemy()
{
while(true)
{
// 敌人巡逻逻辑
MoveToNextWaypoint();
yield return new WaitForSeconds(2f);
// 如果敌人死亡,立即终止巡逻
if(isDead)
{
yield break; // 直接跳出协程
}
}
}
//指令2:yield return StartCoroutine(协程)
//属于是无限套娃啦
// 子协程:播放动画
IEnumerator PlayOpeningAnimation()
{
PlayAnimation("CameraZoomIn");
yield return new WaitForSeconds(2f);
PlayAnimation("LogoFadeIn");
}
// 主协程:等待子协程完成
IEnumerator GameStartSequence()
{
// 先等片头动画完全播放完
yield return StartCoroutine(PlayOpeningAnimation());
// 再触发后续事件
StartGameMusic();
SpawnPlayer();
}
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield return new AsyncOperation |
AsyncOperation 对象 |
SceneManager.LoadSceneAsync("Level2") |
等待异步操作(如场景加载)完成 | 排队进入高速公路收费站 |
yield return new WWW(url) (已过时) |
string (URL地址) |
"http://example.com/data" |
等待网络资源加载完成(Unity 2018后推荐使用UnityWebRequest ) |
旧版物流等待 |
指令1:yield return new AsyncOperation
(场景加载)
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
IEnumerator LoadLevelAsync()
{
// 参数示例:异步加载场景"Level2"
AsyncOperation asyncOp = SceneManager.LoadSceneAsync("Level2");
asyncOp.allowSceneActivation = false; // 禁止自动跳转
// 更新加载进度条
while (!asyncOp.isDone)
{
float progress = Mathf.Clamp01(asyncOp.progress / 0.9f); // Unity加载进度最大到0.9
UpdateLoadingUI(progress); // 更新UI进度条
yield return null; // 每帧更新一次
}
// 手动激活场景(比如点击"继续"按钮后)
asyncOp.allowSceneActivation = true;
}
void UpdateLoadingUI(float progress)
{
Debug.Log($"当前加载进度: {progress * 100}%");
}
}
指令2:yield return new WWW(url)
(已过时 → 替代方案)
using UnityEngine;
using UnityEngine.Networking;
public class WebLoader : MonoBehaviour
{
// 旧版(已过时)
IEnumerator OldDownload()
{
WWW www = new WWW("http://example.com/data.json");
yield return www; // 等待下载完成
Debug.Log(www.text);
}
// 新版替代方案(UnityWebRequest)
IEnumerator NewDownload()
{
using (UnityWebRequest www = UnityWebRequest.Get("http://example.com/data.json"))
{
yield return www.SendWebRequest(); // 等待下载完成
if (www.result == UnityWebRequest.Result.Success)
{
Debug.Log(www.downloadHandler.text);
}
else
{
Debug.LogError($"下载失败: {www.error}");
}
}
}
}
指令 | 参数类型 | 参数示例 | 返回效果 | 比喻说明 |
---|---|---|---|---|
yield return CustomYieldInstruction |
继承CustomYieldInstruction 的子类 |
new WaitAdFinish() |
自定义条件,通过实现keepWaiting 属性控制等待 |
交警手动指挥交通 |
示例:(广告播放等待)
主要是必须继承一个类就可以使用了
using UnityEngine;
using UnityEngine.Advertisements; // 需安装广告包
// 自定义广告等待类
public class WaitAdFinish : UnityEngine.CustomYieldInstruction
{
public override bool keepWaiting
{
get
{
// 当广告未播放完毕时,协程持续等待
return !Advertisement.isFinished;
}
}
}
public class AdManager : MonoBehaviour
{
IEnumerator ShowRewardAd()
{
if (Advertisement.IsReady("rewardedVideo"))
{
Advertisement.Show("rewardedVideo");
// 使用自定义等待类
yield return new WaitAdFinish();
// 广告播放完毕后发放奖励
GiveReward(100);
}
}
void GiveReward(int coins)
{
Debug.Log($"获得 {coins} 金币!");
}
}
请务必注意,上述都只是讲解协程是怎么写的,并没有讲怎么使用,使用就是要通过 StartCoroutine来开启。
操作 | 协程状态 | 比喻说明 |
---|---|---|
物体销毁 | Destroy(gameObject) → 所有协程立即终止 |
整栋楼被爆破,所有供电中断 |
物体失活 | gameObject.SetActive(false) → 所有协程暂停 |
整栋楼拉总闸断电 |
组件失活 | enabled = false → 协程继续执行 |
单个房间关灯,但其他设备(协程)仍运行 |
脚本销毁 | Destroy(this) → 协程立即终止 |
拆除某个房间的电路,关联设备停电 |
停止协程 | StopCoroutine / StopAllCoroutines → 指定或全部协程终止 |
手动关闭某个或所有电器 |
补充知识点: Unity 中 Coroutine
类的解析
Coroutine
的本质 Coroutine
是 Unity 引擎对 协程逻辑的封装对象,本质上是一个由引擎内部管理的 协程句柄。它与 C# 的 IEnumerator
有本质区别:
对比项 | IEnumerator |
Coroutine |
---|---|---|
归属 | C# 语言原生接口 | Unity 引擎封装类 |
作用 | 定义协程逻辑(通过 yield 分步) |
管理协程的生命周期(启动/停止/状态追踪) |
内存管理 | 由 GC 自动回收 | 需通过 StopCoroutine 或销毁对象释放 |
访问方式 | 直接通过迭代器操作 | 通过 StartCoroutine /StopCoroutine |
主要功能基本都在IEnumerator中实现了,这里主要是讲解上面API有些会具备返回值,例如:
// 启动协程并获取 Coroutine 对象
Coroutine myCoroutine = StartCoroutine(MyCoroutine());
这个返回值就是用在关闭协程使用,或者其他地方调用。例如
// 启动协程并获取 Coroutine 对象
Coroutine myCoroutine = StartCoroutine(MyCoroutine());// 停止特定协程
StopCoroutine(myCoroutine);// 停止所有协程
StopAllCoroutines();
1、多线程使用限制
禁止在子线程中调用任何Unity API,需通过主线程队列传递操作指令。
必须手动终止线程,否则应用关闭后线程仍驻留内存,导致资源泄漏。
避免使用 Thread.Sleep 阻塞主线程,优先选择异步任务或协程计时。
2、协程启停规范
协程必须通过 StartCoroutine 启动,直接调用无效。
停止协程需明确调用 StopCoroutine(单个)或 StopAllCoroutines(全部)。
物体销毁时协程自动终止,物体失活时协程暂停,组件失活不影响协程运行。
3、Yield 使用要点
yield return null 表示下一帧继续,需配合循环实现每帧执行。
频繁创建 WaitForSeconds 会引发内存压力,建议提前缓存对象复用。
物理相关操作使用 WaitForFixedUpdate,渲染完成后操作使用 WaitForEndOfFrame。
4、生命周期管理
在 OnDestroy 中统一清理协程和线程,避免残留逻辑引发异常。
协程内访问对象前需检查 null,防止对象已销毁时产生空引用。
5、性能优化
大量协程需分帧处理,避免单帧卡顿(如循环内 yield return null)。
耗时计算交予子线程,结果返回主线程处理,维持帧率稳定。
6、异常处理
协程和线程逻辑需包裹 try-catch,防止未处理异常导致程序崩溃。
使用 yield break 可主动终止协程,类似普通函数的 return。
7、混合使用原则
协程管理主线程轻量任务(如UI动画),线程处理计算密集型任务(如网格生成)。
避免协程嵌套复杂线程操作,优先通过事件或回调解耦逻辑。
常见问题:协程也是在主线程执行任务的吗?是的话,如果协程是死循环,那么会程序卡死吗?
回答:
协程确实在主线程执行。Unity 的协程本质是通过迭代器实现的 分时分步任务调度,所有协程代码都在主线程运行,与 Update
等生命周期函数共享同一线程。
协程在Unity中运行于主线程,本质是通过迭代器实现的任务分时分步调度。若协程包含无yield
的死循环(如while(true)
中未使用yield return
),将完全阻塞主线程,导致画面冻结、操作无响应,程序实质卡死;而含yield
的循环(如while(true) { yield return null; }
)则会在每帧执行一次循环体后让出主线程控制权,允许Unity正常处理渲染、输入等任务,虽逻辑上循环无限,但程序仍能保持流畅响应。因此,协程中所有循环必须搭配yield
语句分割执行步骤,避免真死循环。