UGF学习2-事件

https://gameframework.cn/document/event/

事件是游戏逻辑监听、抛出事件的机制。

Game Framework 中的很多模块在完成操作后都会抛出内置事件,监听这些事件将大大解除游戏逻辑之间的耦合。

除了 Game Framework 内置事件外,使用者也可以定义自己的游戏逻辑事件,游戏中所有事件均派生自 GameEventArgs 类,事件对象使用了引用池技术,以避免使用事件过程中频繁的内存分配。

常用功能

  • 获取事件组件
    EventComponent eventComponent = GameEntry.GetComponent<EventComponent>();

  • 订阅事件
    void Subscribe(int eventID, function);

    • eventID
      事件ID
    • function
      事件响应函数
  • 取消订阅事件
    void Unsubscribe(int eventID, function);

  • 检查是否订阅事件
    bool Check(int eventID, function);

  • 广播事件

    • void Fire(object sender, GameEventArgs e);
      可保证线程安全,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。
      • sender:
        广播发起人(通常为this)
      • e:
        进行广播的事件
    • void FireNow (object sender, GameEventArgs e)
      立即广播,线程不安全
  • 获取某个事件的事件处理函数数量
    int Count(int EventId);

  • 获取待处理事件数量
    EventCount
    这是 EventComponent 的属性

事件响应函数的实现

实现方法如下:

1
2
3
4
private void 事件响应函数名(object sender, GameEventArgs e)
{
// 事件响应内容
}

其中参数为:

  • object sender
    事件发送者
  • GameEventArgs e
    事件

案例:

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 加载数据表成功事件处理函数。
/// </summary>
/// <param name="sender">事件发送者。</param>
/// <param name="e">事件。</param>
private void OnLoadDataTableSuccess(object sender, GameEventArgs e)
{
LoadDataTableSuccessEventArgs ne = (LoadDataTableSuccessEventArgs)e;
Log.Info("数据表 '{0}' 加载成功。", ne.DataTableName);
}

设置默认事件响应函数

定义并设置默认事件处理函数,当任意事件被抛出且不存在任何事件处理函数时,默认事件处理函数将被调用。
void SetDefaultHandler(function);

自定义事件

自定义事件需要继承 GameEventArgs

案例如下:
自定义事件类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 玩家名字改变事件。
public class PlayerNameChangedEventArgs : GameEventArgs
{
// 玩家名字改变事件编号。
public static readonly int EventId = typeof(PlayerNameChangedEventArgs).GetHashCode();

// 初始化玩家名字改变事件的新实例。
public PlayerNameChangedEventArgs()
{
NewPlayerName = null;
}

// 获取玩家名字改变事件编号。
public override int Id
{
get
{
return EventId;
}
}

// 获取新的玩家名字。
public string NewPlayerName
{
get;
private set;
}

// 创建玩家名字改变事件。
// <param name="e">内部事件。</param>
// <returns>创建的玩家名字改变事件。</returns>
public static PlayerNameChangedEventArgs Create(string newPlayerName)
{
// 使用引用池技术,避免频繁内存分配
PlayerNameChangedEventArgs e = ReferencePool.Acquire<PlayerNameChangedEventArgs>();
e.NewPlayerName = newPlayerName;
return e;
}

// 清理玩家名字改变事件。
public override void Clear()
{
// 使用引用池技术,注意清理事件实例
NewPlayerName = null;
}
}

自定义事件处理函数:

1
2
3
4
5
private void OnPlayerNameChanged(object sender, GameEventArgs e)
{
PlayerNameChangedEventArgs ne = (PlayerNameChangedEventArgs)e;
Log.Info("新的玩家名字是 '{0}'.", ne.NewPlayerName);
}

订阅自定义事件:

1
eventComponent.Subscribe(PlayerNameChangedEventArgs.EventId, OnPlayerNameChanged);

广播自定义事件:

1
eventComponent.Fire(this, PlayerNameChangedEventArgs.Create("Ellan"));

常见问题

提取 EventHandler 降低内存分配

由于将方法转换为 EventHandler 时会有内存分配,所以建议将 EventHandler 预定义为临时变量甚至所在类的成员变量,来降低内存开销。

1
2
3
EventHandler<GameEventArgs> loadDataTableSuccess = OnLoadDataTableSuccess;
eventComponent.Subscribe(LoadDataTableSuccessEventArgs.EventId, loadDataTableSuccess);
eventComponent.Unsubscribe(LoadDataTableSuccessEventArgs.EventId, loadDataTableSuccess);

事件订阅与取消订阅不匹配

事件的订阅与取消订阅在使用生命周期内,应当成对出现。比如实体显示时订阅、隐藏时取消订阅,界面打开时订阅、关闭时取消订阅。

Game Framework 会严格检查事件订阅的匹配情况,不允许出现重复订阅,也不允许出现重复取消订阅或取消订阅尚未订阅的事件处理函数,如果出现这些情况,将会抛出异常。

有时,使用者在对象生命期结束前,的确写有取消订阅的代码,但非预期的逻辑导致逻辑块提前跳出,取消订阅的代码最终未被调用,进而下一次订阅时出现重复订阅。

错误地缓存了事件实例

事件处理函数处理事件时,不应该缓存 GameEventArgs 实例,GameEventArgs 实例的有效生命周期,仅限于事件处理函数内。

Game Framework 为了降低内存分配,事件实例使用了引用池技术。一个事件被其所有事件处理函数处理完成后,事件实例会被立刻清理并回收(调用 GameEventArgs 中的 Clear 方法)。后续逻辑访问缓存的事件实例时,将无法访问到任何有效数据,甚至访问到此实例被复用后的数据。

因此,缓存事件实例是完全错误的,这将导致难以追踪的 BUG。

1
2
3
4
5
6
// 这是一个错误的示例!
private PlayerNameChangedEventArgs m_CachedPlayerNameChangedEventArgs = null;
private void OnPlayerNameChanged(object sender, GameEventArgs e)
{
m_CachedPlayerNameChangedEventArgs = (PlayerNameChangedEventArgs)e;
}

自定义事件类的 Clear 方法实现不完整

自定义事件类使用了引用池技术,事件实例是会被复用的。当回收某个事件实例时,不完整的 Clear 实现无法清理干净事件实例中的数据,进而将脏数据带到下一次复用事件实例的逻辑中去,这可能导致难以追踪的 BUG。

手误导致的事件处理函数不生效

订阅成功事件时,同时订阅相匹配的失败事件是一个好习惯,但有时复制粘贴代码会惹祸,如以下示例。

1
2
3
eventComponent.Subscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess);
// 忘记将事件名中的 Success 修改为 Failure
eventComponent.Subscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableFailure);

事件模块不工作

事件模块需要被轮询才能正常调用事件处理函数,请确认正确初始化了 Game Framework。