跳到主要内容

事件指南

GCS · Events

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

适合为 GCS 战斗接入 HUD、战斗日志、成就或外部系统的开发者。

战斗里每个值得关注的时刻都会触发(Raise)一个事件:打出一张卡牌、伤害落地、敌人身上的 Burn 结算一跳、奖励出现。本页讲清三件事:如何从自己的代码订阅这些时刻,如何为具体的 UI 需求选对事件,以及如何避开两个经典 bug(订阅泄漏和显示旧值)。

事件模型

事件层由四部分组成:

部分类型角色
事件名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

事件之后读状态

做 UI 时,把事件当触发器,把 GCSApi 当数据源:

  1. 事件触发。
  2. 用 payload 定位受影响的单位、卡牌或面板。
  3. 通过 GCSApi 读当前值。
  4. 更新 UI。
private void OnUnitHpChanged(UnitHpChangedEventArgs args)
{
var unit = FindUnitById(args.UnitId);
hpBar.SetValue(GCSApi.Hp(unit), GCSApi.MaxHp(unit));
armorLabel.text = GCSApi.Armor(unit).ToString();
}

一次流图(FlowGraph)解析可能连续触发好几个事件。每次事件之后都读一遍最新状态,无论触发了多少个、顺序如何,UI 都是对的。


只用 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


GES bridge

已经用 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 集成