Skip to content

Latest commit

 

History

History
432 lines (326 loc) · 13.9 KB

File metadata and controls

432 lines (326 loc) · 13.9 KB

3-2. 事件模块 - GameEvent

🎯 模块概述

TEngine 事件模块提供两种互补的事件模式:int/string 事件(委托回调,轻量灵活)和接口事件(Source Generator 生成,类型安全)。两种模式共享同一套 GameEvent 静态门面,可混用。

配合 UI 模块实现 MVE(Model - View - Event)事件驱动架构,模块间通过 GameEvent 解耦,UI 内部通过 AddUIEvent 自动管理事件生命周期。


🏗️ 架构概览

TEngine 事件系统由三个核心组件构成:

组件 类型 职责
GameEvent 全局静态门面 持有 static readonly EventMgr _eventMgr,所有方法委托给内部 EventMgr 实例
GameEventMgr 局部作用域管理器 实现 IMemory,用于非 UI 类需要批量管理事件的场景,仅有 AddEvent + Clear()
GameEventHelper Source Generator 生成类 源码中无 .cs 文件,由 [EventInterface] 特性在编译时自动生成,提供 Init() 注册所有接口事件

两种事件模式对比

特性 int/string 事件 接口事件
定义方式 const int 或 string [EventInterface] 的接口
发送 GameEvent.Send(int/string, ...) GameEvent.Get<IXxx>().OnXxx(...)
监听 GameEvent.AddEventListener / AddUIEvent 实现接口 + 自动注册(Source Generator)
类型安全 无编译检查 编译期检查
事件 ID 来源 自定义常量 Source Generator 自动生成 IXxx_Event.OnXxx
适用场景 简单通知、UI 内部事件 模块间通信、多参数复杂事件

⚡ 核心 API

GameEvent 静态方法

Send(发送事件)

// 接口事件版本 推荐使用!
GameEvent.Get<T>().DoSomeThing();

// int 版本:支持 0~6 个泛型参数
GameEvent.Send(int eventType);
GameEvent.Send<T1>(int eventType, T1 arg1);
GameEvent.Send<T1, T2>(int eventType, T1 arg1, T2 arg2);
// ... 最多 Send<T1,T2,T3,T4,T5,T6>

// string 版本:支持 0~5 个泛型参数
GameEvent.Send(string eventType);
GameEvent.Send<T1>(string eventType, T1 arg1);
// ... 最多 Send<T1,T2,T3,T4,T5>

AddEventListener(监听事件)

返回 bool(是否监听成功)。UI 内推荐用 AddUIEvent(自动清理,无需关心返回值)

// int 版本:支持 0~6 个泛型参数
bool GameEvent.AddEventListener(int eventType, Action handler);
bool GameEvent.AddEventListener<T1>(int eventType, Action<T1> handler);
// ... 最多 AddEventListener<T1,T2,T3,T4,T5,T6>

// string 版本:支持 0~5 个泛型参数
bool GameEvent.AddEventListener(string eventType, Action handler);
bool GameEvent.AddEventListener<T1>(string eventType, Action<T1> handler);
// ... 最多 AddEventListener<T1,T2,T3,T4,T5>

RemoveEventListener(移除监听)

// int 版本:支持 0~5 个泛型参数 + Delegate 重载
GameEvent.RemoveEventListener(int eventType, Action handler);
GameEvent.RemoveEventListener<T1>(int eventType, Action<T1> handler);
// ... 最多 RemoveEventListener<T1,T2,T3,T4,T5>
GameEvent.RemoveEventListener(int eventType, Delegate handler);  // Delegate 重载

// string 版本:支持 0~5 个泛型参数
GameEvent.RemoveEventListener(string eventType, Action handler);

Get(接口事件获取)

// 返回接口实例,内部调用 _eventMgr.GetInterface<T>()
T GameEvent.Get<T>();

Shutdown(清除所有事件注册)

// 仅在游戏退出时调用,重置所有事件
GameEvent.Shutdown();

注意:源码中没有 UnRegisterAll<T>()UnRegisterAll() 方法。清除事件请用 RemoveEventListener(单个)、GameEventMgr.Clear()(局部批量)或 GameEvent.Shutdown()(全局,仅退出时)。

GameEventMgr 局部管理器

实现 IMemory,仅支持 int eventType,最多 5 个泛型参数。没有 RemoveEvent 方法,通过 Clear() 一次性移除所有已注册事件。

// 正确初始化方式
private readonly GameEventMgr _eventMgr = new();

// 注册
_eventMgr.AddEvent(int eventType, Action handler);
_eventMgr.AddEvent<T1>(int eventType, Action<T1> handler);
// ... 最多 AddEvent<T1,T2,T3,T4,T5>

// 一次性移除所有
_eventMgr.Clear();

AddUIEvent(UI 自动生命周期管理)

UIBase 子类(UIWindow/UIWidget)中使用,注册的事件随窗口销毁自动清理,无需手动 RemoveEventListener

// 在 RegisterEvent() 中使用,支持 0~4 个泛型参数
AddUIEvent(int eventType, Action handler);
AddUIEvent<T>(int eventType, Action<T> handler);
AddUIEvent<T, U>(int eventType, Action<T, U> handler);
AddUIEvent<T, U, V>(int eventType, Action<T, U, V> handler);
AddUIEvent<T, U, V, W>(int eventType, Action<T, U, V, W> handler);

📖 使用模式

注意:尽量使用接口事件时,事件 ID 由 Source Generator 自动生成(见接口事件章节),无需手动定义。

一、接口事件(类型安全,推荐用于模块间通信)

接口事件通过 [EventInterface] + Source Generator 实现编译期类型检查。

1. 定义事件接口

// 必须标记 [EventInterface] 并指定事件组
[EventInterface(EEventGroup.GroupUI)]
public interface IBattleEvent
{
    void OnHpChanged(int hp);
    void OnGoldChanged();
    void OnBattleEnded(bool isWin);
}

2. Source Generator 自动生成(编译时)

编译后自动生成两个类(源码中不存在 .cs 文件,勿手动创建):

// 自动生成:事件 ID 常量类
public static class IBattleEvent_Event
{
    public static readonly int OnHpChanged   = RuntimeId.ToRuntimeId("IBattleEvent_Event.OnHpChanged");
    public static readonly int OnGoldChanged = RuntimeId.ToRuntimeId("IBattleEvent_Event.OnGoldChanged");
    public static readonly int OnBattleEnded = RuntimeId.ToRuntimeId("IBattleEvent_Event.OnBattleEnded");
}

// 自动生成:接口实现类(内部分发)
public class IBattleEvent_Gen : IBattleEvent
{
    public void OnHpChanged(int hp) { /* 自动分发给所有监听者 */ }
    public void OnGoldChanged() { /* ... */ }
    public void OnBattleEnded(bool isWin) { /* ... */ }
}

3. 必须在 GameApp.Entrance 最先调用 GameEventHelper.Init()

// GameApp.cs
public static void Entrance(object[] objects)
{
    GameEventHelper.Init();                        // ⚠️ 必须第一行!缺少此调用将导致所有接口事件无响应(无报错,极难排查)
    _hotfixAssembly = (List<Assembly>)objects[0];
    Utility.Unity.AddDestroyListener(Release);
    StartGameLogic();
}

4. 发送事件(通过接口方法调用)

// 通过 GameEvent.Get<T>() 获取接口实例并调用
GameEvent.Get<IBattleEvent>().OnHpChanged(newHp);
GameEvent.Get<IBattleEvent>().OnGoldChanged();
GameEvent.Get<IBattleEvent>().OnBattleEnded(true);

5. 监听事件(通过生成的 ID 常量 + AddUIEvent/GameEventMgr)

// UI 内(AddUIEvent 自动清理)
protected override void RegisterEvent()
{
    AddUIEvent<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
    AddUIEvent(IBattleEvent_Event.OnGoldChanged, OnGoldChanged);
    AddUIEvent<bool>(IBattleEvent_Event.OnBattleEnded, OnBattleEnded);
}

// 非 UI 类(GameEventMgr 批量管理)
public void Init()    => GameEvent.AddEventListener<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
public void Dispose() => GameEvent.RemoveEventListener<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);

三、string 事件(仅在事件名需动态拼接时使用)

// 发送
GameEvent.Send("OnGoldChanged");
GameEvent.Send<int>("OnHpChanged", 50);

// 监听(UI 内)
AddUIEvent("OnGoldChanged", OnGoldChanged);
AddUIEvent<int>("OnHpChanged", OnHpChanged);

// 非 UI 类(手动管理)
GameEvent.AddEventListener<int>("OnHpChanged", OnHpChanged);
GameEvent.RemoveEventListener<int>("OnHpChanged", OnHpChanged);

string 版本内部通过 RuntimeId.ToRuntimeId 转为 int 处理,性能略低。优先使用 int/接口事件


🎮 完整示例:MVE 架构(战斗血量)

// 1. 定义接口事件
[EventInterface(EEventGroup.GroupUI)]
public interface IBattleEvent
{
    void OnHpChanged(int hp);
}

// 2. 服务器消息处理层 - 发送事件
class BattleNetworkHandler
{
    private void HandleHpPacket(HpPacket packet)
    {
        // 收到服务器消息,分发给所有关心血量变化的模块
        GameEvent.Get<IBattleEvent>().OnHpChanged(packet.CurrentHp);
    }
}

// 3. 战斗 UI - AddUIEvent 自动生命周期管理
[Window(UILayer.UI, "BattleMainUI")]
public class BattleMainUI : UIWindow
{
    protected override void RegisterEvent()
    {
        // 自动随窗口销毁清理,无需手动 RemoveEventListener
        AddUIEvent<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
    }

    private void OnHpChanged(int hp)
    {
        _txtHp.text = $"HP: {hp}";
    }
}

🔑 API 参数上限速查

类/方法 int 事件 string 事件
GameEvent.Send 0~6 个泛型参数 0~5 个泛型参数
GameEvent.AddEventListener 0~6 个泛型参数 0~5 个泛型参数
GameEvent.RemoveEventListener 0~5 个泛型参数 + Delegate 0~5 个泛型参数 + Delegate
AddUIEvent(UIBase) 0~4 个泛型参数 不推荐(用 int)
GameEventMgr.AddEvent 0~5 个泛型参数 不支持

⚠️ 常见错误

1. 忘记 GameEventHelper.Init()

// ❌ 错误:接口事件全部无响应,无任何报错,极难排查
public static void Entrance(object[] objects)
{
    // GameEventHelper.Init();  ← 缺少!
    StartGameLogic();
}

// ✅ 正确:必须第一行
public static void Entrance(object[] objects)
{
    GameEventHelper.Init();                         // 第一行,不可省略
    _hotfixAssembly = (List<Assembly>)objects[0];
    Utility.Unity.AddDestroyListener(Release);
    StartGameLogic();
}

2. UI 外部使用 AddEventListener(内存泄漏)

// ❌ 错误:退出窗口不会自动清理,造成内存泄漏
public override void OnCreate()
{
    GameEvent.AddEventListener<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
}

// ✅ 正确:UIWindow/UIWidget 中用 AddUIEvent,随窗口销毁自动清理
protected override void RegisterEvent()
{
    AddUIEvent<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
}

3. 手写事件 ID 硬编码数字

// ❌ 错误:手写 int 常量,容易重复或拼错
public const int OnHpChanged = 1001;

// ✅ 正确:使用接口事件的 Source Generator 生成 ID,或用 RuntimeId.ToRuntimeId
public static readonly int OnHpChanged = RuntimeId.ToRuntimeId("IXxx_Event.OnHpChanged");
// 更推荐:直接使用 IBattleEvent_Event.OnHpChanged(Source Generator 自动生成)

4. 事件回调签名不匹配

// ❌ 错误:发送 int,回调接收 string → 运行时异常
GameEvent.Send<int>(IBattleEvent_Event.OnHpChanged, hp);
AddUIEvent<string>(IBattleEvent_Event.OnHpChanged, OnHp);  // 类型不匹配!

// ✅ 正确:接口事件模式可编译期检查,推荐使用
GameEvent.Get<IBattleEvent>().OnHpChanged(hp);  // 类型安全

5. 非 UI 类忘记移除监听

// ❌ 错误:Init 中注册但 Dispose 中没有移除,导致回调引用已销毁对象
public class PlayerSystem
{
    public void Init() => GameEvent.AddEventListener(GameEventDef.OnGoldChanged, OnGoldChanged);
    // 缺少 RemoveEventListener → 内存泄漏
}

// ✅ 正确:使用 GameEventMgr 批量清理
public class PlayerSystem
{
    public void Init()    => GameEvent.AddEventListener(GameEventDef.OnGoldChanged, OnGoldChanged);
    public void Dispose() => GameEvent.RemoveEventListener(GameEventDef.OnGoldChanged, OnGoldChanged);
}

6. 误用不存在的 UnRegisterAll

// ❌ 错误:源码中不存在这些方法
GameEvent.UnRegisterAll<int>(eventType);
GameEvent.UnRegisterAll();

// ✅ 正确:按需使用以下方式
GameEvent.RemoveEventListener(eventType, handler);  // 移除单个
_eventMgr.Clear();                                  // 局部批量(GameEventMgr)
GameEvent.Shutdown();                               // 全局(仅游戏退出时)

7. 误用不存在的 RegisterListener

// ❌ 错误:GameEvent 中不存在 RegisterListener<T>() 方法
GameEvent.RegisterListener<IBattleEvent>(implementation);

// ✅ 正确:接口事件的实现类由 Source Generator 自动生成和注册
// 只需确保 GameEventHelper.Init() 在 GameApp.Entrance 中最先调用
GameEventHelper.Init();

💡 最佳实践

1. 接口事件用于模块间通信

// ✅ 推荐:接口事件,编译期类型安全
[EventInterface(EEventGroup.GroupUI)]
public interface IBattleEvent { void OnHpChanged(int hp); }

GameEvent.Get<IBattleEvent>().OnHpChanged(hp);  // 发送
AddUIEvent<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);  // 监听

2. UI 内部始终用 AddUIEvent

// ✅ 推荐:RegisterEvent 中 AddUIEvent,自动生命周期管理
protected override void RegisterEvent()
{
    AddUIEvent<int>(IBattleEvent_Event.OnHpChanged, OnHpChanged);
    // 无需手动 RemoveEventListener,UI 销毁时自动清理
}

3. 事件命名规范

规则 说明
方法命名 On + 过去式动词 + 名词:OnGoldChangedOnBattleEnded
接口命名 I + 动词/业务名词:IBattleEventITradeEvent
禁止自定义 ID 使用接口事件
禁止 手写 int 常量作为事件 ID,应用 Source Generator 生成的常量

🔗 相关链接