欢迎回来! 今天我们来搞定理解委托和事件的核心概念,用"受伤、得分、游戏结束"三个游戏场景掌握解耦思路!
一、为什么要用事件?先看"耦合"的痛点
假设玩家受伤时,需要同时做三件事:、
玩家受伤
├── UI 血条减少
├── 音效播放"受伤音"
└── 相机震动
不用事件的写法(强耦合):
cs
// 玩家脚本里直接调用所有系统
void TakeDamage(float damage)
{
health -= damage;
UIManager.Instance.UpdateHealthBar(health); // 直接调用UI
AudioManager.Instance.PlayHurtSound(); // 直接调用音效
CameraShake.Instance.Shake(); // 直接调用相机
}
问题出在哪?
| 问题 | 后果 |
|---|---|
| 玩家脚本要知道 UI、音效、相机的存在 | 改一个系统可能连锁修改多处 |
| 删掉音效系统 → 玩家脚本报错 | 牵一发而动全身 |
| 新增"受伤特效"→ 要改玩家脚本 | 功能越多,玩家脚本越臃肿 |
类比 :强耦合就像公司老板直接给每个员工打电话布置任务------老板要记住所有员工的电话,换人了就要改号码。事件就是设一个"广播电台"------老板只管播报消息,员工自己决定要不要收听,互不依赖。
二、委托------函数的"类型"
委托(delegate)本质是:用来存储"函数"的变量类型。
类比 :普通变量存的是数据(int 存数字,string 存文字),委托变量存的是方法(函数)。
cs
// 声明一个委托类型(规定:无参数,无返回值)
delegate void OnDamageHandler();
// 这个委托变量可以"存"符合格式的方法
OnDamageHandler myDelegate;
// 把方法赋给委托
void PlaySound() { Debug.Log("播放音效"); }
myDelegate = PlaySound; // 注意不加括号
// 调用委托 = 调用它存的方法
myDelegate(); // 输出:"播放音效"
三、Action------最常用的内置委托
Unity 开发中,不需要自己声明 delegate,直接用 C# 内置的 Action 就够了:
| 类型 | 说明 | 示例 |
|---|---|---|
Action |
无参数、无返回值 | 游戏结束通知 |
Action<T> |
一个参数、无返回值 | 传递受伤数值 |
Action<T1, T2> |
两个参数 | 传坐标、传名字+分数 |
cs
Action onGameOver; // 无参
Action<float> onDamage; // 带一个float参数(伤害值)
Action<string, int> onScore; // 带两个参数(玩家名、分数)
四、event 关键字------给委托加保护锁
光用 Action 有个隐患:任何地方都可以直接调用或清空它。加上 event 关键字,可以限制只有声明它的类能触发,外部只能订阅/取消订阅。
cs
// 没有 event:任何脚本都能 GameManager.OnGameOver() 直接触发
public static Action OnGameOver;
// 加了 event:外部只能 += 或 -=,不能直接调用
public static event Action OnGameOver;
| 操作 | 有 event | 无 event |
|---|---|---|
订阅(+=) |
允许 | 允许 |
取消订阅(-=) |
允许 | 允许 |
从外部触发(()) |
报错 | 允许(危险!) |
从外部清空(= null) |
报错 | 允许(危险!) |
五、完整三步走------事件的标准用法
- 第一步:声明事件(在"广播站"里)
- 第二步:订阅事件(在"听众"里)
- 第三步:触发事件(事情发生时)
六、完成一个得分事件委托
那么我们来完成一个小小的实列吧!
cs
using UnityEngine;
// ── 得分管理器(发布者)──────────────────
public class ScoreManager : MonoBehaviour
{
public static event Action<int> OnScoreChanged; // 携带当前总分
private static int totalScore = 0;
// 静态方法,任何地方可以调用
public static void AddScore(int points)
{
totalScore += points;
OnScoreChanged?.Invoke(totalScore);
Debug.Log("得分!当前总分:" + totalScore);
}
}
cs
// ── 分数UI(订阅者)──────────────────────
public class ScoreUI : MonoBehaviour
{
void OnEnable()
{
ScoreManager.OnScoreChanged += UpdateScoreDisplay;
}
void OnDisable()
{
ScoreManager.OnScoreChanged -= UpdateScoreDisplay;
}
void UpdateScoreDisplay(int score)
{
Debug.Log("UI显示分数:" + score);
// GetComponent<Text>().text = "分数:" + score;
}
}
使用方式 :敌人死亡时,调用 ScoreManager.AddScore(10) 即可,不需要引用任何 UI 脚本。
七、一定要记住:订阅了就要取消订阅
cs
// 错误写法:只订阅不取消
void Start()
{
GameManager.OnGameOver += DoSomething;
}
// 这个对象销毁了,但 OnGameOver 还记着它 → 报空引用错误!
// 正确写法:成对出现
void OnEnable()
{
GameManager.OnGameOver += DoSomething;
}
void OnDisable()
{
GameManager.OnGameOver -= DoSomething;
}
| 场景 | 订阅位置 | 取消位置 |
|---|---|---|
| 场景内始终存在的对象 | Awake / Start |
OnDestroy |
| 会被禁用/启用的对象 | OnEnable |
OnDisable |
八、知识点总结
| 核心语法 | 作用 |
|---|---|
public static event Action OnXxx |
声明静态事件 |
OnXxx?.Invoke() |
安全触发事件 |
OnXxx += 方法名 |
订阅(开始监听) |
OnXxx -= 方法名 |
取消订阅(停止监听) |
今天的内容就到这里! 接下来我将连续更新90天的Untiy教程从基础到一个网络部分,有兴趣的朋友们可以收藏关注,谢谢!如果有疑问,评论区见。