Event Guide
How to listen to a running battle: typed shortcuts, named subscriptions, custom events, and the refresh pattern that keeps UI correct
The event model
Four pieces make up the event surface:
| Piece | Type | Role |
|---|---|---|
| Event names | GCSEventNames | Stable string constants for the 43 built-in events |
| Payloads | Classes in GCSEventArgs.cs | Typed data delivered with each event |
| Channel | IGCSEventChannel | The low-level raise and subscribe surface |
| Facade | GCSApi | Shortcut methods plus generic subscribe helpers; start here |
An event tells you that something changed, and roughly what; it is not the source of truth; use the payload to identify the changed object, then read its current values from GCSApi
Subscribe with shortcuts
Every built-in event except OnEnemyUnitActing has a GCSApi shortcut that binds the correct name and payload type for you:
private IDisposable _energySubscription;
void OnEnable()
{
_energySubscription = GCSApi.OnEnergyChanged(args =>
{
energyLabel.text = $"{args.CurrentEnergy}/{args.MaxEnergy}";
});
}
void OnDisable()
{
_energySubscription?.Dispose();
}
Each shortcut returns an IDisposable; keep the handle and dispose it when the listener goes away; that is the entire lifetime contract
Subscribe by event name
Subscribe<TArgs> takes an event name and a typed handler; use it when you build subscriptions dynamically, listen to custom events, or wire the one built-in event without a shortcut:
private IDisposable _damageSubscription;
void OnEnable()
{
_damageSubscription = GCSApi.Subscribe<DamageEventArgs>(
GCSEventNames.OnDamageDealt,
args => AddCombatLog(args.SourceUnitId, args.TargetUnitId, args.Amount));
}
The parameterless overload covers events that carry no payload:
private IDisposable _shuffleSubscription;
void OnEnable()
{
_shuffleSubscription = GCSApi.Subscribe(
GCSEventNames.OnDrawPileShuffled,
RefreshDrawPileWidget);
}
OnEnemyUnitActing always goes through this path, since it has no shortcut; the demo listens to it to play the attacker's lunge toward the player:
_actingSubscription = GCSApi.Subscribe<EnemyUnitActingEventArgs>(
GCSEventNames.OnEnemyUnitActing,
args => PlayAttackWindup(args.UnitId, args.Intent));
The channel dispatches on the pair of event name and argument type; A typed handler with the wrong TArgs never fires, and subscribing one event name with two different argument types logs a [GCSEvents] error in the Console and ignores the second subscription; when in doubt, use the shortcuts or check the Event Reference
Pick the right event
| UI or system need | Good events |
|---|---|
| Refresh the full battle HUD | OnBattleStarted, OnTurnStarted, OnTurnEnded, OnEnemyTurnStarted, OnBattleEnded |
| Refresh energy | OnEnergyChanged, OnEnergyGained, OnEnergySpent |
| Refresh hand cards | OnCardDrawn, OnCardDiscarded, OnCardExhausted, OnCardRetained, OnCardAddedToHand, OnCardModified |
| Refresh unit HP and armor | OnUnitHpChanged, OnArmorGained, OnArmorLost, OnUnitDied |
| Refresh status widgets | OnStatusChanged, OnStatusTicked, OnStatusInflicted, OnStatusExpired, OnStatusRemoved |
| Refresh enemy intents | OnEnemyUnitIntentChanged, OnEnemyPhaseChanged |
| Play attack wind-ups on acting enemies | OnEnemyUnitActing |
| Show choices | OnEffectChoiceOffered, OnEffectChoiceSelected, OnEffectChoiceSkipped |
| Show rewards | OnBattleRewardOffered, OnBattleRewardSelected, OnBattleRewardSkipped |
| Log combat details | OnCardPlayed, OnDamageDealt, OnDamageTaken, OnStatusInflicted, OnUnitDied |
Payload-only handling
One-shot effects need only the payload:
| Event | Payload-only use |
|---|---|
OnDamageDealt | Spawn a damage number or a combat log row |
OnDamageTaken | Show a hit reaction on the target |
OnArmorGained | Show an armor gain number |
OnEnergyGained | Show an energy pulse |
OnEffectChoiceOffered | Render the choice candidates from the payload |
OnBattleRewardOffered | Render the reward cards from the payload |
Even then, follow the one-shot effect with a state refresh from GCSApi, so the bar underneath the floating number lands on the right value too
Subscription lifetime
Subscribe once, dispose once; good homes for that pair:
OnEnable/OnDisable- a view model's
Initialize/Dispose - scene controller setup and teardown
Places that leak:
Update- every button render
- every hand rebuild that never disposes the previous subscriptions
A handler running two or three times per event is the classic sign of resubscribing without disposal
Events without a payload
Four built-in events carry no data at all:
OnBattleStartedOnBattleRewardSkippedOnEffectChoiceSkippedOnDrawPileShuffled
Subscribe to these with a plain Action:
_sub = GCSApi.OnDrawPileShuffled(RefreshPiles);
Custom named events
GCSApi.RaiseGameEvent raises an event under any name you choose, with an optional argument dictionary; the FlowGraph node Raise Internal Event does the same thing from inside a graph
GCSApi.RaiseGameEvent("PlayerMarkedTarget", new Dictionary<string, object>
{
["TargetId"] = target.UnitId
});
Subscription shape must match raise shape; A raise with an argument dictionary arrives typed as IReadOnlyDictionary<string, object>; a raise without arguments arrives on the parameterless path:
_sub = GCSApi.Subscribe<IReadOnlyDictionary<string, object>>(
"PlayerMarkedTarget",
args => HighlightUnit((int)args["TargetId"]));
Custom events also wake any On Internal Event entry with the same name inside card, status, and enemy graphs, so authored content can react to a moment your code invented
Reach for custom names only when no built-in event describes the moment; duplicating a built-in state event under a custom name buys nothing and splits your listeners in two
Watch the channel in the Monitor
The Event tab of the Game Card Monitor shows the channel live; the GCS Channel section lists every broadcast in order, and Dispatcher Probes prints each dispatch with its subscriber count; together they split "my listener never ran" into its two real cases: the event never fired, or it fired with subs 0

Projects that already run on Game Event System can forward battle moments into GES and start graph entries from GES raises; that path has its own page: GES integration; for behavior that stays inside GCS, GCSApi.Subscribe and GCSApi.RaiseGameEvent remain the simpler tools
Troubleshooting
| Symptom | Check |
|---|---|
| Handler fires two or more times per event | You resubscribed without disposing the previous handle |
| Handler never fires | Event name and payload type must both match; see the Event Reference; also look for a [GCSEvents] argument-type error in the Console |
| UI shows stale values | Refresh from GCSApi after the event instead of caching payload values |
| Card buttons stay enabled during a choice | Gate them on GCSApi.IsWaitingForChoice |
| Reward UI never closes | Finish the flow with GCSApi.ApplyReward or GCSApi.SkipReward |
| GES listeners receive nothing | The bridge must be installed and the event picked on the node; see GES integration |