表现层
运行时状态如何到达 UI、单位视图、VFX、音频和浮动文本,以及为什么玩法从不等待缺失的视觉资产

表现边界
| 区域 | 拥有 | 不拥有 |
|---|---|---|
| Runtime | HP、护甲、能量、牌堆、状态、阶段、事件 | 像素布局、声音路由、粒子质量 |
| UI | 手牌 widget、单位血条、状态图标、意图 widget、选择/奖励面板 | 战斗规则或出牌合法性 |
| Unit view | 单位 sprite、程序化运动、受击/治疗/死亡反应、状态视觉 | 伤害计算或状态层数 |
| FX nodes | VFX、SFX、浮动文本、闪烁、滑动、震屏、动画的请求 | 最终资产打磨、渲染管线兼容、mixer 设置 |
即使视觉效果播放失败,玩法也照常结算
事件驱动的 UI
反应式循环始终是同一套:controller 修改状态并触发事件,你的 handler 被唤醒,通过 GCSApi 读取最新状态,再更新 widget
自定义 UI 应该订阅运行时事件,然后读取当前状态:
private IDisposable _energySub;
void OnEnable()
{
_energySub = GCSApi.OnEnergyChanged(_ => RefreshEnergy());
RefreshEnergy();
}
void OnDisable()
{
_energySub?.Dispose();
}
void RefreshEnergy()
{
energyLabel.text = $"{GCSApi.PlayerEnergy}/{GCSApi.MaxEnergy}";
}
事件告诉你什么时候该刷新,GCSApi 告诉你当前值是什么
核心 UI 刷新事件
| UI 区域 | 监听事件 | 读取状态 |
|---|---|---|
| 回合与阶段标签 | OnTurnStarted、OnTurnEnded、OnEnemyTurnStarted、OnBattleEnded | GCSApi.Phase、GCSApi.TurnNumber、GCSApi.IsPlayerTurn |
| 能量 | OnEnergyChanged、OnEnergyGained、OnEnergySpent | GCSApi.PlayerEnergy、GCSApi.MaxEnergy |
| 手牌 | 卡牌生命周期事件、OnCardModified、OnEnergyChanged | GCSApi.Hand()、GCSApi.CanPlayCard、GCSApi.GetEffectiveEnergyCost |
| 单位条 | OnUnitHpChanged、OnArmorGained、OnArmorLost、OnUnitDied | GCSApi.Hp、GCSApi.MaxHp、GCSApi.Armor、GCSApi.IsAlive |
| 状态 widget | OnStatusChanged、OnStatusTicked、状态 transition 事件 | GCSApi.StatusesOn(unit) |
| 敌人意图 | OnEnemyUnitIntentChanged、OnEnemyPhaseChanged | GCSApi.PendingIntents、GCSApi.CurrentIntentTag、GCSApi.CurrentIntentValue |
| 选择 UI | OnEffectChoiceOffered、OnEffectChoiceSelected、OnEffectChoiceSkipped | Choice payload 加 GCSApi.IsWaitingForChoice |
| 奖励 UI | OnBattleRewardOffered、OnBattleRewardSelected、OnBattleRewardSkipped | Reward payload 加 GCSApi.IsWaitingForReward |
UnitView 行为
内置的 UnitView 绑定到一个 UnitState,并订阅:
OnUnitHpChangedOnUnitDiedOnStatusChangedOnStatusTicked
它用这些事件播放受击、治疗、死亡和状态动画,同时按 UnitId 注册自身,让表现节点能找到目标单位的 view
自定义项目可以复用 UnitView、派生自己的 view 行为,或整个替换掉它,稳定的集成点是运行时事件 payload 加 UnitId
浮动文本
FloatingTextSystem 暴露一个表现请求事件:
| 请求字段 | 含义 |
|---|---|
Target | 接收文本的单位 |
Kind | Damage、Heal、Armor、Block 或 Custom |
Content | Amount 或 Text |
Amount | 要显示的数字 |
Text | 自定义标签 |
Color, UseCustomColor | 可选的颜色覆盖 |
IncludeSign | 是否显示正负号 |
ScaleMultiplier, DurationMultiplier | 单次请求的视觉微调 |
Offset | 目标附近的位置偏移 |
FlowGraph 的 Show Floating Text 节点会发出这些请求,自定义浮动文本 presenter 可以订阅 FloatingTextSystem.Requested
FlowGraph 表现节点
| 节点 | 运行时效果 |
|---|---|
| Play VFX | 在单位或位置上实例化或请求 VFX prefab |
| Play Animation | 向目标单位的表现发送 animation trigger |
| Play SFX | 播放一次性的 audio clip |
| Flash Unit | 目标 view 存在时调用 UnitView.Flash |
| Slide Unit | 短暂移动目标的表现 |
| Show Floating Text | 发送 FloatingTextRequest |
| Shake Camera | 请求镜头震动 |
在玩法状态改变之后再用这些节点,让视觉反映真实结果
动画 Gate 与等待
状态机可以推迟转换,等表现类等待完成
| 功能 | 用途 |
|---|---|
| Animation gate | 在 gated coroutine 或动画运行期间扣住阶段转换 |
| Flow wait | 让图按秒或按 animation id 延迟后续执行 |
| Enemy gaps | 在敌人行动之间加入可读的间隔 |
Game Card Monitor 的 Gate 页签显示当前谁持有 animation gate、acquire/release 日志和超时时间,用于定位卡住的转换:

这也是自定义 UI 应该反应式处理状态变化的原因,一次出牌可能引发状态变化、动画、等待、选择和后续转换
VFX、音频与渲染管线范围
表现的图契约是稳定的,最终资产则是另一回事:
- VFX prefab 可以之后替换或升级
- SFX clip 和 mixer 路由可以之后配置
- 渲染管线相关内容(材质、shader、后处理)属于资产层,不属于图
玩法图只请求需要的反馈,具体渲染由资产和表现层决定,这样切换渲染管线时不需要修改玩法图
失败行为
| 缺失项 | 预期结果 |
|---|---|
| 缺少 unit view | 玩法照常结算;针对该单位的视觉请求可能被跳过 |
| 缺少 VFX prefab | 玩法照常结算;没有粒子出现 |
| 缺少 SFX clip | 玩法照常结算;没有声音 |
| 缺少浮动文本 presenter | 玩法照常结算;请求没有可见的订阅者 |
| Animation wait 未安排 | 执行立即继续 |
运行时日志和 Monitor 用于诊断表现缺失,可选表现资产没有配置时,战斗仍应继续结算