Custom Ports
Fields give you input ports for free. INodePorts.DeclarePorts() adds the rest: result outputs, named inputs, dropdown-fed strings, and control branches. All of it lives in your runtime assembly.
For programmers deciding how a custom node looks and wires in the FlowGraph editor.Ports decide what a FlowGraph author can wire into or out of your node. GCS layers the work in two steps: public fields infer the common ports automatically, and INodePorts.DeclarePorts() declares what fields cannot express. Start with fields; declare only what is missing.
Built-in nodes follow the same rules your nodes will. In the demo card Rapid Fire below, Deal Damage reads Amount from the field on its node body, while its Target port is wired from a Random Elements selector fed by All Enemies. The Loop node drives the control flow around it.

Inferred scalar inputsโ
Public scalar fields can become input ports without any declaration:
| Field type | Port behavior |
|---|---|
int | Numeric input, read with PullInputOr<int>. |
float | Decimal input, read with PullInputOr<float>. |
bool | True/false input, read with PullInputOr<bool>. |
string | Text 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. When the author wires another node into the port, the connected value overrides the 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 whether the author picks a dropdown value (Self, Opponent, AllEnemies, AllUnits for units; ThisCard, HandPile, DrawPile, DiscardPile, ExhaustPile, AllPiles for cards) or exposes and wires the 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 author choice 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 the author should pick a mode or an asset once, rather than feed 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.
| Factory | Direction | Use it for |
|---|---|---|
NodePort.In(name, dataType, label) | Input | A named data input that fields cannot infer, such as a unit or card reference. |
NodePort.Out(name, dataType, label, choices) | Output | A plain data output that exposes a value. |
NodePort.ScalarIn(name, dataType, label) | Input | Overriding a field's auto-inferred port, usually to relabel or re-type it. |
NodePort.ResultOut(name, dataType, label, choices) | Output | A value published with ctx.StoreResult, or returned from Evaluate. |
NodePort.ControlIn(name) | Input | A named control-flow entry. |
NodePort.ControlOut(name) | Output | A 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 what the author should wire:
| Data type | Typical use |
|---|---|
Int, Float, Bool, String | Scalar values and text. |
UnitRef, UnitCollection, UnitRefOrCollection | A single unit, a unit list, or either. |
CardRef, CardCollection, CardRefOrCollection | A single card, a card list, or either. |
StatusRef, StatusCollection | A status definition or a status set. |
ValuePoint | A hook or value-modification point. |
Vector2, Vector3 | Presentation positions or offsets. |
Any, AnyCollection | Advanced adapters where a narrow type would mislead. |
None | Control-flow ports only. |
Avoid String for anything the author perceives as a list: card type, tag, rarity, or a project-specific mode. If the possible values are known, attach a choices supplier instead.
Dropdown choices on string outputsโ
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 the author edits on the node.
- Read field-backed scalars with
PullInputOr(this, nameof(Field), Field). - Use
<Name>Sourcefields 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
Anyfor ports that are truly polymorphic.