Skip to main content

Event Guide

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:

PieceTypeRole
Event namesGCSEventNamesStable string constants for the 43 built-in events
PayloadsClasses in GCSEventArgs.csTyped data delivered with each event
ChannelIGCSEventChannelThe low-level raise and subscribe surface
FacadeGCSApiShortcut 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));
Name and payload type must both match

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 needGood events
Refresh the full battle HUDOnBattleStarted, OnTurnStarted, OnTurnEnded, OnEnemyTurnStarted, OnBattleEnded
Refresh energyOnEnergyChanged, OnEnergyGained, OnEnergySpent
Refresh hand cardsOnCardDrawn, OnCardDiscarded, OnCardExhausted, OnCardRetained, OnCardAddedToHand, OnCardModified
Refresh unit HP and armorOnUnitHpChanged, OnArmorGained, OnArmorLost, OnUnitDied
Refresh status widgetsOnStatusChanged, OnStatusTicked, OnStatusInflicted, OnStatusExpired, OnStatusRemoved
Refresh enemy intentsOnEnemyUnitIntentChanged, OnEnemyPhaseChanged
Play attack wind-ups on acting enemiesOnEnemyUnitActing
Show choicesOnEffectChoiceOffered, OnEffectChoiceSelected, OnEffectChoiceSkipped
Show rewardsOnBattleRewardOffered, OnBattleRewardSelected, OnBattleRewardSkipped
Log combat detailsOnCardPlayed, OnDamageDealt, OnDamageTaken, OnStatusInflicted, OnUnitDied

Payload-only handling

One-shot effects need only the payload:

EventPayload-only use
OnDamageDealtSpawn a damage number or a combat log row
OnDamageTakenShow a hit reaction on the target
OnArmorGainedShow an armor gain number
OnEnergyGainedShow an energy pulse
OnEffectChoiceOfferedRender the choice candidates from the payload
OnBattleRewardOfferedRender 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:

  • OnBattleStarted
  • OnBattleRewardSkipped
  • OnEffectChoiceSkipped
  • OnDrawPileShuffled

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

Game Card Monitor Event tab with the chronological GCS Channel log and Dispatcher Probes showing subscriber counts

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

SymptomCheck
Handler fires two or more times per eventYou resubscribed without disposing the previous handle
Handler never firesEvent 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 valuesRefresh from GCSApi after the event instead of caching payload values
Card buttons stay enabled during a choiceGate them on GCSApi.IsWaitingForChoice
Reward UI never closesFinish the flow with GCSApi.ApplyReward or GCSApi.SkipReward
GES listeners receive nothingThe bridge must be installed and the event picked on the node; see GES integration