Skip to main content

Custom UI integration

Guide

Subscribe to battle events, read current state through GCSApi, then submit plays, turns, choices, and rewards back to runtime

The diagram above shows the only two-way relationship custom UI needs to maintain with GCS. Runtime sends events when state changes, UI reads current state again after receiving them, and player actions return to the same battle through GCSApi. The interface does not keep a second authoritative copy of battle data

The snippets contain only the components that interact with GCS; place them in the project's presenter or MonoBehaviour, then add using System;, using System.Collections.Generic;, and using TinyGiants.GCS.Runtime; for the types the class uses

Integration checklist

  • Place and configure a GameCardManager

  • Start battles with GCSApi.StartBattle

  • Subscribe to runtime events in UI lifecycle methods

  • Refresh UI from GCSApi after each event

  • Use GCSApi.CanPlayCard and GCSApi.TryPlayCard for card buttons

  • Use GCSApi.EndPlayerTurn for the end-turn button

  • Use GCSApi.ApplyReward or GCSApi.SkipReward for reward screens

  • Dispose every subscription

  • Keep Demo-specific View components replaceable

Subscription lifetime

Use a small disposable list per UI object

private readonly List<IDisposable> _subscriptions = new();

void OnEnable()
{
_subscriptions.Add(GCSApi.OnBattleStarted(RefreshAll));
_subscriptions.Add(GCSApi.OnEnergyChanged(_ => RefreshEnergy()));
_subscriptions.Add(GCSApi.OnCardDrawn(_ => RefreshHand()));
_subscriptions.Add(GCSApi.OnCardDiscarded(_ => RefreshHand()));
_subscriptions.Add(GCSApi.OnCardExhausted(_ => RefreshHand()));
_subscriptions.Add(GCSApi.OnUnitHpChanged(_ => RefreshUnits()));
_subscriptions.Add(GCSApi.OnStatusChanged(_ => RefreshStatuses()));
}

void OnDisable()
{
foreach (var subscription in _subscriptions)
{
subscription.Dispose();
}
_subscriptions.Clear();
}
tip

Dispose each subscription when its GameObject is destroyed. Stale listeners accumulate across reloads and cause duplicate UI refreshes

Battle HUD

Refresh the high-level HUD fields from state:

WidgetRead
Phase labelGCSApi.Phase
Turn labelGCSApi.TurnNumber
EnergyGCSApi.PlayerEnergy, GCSApi.MaxEnergy
End turn buttonGCSApi.Phase == BattlePhase.PlayerPhase
Battle result overlayOnBattleEnded and BattleEndedEventArgs.Won

Read the phase from GCSApi.Phase and phase events. A graph can end the turn, grant an extra turn, open a choice, or finish the battle without a UI click

When your HUD disagrees with the game

Open Game Card Monitor:

Tools > TinyGiants > GCS > Game Card Monitor

Check the Battle tab; if the Monitor shows the correct battle state but the HUD does not, inspect the HUD event subscriptions and refresh path

Game Card Monitor on the Battle tab during a running fight, showing the same phase, turn, player, and enemy state a custom HUD should display

Hand UI

Rebuild the complete hand from GCSApi.Hand(), or update individual cards by instance ID. The method returns the live hand pile at call time, including cards drawn during the current turn

void RefreshHand()
{
foreach (var card in GCSApi.Hand())
{
var active = GCSApi.GetActiveCard(card);
var cost = GCSApi.GetEffectiveEnergyCost(card);
var playable = GCSApi.CanPlayCard(card);
RenderCard(card, active, cost, playable);
}
}

For click handling:

void OnCardClicked(CardInstance card)
{
var active = GCSApi.GetActiveCard(card);
if (GCSApi.RequiresTarget(active))
{
OpenTargetPicker(card);
return;
}

GCSApi.TryPlayCard(card);
}

When target selection completes:

void OnTargetSelected(CardInstance card, UnitState target)
{
GCSApi.TryPlayCard(card, target);
}

Unit panels

Build unit panels from runtime units

Panel valueRead
HPGCSApi.Hp(unit), GCSApi.MaxHp(unit)
ArmorGCSApi.Armor(unit)
Alive/deadGCSApi.IsAlive(unit)
HP percentGCSApi.HpPercent(unit)
StatusesGCSApi.StatusesOn(unit)
Enemy intentsGCSApi.PendingIntents(enemy)

Unit panels can refresh in response to these events:

  • OnUnitHpChanged

  • OnUnitDied

  • OnArmorGained

  • OnArmorLost

  • OnStatusChanged

  • OnStatusTicked

  • OnEnemyUnitIntentChanged

  • OnEnemyPhaseChanged

Status controls

Status UI should read the full current map, not only the values in the event payload; use StatusChangedEventArgs.UnitId to locate which unit changed, then re-read everything on it

void RefreshStatusList(UnitState unit)
{
foreach (var pair in GCSApi.StatusesOn(unit))
{
var status = pair.Key;
var stacks = pair.Value;
RenderStatus(status.Icon, status.DisplayName, stacks, status.ShowStackCount, status.IsDebuff);
}
}

Enemy intents

Intent UI can show the current top intent or the full pending list

void RenderIntent(UnitState enemy)
{
var tag = GCSApi.CurrentIntentTag(enemy);
var value = GCSApi.CurrentIntentValue(enemy);
var pending = GCSApi.PendingIntents(enemy);
RenderIntentIcons(tag, value, pending);
}
tip

Refresh when OnEnemyUnitIntentChanged fires and after enemy phase changes

Choice UI

When a Choice node asks the player to pick cards, units, or statuses, GCS raises OnEffectChoiceOffered

Payload fieldUse
PromptTextLabel for the choice panel
CandidatesOptions to render
AllowSkipWhether to show a skip button

The choice panel must implement IChoicePresenter. When a Choice opens, runtime passes the prompt text, candidates, minPicks, maxPicks, onPicked, and onSkipped. The panel enables or disables confirmation according to the selection limits and returns the original candidate objects through the matching callback. Without an IChoicePresenter in the scene, or when the candidate list is empty, the Choice node resolves as skipped. After a pick, GCS stores the selected values and raises OnEffectChoiceSelected with every selected index and candidate; after an explicit or automatic skip, it raises OnEffectChoiceSkipped

tip

Disable normal card buttons while GCSApi.IsWaitingForChoice is true

Reward UI

After a won battle, GCS may raise OnBattleRewardOffered

private void ShowReward(BattleRewardOfferedEventArgs args)
{
foreach (var card in args.CardCandidates)
{
RenderRewardCard(card);
}
}

public void PickReward(GameCard card)
{
GCSApi.ApplyReward(RewardChoice.Card, card);
}

public void SkipReward()
{
GCSApi.SkipReward();
}

RewardChoice also offers Energy and Cost; pass null as the card for those; applying or skipping resolves the reward step and raises OnBattleEnded; use GCSApi.IsWaitingForReward to decide whether the reward controls should be visible

Floating text and presentation

For custom floating text, subscribe to FloatingTextSystem.Requested; the handler receives a FloatingTextRequest

void OnEnable()
{
FloatingTextSystem.Requested += ShowFloatingText;
}

void OnDisable()
{
FloatingTextSystem.Requested -= ShowFloatingText;
}

The built-in UnitView registers itself by UnitId, allowing FlowGraph presentation nodes to locate the target view. Custom unit visuals bind their own View Model to the Unit ID in event Payloads. The Demo HandController, CardView, and reward UI classes serve only the sample scenes; production UI should bind to stable runtime events and IDs

Refresh strategy

StrategyUse when
Full rebuildHand, reward choices, and small status lists
Patch by IDUnit panels, Intent controls, floating text targets
Debounced refreshLarge custom combat logs or analytics panels
Immediate animation plus refreshDamage, heal, armor, and card movement

Visible battle UI can usually refresh completely from GCSApi. Switch to partial or debounced updates only after profiling confirms a real bottleneck here

Common mistakes

MistakeBetter approach
Keeping a local copy of hand cards and mutating it manuallyRebuild from GCSApi.Hand() after card events
Enabling card buttons based only on energyUse GCSApi.CanPlayCard(card)
Forgetting to unsubscribeStore and dispose the IDisposable handle
Treating an event Payload as complete stateUse the Payload to locate what changed, then read current state
Depending on Demo UI classes in production UIDepend on GCSApi, events, and the project's own View classes
Blocking gameplay because a VFX or SFX asset is missingLet gameplay resolve and skip the missing presentation