使用说明 · 从零到能写业务

事件系统 · 使用说明

这份文档只回答一个问题:我该怎么用它?(用名字订阅、带 owner 退订,1~4 个参数都支持)

读完你能做到:3 分钟接上第一个事件 · 知道"退订"为什么只要一行 · 分清广播/单播该用哪个 · 不会写出"对象销毁了回调还在跑"

〇它是干什么的

先花 30 秒建立直觉,再动手写。

只学 3 个东西就能干活(真的)
RevEvent.AddEventListener<int>("HpChanged", OnHpChanged, owner: this);   // ① 订阅(一定带 owner)
RevEvent.DispatchEvent("HpChanged", 100);                            // ② 广播(谁想听谁听)
RevEvent.RemoveAllByOwner(this);                                     // ③ 退订全部(对象销毁时一行搞定)

void OnHpChanged(int hp) { hpText.text = hp.ToString(); }      // 回调:参数类型与订阅时一致

不想要参数也行:AddEventListener("GameStart", OnStart, owner: this)(无参版本 ✓)。
需要 2/3/4 个参数、优先级、按名字清空……都在第二节的对照表里。

人话 事件系统 = "广播":广播方不知道谁在听,听的人自己订阅;名字是约定,参数是类型安全的。

它最重要的价值不是"解耦"这个说法,而是 owner:对象销毁时一行退订全部 —— 这是 90% 的事件 bug(空引用、重复触发)的解药。

它替你解决的问题怎么做的
找不到委托引用没法退订owner:注册时带上自己,销毁时 RemoveAllByOwner(this)
参数到处强转(object 满天飞)泛型订阅:AddEventListener<T1> … 最多 4 个参数,类型编译期就定死
对象销毁了回调还在跑owner 机制 + 框架侧校验(见第三节第 2 条)
不知道有哪些事件、谁在听GetAllEventNames() / GetListenerCount(name) / HasListener(name)
某个回调抛异常炸掉整条广播统一异常出口 RevEvent.OnException(可接管上报),不会被静默吞掉

一3 分钟接上第一个事件

场景:战斗里血量变了,血条自己更新 —— 两边互不认识。

// ① 血条(订阅方):挂到血条物体上
public class HpBar : MonoBehaviour
{
    [SerializeField] private Image fill;

    void OnEnable()
    {
        // 订阅:事件名 + 处理函数 + owner(this)
        RevEvent.AddEventListener<int, int>("HpChanged", OnHpChanged, owner: this);
    }

    void OnDisable()
    {
        // 退订:一行,按 owner 全清(不用记委托、不用记事件名)
        RevEvent.RemoveAllByOwner(this);
    }

    void OnHpChanged(int cur, int max)
    {
        fill.fillAmount = max > 0 ? (float)cur / max : 0f;
    }
}

// ② 战斗逻辑(广播方):谁都不用认识,直接广播
RevEvent.DispatchEvent("HpChanged", hp, hpMax);

// ③ 无参事件(开服、回大厅、切语言…)
RevEvent.AddEventListener("EnterLobby", () => Refresh(), owner: this);
RevEvent.DispatchEvent("EnterLobby");
就这样,没有初始化步骤 不需要注册事件中心、不需要在场景里摆"EventSystem"物体。事件名就是字符串;框架不做去重、也不设订阅数量上限 —— 它保证的是"发出去没人收一定看得见"(DispatchEvent 的返回值 / RevEvent.LogNoListener)。
OnEnable/OnDisable 还是 Start/OnDestroy? 两者都行,但语义不同:OnEnable/OnDisable = "显示时听、隐藏时不听"(推荐,能省掉无效回调); Start/OnDestroy = "创建到销毁期间一直听"。别在 OnEnable 里订阅却在 OnDestroy 里退订 —— 会重复订阅。

二"我要做 X" 对照表(全部 API)

入口统一是 RevEvent。

我想…这么写
订阅(无参 / 1 参)RevEvent.AddEventListener(name, handler, owner: this) · AddEventListener<T1>(name, h, owner: this)
订阅(2 / 3 / 4 个参数)AddEventListener<T1,T2>(…) · <T1,T2,T3> · <T1,T2,T3,T4>
指定回调先后顺序多传一个 priority(默认 0,需要时再用)
广播(对应参数个数)RevEvent.DispatchEvent(name) · DispatchEvent<T1>(name, a1) · 以此类推到 4 个参数
退订某一个处理函数RevEvent.RemoveEventListener(name, handler)(泛型版本同理)
退订"我的全部订阅"RevEvent.RemoveAllByOwner(this) ← 最常用
清掉某个事件的所有订阅RevEvent.Clear(name)
清掉全部(换关卡时)RevEvent.ClearAll() · RevEvent.ResetAll()
查有没有人在听 / 查有哪些事件RevEvent.HasListener(name) · GetListenerCount(name) · GetAllEventNames()
接自己的日志 / 异常处理RevEvent.Log = msg => … · RevEvent.OnException = (e, ctx) => …
广播的返回值有用 DispatchEvent 返回实际被调用的订阅数(int)。排查"为什么没人响应"时直接看它是不是 0: int n = RevEvent.DispatchEvent("EnterLobby"); if (n == 0) RevLog.Warn("没人监听 EnterLobby", "Event");

三三条必守约定(踩了就出 bug)

约定反例 ✗正例 ✓
① 事件名用常量 DispatchEvent("HpChanged") 和 AddEventListener("HpChange", …)(打错一个字母,静默失效) 定义 static class EventIds { public const string HpChanged = "HpChanged"; },两边都用常量
② 订阅一定带 owner AddEventListener<int>("HpChanged", OnHp)(没 owner → 退订时找不到它) owner: this,销毁时 RemoveAllByOwner(this)
③ 参数类型两端一致 订阅 Action<int>、广播却传 float(编译期就会拦,注意别用 object 绕) 参数类型用具体类型;复合数据传一个数据类(AddEventListener<DamageInfo>)
第 ② 条为什么最要命 Unity 对象销毁后,事件中心并不知道。如果订阅没带 owner,那条订阅会一直留在那里 —— 下次广播时调到一个已销毁对象的方法:轻则报"MissingReferenceException",重则逻辑错乱。 带 owner + 销毁时 RemoveAllByOwner,是唯一稳妥的写法。

四广播 vs 直接调用:什么时候该用事件

不该用事件(直接调用更好)
A 明确拥有 B,B 就是给 A 用的:
面板 → 它自己的按钮 · 战斗系统 → 它的伤害计算器 · 加载器 → 它的回调。
直接调用:看得见数据流、能断点、能返回结果。
该用事件(一对多 / 互不认识)
一个变化,多处反应,且广播方不该知道有谁:
血量变化 → 血条 / 飘字 / 音效 / 成就统计 · 回大厅 → UI、缓存、音频、埋点 各自收尾。
广播:加一个新系统不用改老代码。
一条实用判据 "如果我加了第四个监听者,广播方要不要改代码?" —— 要改就别用事件;不用改就用事件。

五调试与异常(OnException / Log 钩子)

// ① 接住回调里的异常(默认会被框架记下并继续广播,不会炸掉整条链)
RevEvent.OnException = (ex, context) =>
{
    RevLog.Exception(ex, "事件回调异常:" + context, "Event");
    // 需要上报就挂这里(框架不硬编码任何上报 SDK)
};

// ② 想看事件流量(开发期临时开)
RevEvent.Log = msg => Debug.Log("[Event] " + msg);

// ③ 排查"为什么没反应"
Debug.Log($"EnterLobby 订阅数:{RevEvent.GetListenerCount("EnterLobby")}");
Debug.Log(string.Join(", ", RevEvent.GetAllEventNames()));   // 当前所有事件名
排查顺序(30 秒) ① GetListenerCount(名字) 是 0 吗 → 说明没人订阅(名字打错了?订阅方还没 OnEnable?)
② 订阅数对,但逻辑没跑 → 看参数类型/值是不是发错了 → 开 RevEvent.Log 看有没有广播出来
③ 报空引用 → 十有八九是没带 owner 的老订阅指向了已销毁对象(第三节第 2 条)

六新手最容易踩的 5 个坑

坑正确做法
① 事件名硬编码 + 打错字统一常量类(或用 nameof);排查时用 GetAllEventNames() 对照
② 订阅不带 owner永远 owner: this;对象销毁/禁用时 RemoveAllByOwner(this)
③ OnEnable 订阅、OnDestroy 退订成对:OnEnable ↔ OnDisable(否则每次显示都加一条,重复触发)
④ 用事件当"函数调用"(一发一收还要结果)需要返回值/需要知道谁处理了 → 直接调用(第四节);事件是广播,不是 RPC
⑤ 回调里做重活(加载大资源、开界面)回调里只做标记/转发,重活丢给状态机或下一帧(RevTimer.NextFrame)

七相关文档