跳到主要内容

运行时架构

Guide

GCS 运行时的分层结构:manager、状态机、状态视图、controller、流图执行、事件与表现层

上图呈现了 GCS 从项目请求到可见战斗反馈的完整运行链路, 项目代码和自定义 UI 从 GCSApi 提交操作,GameCardManager 将请求交给 BattleStateMachine, 状态机在同一份 BattleStateView 中保存战斗事实,并让 GameCardFlowExecutor 通过执行上下文结算卡牌、状态与敌人行为, 运行时事件和表现请求再把结算结果交给 Core 表现组件、Demo UI 或项目自己的集成层, 这条链路由此划定了规则与状态的所有权,也确定了外部代码读取状态、提交操作和监听变化的入口

从 Encounter 进入第一个玩家回合

这条链路由 GCSApi.StartBattle(encounter) 启动,但真正保存场景配置和运行数据的是 GameCardManagerGCSApi 只负责提供统一、稳定的静态入口

环节接收的内容交付的结果
GCSApi项目选择的 GameEncounter在场景中存在 GameCardManager 时,把启动请求交给当前 Manager
GameCardManagerEncounter、已启用的 Database、Run Master Deck 与 Run Energy Bonus释放上一场战斗的状态机,创建新的 BattleStateMachine,绑定场景中的首个 IChoicePresenter,并注入首个已启用的 Status、Card 与 Enemy Database
BattleStateMachineEncounter 与 Manager 提供的运行数据创建 BattleStateView,再生成玩家、敌人和卡牌的本场实例
初始化流程玩家 Starting Deck 或 Run Master Deck、Encounter Rules 与参战单位洗牌、优先安排 Innate、抽取起手牌,并把 TurnNumber 设为 1
启动事件与行为已经建立的初始状态同步发布 OnBattleStarted,解析敌人 Intent,执行 OnBattleStart 状态行为,再尝试进入 PlayerTurnStart
Manager 更新循环因 Gate 或 Choice 暂停的状态机推进每帧调用状态机 Tick(),当 Gate 清空且 Choice 结束时,继续待处理的阶段转换或敌人步骤

场景中没有 GameCardManager 时,GCSApi.IsReadyfalse,大多数静态入口只会返回空值、默认值或不执行操作,因此启动战斗前应先在场景中配置 GameCardManager,并启用战斗所需的 Database

OnBattleStarted 会在 Phase 仍为 Initialize 时同步通知订阅者,此时首组敌人 Intent 还没有完成解析

需要 Intent 的 UI 应等待 OnEnemyUnitIntentChanged,完整启动顺序见战斗阶段与执行顺序,场景入口见战斗启动

初始化完成后,状态机会根据 Encounter 为本场战斗创建独立的单位、卡牌和牌堆状态,后续变化统一保存在当前 BattleStateView 中,不会回写 Encounter 的创作定义

同一份状态,通过两种权限访问

BattleStateView 是当前战斗的权威状态对象,它同时实现 IBattleStateIBattleController,而 IBattleController 继承 IBattleState

GCSApi.BattleGCSApi.Controller 返回的不是两份状态,而是同一个 BattleStateView 的两种接口视角

入口适用范围使用方式
GCSApi 查询方法阶段、回合、能量、单位、牌堆、状态、Intent 与等待状态项目代码优先使用语义明确的查询方法,例如 Hand()Player()AliveEnemies()Hp()StatusesOn()
GCSApi.BattleGCSApi 没有直接封装的只读战斗信息通过 IBattleState 读取回合、当前行动方、能量、玩家、存活敌人、四个牌堆、本回合已打出或已抽取的卡牌,以及有效费用
GCSApi 修改方法伤害、治疗、护甲、能量、状态、卡牌、奖励与回合操作外部系统优先使用命名明确的公开方法,把修改交回运行时
GCSApi.Controller公开方法没有覆盖的高级运行时操作通过 IBattleController 调用受支持的修改方法,由运行时执行相应的约束检查、Hook 与通知流程

IBattleState 限制的是接口能力,并不会把其中的 UnitStateCardInstance 或集合复制成不可变快照,外部代码仍然必须把这些实时对象视为只读数据

注意

不要直接赋值 UnitState.CurrentHp,也不要修改 CardPile.CardsUnitState.Statuses,这些写法即使能够编译,也会绕过相应的规则入口、状态检查或通知,运行时修改必须通过 GCSApi 或受支持的 IBattleController 方法完成

Play Mode 下打开 Game Card MonitorBattle 页签,可以直接核对这份共享状态中的 Encounter、Phase、Turn、玩家快照与敌人列表

Game Card Monitor 的 Battle 页签,显示当前 Encounter、阶段、回合、玩家状态与敌人列表

Battle 页签呈现的正是 GCSApi 与 FlowGraph 同时读取的当前事实

创作资产与运行实例的对应关系见运行时数据类型,手牌、抽牌堆、弃牌堆、消耗牌堆与 Run Master Deck 的生命周期见卡牌区域与牌堆

BattleStateView 建立后,卡牌、状态和敌人不必各自实现一套战斗修改逻辑,它们都会把行为交给共享的 FlowGraph 执行层

FlowGraph 在执行上下文中结算行为

BattleStateMachine 持有一个 GameCardFlowExecutor,运行敌人行为时也会复用这个执行器,因此卡牌触发、状态触发和敌人 Pattern 会在同一套节点语义、目标解析与控制接口上完成结算

每次图执行都会使用一份上下文,其中包含行为归属、目标、卡牌、状态与当前战斗,纯计算节点通过 IEvaluationContext 读取这些内容,修改型节点则通过 IExecutionContext 获得 Controller

执行层对象提供的能力
IEvaluationContext提供 SourceHostTargetAttacker、当前 CardStatus,并读取 Battle、变量、端口输入和前序节点输出
内容解析方法通过 ResolveStatusIdResolveCardIdResolveEnemyUnitId 查询内容,当前战斗使用各类型首个已启用的 Database
IExecutionContext在读取能力之上提供 Controller,用于生命、护甲、能量、状态、卡牌、单位与流程修改
GameCardFlowExecutor遍历图连接,协调控制分支、Choice、Wait 与后续执行

SourceNodeOperatorNodeSelectorNode 只通过 IEvaluationContext 读取和计算,MutatorNodeVfxNodeControlNodePatternNode 才通过 IExecutionContext 执行修改或控制流程

以一次玩家出牌为例,自定义 UI 可以先用 GCSApi.CanPlayCard(card) 初步判断按钮是否可用,再把玩家选择交给 GCSApi.TryPlayCard(card, target)

TryPlayCard 会先确认当前处于 PlayerPhase、玩家单位存在、卡牌仍在手牌中且有效卡牌定义存在,再计算有效费用并检查能量,费用计算本身可能运行一次 OnPlayCard 费用 Hook

能量足够后,TryPlayCard 还会再次运行 OnPlayCard 取消检查,再依次检查 UnplayableOnCardPlayable

请求被接受后,状态机才会扣除能量、发布 OnCardPlayed、执行卡牌的 On Play FlowGraph,并把卡牌移出手牌

CanPlayCard 会计算有效费用,因此同样可能运行用于费用计算的 OnPlayCard Hook,但它不检查手牌归属,也不会执行后续的 OnPlayCard 取消检查与 OnCardPlayable

传入 TryPlayCard 的目标也不会由该方法校验阵营或存活状态,因此界面必须按卡牌 TargetMode 约束目标,并以 TryPlayCard 返回的 bool 判断本次出牌最终是否被接受

自定义节点应按节点职责选择相同的上下文边界,具体写法见执行上下文自定义节点

Game Card Monitor 的 Flow 页签会按时间倒序保留每次图执行的所有者、触发时机、节点数与耗时,从而可以用同一种格式追踪卡牌、状态和敌人行为

Game Card Monitor 的 Flow 页签,按时间倒序显示 FlowGraph 所有者、触发时机、节点数与耗时

Flow 记录能够确认行为的所有者、触发时机、执行规模与耗时,但记录出现并不等于状态机必须立刻切换阶段,Choice、Wait 与表现 Gate 仍可能暂停后续分支,或延后尚未完成的阶段转换

执行可以等待,推进权仍归状态机

等待机制只负责保护结算顺序,不会把阶段、胜负或实时状态的所有权交给 UI 和动画

机制暂停的范围恢复条件与边界
Choice当前 Flow 分支,并阻止待处理的阶段转换继续场景中存在 IChoicePresenter 且候选非空时等待回调,没有 Presenter 或没有候选时直接走 Skipped 分支
秒数 Wait当前 Flow 后续分支,并在等待期间持有 Gate使用不受 Time.timeScale 影响的实时秒数,到时释放 Gate 并继续
Animation Wait当前 Flow 后续分支等待同名 Gate holder 释放,命名等待默认 5 秒后停止等待并记录警告,但不会替持有者清除泄漏的 Gate
Animation Gate状态机待处理的阶段转换或敌人步骤表现组件必须成对调用 Acquire 与 Release,FX 节点本身不会自动等待动画、VFX 或 SFX 完成
Reward Wait已经进入 BattleWon 后的最终结束通知调用 GCSApi.ApplyRewardGCSApi.SkipReward 后,状态机才会发布胜利的 OnBattleEnded

Choice 不会自动拒绝所有公开输入,TryPlayCardEndPlayerTurn 也不会替自定义界面检查 IsWaitingForChoice,因此 Choice UI 出现时应覆盖并锁定无关交互,直到选择回调完成

需要让视觉动作严格排队时,应在图中显式连接 Wait,或让表现协程成对持有和释放 Animation Gate,精确阶段推进见战斗阶段与执行顺序,Gate、FX 与失败行为见表现层

这些等待只改变后续推进的时机,状态变化一旦生效,仍要同步给 UI 和项目系统,音频、VFX 等表现也要接收对应请求,这两类协作分别由运行时事件与表现请求完成

事件与表现层接收运行结果

GCSEvents.Channel 是独立于 Manager 的全局事件通道,BattleStateMachineBattleStateView 和其他运行流程都可以向它发布通知,事件发布时会同步调用订阅者,订阅方法会返回可释放的 IDisposable

事件负责告诉外部某项变化已经发生,界面再从 GCSApi 读取当前状态并刷新对应控件,这样同一次操作触发多个通知时,显示结果仍以权威状态为准

部分 FX 节点不会经过 GCSEvents,而是直接向 UnitPresentationSystemFloatingTextSystem 发出表现请求,或直接实例化 VFX、播放 SFX 与驱动镜头,这条表现链路不会改变规则所有权,也不会自动等待视觉播放完成

Core Runtime 提供 UnitView、单位表现路由、浮动文本请求、内置 FX 节点与 Animation Gate,Demo 中的 HandControllerBattleBoardCardRewardViewEnemyUnitIntentIndicatorTurnBanner 和场景 Presenter 则是可以检查、复用或替换的示例实现

Demo 中负责战斗启动、出牌、结束回合和奖励的组件会直接调用 GameCardManager,这是示例工程内部的实现方式,正式项目应从 GCSApi 接入,避免依赖 Manager 与状态机的具体结构

正式项目可以完全重做手牌、HUD、奖励界面和战斗布局,只要通过事件或表现请求接收结果,并把出牌、结束回合、Choice 与奖励决策重新提交给 GCSApi 或对应的公开接口,玩法规则就无需随 UI 一起重写

自定义 Choice UI 必须实现 IChoicePresenter 才能把选择结果回传给 Flow,仅监听 OnEffectChoiceOffered 无法完成节点回调

普通事件订阅、生命周期与刷新策略见事件使用指南自定义 UI 接入

GES 只是一条可选的双向桥接通道,不会取代核心 GCSEvents,需要让 GES 事件启动 FlowGraph 或从图中发布 GES 事件时,再接入 GES 集成

按集成目标选择稳定入口

沿着同一条运行链路接入时,项目代码只需要根据目标选择公开入口,不必依赖状态机和执行器的内部实现

集成目标稳定入口使用要点
检查场景是否可用GCSApi.IsReady只表示当前场景中存在 GameCardManager,Database 与具体战斗内容仍需单独配置
启动与控制战斗StartBattleTryPlayCardEndPlayerTurnApplyRewardSkipReward先提交用户意图,再根据返回值、事件和当前状态更新界面
读取战斗状态GCSApi 查询方法与 GCSApi.Battle无战斗时优先使用始终返回空集合的 Hand() 等方法,不要假设 CurrentHand 一定非空
修改运行状态GCSApi 修改方法,必要时使用 GCSApi.Controller把返回的状态对象视为只读,不直接改字段或集合
订阅运行事件类型化快捷订阅、GCSApi.Subscribe<TArgs>()GCSApi.Events在组件停用或销毁时释放订阅,事件后重新读取当前状态
浏览创作内容Cards()Decks()PlayerUnits()EnemyUnits()Statuses()Encounters()Find* 方法内容发现会遍历 Manager 当前启用的对应 Database
扩展 FlowGraph节点基类、IEvaluationContextIExecutionContextIBattleController使用上下文解析目标、内容与端口,不依赖 EffectActionContext 的具体实现
替换 Choice UIIChoicePresenter由 Presenter 调用选择或跳过回调,并在等待期间管理输入状态

这些入口的调用流程与代码示例见 API 使用指南,完整成员和事件参数见 API 参考事件参考

确定公开入口后,可以按问题所在的环节继续查阅对应的权威页面

需要深入的内容权威页面
创作定义如何变成单位、卡牌与战斗实例运行时数据类型
阶段、回合、出牌、敌人行动与奖励的精确顺序战斗阶段与执行顺序
手牌、抽牌堆、弃牌堆、消耗牌堆与 Run Master Deck卡牌区域与牌堆
状态层数、Hook、衰减、归因与运行时事件状态系统
UI、UnitView、VFX、音频、浮动文本与 Gate表现层