Unity基础学习(六)Mono中的重要内容(2)协同程序
目录
一、什么是协同程序
二、协程和线程的区别
三、协程怎么使用
四、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的不同含义(以交通信号灯示例)
指令 | 等效信号 | 执行时机 | 现实生活场景 |
---|---|---|---|
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 属性) | 交警手动指挥交通 |
来,咱们逐个来解释说明!
1. 基础等待指令
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
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名字应该都能理解。
2. 条件等待指令
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
yield return new WaitUntil(条件) | Func<bool> (返回bool 的委托) | () => playerHealth > 50 | 每帧检查条件,条件为true 时继续 | 安检闸机:必须满足条件才放行 |
yield return new WaitWhile(条件) | Func<bool> (返回bool 的委托) | () => isGamePaused | 每帧检查条件,条件为false 时继续 | 临时管制:条件解除后恢复通行 |
示例:
// Lambda表达式形式(最常用)
yield return new WaitUntil(() => player.isAlive); // 方法名形式(需定义返回bool的方法)
bool CheckPlayerReady() => player.isReady;
yield return new WaitUntil(CheckPlayerReady);
3. 时间控制(抗干扰版)
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
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键恢复游戏");
}
4. 协程流程控制
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
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();
}
5. 异步操作等待
指令 | 参数类型 | 参数示例 | 返回效果 | 说明 |
---|---|---|---|---|
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.9UpdateLoadingUI(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}");}}}
}
6. 自定义等待(高级)
指令 | 参数类型 | 参数示例 | 返回效果 | 比喻说明 |
---|---|---|---|---|
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
语句分割执行步骤,避免真死循环。