战斗启动
每次进入场景都运行同一场遭遇战时使用 Bootstrap,由地图、任务或关卡流程动态选择遭遇战时调用 GCSApi.StartBattle,两种入口共享同一套战斗生命周期
先确定 Encounter 从哪里来
- 一个场景一场遭遇战
- 地图或流程控制
每次进入 Play Mode 都要启动同一场战斗时,请把遭遇战保存在 GameCardEncounterBootstrap 的 Default Encounter 中,Demo 场景、独立战斗测试以及按遭遇战单独加载的场景都适合这样启动
地图节点、任务状态、关卡分支或匹配结果需要动态选出遭遇战时,关闭 Bootstrap 的自动启动,再由项目流程调用 GCSApi.StartBattle(encounter)
固定场景交给 Bootstrap
在 GameCardEncounterBootstrap 的 Default Encounter 中选择要运行的遭遇战,保持 Auto Start On Play 开启,Inspector 顶部会显示 Ready — starts on Play

进入 Play Mode 后,Bootstrap 会使用 Default Encounter 的直接引用发起战斗,Ready 只校验编辑器发现与场景配置,运行时不会再次检查该 Encounter Database 是否 Active,这条直接引用链会从 GameEncounter 继续延伸到玩家、敌人、起始卡组与卡牌,即使对应数据库处于 Inactive,这些 ScriptableObject 引用仍然有效
Active 控制 Game Card Editor、选择器与 GCSApi 查询的发现范围,并参与部分运行时 ID 解析,具体边界见数据库与内容发现
GameCardEncounterBootstrap 必须与 GameCardManager 位于同一 GameObject,添加时 Unity 会自动补上缺失的 Manager,除保存固定 Encounter 外,Bootstrap 还可以在 StartBattle 返回后触发 UnityEvent 或 GES 事件
| 字段 | 默认值 | 作用 |
|---|---|---|
Default Encounter | 空 | 进入 Play Mode 时传给 GameCardManager.StartBattle 的遭遇战 |
Auto Start On Play | 开启 | 在组件的 Start 阶段启动 Default Encounter |
Use GES Events | 关闭 | 安装 GES 后,在 UnityEvent 与 GES 事件列表之间选择 Bootstrap 输出方式 |
On Bootstrap | 空 | 未使用 GES 时,在 StartBattle 返回后调用 |
On Bootstrap Events | 空 | 使用 GES 时,在 StartBattle 返回后按列表顺序触发 |
Default Encounter 为空或当前没有可用的 Manager 时,Bootstrap 只会记录警告,不会创建战斗
这些输出属于 Bootstrap 组件,不属于通用战斗生命周期,通过代码直接启动的遭遇战不会触发它们
需要把固定入口接入 GES 时,可以查看 GES 集成
动态流程调用 GCSApi.StartBattle
Encounter 改由地图或关卡流程在运行时选出后,启动职责也随之交给项目代码,场景仍然保留 GameCardManager,但要关闭 Auto Start On Play,并在流程协调器中保存当前的 activeEncounter
private GameEncounter activeEncounter;
public void StartEncounter(GameEncounter encounter)
{
if (encounter == null || activeEncounter != null || !GCSApi.IsReady) return;
activeEncounter = encounter;
GCSApi.StartBattle(encounter);
}
GCSApi.IsReady 仅表示场景中已存在可用的 Manager。当同一个 Manager 再次接收到 StartBattle 指令时,会直接清理当前状态机并创建新战斗。在此过程中,旧战斗不会先触发 OnBattleEnded 事件,因此,地图流程如果需要保存自己的 activeEncounter,只要其不为空,就必须拒绝再次启动。不能仅通过检查 GCSApi.IsBattleActive 来做判断。因为在战斗胜利后的奖励等待阶段,战斗推进虽然已经停止,但由于 OnBattleEnded 尚未把完整结果交回项目流程,此时战斗实际上还未完全结束。所以,activeEncounter 应当在收到 OnBattleEnded 广播时再进行释放,而地图画面也应当在这个交接点(数据交接完毕后)进行恢复
两种入口在 Manager 中汇合
Bootstrap 和 GCSApi 都会把选出的 GameEncounter 交给 GameCardManager.StartBattle,从这一调用开始,地图、HUD、结果界面与存档系统只需要监听 GCS 生命周期事件,不必识别战斗由哪个入口启动
| 事件 | 参数 | 发出时机 |
|---|---|---|
OnBattleStarted | 无 | 玩家、敌人、初始牌库和起手手牌已经创建,TurnNumber 为 1,随后继续解析敌人意图、处理战斗开始状态并进入首个玩家回合 |
OnBattleRewardOffered | BattleRewardOfferedEventArgs | 胜利已经确定并生成奖励候选,战斗等待玩家选择或跳过 |
OnBattleRewardSelected | BattleRewardSelectedEventArgs | 玩家已经确认一项奖励并将结果应用到 Run |
OnBattleRewardSkipped | 无 | 玩家已经跳过本场奖励 |
OnBattleEnded | BattleEndedEventArgs | 失败时在战斗结束状态处理后发出,胜利时在奖励被选择或跳过后发出,Won 给出最终结果 |
在实际对接和实现上述生命周期事件时,为了保证流程的正确性与内存安全,请严格遵守以下开发规范与调用细节:
-
OnBattleStarted会在StartBattle内同步发出,需要接收本场启动通知的对象必须在调用前订阅 -
GCSApi.OnBattleStarted、GCSApi.OnBattleEnded与通用GCSApi.Subscribe都会返回IDisposable,监听对象禁用时应当释放对应句柄 -
GCS 会在
OnBattleStarted之前为每张起手牌发出OnCardDrawn,需要播放起手抽牌动画或逐张处理起手牌的对象,也必须在调用StartBattle前完成订阅 -
OnBattleRewardOffered只会发布候选并暂停胜利流程,奖励 UI 或项目逻辑必须调用GCSApi.ApplyReward(choice, pickedCard)或GCSApi.SkipReward(),否则OnBattleEnded不会发出本场胜利结果 -
奖励 UI 只能在
GCSApi.IsWaitingForReward为true时提交一次,首次点击后应当立即禁用选择与跳过按钮 -
RewardChoice.Card必须传入OnBattleRewardOffered候选中的有效卡牌,重复调用ApplyReward可能再次修改 Run 状态,但不会再次发出奖励选择或战斗结束事件 -
启动入口汇合以后,失败路径会直接结束,胜利路径则会停在奖励选择,只有应用或跳过奖励后,两条路径才会在
OnBattleEnded重新汇合
上图把两种启动入口收束到同一个 OnBattleEnded 返回点,项目代码只有收到该事件,才能确认本场流程已经结束并取得最终胜负结果,而Bootstrap 的 On Bootstrap 与 GES 列表不属于这套生命周期,它们只会在 StartBattle 返回后触发,地图和长期流程应以 GCS 生命周期事件为准,具体的阶段、卡牌、单位与 FlowGraph 的其余事件请详见事件指南和事件参考
用协调器连接地图与战斗
地图协调器在调用 StartBattle 前切换到战斗表现,收到 OnBattleEnded 后再恢复地图,而activeEncounter 会在启动时写入、结束时清空,用于阻止战斗结束前重复启动
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);
}
}
把协调器与 GameCardManager 放在切换期间始终保持激活的对象上,MapRoot 与 BattleRoot 作为协调器控制的子对象,StartMapEncounter 会先激活 BattleRoot,再调用 StartBattle,依赖 OnEnable 完成事件订阅的 HUD 与表现组件才能接收同步发出的启动和起手抽牌事件
如果胜负演出需要继续占用战斗画面,可以在 HandleBattleEnded 中等待演出完成后再切换根对象,BattleEndedEventArgs 仍然负责把结果传给地图进度与存档系统
表现层与战斗状态的连接方式具体请见自定义 UI 集成
多场 Run 先初始化 Master Deck
地图恢复后如果还要启动下一场,activeEncounter 会在本场结束时清空,Run 层级的 Master Deck 则必须在战斗之间继续保留。若Master Deck 为空时,GCS 会用当前 Encounter 玩家单位的 StartingDeck 临时构建本场牌堆,但不会把这份回退结果写回 Master Deck,而如果第一场胜利后只加入一张奖励卡,则Master Deck 中就只会有这张奖励,下一场也不会再次回退到起始卡组,因此,在开始一局新 Run 时,请先清空上一局的持久状态,再把起始卡组逐张写入 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);
}
}
Master Deck 保存卡牌 GUID,进入战斗时,GCS 只会从当前 Encounter 的玩家 StartingDeck 与 RewardDeck 中解析这些 GUID,无法在这两个卡组中找到的条目会被跳过,因此,Run 中每个 Encounter 的 StartingDeck 或 RewardDeck 都要包含 Master Deck 可能出现的所有卡牌资产
GCSApi.ClearMasterDeck 会一起清空旧 Master Deck 与 Run 能量加成,只在开始新 Run 时调用
后续遭遇战之间不要清空 Master Deck,它会保存奖励、升级、移除与费用变化后的卡组结果,进入战斗时仍受上述卡牌解析范围限制
需要继续管理 Run 卡组时,手牌与牌堆和 GCSApi 指南提供了完整接口
提前退出时主动中止战斗
正常战斗都会以 OnBattleEnded 完成交接,而当玩家退出尚未结束的战斗时,系统会调用 GCSApi.DisposeBattle() 清理当前战斗状态,该接口不会发出 OnBattleEnded,也不会清空 Master Deck 或 Run 能量加成,此时地图恢复、失败判定与存档处理需由项目流程完成
不要用新的 StartBattle 代替中止处理,它虽然会清理旧状态机,却不会补发旧战斗的结束事件
建议先由项目流程完成地图恢复、结果记录与 activeEncounter 清理,再启动下一场,这样地图状态与战斗状态才不会脱节
进入 Play Mode 前检查
- 场景中只有一个处于启用状态的
GameCardManager,GCSApi.IsReady返回true - 固定场景已选择
Default Encounter并开启Auto Start On Play - 动态流程已关闭
Auto Start On Play,由项目代码持有activeEncounter并调用GCSApi.StartBattle - 需要接收
OnBattleStarted或起手OnCardDrawn的对象已在启动前订阅,并在对象禁用时释放所有订阅句柄 - 多场 Run 在第一场战斗前初始化 Master Deck,每个 Encounter 的
StartingDeck或RewardDeck都能解析其中的卡牌 - 胜利路径只提交一次
GCSApi.ApplyReward或GCSApi.SkipReward,并在首次点击后禁用奖励按钮,再等待OnBattleEnded - 中止路径会主动恢复地图、记录结果并清理
activeEncounter
完成启动入口、生命周期与 Run 状态的连接后,进入战斗场景设置,把 Manager、战场、手牌、HUD 与表现对象组装成可运行的战斗场景