跳到主要内容

事件使用指南

Guide

使用类型化快捷方法或事件名订阅战斗变化,并在正确生命周期中刷新 UI、释放监听

事件模型

事件层由四部分组成

部分类型角色
事件名GCSEventNames43 个内置事件的稳定字符串常量
PayloadGCSEventArgs.cs 中的 class随事件送达的类型化数据
ChannelIGCSEventChannel事件发布与订阅接口
统一入口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));
事件名和 payload 类型必须同时匹配

Channel 按「事件名 + 参数类型」共同派发,TArgs 不匹配的回调不会触发,同一个事件名先后使用两种不同参数类型订阅时,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

  • 每次按钮渲染

  • 每次重建手牌却不释放旧订阅

提示

同一个回调在一次事件中执行多次,通常说明旧订阅没有释放

无 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 这两种情况

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

上图把可读的事件详情与底层派发记录放在一起,先确认目标事件已经进入 GCS Channel,再检查对应 probe 的订阅者数量,便能判断问题位于玩法时机还是订阅生命周期

提示

项目同时使用 Game Event System 时,可以把战斗时机转发到 GES,也可以让 GES 事件启动 FlowGraph 入口,具体接法见 GES 集成,行为完全留在 GCS 内部时,继续使用 GCSApi.SubscribeGCSApi.RaiseGameEvent 即可

故障排查

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