跳到主要内容

自定义端口

Guide

为自定义节点配置 FlowGraph 契约所需的输入、输出、下拉选项与控制分支

Demo 卡牌 Rapid Fire 使用的端口规则与自定义节点一致:Deal Damage 直接读取 Amount 字段,TargetAll EnemiesRandom Elements 接入,Loop 控制重复次数

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

上图同时展示了固定字段、数据端口与控制端口,Amount 直接使用节点上保存的值,Target 读取上游选择结果,Loop 的控制分支决定节点重复执行的次数,自定义节点也沿用同一套端口规则

使用推断的标量输入

Public 标量字段无需显式声明即可成为输入端口

字段类型端口行为
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;

通过上下文辅助方法读取后,同一份代码可以同时支持下拉值与端口连线,单位来源包括 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

  • Result 输出

  • 命名的非标量输入

  • 命名的控制流分支

  • 带下拉选项的字符串输出

  • 需要自定义显示名的标量输入

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() 位于项目自己的运行时程序集中,不需要引用 GCS 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 一个名称时执行单个分支,返回多个名称时并行分发,不返回名称时终止当前路径,返回值无法匹配已声明的控制输出时,当前路径同样终止

声明 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;
}

端口创作检查清单

  • 直接在节点上编辑的值使用字段

  • 字段对应的标量输入使用 PullInputOr(this, nameof(Field), Field) 读取

  • 单位和卡牌选择使用 <Name>Source 字段

  • 输出使用 NodePort.ResultOut 显式声明

  • 已保存内容开始连接后保持端口名稳定

  • 字符串值只允许从固定列表中选择时提供下拉选项

  • Any 只用于确实需要多态数据的端口