跳到主要内容

自定义端口

Guide

public 字段自动推断常用输入端口,INodePorts.DeclarePorts() 在 runtime assembly 中声明结果输出、命名输入、下拉选项与控制分支

Demo 卡牌 Rapid Fire 使用的端口规则与自定义节点一致:Deal Damage 直接读取 Amount 字段,TargetAll EnemiesRandom 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 中,不需要引用 GCS editor assembly

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

端口创作检查清单

  • 直接在节点上编辑的值,用字段
  • 字段背后的 scalar 输入,用 PullInputOr(this, nameof(Field), Field) 读取
  • 单位和卡牌选择用 <Name>Source 字段
  • 输出用 NodePort.ResultOut 显式声明
  • 内容存在之后,端口名保持稳定
  • 字符串值只允许从固定列表中选择时,提供下拉选项
  • Any 只留给确实需要多态的端口