API 使用指南
一个覆盖战斗运行时的静态门面:GCSApi 负责启动战斗、打出卡牌、读取实时状态、安全修改状态,并提供事件订阅
调用前先做保护
场景中存在 GameCardManager 后,GCSApi.IsReady 为 true,大多数调用在没有 manager 时也是安全的:它们返回空列表、false、0 或 null,不会抛异常,即便如此,UI 代码仍应保护那些只在活动战斗中才有意义的路径
if (!GCSApi.IsReady)
{
return;
}
if (!GCSApi.IsBattleActive)
{
GCSApi.StartBattle(encounter);
}
启动战斗
StartBattle 从一个 GameEncounter 资产创建实时战斗,已有战斗会先被销毁,起手手牌在调用返回前就已抽好,OnBattleStarted 也已触发,第一回合的敌人意图同样已解析完毕
public GameEncounter Encounter;
public void Begin()
{
GCSApi.StartBattle(Encounter);
}
StartBattle 返回后,起始手牌、玩家能量、敌人阵容和第一组 Intent 都已经可供表现层读取:

战斗建立后,使用事件驱动 UI,可以在状态变化时精确刷新而无需持续轮询:
private IDisposable _battleStarted;
void OnEnable()
{
_battleStarted = GCSApi.OnBattleStarted(RefreshAll);
}
void OnDisable()
{
_battleStarted?.Dispose();
}
打出卡牌
读取手牌,检查可打出性,然后调用 TryPlayCard
public void TryPlayFirstCard(UnitState target)
{
foreach (var card in GCSApi.Hand())
{
if (!GCSApi.CanPlayCard(card)) continue;
GCSApi.TryPlayCard(card, target);
return;
}
}
不是每张卡都需要玩家选目标,RequiresTarget 只对"指定单个敌方单位"的卡返回 true;对自身、全体敌人和无目标的卡,引擎会自动解析目标,直接打出即可,不需要弹出选择器
var activeCard = GCSApi.GetActiveCard(card);
if (GCSApi.RequiresTarget(activeCard))
{
ShowTargetPicker(card);
}
else
{
GCSApi.TryPlayCard(card);
}
出牌被拒绝时 TryPlayCard 返回 false:没有活动战斗、卡不在手牌里、能量不够、卡带有 Unplayable 关键字,或某个出牌 Hook 取消了这次操作
结束玩家回合
public void EndTurnButton()
{
if (GCSApi.Phase == BattlePhase.PlayerPhase)
{
GCSApi.EndPlayerTurn();
}
}
EndPlayerTurn 会推进到敌人阶段,在 PlayerPhase 之外不做任何事,所以残留按钮上的误点不会打乱回合顺序
为 UI 读取状态
常见的 UI 读取都有命名清晰的 helper:
turnLabel.text = GCSApi.TurnNumber.ToString();
energyLabel.text = $"{GCSApi.PlayerEnergy}/{GCSApi.MaxEnergy}";
var player = GCSApi.Player();
hpLabel.text = $"{GCSApi.Hp(player)}/{GCSApi.MaxHp(player)}";
foreach (var enemy in GCSApi.AliveEnemies())
{
RenderEnemy(enemy, GCSApi.CurrentIntentTag(enemy), GCSApi.CurrentIntentValue(enemy));
}
事件触发后,从 API 的当前状态刷新,缓存副本可能已经过期,运行时状态才是 UI 应呈现的事实来源
安全修改战斗状态
外部系统通过 GCSApi helper 修改状态,这些调用会进入运行时 controller,所以 Hook 照常执行,事件照常触发
var player = GCSApi.Player();
GCSApi.GainArmor(player, 5);
GCSApi.DrawCards(1);
GCSApi.ApplyStatus(player, shieldedStatus, 2, player);
直接写入 UnitState.CurrentHp、UnitState.Statuses 或各个牌堆列表会跳过 Hook 和事件,状态反应与 UI 刷新都不会收到这次变化,请使用 GCSApi 修改方法或 controller
胜负在阶段转换和出牌之后判定,不在每次 HP 写入时判定,用修改 helper 把敌人打到 0 HP,会在下一次判定点生效,不会当场结束战斗
订阅事件
内置事件有快捷方法,全部返回 IDisposable handle:
private readonly List<IDisposable> _subs = new();
void OnEnable()
{
_subs.Add(GCSApi.OnEnergyChanged(_ => RefreshEnergy()));
_subs.Add(GCSApi.OnUnitHpChanged(_ => RefreshUnits()));
_subs.Add(GCSApi.OnCardDrawn(_ => RefreshHand()));
}
void OnDisable()
{
foreach (var sub in _subs) sub.Dispose();
_subs.Clear();
}
自定义事件名,或想直接操作 channel 时,用 Subscribe<TArgs>:
_subs.Add(GCSApi.Subscribe<DamageEventArgs>(
GCSEventNames.OnDamageDealt,
args => LogDamage(args.SourceUnitId, args.TargetUnitId, args.Amount)));
无参数事件用 Subscribe(string, Action),所有内置事件名都是 GCSEventNames 上的常量,payload 类型在 GCSEventArgs 中,经 RaiseGameEvent 触发的自定义事件,payload 以 IReadOnlyDictionary<string, object> 送达,订阅时 TArgs 就用它
内容发现
GCSApi 可以枚举当前活动数据库和其中的内容实体
foreach (var card in GCSApi.Cards())
{
AddCardToBrowser(card);
}
var poison = GCSApi.FindStatus(poisonGuid);
枚举方法每次构建新列表,可能产生分配,每帧都刷新的菜单应缓存结果
维护 master deck
master deck 是 run 层级的牌组,后续战斗都从它构建,StartBattle 会用它填充抽牌堆
GCSApi.ClearMasterDeck();
GCSApi.AddCardToMasterDeck(strike);
GCSApi.AddCardToMasterDeck(defend, isUpgraded: true);
GCSApi.StartBattle(encounter);
修改 master deck 不会改写已经开始的战斗里的运行时牌堆
扩展注册表
做工具或调试面板时,四个反射注册表都有对应 helper:
var nodes = GCSApi.NodeTypes();
var cardTypes = GCSApi.CardTypes();
var tags = GCSApi.CardTags();
var tokens = GCSApi.DescriptionTokens();
结果包含从所有引用了 TinyGiants.GCS.Runtime 的已加载程序集中发现的自定义节点、卡牌类型、标签和描述 token
完整成员签名和返回类型见 API 参考