跳到主要内容

事件使用指南

Guide

如何监听一场进行中的战斗:typed 快捷方法、按名订阅、自定义事件,以及让 UI 始终正确的刷新模式

事件模型

事件层由四部分组成:

部分类型角色
事件名GCSEventNames43 个内置事件的稳定字符串常量
PayloadGCSEventArgs.cs 中的 class随事件送达的类型化数据
ChannelIGCSEventChannel底层的 raise 与 subscribe 接口
FacadeGCSApi快捷方法加通用订阅 helper,从这里开始

事件只负责通知变化及其关联对象,不保存当前事实;先用 payload 定位对象,再从 GCSApi 读取最新状态

使用快捷方法订阅

OnEnemyUnitActing 外,每个内置事件都有对应的 GCSApi 快捷方法,事件名和 payload 类型都已替你绑好:

private IDisposable _energySubscription;

void OnEnable()
{
_energySubscription = GCSApi.OnEnergyChanged(args =>
{
energyLabel.text = $"{args.CurrentEnergy}/{args.MaxEnergy}";
});
}

void OnDisable()
{
_energySubscription?.Dispose();
}

每个快捷方法都返回 IDisposable;保存这个句柄,并在监听对象停用或销毁时释放它

按事件名订阅

Subscribe<TArgs> 接收事件名和类型化 handler,动态构建订阅、监听自定义事件,或订阅那个没有快捷方法的内置事件时用它:

private IDisposable _damageSubscription;

void OnEnable()
{
_damageSubscription = GCSApi.Subscribe<DamageEventArgs>(
GCSEventNames.OnDamageDealt,
args => AddCombatLog(args.SourceUnitId, args.TargetUnitId, args.Amount));
}

无 payload 的事件用无参数 overload:

private IDisposable _shuffleSubscription;

void OnEnable()
{
_shuffleSubscription = GCSApi.Subscribe(
GCSEventNames.OnDrawPileShuffled,
RefreshDrawPileWidget);
}

OnEnemyUnitActing 没有快捷方法,只能走这条路径,演示项目就用它播放攻击者扑向玩家的突进动画:

_actingSubscription = GCSApi.Subscribe<EnemyUnitActingEventArgs>(
GCSEventNames.OnEnemyUnitActing,
args => PlayAttackWindup(args.UnitId, args.Intent));
事件名和 payload 类型必须同时匹配

Channel 按「事件名 + 参数类型」这一对来派发,TArgs 写错的 typed handler 永远不会触发;同一个事件名先后用两种不同参数类型订阅时,Console 会打出 [GCSEvents] 错误,第二次订阅被忽略,拿不准就用快捷方法,或查事件参考

选对事件

UI 或系统需求合适事件
刷新完整战斗 HUDOnBattleStartedOnTurnStartedOnTurnEndedOnEnemyTurnStartedOnBattleEnded
刷新能量OnEnergyChangedOnEnergyGainedOnEnergySpent
刷新手牌OnCardDrawnOnCardDiscardedOnCardExhaustedOnCardRetainedOnCardAddedToHandOnCardModified
刷新单位 HP 与护甲OnUnitHpChangedOnArmorGainedOnArmorLostOnUnitDied
刷新状态 widgetOnStatusChangedOnStatusTickedOnStatusInflictedOnStatusExpiredOnStatusRemoved
刷新敌人意图OnEnemyUnitIntentChangedOnEnemyPhaseChanged
敌人行动时播放攻击前摇OnEnemyUnitActing
显示选择OnEffectChoiceOfferedOnEffectChoiceSelectedOnEffectChoiceSkipped
显示奖励OnBattleRewardOfferedOnBattleRewardSelectedOnBattleRewardSkipped
记录战斗细节OnCardPlayedOnDamageDealtOnDamageTakenOnStatusInflictedOnUnitDied

仅使用 payload 的场景

一次性表现只需要 payload 本身:

事件只用 payload 的用途
OnDamageDealt生成伤害数字或战斗日志行
OnDamageTaken在目标身上播放受击反馈
OnArmorGained显示护甲获得数字
OnEnergyGained显示能量脉冲
OnEffectChoiceOffered用 payload 渲染候选项
OnBattleRewardOffered用 payload 渲染奖励卡牌

即便如此,一次性表现之后也要从 GCSApi 刷一遍状态,飘字下面那条血条才会落在正确的值上

订阅生命周期

订阅一次,释放一次,这对操作的合适位置:

  • OnEnable / OnDisable
  • view model 的 Initialize / Dispose
  • scene controller 的 setup 与 teardown

会泄漏的位置:

  • Update
  • 每次按钮渲染
  • 每次重建手牌却不释放旧订阅

一个 handler 每次事件跑两三遍,就是重复订阅又没释放的典型症状

无 payload 的事件

四个内置事件完全不带数据:

  • OnBattleStarted
  • OnBattleRewardSkipped
  • OnEffectChoiceSkipped
  • OnDrawPileShuffled

它们用普通 Action 订阅:

_sub = GCSApi.OnDrawPileShuffled(RefreshPiles);

自定义命名事件

GCSApi.RaiseGameEvent 可以用任意名字触发事件,并附带可选的参数字典,FlowGraph 节点 Raise Internal Event 在图里做同一件事

GCSApi.RaiseGameEvent("PlayerMarkedTarget", new Dictionary<string, object>
{
["TargetId"] = target.UnitId
});

订阅的形态必须和触发的形态一致,带参数字典的触发以 IReadOnlyDictionary<string, object> 类型送达;不带参数的触发走无参数路径:

_sub = GCSApi.Subscribe<IReadOnlyDictionary<string, object>>(
"PlayerMarkedTarget",
args => HighlightUnit((int)args["TargetId"]));

自定义事件还会唤醒卡牌、状态、敌人图里同名的 On Internal Event 入口,所以做好的内容也能响应你代码里发明的时刻

只有在没有内置事件能描述这个时刻时才用自定义名字,用自定义名字复刻一个内置状态事件毫无收益,还把监听器拆成了两拨

在 Monitor 里看事件通道

Game Card Monitor 的 Event 页签实时展示整条通道,GCS Channel 区按顺序列出每次广播,Dispatcher Probes 区显示每次派发的订阅者数量,两者合起来,"监听器没有响应"就拆成了两种明确情况:事件没有触发,或者触发了但 subs 0

Game Card Monitor 的 Event 页签,按时间排列的 GCS Channel 日志与显示订阅者数量的 Dispatcher Probes

已经用 Game Event System 的项目,可以把战斗时刻转发进 GES,也可以让 GES 的触发启动图入口,这条路线有专门的页面:GES 集成,行为完全留在 GCS 内部时,GCSApi.SubscribeGCSApi.RaiseGameEvent 仍然是更简单的工具

故障排查

症状检查项
Handler 每次事件跑两遍以上重复订阅了,且没释放旧句柄
Handler 从不触发事件名和 payload 类型必须同时匹配,见事件参考;同时留意 Console 里的 [GCSEvents] 参数类型错误
UI 显示旧值事件之后从 GCSApi 刷新,payload 只用于确认发生了什么
选择期间卡牌按钮仍可点击GCSApi.IsWaitingForChoice 把按钮关掉
奖励 UI 一直不关闭GCSApi.ApplyRewardGCSApi.SkipReward 结束流程
GES 监听器收不到任何事件Bridge 必须已安装,节点上必须选好事件,见 GES 集成