协程
协程 即“协同程序”。在 Unity 和 C# 的语境下,它是一种可以暂停执行(交出控制权)并在之后的特定时机恢复执行的特殊方法。
协程像是一个特殊的函数,可以在设定的地方停下,让其他程序继续执行,它暂时下班,等到特定的时间再继续执行。
普通方法在被调用时,会在一帧内从头到尾执行完毕。如果方法内部包含无限循环或耗时操作,会导致主线程阻塞,游戏画面卡死。
协程的核心意义在于分步执行。它允许你在代码中打上“暂停标记”,让程序在此处停下,去渲染游戏画面或执行其他脚本,等待满足条件(如经过了一定时间、到了下一帧)后,再回到暂停的位置继续向下执行。
void Start()
{
Debug.Log("步骤 1:StartCoroutine 之前");
StartCoroutine(TestRoutine());
Debug.Log("步骤 3:StartCoroutine 之后");
}
IEnumerator TestRoutine()
{
Debug.Log("步骤 2:遇到第一个 yield 之前");
yield return null; // 暂停,等待下一帧
Debug.Log("步骤 4:在第二帧恢复运行");
yield return new WaitForSeconds(1f); // 暂停,等待1秒
Debug.Log("步骤 5:1秒后恢复运行");
}
如上,其实同一时间内仍然是只有一条线在运行:
第1帧 | 主线程 - 输出 1 - 启动协程,主控制权转到协程 - 主线程停止;
第1帧 | 协程-输出协程内的2,遇到yield return,主控制权转回主线程,输出3;
第2帧 | 主线程 - 正常执行Update -发现条件满足,转到协程 - 主线程停止;
第2帧 | 协程 - 从上次暂停的位置继续往下运行,输出4 - 遇到 yield return ,记录进度并交还控制权 - 主控制权转回主线程;
第n帧 | 主线程 - 正常执行;
// 第n帧 | 协程 - 挂起,并没有执行;
1秒后 | 主线程 - 正常执行Update -发现条件满足,转到协程 - 主线程停止;
1秒后 | 协程 - 从上次暂停的位置继续往下运行,输出5 - 彻底终止,转回主线程;
常用的 yield return 类型:
yield return null;:等待下一帧继续。yield return new WaitForSeconds(time);:等待指定的现实秒数(受游戏时间缩放Time.timeScale影响)。yield return new WaitForSecondsRealtime(time);:等待现实秒数(不受游戏时间缩放影响)。yield return new WaitUntil(condition);:等待直到指定的条件表达式为true时继续。
如果你需要在协程运行到特定条件时自行结束,可以在协程方法体内部使用 yield break;
如果你需要精确地找到协程本身来控制它的启动停止等行为,你可以使用Coroutine声明一个协程引用变量,通过操作它来操作协程本身。
// 声明
private Coroutine nameCoroutine = null;
// 启动协程,TestCounter为要启动的协程名
nameCoroutine = StartCoroutine(TestCounter())
// 停止协程
StopCoroutine(nameCoroutine);
事件
事件(Event)是对象之间的一种通信机制,允许一个对象在发生特定事情时,向所有感兴趣的订阅者发送通知。
**事件就像一个表格,里面是待办清单。
订阅事件,其实就是将脚本中的函数放入表格当做待办事件。
当触发事件时,就去执行表格里的所有待办事件。**
在脚本中“订阅”的行为,就是将脚本中写好的函数传递给事件。
当事件被触发(Invoke)时,它会自动遍历所有已订阅的函数列表,并按顺序执行它们。
- 事件发布者:定义事件并在适当时机触发它的对象
- 事件订阅者:注册了对事件感兴趣、事件触发时会执行相应代码的对象
- 事件处理程序:订阅者提供的、在事件触发时被调用的方法。
Unity 提供了 UnityEvent,它可以在 Inspector 面板中拖拽绑定。
对于无参的事件:
- 声明 = 在 发布者 脚本里写
public UnityEvent 事件名; - 订阅 = 在 接收者 脚本里写
发布者.事件名.AddListener(你的函数); - 调用 = 在 发布者 脚本里写
事件名.Invoke();
对于有参的事件:
- 声明:在 发布者 脚本里写
public UnityEvent<参数类型> 事件名; - 订阅:在 接收者 脚本里写
发布者.事件名.AddListener(你的函数);
订阅者的函数必须拥有完全相同的参数列表。 - 调用:在 发布者 脚本里写
事件名.Invoke(具体的值); - 原则:发布者负责“给数据”,订阅者负责“接数据”,只要数据类型对得上,自动传输就完成了。
public class Player : MonoBehaviour
{
// 【声明】尖括号里写参数类型。这里表示:这个事件会传递一个 int 值出去
public UnityEvent<int> onHealthChanged;
private int currentHealth = 100;
public void TakeDamage(int damage)
{
currentHealth -= damage;
// 【调用/触发】必须把具体的数值传进去
// 这就相当于:通知所有订阅者,并塞给他们一个 int 数字
onHealthChanged.Invoke(currentHealth);
}
}
public class UIManager : MonoBehaviour
{
public Player player;
void Start()
{
// 【订阅】把方法挂到事件上去
player.onHealthChanged.AddListener(UpdateHealthBar);
}
// 【自定义函数】注意:这里的参数类型、数量必须和声明事件时完全一致!
// 这个形参 newHealth 就是发布者 Invoke 时塞过来的数据
private void UpdateHealthBar(int newHealth)
{
Debug.Log($"UI收到新血量:{newHealth},更新血条长度");
// 这里写更新 Slider.value = newHealth;
}
void OnDestroy()
{
// 取消订阅
player.onHealthChanged.RemoveListener(UpdateHealthBar);
}
}
注意,虽然不明确禁止,但最好是“谁声明事件,谁就负责触发它”。
因为“事件”代表的是“声明者自身状态发生了某种变化”,如果一个外部脚本也需要触发这个事件,那么最好的办法是在声明者内部创建一个公有方法,让外部脚本调用这个方法。这样比较容易溯源,也可以在这个公有方法中放入用来检验的程序。
