执行上下文
在自定义节点中读取连线数值与目标,并通过正确的上下文 API 发布结果或修改战斗状态
区分只读与可写上下文
自定义节点会按所属家族的运行职责获得对应上下文
| Context | 发给谁 | 能力 |
|---|---|---|
IEvaluationContext | OperatorNode、SourceNode、SelectorNode | 读战斗状态、已连接输入、actor、变量、事件参数和上游结果 |
IExecutionContext | MutatorNode、VfxNode、ControlNode、PatternNode | 上面全部,外加战斗修改、结果发布、Hook 写入、取消和变量写入 |
只计算数据的节点使用只读上下文,修改战斗的节点则必须通过 IExecutionContext.Controller 提交每项变化
读取已连接输入
标量字段可以暴露为端口时,使用 PullInputOr 读取
public int Amount = 5;
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(Amount), Amount);
}
端口已经连接时,context 读取连接值,端口没有连接时,PullInputOr 返回节点字段值
只有 default(T) 可以作为合法回退值时,才使用 PullInput<T>(this, "PortName")
解析单位和卡牌
Source 字段配合上下文辅助方法完成单位与卡牌选择
public UnitSource TargetSource = UnitSource.Opponent;
public CardSource CardsSource = CardSource.HandPile;
public override void Execute(IExecutionContext ctx)
{
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
{
if (target == null || target.IsDead) continue;
ctx.Controller.DealDamage(target, 4, ctx.Source);
}
var cards = ctx.ResolveCards(this, CardsSource, nameof(CardsSource));
}
ResolveUnits 和 ResolveCards 支持两种输入方式:节点下拉值,或暴露并连接后覆盖下拉值的端口,单位来源包括 Self、Opponent、AllEnemies 和 AllUnits;卡牌来源包括 ThisCard、HandPile、DrawPile、DiscardPile、ExhaustPile 和 AllPiles
只读取第一个解析结果时,使用 ResolveUnit 或 ResolveCard
节点也可以完全不持有 UnitSource 字段,直接消费另一个节点产出的单位集合:用 NodePort.In("Units", PortDataType.UnitRefOrCollection) 声明一个单位输入,再用 ctx.PullTargets(NodeId, "Units") 读它(IExecutionContext 成员),没接线时结果为空
读取当前 Actor
上下文提供以下执行对象与当前数据
| 属性 | 含义 |
|---|---|
Battle | 只读战斗状态:牌堆、单位、能量、回合计数 |
Host | 拥有当前运行行为的单位:出这张卡的玩家单位、状态所在的单位,或正在行动的敌人 |
Source | 造成这个效果的单位:卡牌施放者,或最近一次成功状态施加者,未知时回退到持有者 |
Target | 当前卡牌、操作或反应选中或实际影响的单位 |
Attacker | 伤害和反应上下文里的实际攻击方,状态绑定不会用已记录施加者覆盖它 |
Card | 效果由卡牌触发时,当前正在打出的卡牌实例 |
PlayedCard | "打出卡牌之后"的反应上下文里,刚刚打出的那张卡 |
Status | 效果由状态触发时,当前正在结算的状态定义 |
IncomingStatus | 状态施加 Hook 里,即将被施加的状态 |
Incoming | 修改型 Hook 可以改的值:落地前的原始伤害、治疗或护甲数字 |
节点可能从多种入口运行时,必须对可空对象进行 null 检查,同时支持 Card 与 Status 的节点不能假设 ctx.Card 始终存在
玩家作为 Host 时,UnitSource.Opponent 会优先使用存活且非玩家的 Target,其次使用存活的 Attacker,最后回退到第一个存活敌人;这样既保留提交卡牌目标的最高优先级,也让玩家状态反应能够解析真正发动攻击的单位
通过 Controller 修改状态
ctx.Controller 是改战斗状态的唯一通道:
ctx.Controller.DealDamage(target, amount, ctx.Source);
ctx.Controller.GainHp(target, amount);
var burn = ctx.ResolveStatusId("Burn");
if (burn != null) ctx.Controller.ApplyStatus(target, burn, stacks, ctx.Source);
伤害、治疗、护甲、状态、卡牌移动、能量和内部事件全部通过 Controller 提交,不要直接写 Model 对象上的 HP、状态、牌堆或卡牌状态,只有受支持的修改入口会触发 Hook、事件、表现和诊断记录
上图说明 Controller 会把一次状态修改同时交给 Hook、事件、表现层与 Monitor,因此,Monitor 的 Unit 页会显示经 ctx.Controller.ApplyStatus 施加的 Poison 层数,直接写字段则会绕过这些后续结果

发布输出
声明了 result 输出的 mutator 用 StoreResult 发布值:
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut("DamageDealt", PortDataType.Int);
}
public override void Execute(IExecutionContext ctx)
{
int dealt = 0;
// deal damage...
ctx.StoreResult(this, "DamageDealt", dealt);
}
下游节点通常沿连接用 PullInputOr 读这个值,ctx.GetResult<T>(producerNodeId, outputPortName) 留给高级场景,用于刻意按 id 寻址某个已知的生产者节点
在 Hook 中修改或取消数值
从 Hook 入口运行的 Behavior 可以调整或取消传入值
public override void Execute(IExecutionContext ctx)
{
if (ctx.Incoming <= 0) return;
ctx.WriteHookResult(ctx.Incoming + 2);
}
WriteHookResult(value) 替换传入数值,CancelHook() 取消待执行操作,GCS 内置 18 个 Hook 入口,覆盖伤害、HP、护甲、能量、状态、卡牌费用、可打出性、抽牌数与手牌上限,因此可以表达以下规则:
-
提高造成的伤害
-
减少受到的伤害
-
将下一张卡的费用改为 0
-
阻止指定状态被施加
-
改变每回合抽牌数量
Hook 方法只应在对应 Hook 入口启动的 Behavior 中调用,在其他路径中调用不会改写当前结算
Hook 求值必须同步结束,直接可达的 Wait、Choice 或 Delayed Trigger 会使该 Hook 路径失效并被跳过;嵌套 Controller 操作在 Hook 求值期间间接触达这些节点时,运行时会直接走 Out、Skipped 或 Done,不会登记延迟任务,因此自定义 Hook 不能依赖后续恢复路径
变量
变量用于让同一数值跨节点路径或跨战斗时机继续使用
ctx.SetVariable("Combo", battleScope: true, value: combo + 1);
int combo = ctx.GetVariable<int>("Combo", battleScope: true);
battleScope: false 时变量只在当前效果结算中有效,battleScope: true 时则可以跨卡牌与回合保留到本场战斗结束,尚未写入的变量读取为 default(T)
值应该沿可见的图连接流动时,优先用 result 输出;图中相距很远的几处需要同一个命名状态时,才用变量
事件参数
由 On Internal Event 入口触发的图,可以读到配对的 Raise Internal Event 节点附带的参数:
object raw = ctx.GetEventArg("Amount");
int amount = raw is int value ? value : 0;
不在事件响应里、或者没有发送该名字的参数时,GetEventArg 返回 null,参数名是触发图和响应图之间的契约,建议保持稳定且含义明确
定义查找
context 能从节点上保存的标识(GUID 或显示名)解析项目定义:
| 方法 | 用途 |
|---|---|
ResolveStatusId(string statusId) | 查找状态定义 |
ResolveCardId(string cardId) | 查找卡牌定义 |
ResolveEnemyUnitId(string unitId) | 查找敌人单位定义 |
StatusStacksOf(unit, statusId) | 读取某单位身上一个状态的层数 |
HasStatusOf(unit, statusId) | 检查单位是否至少有一层该状态 |
普通创作场景里,内置的 selector 和状态节点已经覆盖了这些,因此,只有自定义行为本身需要直接查找时,才需要在节点里用这些 helper
完整的 Mutator 模式
大多数自定义 mutator 都是这个形状(Total 已在 DeclarePorts 里用 NodePort.ResultOut 声明):
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(Amount), Amount);
if (amount <= 0) return;
int total = 0;
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
{
if (target == null || target.IsDead) continue;
ctx.Controller.GainHp(target, amount);
total += amount;
}
ctx.StoreResult(this, "Total", total);
}
采用这种模式设计的节点,能够保证极高的一致性与可预测性,完美兼顾以下特点:
- 字段与连线的优先级(连线输入自动覆盖本地字段)
- 兼容两种目标解析模式
- 自动过滤无效或死亡单位
- 确保所有状态变更均通过 Controller 统一执行
- 并将计算结果准确输出至声明的端口上