Skip to main content

Custom Ports

Guide

Public fields infer common input ports; INodePorts.DeclarePorts() adds result outputs, named inputs, dropdown-fed strings, and control branches from the runtime assembly

The demo card Rapid Fire uses the same port rules as a custom node: Deal Damage reads Amount from its node field, receives Target from All Enemies through Random Elements, and runs once per Loop iteration

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

Inferred scalar inputs

Public scalar fields can become input ports without any declaration:

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

The field value is the default shown on the node; after you expose and wire the port, the connected value overrides that field

Source fields become target ports

A UnitSource or CardSource field named <Name>Source creates a matching data input named <Name>; the port name is the field name without the Source suffix: TargetSource gives Target, CardsSource gives Cards

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

Read them through the context helpers, so the same code works with a dropdown value (Self, Opponent, AllEnemies, AllUnits for units; ThisCard, HandPile, DrawPile, DiscardPile, ExhaustPile, AllPiles for cards) or an exposed and wired port:

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

Configuration fields are not ports

Not every setting should be wireable; enum fields, asset references, colors, and vectors stay as configuration rows on the node body

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

Use a configuration row when you select a mode or asset once instead of feeding a dynamic value on every run

Declare explicit ports with INodePorts

Implement INodePorts when your node needs a port fields cannot infer:

  • a result output;
  • a named non-scalar input;
  • a named control-flow branch;
  • a string output with dropdown choices;
  • a relabeled scalar input
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() lives in your runtime assembly; it never requires an editor reference

NodePort factories

Build ports with the static factory methods instead of 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 auto-inferred port, usually to relabel or re-type it
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 a control or pattern node routes to

On a name clash, an explicitly declared port wins over the auto-promoted one; that is the whole trick behind ScalarIn: declare a port under a field's name and you replace the label or data type inference produced

Port names are saved into graph connections; keep them short and stable

Pick the narrowest data type

Choose the PortDataType that matches the value the port accepts:

Data typeTypical use
Int, Float, Bool, StringScalar values and text
UnitRef, UnitCollection, UnitRefOrCollectionA single unit, a unit list, or either
CardRef, CardCollection, CardRefOrCollectionA single card, a card list, or either
StatusRef, StatusCollectionA status definition or a status set
ValuePointA hook or value-modification point
Vector2, Vector3Presentation positions or offsets
Any, AnyCollectionAdvanced adapters where a narrow type would mislead
NoneControl-flow ports only

Avoid String for fixed lists such as card type, tag, rarity, or a project-specific mode; when the possible values are known, attach a choices supplier instead

A String output port can carry a choices supplier; when that output feeds a consumer with a value picker (the built-in Switch node's cases, for example), the picker lists your values instead of showing a free-text box; the built-in card type, rarity, and tier outputs use exactly this mechanism

using System.Linq;

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

The supplier runs each time the picker opens, so the list reflects live project content

Control branches

A ControlNode returns branch names from DecideNext; declare each branch with NodePort.ControlOut, and return exactly those names

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 one name for a single branch, several to fan out, or none to stop the path; A returned string that matches no declared control output leads nowhere

Result outputs

Mutator-style nodes publish results with ctx.StoreResult(this, portName, value) after declaring the port:

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 the value from Evaluate instead; with multiple outputs, switch on 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 scalars 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 once content exists
  • Attach dropdown choices when a string value is really a list
  • Reserve Any for ports that must accept more than one value type