Custom Ports
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

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; 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
| 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 the value the port accepts:
| 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 fixed lists such as card type, tag, rarity, or a project-specific mode; when 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 edited directly 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 must accept more than one value type