Skip to main content

Custom ports

Guide

Configure the inputs, outputs, dropdown choices, and control branches required by a custom node's FlowGraph contract

The Demo card Rapid Fire uses the same port rules as a custom node: Deal Damage reads the Amount field directly, Target is wired from All Enemies through Random Elements, and Loop controls the number of repetitions

Rapid Fire card graph in the FlowGraph window: a Loop node repeats Deal Damage four times, with its Target port wired from All Enemies through Random Elements

The image above shows fixed fields, data ports, and control ports together; Amount uses the value saved on the node, Target reads the upstream selection, and the Loop control branch determines how many times the node runs; custom nodes follow the same port rules

Use inferred scalar inputs

Public scalar fields can become input ports without an explicit declaration

Field typePort behavior
intNumeric input, read with PullInputOr<int>
floatDecimal input, read with PullInputOr<float>
boolTrue or false input, read with PullInputOr<bool>
stringText input, read with 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);
}
tip

The field value is the default shown on the node; after another node is connected to the port, the connected value overrides the field value

Promote Source fields to target ports

A field named <Name>Source and typed as UnitSource or CardSource creates a data input with the suffix removed: TargetSource gives Target, and CardsSource gives Cards

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

Read them through the context helpers so the same code supports both dropdown values and port connections; unit sources include Self, Opponent, AllEnemies, and AllUnits, while card sources include ThisCard, HandPile, DrawPile, DiscardPile, ExhaustPile, and 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));

Distinguish configuration fields from ports

Not every setting needs a connection; enum fields, asset references, colors, and vectors usually remain configuration rows on the node

public DamageStyle Style = DamageStyle.Direct;
public GameObject VfxPrefab;
tip

Use a configuration row when a mode or asset is selected once and does not need a dynamic value at runtime

Declare explicit ports

Implement INodePorts when the node needs ports that fields cannot infer:

  • Result outputs

  • Named non-scalar inputs

  • Named control-flow branches

  • String outputs with dropdown choices

  • Scalar inputs that need a custom display name

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

DeclarePorts() belongs in your project's runtime assembly and does not require a reference to the GCS Editor assembly

NodePort factory methods

Build ports with the static factory methods; they provide consistent directions, types, and defaults, avoiding the incomplete contracts that can result from filling in NodePort fields by hand

FactoryDirectionUse it for
NodePort.In(name, dataType, label)InputA named data input that fields cannot infer, such as a unit or card reference
NodePort.Out(name, dataType, label, choices)OutputA plain data output that exposes a value
NodePort.ScalarIn(name, dataType, label)InputOverriding a field's inferred port, usually to change its display name or type
NodePort.ResultOut(name, dataType, label, choices)OutputA value published with ctx.StoreResult or returned from Evaluate
NodePort.ControlIn(name)InputA named control-flow entry
NodePort.ControlOut(name)OutputA branch that a control or pattern node can route to
tip

When names collide, an explicitly declared port overrides the inferred port with the same name; this is the purpose of ScalarIn: declare a port with the field name to change the inferred display name or data type; port names are also saved in graph connections, so keep them short and stable

Choose the exact data type

Choose PortDataType according to the data the port must accept:

Data typeTypical use
Int, Float, Bool, StringScalar values and text
UnitRef, UnitCollection, UnitRefOrCollectionOne unit, a unit list, or either
CardRef, CardCollection, CardRefOrCollectionOne card, a card list, or either
StatusRef, StatusCollectionA status definition or status collection
ValuePointA Hook or value-modification point
Vector2, Vector3Presentation-layer positions or offsets
Any, AnyCollectionAdvanced adapters where a narrow type would be misleading
NoneControl-flow ports only
tip

Card types, tags, rarities, and project-specific modes are fixed options; even when the serialized value uses String, provide a Choices Supplier so the editor shows an option list

Provide dropdown choices for string outputs

A String output port can carry a choices supplier; when connected to a node with a value picker, such as a case on the built-in Switch, the picker lists the supplied options instead of showing a free-text field; built-in card type, rarity, and tier outputs use the same mechanism

using System.Linq;

public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut(
"CardType",
PortDataType.String,
choices: () => GCSApi.CardTypes().Select(t => t.Id).ToList());
}
tip

The supplier runs whenever the picker opens, so the list always reflects the project's current content

Control branches

A ControlNode returns branch names from DecideNext; declare every branch with NodePort.ControlOut, and make each returned name match exactly

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

yield one name to run one branch, return several names to fan out in parallel, and return no names to end the current path; the path also ends when a returned value does not match a declared control output

Declare Result outputs

A mutator-style node declares the port first, then publishes its result with 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, and SelectorNode return values directly from Evaluate; when there are multiple outputs, route by outPort:

public override object Evaluate(IEvaluationContext ctx, string outPort)
{
return outPort == "MissingHp"
? ctx.Source.MaxHp - ctx.Source.CurrentHp
: ctx.Source.CurrentHp;
}

Port authoring checklist

  • Use fields for values edited directly on the node

  • Read field-backed scalar inputs with PullInputOr(this, nameof(Field), Field)

  • Use <Name>Source fields for unit and card selection

  • Declare outputs explicitly with NodePort.ResultOut

  • Keep port names stable after saved content starts connecting to them

  • Provide dropdown choices when a string value must come from a fixed list

  • Use Any only for ports that genuinely need polymorphic data