游戏逻辑监听、抛出事件的机制。
Game Framework 中的很多模块在完成操作后都会抛出内置事件,监听这些事件将大大解除游戏逻辑之间的耦合。
除了 Game Framework 内置事件外,使用者也可以定义自己的游戏逻辑事件,游戏中所有事件均派生自 GameEventArgs 类,事件对象使用了引用池技术,以避免使用事件过程中频繁的内存分配。
常规用法
获取事件组件
1 |
EventComponent eventComponent = GameEntry.GetComponent<EventComponent>(); |
订阅事件
这里以订阅“加载数据表成功事件”为例。
1 |
eventComponent.Subscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess); |
取消订阅事件
这里以取消订阅“加载数据表成功事件”为例。
1 |
eventComponent.Unsubscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess); |
检查是否订阅事件
这里以检查是否订阅“加载数据表成功事件”为例。
1 |
bool isSubscribed = eventComponent.Check(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess); |
实现事件处理函数
这里以实现“加载数据表成功事件”处理函数为例。
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); } |
抛出事件
当事件被抛出并分发后,订阅过此事件的所有事件处理函数都会被调用。
1 2 3 4 5 6 7 8 |
// 创建事件 LoadDataTableSuccessEventArgs e = LoadDataTableSuccessEventArgs.Create(/* 此示例忽略事件参数 */); // 抛出事件(常规模式),这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一次轮询分发。 eventComponent.Fire(this, e); // 抛出事件(立即模式),这个操作不是线程安全的,事件会立刻分发。 eventComponent.FireNow(this, e); |
自定义游戏逻辑事件
如玩家的名字发生变化的时候,抛出一个自定义事件 PlayerNameChangedEventArgs,由相关的界面(可能有多个)去订阅这个事件,当接收到这个事件时,这些界面刷新显示数据,从而解除游戏逻辑层和界面显示层的耦合关系。
由于事件实例使用了引用池技术,请注意正确实现 Clear 方法进行事件实例的清理操作。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/// <summary> /// 玩家名字改变事件。 /// </summary> public class PlayerNameChangedEventArgs : GameEventArgs { /// <summary> /// 玩家名字改变事件编号。 /// </summary> public static readonly int EventId = typeof(PlayerNameChangedEventArgs).GetHashCode(); /// <summary> /// 初始化玩家名字改变事件的新实例。 /// </summary> public PlayerNameChangedEventArgs() { NewPlayerName = null; } /// <summary> /// 获取玩家名字改变事件编号。 /// </summary> public override int Id { get { return EventId; } } /// <summary> /// 获取新的玩家名字。 /// </summary> public string NewPlayerName { get; private set; } /// <summary> /// 创建玩家名字改变事件。 /// </summary> /// <param name="e">内部事件。</param> /// <returns>创建的玩家名字改变事件。</returns> public static PlayerNameChangedEventArgs Create(string newPlayerName) { // 使用引用池技术,避免频繁内存分配 PlayerNameChangedEventArgs e = ReferencePool.Acquire<PlayerNameChangedEventArgs>(); e.NewPlayerName = newPlayerName; return e; } /// <summary> /// 清理玩家名字改变事件。 /// </summary> public override void Clear() { // 使用引用池技术,注意清理事件实例 NewPlayerName = null; } } |
1 2 3 4 5 6 |
// 自定义事件处理函数 private void OnPlayerNameChanged(object sender, GameEventArgs e) { PlayerNameChangedEventArgs ne = (PlayerNameChangedEventArgs)e; Log.Info("新的玩家名字是 '{0}'.", ne.NewPlayerName); } |
1 2 3 4 5 |
// 订阅自定义事件 eventComponent.Subscribe(PlayerNameChangedEventArgs.EventId, OnPlayerNameChanged); // 抛出自定义事件 eventComponent.Fire(this, PlayerNameChangedEventArgs.Create("Ellan")); |
获取某个事件的事件处理函数数量
这里以获取“加载数据表成功事件”的事件处理函数数量为例。
1 |
int eventHandlerCount = eventComponent.Count(LoadDataTableSuccessEventArgs.EventId); |
获取待处理事件数量
1 |
int eventCount = eventComponent.EventCount; |
设置默认事件处理函数
定义并设置默认事件处理函数,当任意事件被抛出且不存在任何事件处理函数时,默认事件处理函数将被调用。
1 2 3 4 5 |
// 默认事件处理函数 private void OnDefaultEvent(object sender, GameEventArgs e) { Log.Info("默认事件处理函数被触发,当前事件编号是 '{0}'。", e.Id.ToString()); } |
1 |
eventComponent.SetDefaultHandler(OnDefaultEvent); |
参考手册
最佳实践
提取 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。