跳到主要内容

自定义端口

GCS · Extending GCS

字段免费送你输入端口。INodePorts.DeclarePorts() 补上剩下的:result 输出、命名输入、带下拉的字符串端口和控制分支。全部住在你的 runtime assembly 里。

面向决定自定义节点在 FlowGraph 编辑器里长什么样、怎么接线的程序员。

端口决定 FlowGraph 作者能往你的节点里连什么、从里面接走什么。GCS 把这件事分成两层:public 字段自动推断出常见端口,INodePorts.DeclarePorts() 声明字段表达不了的部分。先用字段,缺什么再声明什么。

内置节点遵守的规则和你的节点完全一样。下面这张 demo 卡 Rapid Fire 里,Deal DamageAmount 直接读节点上的字段,Target 端口则由 All Enemies 传给 Random Elements 后接线进来,外面由 Loop 节点驱动控制流。

FlowGraph 窗口中的 Rapid Fire 卡牌图:Loop 节点重复执行四次 Deal Damage,其 Target 端口由 All Enemies 经 Random Elements 接线而来


推断出的 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>SourceUnitSourceCardSource 字段会创建一个同名去掉后缀的数据输入:TargetSource 给出 TargetCardsSource 给出 Cards

public UnitSource TargetSource = UnitSource.Opponent;
public CardSource CardsSource = CardSource.HandPile;

通过 context helper 读取,同一份代码就能同时支持作者选下拉值(单位是 SelfOpponentAllEnemiesAllUnits;卡牌是 ThisCardHandPileDrawPileDiscardPileExhaustPileAllPiles)或暴露端口后接线:

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, Stringscalar 值与文本
UnitRef, UnitCollection, UnitRefOrCollection单个单位、单位列表,或两者皆可
CardRef, CardCollection, CardRefOrCollection单张卡牌、卡牌列表,或两者皆可
StatusRef, StatusCollection状态定义或状态集合
ValuePointHook 或数值修改点
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 在选择器每次打开时运行,列表始终反映项目的实时内容。


控制分支

ControlNodeDecideNext 返回分支名。每个分支用 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);
}

OperatorNodeSourceNodeSelectorNode 则直接从 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 只留给确实需要多态的端口