事件使用指南
使用类型化快捷方法或事件名订阅战斗变化,并在正确生命周期中刷新 UI、释放监听
事件模型
事件层由四部分组成
| 部分 | 类型 | 角色 |
|---|---|---|
| 事件名 | GCSEventNames | 43 个内置事件的稳定字符串常量 |
| Payload | GCSEventArgs.cs 中的 class | 随事件送达的类型化数据 |
| Channel | IGCSEventChannel | 事件发布与订阅接口 |
| 统一入口 | GCSApi | 类型化快捷方法与通用订阅方法,项目代码从这里开始 |
事件只负责通知变化及其关联对象,不保存当前事实;先用 payload 定位对象,再从 GCSApi 读取最新状态
上图展示了事件从战斗结算到界面刷新的完整路径,Channel 负责派发,GCSApi 提供类型化或按名订阅入口,回调收到通知后再读取实时状态,避免把 Payload 误当成长期保存的战斗副本
使用快捷方法订阅
每个内置事件都有对应的 GCSApi 快捷方法,事件名与 Payload 类型已经由方法签名确定
private IDisposable _energySubscription;
void OnEnable()
{
_energySubscription = GCSApi.OnEnergyChanged(args =>
{
energyLabel.text = $"{args.CurrentEnergy}/{args.MaxEnergy}";
});
}
void OnDisable()
{
_energySubscription?.Dispose();
}
每个快捷方法都返回 IDisposable,应保存这份句柄,并在监听对象停用或销毁时释放
按事件名订阅
Subscribe<TArgs> 接收事件名和类型化回调,适合动态构建订阅或监听自定义事件
private IDisposable _damageSubscription;
void OnEnable()
{
_damageSubscription = GCSApi.Subscribe<DamageEventArgs>(
GCSEventNames.OnDamageDealt,
args => AddCombatLog(args.SourceUnitId, args.TargetUnitId, args.Amount));
}
没有 Payload 的事件使用无参数重载
private IDisposable _shuffleSubscription;
void OnEnable()
{
_shuffleSubscription = GCSApi.Subscribe(
GCSEventNames.OnDrawPileShuffled,
RefreshDrawPileWidget);
}
随包 Demo 使用 OnEnemyUnitActing 快捷方法播放攻击者扑向玩家的突进动画
_actingSubscription = GCSApi.OnEnemyUnitActing(
args => PlayAttackWindup(args.UnitId, args.Intent));
Channel 按「事件名 + 参数类型」共同派发,TArgs 不匹配的回调不会触发,同一个事件名先后使用两种不同参数类型订阅时,Console 会输出 [GCSEvents] 错误并忽略第二次订阅,无法确认类型时,应使用快捷方法或查阅事件参考
选对事件
| UI 或系统需求 | 合适事件 |
|---|---|
| 刷新完整战斗 HUD | OnBattleStarted、OnTurnStarted、OnTurnEnded、OnEnemyTurnStarted、OnBattleEnded |
| 刷新能量 | OnEnergyChanged、OnEnergyGained、OnEnergySpent |
| 刷新手牌 | OnCardDrawn、OnCardDiscarded、OnCardExhausted、OnCardRetained、OnCardAddedToHand、OnCardModified |
| 刷新单位 HP 与护甲 | OnUnitHpChanged、OnArmorGained、OnArmorLost、OnUnitDied |
| 刷新状态 widget | OnStatusChanged、OnStatusTicked、OnStatusInflicted、OnStatusExpired、OnStatusRemoved |
| 刷新敌人意图 | OnEnemyUnitIntentChanged、OnEnemyPhaseChanged |
| 敌人行动时播放攻击前摇 | OnEnemyUnitActing |
| 显示选择 | OnEffectChoiceOffered、OnEffectChoiceSelected、OnEffectChoiceSkipped |
| 显示奖励 | OnBattleRewardOffered、OnBattleRewardSelected、OnBattleRewardSkipped |
| 记录战斗细节 | OnCardPlayed、OnDamageDealt、OnDamageTaken、OnStatusInflicted、OnUnitDied |
只使用 Payload 的场景
一次性表现可以直接使用 Payload 中的本次变化数据
| 事件 | 只用 payload 的用途 |
|---|---|
OnDamageDealt | 生成伤害数字或战斗日志行 |
OnDamageTaken | 在目标身上播放受击反馈 |
OnArmorGained | 显示护甲获得数字 |
OnEnergyGained | 显示能量脉冲 |
OnEffectChoiceOffered | 用 payload 渲染候选项 |
OnBattleRewardOffered | 用 payload 渲染奖励卡牌 |
一次性表现完成后仍应从 GCSApi 刷新状态,使飘字和血条落在同一结果上
订阅生命周期
订阅与释放需要成对出现,适合放在以下生命周期:
-
OnEnable/OnDisable -
View Model 的
Initialize/Dispose -
Scene Controller 的 Setup 与 Teardown
不要在以下重复调用位置建立未释放的订阅:
-
Update -
每次按钮渲染
-
每次重建手牌却不释放旧订阅
同一个回调在一次事件中执行多次,通常说明旧订阅没有释放
无 payload 的事件
四个内置事件不携带 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"]));
自定义事件还会触发 Card、Status 与 Enemy Behavior 中同名的 On Internal Event 入口,因此,项目代码定义的业务时机也可以直接进入既有 FlowGraph
只有在内置事件无法准确表达业务时机时才应增加自定义名称,重复发布已有状态事件会形成两套语义相同的订阅入口,增加维护和排查成本
在 Monitor 中检查事件通道
Game Card Monitor 的 Event 页签实时展示整条通道,GCS Channel 区按顺序列出每次广播,Dispatcher Probes 区显示每次派发的订阅者数量,两项证据可以区分事件没有触发,以及事件已经触发但当时 subs 0 这两种情况

上图把可读的事件详情与底层派发记录放在一起,先确认目标事件已经进入 GCS Channel,再检查对应 probe 的订阅者数量,便能判断问题位于玩法时机还是订阅生命周期
项目同时使用 Game Event System 时,可以把战斗时机转发到 GES,也可以让 GES 事件启动 FlowGraph 入口,具体接法见 GES 集成,行为完全留在 GCS 内部时,继续使用 GCSApi.Subscribe 与 GCSApi.RaiseGameEvent 即可
故障排查
| 症状 | 检查项 |
|---|---|
| Handler 每次事件跑两遍以上 | 重复订阅了,且没释放旧句柄 |
| Handler 从不触发 | 事件名和 payload 类型必须同时匹配,见事件参考;同时留意 Console 里的 [GCSEvents] 参数类型错误 |
| UI 显示旧值 | 事件之后从 GCSApi 刷新,payload 只用于确认发生了什么 |
| 选择期间卡牌按钮仍可点击 | 用 GCSApi.IsWaitingForChoice 把按钮关掉 |
| 奖励 UI 一直不关闭 | 用 GCSApi.ApplyReward 或 GCSApi.SkipReward 结束流程 |
| GES 监听器收不到任何事件 | Bridge 必须已安装,节点上必须选好事件,见 GES 集成 |