Kirisame1969の魔法屋
首页项目归档轨迹照片墙音乐说说杂谈友链关于
封面

我要成为Unity糕手——VER.004

写作时间:2026-09-01 17:26:16
# 我要成为unity糕手
# 编程语言
# 协程
# 事件

协程

协程 即“协同程序”。在 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);
    }
}

注意,虽然不明确禁止,但最好是“谁声明事件,谁就负责触发它”。
因为“事件”代表的是“声明者自身状态发生了某种变化”,如果一个外部脚本也需要触发这个事件,那么最好的办法是在声明者内部创建一个公有方法,让外部脚本调用这个方法。这样比较容易溯源,也可以在这个公有方法中放入用来检验的程序。

avatar

Kirisame1969

软件工程在读小登,正在学习C#与神秘Unity魔法

RECOMMENDED

我要成为Unity糕手——VER.001

2026-06-06 20:24:14

我要成为Unity糕手——VER.002

2026-06-16 14:33:58

我要成为Unity糕手——VER.003

2026-09-01 14:21:54

Table of Contents