跳到主要内容

自定义 UI 接入

Guide

订阅战斗事件,通过 GCSApi 读取当前状态,再把出牌、回合、选择和奖励操作提交回运行时

上图展示了自定义 UI 与 GCS 之间唯一需要维持的双向关系,运行时用事件通知变化,UI 收到通知后重新读取当前状态,玩家操作再通过 GCSApi 提交回同一场战斗,界面本身不保存另一份权威战斗数据

代码片段只包含与 GCS 交互的组件部分,将它们放入项目的 presenter 或 MonoBehaviour,并按实际使用的类型添加 using System;using System.Collections.Generic;using TinyGiants.GCS.Runtime;

接入检查清单

  • 放置并配置一个 GameCardManager

  • GCSApi.StartBattle 启动战斗

  • 在 UI 生命周期方法中订阅运行时事件

  • 每次事件触发后,从 GCSApi 刷新 UI

  • 卡牌按钮使用 GCSApi.CanPlayCardGCSApi.TryPlayCard

  • 结束回合按钮使用 GCSApi.EndPlayerTurn

  • 奖励界面使用 GCSApi.ApplyRewardGCSApi.SkipReward

  • 释放每一个订阅

  • 让 Demo 专用 View 组件保持可替换

订阅生命周期

每个 UI 对象用一个小的 disposable 列表管理订阅

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();
}
提示

GameObject 销毁时应释放它持有的订阅,否则陈旧监听器会在重新加载后叠加,造成 UI 重复刷新

战斗 HUD

高层 HUD 字段从状态刷新:

Widget读取
Phase labelGCSApi.Phase
Turn labelGCSApi.TurnNumber
EnergyGCSApi.PlayerEnergyGCSApi.MaxEnergy
End turn buttonGCSApi.Phase == BattlePhase.PlayerPhase
Battle result overlayOnBattleEndedBattleEndedEventArgs.Won

阶段应以 GCSApi.Phase 和阶段事件为准,因为行为图可以结束回合、给予额外回合、打开选择,甚至在没有 UI 输入的情况下直接结束战斗

HUD 和游戏对不上时

打开 Game Card Monitor:

Tools > TinyGiants > GCS > Game Card Monitor

查看 Battle 标签页,如果 Monitor 中的战斗状态正确,而 HUD 显示错误,应检查 HUD 的事件订阅和刷新路径

运行中战斗里 Game Card Monitor 的 Battle 标签页,显示自定义 HUD 应当呈现的同一份阶段、回合、玩家和敌人状态

手牌 UI

手牌可以从 GCSApi.Hand() 完整重建,也可以按实例 ID 局部更新,该方法返回调用时刻的实时手牌堆,本回合新抽的卡也包含在内

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);
}
}

点击处理:

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

GCSApi.TryPlayCard(card);
}

目标选择完成后:

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

单位面板

单位面板从运行时单位构建

面板值读取
HPGCSApi.Hp(unit)GCSApi.MaxHp(unit)
护甲GCSApi.Armor(unit)
存活/死亡GCSApi.IsAlive(unit)
HP 百分比GCSApi.HpPercent(unit)
状态GCSApi.StatusesOn(unit)
敌人意图GCSApi.PendingIntents(enemy)

单位面板可以由以下事件触发刷新:

  • OnUnitHpChanged

  • OnUnitDied

  • OnArmorGained

  • OnArmorLost

  • OnStatusChanged

  • OnStatusTicked

  • OnEnemyUnitIntentChanged

  • OnEnemyPhaseChanged

状态控件

状态 UI 应读取完整的当前状态映射,而不是只用事件 payload 里的值,用 StatusChangedEventArgs.UnitId 定位是哪个单位变了,然后重新读取它身上的全部状态

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);
}
}

敌人意图

意图 UI 可以只显示当前顶部意图,也可以显示完整的待执行列表

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

请在 OnEnemyUnitIntentChanged 触发时刷新,敌人阶段变化后也要刷新

选择 UI

Choice 节点要求玩家选择卡牌、单位或状态时,GCS 触发 OnEffectChoiceOffered

Payload 字段用途
PromptText选择面板的标题文案
Candidates要渲染的选项
AllowSkip是否显示跳过按钮

选择面板需要实现 IChoicePresenter,打开选择时,运行时会传入提示文本、候选项、minPicksmaxPicksonPickedonSkipped,面板根据选择数量限制启用或禁用确认按钮,并通过对应回调返回原始候选对象,场景中没有 IChoicePresenter 或候选列表为空时,Choice 节点会按跳过处理,玩家选定后 GCS 会先保存选择结果,再通过 OnEffectChoiceSelected 提供全部已选索引与候选对象,显式或自动跳过都会触发 OnEffectChoiceSkipped

提示

GCSApi.IsWaitingForChoicetrue 期间,应禁用普通卡牌按钮

奖励 UI

战斗胜利后,GCS 可能触发 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 还有 EnergyCost 两个选项,选它们时卡牌参数传 null,应用或跳过都会结算奖励环节并触发 OnBattleEnded,用 GCSApi.IsWaitingForReward 判断奖励控件是否应该可见

飘字与表现层

自定义飘字订阅 FloatingTextSystem.Requested,处理函数会收到一个 FloatingTextRequest

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

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

内置 UnitView 会按 UnitId 注册自身,使 FlowGraph 表现节点能够找到目标视图,自定义单位视觉则把自己的 View Model 绑定到事件 Payload 中的 Unit ID,Demo 的 HandControllerCardView 和奖励 UI 类只服务于示例场景,正式项目应绑定稳定的运行时事件与 ID

刷新策略

策略适用场景
完整重建手牌、奖励选项、小型状态列表
按 ID 局部更新单位面板、意图控件、飘字目标
防抖刷新大型自定义战斗日志或分析面板
立即动画加刷新伤害、治疗、护甲和卡牌移动

可见战斗 UI 通常可以直接从 GCSApi 完整刷新,只有性能分析确认这里形成实际瓶颈后,再改为局部更新或防抖

常见错误

错误更好的做法
保留一份本地手牌副本并手动修改在卡牌事件后从 GCSApi.Hand() 重建
只根据能量启用卡牌按钮使用 GCSApi.CanPlayCard(card)
忘记取消订阅保存并释放 IDisposable 句柄
把事件 Payload 当成完整状态用 Payload 定位变化对象,再读取当前状态
正式 UI 依赖 Demo UI 类依赖 GCSApi、事件和项目自己的 View 类
因为 VFX 或 SFX 资源缺失而阻塞玩法让玩法正常结算,缺少资源时跳过对应表现