自定义端口
字段免费送你输入端口。INodePorts.DeclarePorts() 补上剩下的:result 输出、命名输入、带下拉的字符串端口和控制分支。全部住在你的 runtime assembly 里。
面向决定自定义节点在 FlowGraph 编辑器里长什么样、怎么接线的程序员。端口决定 FlowGraph 作者能往你的节点里连什么、从里面接走什么。GCS 把这件事分成两层:public 字段自动推断出常见端口,INodePorts.DeclarePorts() 声明字段表达不了的部分。先用字段,缺什么再声明什么。
内置节点遵守的规则和你的节点完全一样。下面这张 demo 卡 Rapid Fire 里,Deal Damage 的 Amount 直接读节点上的字段,Target 端口则由 All Enemies 传给 Random Elements 后接线进来,外面由 Loop 节点驱动控制流。

推断出的 scalar 输入
public scalar 字段不需要任何声明就能变成输入端口:
| 字段类型 | 端口行为 |
|---|---|
int | 数字输入,用 PullInputOr<int> 读取 |
float | 小数输入,用 PullInputOr<float> 读取 |
bool | 真/假输入,用 PullInputOr<bool> 读取 |
string | 文本输入,用 PullInputOr<string> 读取 |
public int Amount = 5;
public bool OnlyIfWounded = true;
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(Amount), Amount);
bool onlyIfWounded = ctx.PullInputOr(this, nameof(OnlyIfWounded), OnlyIfWounded);
}
字段值是节点上显示的默认值。作者把别的节点接进端口后,连接值覆盖字段值。
Source 字段变成目标端口
名为 <Name>Source 的 UnitSource 或 CardSource 字段会创建一个同名去掉后缀的数据输入:TargetSource 给出 Target,CardsSource 给出 Cards。
public UnitSource TargetSource = UnitSource.Opponent;
public CardSource CardsSource = CardSource.HandPile;
通过 context helper 读取,同一份代码就能同时支持作者选下拉值(单位是 Self、Opponent、AllEnemies、AllUnits;卡牌是 ThisCard、HandPile、DrawPile、DiscardPile、ExhaustPile、AllPiles)或暴露端口后接线:
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
{
if (target == null || target.IsDead) continue;
ctx.Controller.DealDamage(target, 3, ctx.Source);
}
var cards = ctx.ResolveCards(this, CardsSource, nameof(CardsSource));
配置字段不是端口
不是每个作者选择都该能接线。枚举字段、资产引用、颜色、向量留在节点上做配置行。
public DamageStyle Style = DamageStyle.Direct;
public GameObject VfxPrefab;
作者只需选一次模式或资产、而不是每次运行都喂动态值时,就用配置行。
用 INodePorts 声明显式端口
节点需要字段推断不出的端口时,实现 INodePorts:
- result 输出;
- 命名的非 scalar 输入;
- 命名的控制流分支;
- 带下拉选项的字符串输出;
- 换显示名的 scalar 输入。
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
[Serializable]
[FlowNode("Operator", "Add Values")]
public sealed class AddValuesNode : OperatorNode, INodePorts
{
public int A;
public int B;
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut("Result", PortDataType.Int);
}
public override object Evaluate(IEvaluationContext ctx, string outPort)
=> ctx.PullInputOr(this, nameof(A), A) + ctx.PullInputOr(this, nameof(B), B);
}
DeclarePorts() 住在你的 runtime assembly 里,从头到尾不需要 editor 引用。
NodePort 工厂方法
用静态工厂方法构建端口,不要手动填 NodePort 的字段。
| Factory | 方向 | 适用场景 |
|---|---|---|
NodePort.In(name, dataType, label) | 输入 | 字段推断不出的命名数据输入,例如单位或卡牌引用 |
NodePort.Out(name, dataType, label, choices) | 输出 | 暴露一个值的普通数据输出 |
NodePort.ScalarIn(name, dataType, label) | 输入 | 覆盖字段自动推断出的端口,通常为了改显示名或类型 |
NodePort.ResultOut(name, dataType, label, choices) | 输出 | 用 ctx.StoreResult 发布、或从 Evaluate 返回的值 |
NodePort.ControlIn(name) | 输入 | 命名的控制流入口 |
NodePort.ControlOut(name) | 输出 | control 或 pattern 节点可以路由到的分支 |
名字撞上时,显式声明的端口覆盖自动推断的同名端口。ScalarIn 的全部意义就在这里:用字段名声明一个端口,就能改掉推断结果的显示名或数据类型。
端口名会保存进图连接里。保持短且稳定。
选最窄的数据类型
按作者应该连什么来选 PortDataType:
| 数据类型 | 常见用途 |
|---|---|
Int, Float, Bool, String | scalar 值与文本 |
UnitRef, UnitCollection, UnitRefOrCollection | 单个单位、单位列表,或两者皆可 |
CardRef, CardCollection, CardRefOrCollection | 单张卡牌、卡牌列表,或两者皆可 |
StatusRef, StatusCollection | 状态定义或状态集合 |
ValuePoint | Hook 或数值修改点 |
Vector2, Vector3 | 表现层位置或偏移 |
Any, AnyCollection | 窄类型反而误导时的高级适配器 |
None | 仅限控制流端口 |
内容作者视角中属于"列表"的内容不要做成 String:卡牌类型、标签、稀有度、项目专属模式都算。可选值已知时,挂一个 choices supplier。
字符串输出上的下拉选项
String 输出端口可以携带一个 choices supplier。这个输出接到带值选择器的消费方(比如内置 Switch 节点的 case)时,选择器列出你给的值,而不是显示自由文本框。内置的卡牌类型、稀有度、层级输出用的正是同一套机制。
using System.Linq;
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut(
"CardType",
PortDataType.String,
choices: () => GCSApi.CardTypes().Select(t => t.Id).ToList());
}
supplier 在选择器每次打开时运行,列表始终反映项目的实时内容。
控制分支
ControlNode 从 DecideNext 返回分支名。每个分支用 NodePort.ControlOut 声明,返回的名字必须严格对应。
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
[Serializable]
[FlowNode("Control", "If Combo Ready")]
public sealed class IfComboReadyNode : ControlNode, INodePorts
{
public int RequiredCombo = 3;
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ControlOut("Ready");
yield return NodePort.ControlOut("Not Ready");
}
public override IEnumerable<string> DecideNext(IExecutionContext ctx)
{
int combo = ctx.GetVariable<int>("Combo", battleScope: true);
yield return combo >= RequiredCombo ? "Ready" : "Not Ready";
}
}
yield 一个名字走单分支,yield 多个就扇出,一个不 yield 则这条路径停止。返回的字符串匹配不到已声明的控制输出时,流程哪儿也去不了。
Result 输出
mutator 风格的节点先声明端口,再用 ctx.StoreResult(this, portName, value) 发布结果:
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut("Healed", PortDataType.Int);
}
public override void Execute(IExecutionContext ctx)
{
int healed = 0;
// heal targets...
ctx.StoreResult(this, "Healed", healed);
}
OperatorNode、SourceNode、SelectorNode 则直接从 Evaluate 返回值。有多个输出时,按 outPort 分流:
public override object Evaluate(IEvaluationContext ctx, string outPort)
{
return outPort == "MissingHp"
? ctx.Source.MaxHp - ctx.Source.CurrentHp
: ctx.Source.CurrentHp;
}
端口创作检查清单
- 作者在节点上直接编辑的值,用字段
- 字段背后的 scalar 输入,用
PullInputOr(this, nameof(Field), Field)读取 - 单位和卡牌选择用
<Name>Source字段 - 输出用
NodePort.ResultOut显式声明 - 内容存在之后,端口名保持稳定
- 字符串值本质上是列表时,挂上下拉选项
Any只留给确实需要多态的端口