Skip to main content

Starting a Battle

Guide

Use Bootstrap when a scene always runs the same encounter, or call GCSApi.StartBattle when a map, quest, or level flow selects the encounter dynamically. Both entry points share the same battle lifecycle

Decide where the Encounter comes from

When the same battle should start every time Play Mode begins, save the encounter in Default Encounter on GameCardEncounterBootstrap. This suits Demo scenes, isolated battle tests, and scenes loaded specifically for one encounter.

Let Bootstrap handle fixed scenes

Select the Encounter to run in Default Encounter on GameCardEncounterBootstrap, leave Auto Start On Play enabled, and confirm that the Inspector shows Ready — starts on Play at the top.

Game Card Encounter Bootstrap with Fenmoss Hollow selected, Ready — starts on Play shown, and Auto Start On Play enabled

When Play Mode begins, Bootstrap starts the battle through the direct reference in Default Encounter. Ready validates Editor discovery and scene configuration only. At runtime, it does not check again whether that Encounter Database is Active. The direct-reference chain continues from GameEncounter to the player, enemies, starting deck, and cards, so these ScriptableObject references remain valid even if their databases are Inactive.

Active controls the discovery scope for Game Card Editor, selectors, and GCSApi queries and participates in some runtime ID resolution. Databases and Content Discovery defines the exact boundaries.

warning

GameCardEncounterBootstrap must be on the same GameObject as GameCardManager. When added, Unity supplies a missing Manager automatically. In addition to storing a fixed Encounter, Bootstrap can invoke UnityEvents or GES events after StartBattle returns.

FieldDefaultPurpose
Default EncounterEmptyEncounter passed to GameCardManager.StartBattle when Play Mode begins
Auto Start On PlayEnabledStarts Default Encounter during the component's Start phase
Use GES EventsDisabledAfter GES is installed, selects between UnityEvent and GES event lists for Bootstrap output
On BootstrapEmptyInvoked after StartBattle returns when GES is not used
On Bootstrap EventsEmptyRaised in list order after StartBattle returns when GES is used
tip

If Default Encounter is empty or no valid Manager is available, Bootstrap records a warning and does not create a battle.

These outputs belong to the Bootstrap component, not the shared battle lifecycle. Encounters started directly from code do not trigger them.

See GES Integration to connect a fixed entry point to GES.

Call GCSApi.StartBattle from a dynamic flow

Once a map or level flow selects the Encounter at runtime, project code also owns startup. Keep GameCardManager in the scene, disable Auto Start On Play, and store the current activeEncounter in the flow coordinator.

private GameEncounter activeEncounter;

public void StartEncounter(GameEncounter encounter)
{
if (encounter == null || activeEncounter != null || !GCSApi.IsReady) return;

activeEncounter = encounter;
GCSApi.StartBattle(encounter);
}
warning

GCSApi.IsReady means only that the scene contains a valid Manager. If the same Manager receives another StartBattle command, it clears the current state machine immediately and creates a new battle. The old battle does not raise OnBattleEnded first. If a map flow stores its own activeEncounter, it must reject another startup while that value is not null. Checking only GCSApi.IsBattleActive is not sufficient. During the reward wait after victory, battle progression has stopped, but the battle is not fully complete because OnBattleEnded has not yet returned the final result to the project flow. Release activeEncounter only when OnBattleEnded is received, and restore the map at the same handoff point after the data transfer is complete.

Both entry points converge in Manager

Bootstrap and GCSApi both pass the selected GameEncounter to GameCardManager.StartBattle. From that call onward, the map, HUD, results UI, and save system need only listen to the GCS lifecycle events. They do not need to know which entry point started the battle.

EventArgumentsTiming
OnBattleStartedNoneThe player, enemies, initial library, and opening hand have been created and TurnNumber is 1. The runtime then resolves enemy intents, handles battle-start statuses, and enters the first player turn
OnBattleRewardOfferedBattleRewardOfferedEventArgsVictory is confirmed and reward candidates have been generated. The battle waits for a selection or Skip
OnBattleRewardSelectedBattleRewardSelectedEventArgsThe player has confirmed a reward and its result has been applied to the Run
OnBattleRewardSkippedNoneThe player has skipped this battle's reward
OnBattleEndedBattleEndedEventArgsOn defeat, raised after battle-end state handling. On victory, raised after a reward is selected or skipped. Won carries the final result

Follow these lifecycle and call details to keep the integration correct and memory-safe:

  • OnBattleStarted is raised synchronously inside StartBattle. Any object that needs the startup notification for this battle must subscribe before the call.

  • GCSApi.OnBattleStarted, GCSApi.OnBattleEnded, and the general GCSApi.Subscribe all return IDisposable. Dispose the corresponding handle when the listener is disabled.

  • GCS raises OnCardDrawn for every opening-hand card before OnBattleStarted. Objects that animate or process opening cards individually must also subscribe before StartBattle is called.

  • OnBattleRewardOffered only publishes candidates and pauses the victory flow. The reward UI or project logic must call GCSApi.ApplyReward(choice, pickedCard) or GCSApi.SkipReward(). Otherwise, the battle never raises its victory result through OnBattleEnded.

  • Reward UI can submit only once while GCSApi.IsWaitingForReward is true. Disable selection and Skip buttons immediately after the first click.

  • RewardChoice.Card requires a valid card from the OnBattleRewardOffered candidates. Repeated calls to ApplyReward can modify Run state again but do not raise reward-selection or battle-end events again.

  • After the entry points converge, the defeat path ends immediately, while the victory path waits for a reward choice. The two paths converge again at OnBattleEnded only after the reward is applied or skipped.

The diagram above brings both startup paths to the same OnBattleEnded return point. Project code can confirm that the battle flow has ended and read its final result only after receiving this event. Bootstrap's On Bootstrap and GES lists are not part of this lifecycle. They run only after StartBattle returns. Maps and long-lived flows should use GCS lifecycle events instead. See the Event Guide and Event Reference for the remaining phase, card, unit, and FlowGraph events.

Connect the map and battle with a coordinator

The map coordinator switches to battle presentation before calling StartBattle and restores the map after receiving OnBattleEnded. It writes activeEncounter at startup and clears it at completion, preventing another battle from starting before the current one ends.

using System;
using TinyGiants.GCS.Runtime;
using UnityEngine;

public sealed class MapEncounterFlow : MonoBehaviour
{
[SerializeField] private GameObject mapRoot;
[SerializeField] private GameObject battleRoot;

public event Action<GameEncounter> EncounterStarted;
public event Action<GameEncounter, bool> EncounterFinished;

private IDisposable battleStartedSubscription;
private IDisposable battleEndedSubscription;
private GameEncounter activeEncounter;

private void OnEnable()
{
battleStartedSubscription = GCSApi.OnBattleStarted(HandleBattleStarted);
battleEndedSubscription = GCSApi.OnBattleEnded(HandleBattleEnded);
}

private void OnDisable()
{
battleStartedSubscription?.Dispose();
battleStartedSubscription = null;
battleEndedSubscription?.Dispose();
battleEndedSubscription = null;
}

public void StartMapEncounter(GameEncounter encounter)
{
if (encounter == null || activeEncounter != null || !GCSApi.IsReady) return;

activeEncounter = encounter;
battleRoot.SetActive(true);
mapRoot.SetActive(false);
GCSApi.StartBattle(encounter);
}

private void HandleBattleStarted()
{
if (activeEncounter != null) EncounterStarted?.Invoke(activeEncounter);
}

private void HandleBattleEnded(BattleEndedEventArgs args)
{
if (activeEncounter == null) return;

var finishedEncounter = activeEncounter;
activeEncounter = null;
battleRoot.SetActive(false);
mapRoot.SetActive(true);
EncounterFinished?.Invoke(finishedEncounter, args.Won);
}
}

Place the coordinator and GameCardManager on objects that remain active throughout the switch. Use MapRoot and BattleRoot as child objects controlled by the coordinator. StartMapEncounter activates BattleRoot before calling StartBattle, allowing HUD and presentation components that subscribe in OnEnable to receive the synchronously raised startup and opening-hand draw events.

tip

If the victory or defeat presentation must continue using the battle view, wait for it to finish inside HandleBattleEnded before switching the root objects. BattleEndedEventArgs still carries the result to map progression and the save system.

See Custom UI Integration for the exact connection between presentation and battle state.

Initialize the Master Deck before a multi-battle Run

If the map starts another battle after returning, activeEncounter is cleared when the current battle ends, but the Run-level Master Deck must persist between battles. When the Master Deck is empty, GCS builds the current battle library temporarily from the current Encounter player's StartingDeck, but does not write that fallback into the Master Deck. If the first victory then adds only one reward card, the Master Deck contains only that reward, and the next battle does not fall back to the starting deck again. At the beginning of a new Run, clear the previous Run state, then write every card from the starting deck into the Master Deck.

using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
using UnityEngine;

public sealed class RunDeckInitializer : MonoBehaviour
{
[SerializeField] private GameDeck startingDeck;

public void BeginNewRun()
{
if (startingDeck == null || startingDeck.Entries == null || !GCSApi.IsReady) return;

var masterDeck = new List<MasterDeckEntry>();
foreach (var entry in startingDeck.Entries)
{
if (entry?.Card == null) continue;

for (var copy = 0; copy < entry.Copies; copy++)
{
masterDeck.Add(new MasterDeckEntry
{
CardGuid = entry.Card.Guid
});
}
}

GCSApi.ClearMasterDeck();
GCSApi.SetMasterDeck(masterDeck);
}
}

The Master Deck stores card GUIDs. When a battle starts, GCS resolves those GUIDs only from the current Encounter player's StartingDeck and the Encounter RewardDeck. Entries not found in either deck are skipped. Every Encounter in a Run must therefore include all card assets that can appear in the Master Deck in its StartingDeck or RewardDeck.

tip

GCSApi.ClearMasterDeck clears both the old Master Deck and the Run energy bonus. Call it only when starting a new Run.

Do not clear the Master Deck between later encounters. It preserves the deck after rewards, upgrades, removals, and cost changes, while still following the card-resolution scope above when a battle starts.

Continue with Hand and Card Piles and the GCSApi Guide for the complete Run deck APIs.

Dispose the battle explicitly when leaving early

Normal battles hand control back through OnBattleEnded. When the player leaves an unfinished battle, call GCSApi.DisposeBattle() to clear the current battle state. This API does not raise OnBattleEnded and does not clear the Master Deck or Run energy bonus. The project flow must restore the map, determine the outcome, and save progress itself.

tip

Do not start a new battle as a substitute for disposal. A new StartBattle call clears the old state machine but does not raise the old battle's ending event.

Let the project flow restore the map, record the result, and clear activeEncounter before starting another battle. This keeps the map and battle states aligned.

Checklist before entering Play Mode

  1. The scene contains only one enabled GameCardManager, and GCSApi.IsReady returns true.
  2. A fixed scene has a Default Encounter selected with Auto Start On Play enabled.
  3. A dynamic flow has Auto Start On Play disabled, stores activeEncounter in project code, and calls GCSApi.StartBattle itself.
  4. Objects that need OnBattleStarted or opening-hand OnCardDrawn have subscribed before startup and dispose every subscription handle when disabled.
  5. A multi-battle Run initializes its Master Deck before the first battle, and every Encounter can resolve its cards through StartingDeck or RewardDeck.
  6. The victory path calls GCSApi.ApplyReward or GCSApi.SkipReward only once, disables reward buttons after the first click, then waits for OnBattleEnded.
  7. The disposal path restores the map, records the result, and clears activeEncounter explicitly.

After connecting the startup entry, lifecycle, and Run state, continue to Battle Scene Setup and assemble the Manager, battlefield, hand, HUD, and presentation objects into a playable battle scene.