Unity 中的 C# 事件
本课属于 Coddy Unity 学习旅程的“Unity 游戏架构”单元。第 12 课,共 64 课。
分数变化时,有好几处会关注它:分数文本、音效、成就。如果 ScoreKeeper 要逐一调用它们,就需要为每一处都添加一个字段,每增加一项新功能都意味着要修改它。事件则反过来:ScoreKeeper 发布分数变化,关注它的对象自行订阅。
两个 C# 关键字让这一切成为可能。Action<int> 是一种方法类型,它接收一个 int,不返回任何内容。这种类型的 event 是一组这样的方法:其他代码用 += 添加自己的方法,事件所有者用 Invoke 调用所有方法。用简单的 C# 表示:
using System;
class Alarm
{
public event Action<int> Rang;
public void Ring(int hour)
{
Rang?.Invoke(hour);
}
}
class Sleeper
{
public void WakeUp(int hour)
{
Console.WriteLine("Up at " + hour);
}
}
// 在其他地方
Alarm alarm = new Alarm();
Sleeper sleeper = new Sleeper();
alarm.Rang += sleeper.WakeUp;
alarm.Ring(7); // 7 点起床在 Unity 中,发布者和监听者都是组件,通常位于不同的 GameObject 上:
using System;
using UnityEngine;
public class ScoreKeeper : MonoBehaviour
{
public event Action<int> OnScoreChanged;
private int score;
public void AddPoints(int points)
{
score += points;
OnScoreChanged?.Invoke(score);
}
}// ScoreLabel.cs,挂在 HUD 的文本上
public ScoreKeeper keeper;
public TMP_Text label;
void Start()
{
keeper.OnScoreChanged += ShowScore;
}
void ShowScore(int score)
{
label.text = "Score: " + score;
}Action<int> 表示每个监听者都是一个接收一个 int 的方法,这里接收的是新分数。Invoke 会按订阅顺序调用每个已订阅的方法。如果没有监听者,事件就是 null,因此直接调用 OnScoreChanged(score) 会抛出 NullReferenceException,而 ?. 则会跳过这次调用。event 关键字限制其他脚本只能使用 += 和 -=:在 ScoreLabel 中,keeper.OnScoreChanged(5) 或 keeper.OnScoreChanged = null 都无法通过编译。
ScoreKeeper 不需要用于标签的字段,也不知道标签的存在。连击计数器或音效播放器也能以相同方式订阅,而 ScoreKeeper 无需改变;没有 HUD 的测试场景也能正常运行,不会报错。监听者通过 Inspector 引用来找到发布者,如此处所示,也可以用你熟悉的其他方式,例如 GetComponent 或单例。对象销毁后,使用 -= 移除监听者很重要,这会在下一课介绍。
挑战
简单让 fighter 宣布它的连击数。
- 在
Fighter.cs中,添加using System;和public event Action<int> OnComboChanged;。 - 将使用
comboText和flame的代码行替换为OnComboChanged?.Invoke(combo);,并删除这两个字段。 - 在
ComboText和ComboFlame的Start()中,分别写入fighter.OnComboChanged += Show;和+= Grow;。
点击“开始”,然后按几次空格键:连击数会增加,然后清零。接着点击“检查”。
亲手试试
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
public class Fighter : MonoBehaviour
{
public float comboWindow = 1f;
public TMP_Text comboText;
public Transform flame;
private int combo;
private float sinceHit;
void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame)
{
combo++;
sinceHit = 0f;
comboText.text = "Combo x" + combo;
flame.localScale = Vector3.one * (0.5f + 0.25f * combo);
}
else if (combo > 0)
{
sinceHit += Time.deltaTime;
if (sinceHit > comboWindow)
{
combo = 0;
comboText.text = "";
flame.localScale = Vector3.one * 0.5f;
}
}
}
}
Unity 游戏架构中的所有课
自己练习: Unity 在线编辑器