Execution Context
自定义节点需要访问的内容都经过 ctx:接线输入、解析出的目标、上下文 actor、controller 修改、发布的结果、Hook 和变量。
面向编写节点 Execute、Evaluate、Play 或 DecideNext 代码的程序员。execution context 是自定义节点读取当前战斗、消费已连接端口值、解析目标、修改状态、发布结果和参与 Hook 的受支持方式。使用它,不要绕过 runtime 直接访问内部对象;卡牌、状态、敌人、事件、Monitor 输出和表现层的一致性都依赖这一点。
只读与可写两种 context
GCS 按节点家族发放两种 context interface:
| Context | 发给谁 | 能力 |
|---|---|---|
IEvaluationContext | OperatorNode、SourceNode、SelectorNode | 读战斗状态、已连接输入、actor、变量、事件参数和上游结果 |
IExecutionContext | MutatorNode、VfxNode、ControlNode、PatternNode | 上面全部,外加战斗修改、结果发布、Hook 写入、取消和变量写入 |
只算值的节点拿到只读 context,绝不修改战斗状态。要改战斗的节点,每处修改都走 IExecutionContext.Controller。
先拉取已连接输入
scalar 字段能暴露成端口时,用 PullInputOr 读它:
public int Amount = 5;
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(Amount), Amount);
}
这给作者的行为正是他们期待的:接了线的端口优先,没接线就回落到节点上填的字段值。只有 default(T) 是合理回落值时才用 PullInput<T>(this, "PortName");面向作者的字段用 PullInputOr 更清楚。
解析单位和卡牌
source 字段配合 context helper 做目标与卡牌选择:
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 成员)。没接线时结果为空。
context 上的 actor
context 暴露的这些 actor,让卡牌和状态写起来自然:
| 属性 | 含义 |
|---|---|
Battle | 只读战斗状态:牌堆、单位、能量、回合计数 |
Host | 拥有当前运行行为的单位:出这张卡的玩家单位、状态所在的单位,或正在行动的敌人 |
Source | 造成这个效果的单位:卡牌施放者或状态施加者 |
Target | 卡牌或效果有目标时,玩家选中的单位 |
Attacker | 伤害和反应上下文里的攻击方单位 |
Card | 效果由卡牌触发时,当前正在打出的卡牌实例 |
PlayedCard | "打出卡牌之后"的反应上下文里,刚刚打出的那张卡 |
Status | 效果由状态触发时,当前正在结算的状态定义 |
IncomingStatus | 状态施加 Hook 里,即将被施加的状态 |
Incoming | 修改型 Hook 可以改的值:落地前的原始伤害、治疗或护甲数字 |
节点可能从多种入口运行时,这些都要做 null 检查。一个卡牌和状态都能用的节点,不能假设 ctx.Card 一定存在。
通过 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、状态、牌堆或卡牌状态。controller 这条路才会触发系统的其余部分:
经过 controller 改动的状态,每个工具都看得见。下面 Monitor 的 Unit 页里,敌人身上的 Poison 层数来自一次 controller 的 ApplyStatus 调用;直接写字段的话,这个面板、状态 Hook 和单位头顶的状态 widget 都会一无所知:

发布输出
声明了 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 入口运行的图可以调整或取消传入值:
public override void Execute(IExecutionContext ctx)
{
if (ctx.Incoming <= 0) return;
ctx.WriteHookResult(ctx.Incoming + 2);
}
WriteHookResult(value) 替换传入的数字,CancelHook() 直接取消待执行的动作。GCS 内置 18 个 Hook 入口,覆盖造成与受到的伤害、受到的攻击、HP 增减、护甲的获得/失去/重置、能量的获得/消耗/每回合量、状态的施加与承受、状态伤害、卡牌费用、可打出性、抽牌数和手牌上限,所以这两个调用就能做出:
- 提高造成的伤害;
- 减少受到的伤害;
- 让下一张卡免费;
- 阻止某个状态被施加;
- 改变一回合抽几张牌。
只在运行于 Hook 入口的图中调用 Hook 方法。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,输出落在已声明的端口上。