跳到主要内容

战斗启动

Guide

每次进入场景都运行同一场遭遇战时使用 Bootstrap,由地图、任务或关卡流程动态选择遭遇战时调用 GCSApi.StartBattle,两种入口共享同一套战斗生命周期

先确定 Encounter 从哪里来

每次进入 Play Mode 都要启动同一场战斗时,请把遭遇战保存在 GameCardEncounterBootstrapDefault Encounter 中,Demo 场景、独立战斗测试以及按遭遇战单独加载的场景都适合这样启动

固定场景交给 Bootstrap

GameCardEncounterBootstrapDefault Encounter 中选择要运行的遭遇战,保持 Auto Start On Play 开启,Inspector 顶部会显示 Ready — starts on Play

Game Card Encounter Bootstrap 中已选择 Fenmoss Hollow,状态为 Ready — starts on Play,并开启 Auto Start 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,随后继续解析敌人意图、处理战斗开始状态并进入首个玩家回合
OnBattleRewardOfferedBattleRewardOfferedEventArgs胜利已经确定并生成奖励候选,战斗等待玩家选择或跳过
OnBattleRewardSelectedBattleRewardSelectedEventArgs玩家已经确认一项奖励并将结果应用到 Run
OnBattleRewardSkipped玩家已经跳过本场奖励
OnBattleEndedBattleEndedEventArgs失败时在战斗结束状态处理后发出,胜利时在奖励被选择或跳过后发出,Won 给出最终结果

在实际对接和实现上述生命周期事件时,为了保证流程的正确性与内存安全,请严格遵守以下开发规范与调用细节

  • OnBattleStarted 会在 StartBattle 内同步发出,需要接收本场启动通知的对象必须在调用前订阅

  • GCSApi.OnBattleStartedGCSApi.OnBattleEnded 与通用 GCSApi.Subscribe 都会返回 IDisposable,监听对象禁用时应当释放对应句柄

  • GCS 会在 OnBattleStarted 之前为每张起手牌发出 OnCardDrawn,需要播放起手抽牌动画或逐张处理起手牌的对象,也必须在调用 StartBattle 前完成订阅

  • OnBattleRewardOffered 只会发布候选并暂停胜利流程,奖励 UI 或项目逻辑必须调用 GCSApi.ApplyReward(choice, pickedCard)GCSApi.SkipReward(),否则 OnBattleEnded 不会发出本场胜利结果

  • 奖励 UI 只能在 GCSApi.IsWaitingForRewardtrue 时提交一次,首次点击后应当立即禁用选择与跳过按钮

  • 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 放在切换期间始终保持激活的对象上,MapRootBattleRoot 作为协调器控制的子对象,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 的玩家 StartingDeckRewardDeck 中解析这些 GUID,无法在这两个卡组中找到的条目会被跳过,因此,Run 中每个 Encounter 的 StartingDeckRewardDeck 都要包含 Master Deck 可能出现的所有卡牌资产

提示

GCSApi.ClearMasterDeck 会一起清空旧 Master Deck 与 Run 能量加成,只在开始新 Run 时调用

后续遭遇战之间不要清空 Master Deck,它会保存奖励、升级、移除与费用变化后的卡组结果,进入战斗时仍受上述卡牌解析范围限制

需要继续管理 Run 卡组时,手牌与牌堆GCSApi 指南提供了完整接口

提前退出时主动中止战斗

正常战斗都会以 OnBattleEnded 完成交接,而当玩家退出尚未结束的战斗时,系统会调用 GCSApi.DisposeBattle() 清理当前战斗状态,该接口不会发出 OnBattleEnded,也不会清空 Master Deck 或 Run 能量加成,此时地图恢复、失败判定与存档处理需由项目流程完成

提示

不要用新的 StartBattle 代替中止处理,它虽然会清理旧状态机,却不会补发旧战斗的结束事件

建议先由项目流程完成地图恢复、结果记录与 activeEncounter 清理,再启动下一场,这样地图状态与战斗状态才不会脱节

进入 Play Mode 前检查

  1. 场景中只有一个处于启用状态的 GameCardManagerGCSApi.IsReady 返回 true
  2. 固定场景已选择 Default Encounter 并开启 Auto Start On Play
  3. 动态流程已关闭 Auto Start On Play,由项目代码持有 activeEncounter 并调用 GCSApi.StartBattle
  4. 需要接收 OnBattleStarted 或起手 OnCardDrawn 的对象已在启动前订阅,并在对象禁用时释放所有订阅句柄
  5. 多场 Run 在第一场战斗前初始化 Master Deck,每个 Encounter 的 StartingDeckRewardDeck 都能解析其中的卡牌
  6. 胜利路径只提交一次 GCSApi.ApplyRewardGCSApi.SkipReward,并在首次点击后禁用奖励按钮,再等待 OnBattleEnded
  7. 中止路径会主动恢复地图、记录结果并清理 activeEncounter

完成启动入口、生命周期与 Run 状态的连接后,进入战斗场景设置,把 Manager、战场、手牌、HUD 与表现对象组装成可运行的战斗场景