Skip to main content

Custom nodes

Guide

Learn how to create and use custom nodes, then implement project-specific FlowGraph nodes from the seven downloadable templates and complete examples

If the built-in nodes cannot express a rule your game needs, choose one of the seven public base classes for the job and copy its template. Use the matching example to register the node, declare its ports, and implement its logic. After Unity compiles, pick your node from Add Node in FlowGraph and save the Behavior.

Find the extension point by node category​

Of the nine menu categories, Action, Event, Operator, Get, Flow, FX, and Intent can be extended through seven public base classes; Entry and Hook are driven by GCS lifecycle events and resolution points, so they do not expose general authoring base classes for project nodes:

Add Node categoryResponsibility in FlowGraphPublic extension baseRuntime entry point
EntryStarts a Behavior after battle, turn, card, status, and unit eventsNo general extensionRegistered and triggered by GCS
HookEnters a Hook path before a value or rule is committedNo general extensionRegistered and triggered by the Hook system
ActionChanges HP, armor, energy, cards, statuses, variables, and unitsMutatorNodeExecute(IExecutionContext)
EventPublishes an event to GCS or GESMutatorNodeExecute(IExecutionContext)
OperatorCalculates, compares, converts, and combines dataOperatorNodeEvaluate(IEvaluationContext, string)
GetReads data or selects unit and card referencesSourceNode, SelectorNodeEvaluate(IEvaluationContext, string)
FlowOrganizes execution paths by conditionControlNodeDecideNext(IExecutionContext)
FXRequests prefab, audio, animation, camera, and UI presentationVfxNodePlay(IExecutionContext)
IntentChooses the next intent for an EnemyPatternNodeDecideNext(IExecutionContext)
tip
  • Use Group on the canvas to organize or reuse connected nodes. For a custom node, the base class defines how it runs, and the registered category determines where it appears in the menu.
  • [FlowNode("Event", ...)] can place a MutatorNode in the Event group, but it does not change the runtime contract that lets the node modify state through Execute
Extension boundary

Start project nodes only from MutatorNode, OperatorNode, SourceNode, SelectorNode, ControlNode, PatternNode, and VfxNode; do not inherit EffectFlowNode directly or treat Entry, Hook, or Group as custom node families

Start from the downloadable templates​

Download the node samples

Download GCS-Samples.zip and extract it outside Unity, the archive contains source and Assembly Definitions without .meta files and is not imported automatically with GCS, copy required templates or examples from Samples~ into a project runtime assembly under Assets/

GCS-Samples.zip
└── Samples~/
└── CustomNodes/
├── Templates/
│ ├── MutatorNodeTemplate.cs
│ ├── OperatorNodeTemplate.cs
│ ├── SourceNodeTemplate.cs
│ ├── SelectorNodeTemplate.cs
│ ├── ControlNodeTemplate.cs
│ ├── PatternNodeTemplate.cs
│ └── VfxNodeTemplate.cs
└── Examples/
├── HealIfBelowHalfNode.cs
├── AddValuesNode.cs
├── UnitHealthNode.cs
├── LowestHealthUnitNode.cs
├── BranchByHealthNode.cs
├── PeriodicSpecialIntentNode.cs
└── SpawnTargetEffectNode.cs

Templates/ provides one documented template per node family, and Examples/ provides one complete implementation per family

TemplateMatching exampleRule it is suited to express
MutatorNodeTemplate.csHealIfBelowHalfNode.csChange battle state and publish the actual result
OperatorNodeTemplate.csAddValuesNode.csProduce a calculated value for other nodes to pull
SourceNodeTemplate.csUnitHealthNode.csRead objects and data from the context
SelectorNodeTemplate.csLowestHealthUnitNode.csReturn a unit or card reference from a candidate set
ControlNodeTemplate.csBranchByHealthNode.csSelect one or more execution branches by condition
PatternNodeTemplate.csPeriodicSpecialIntentNode.csSelect and advance a multi-turn intent pattern for an enemy
VfxNodeTemplate.csSpawnTargetEffectNode.csPlay presentation that does not affect rule resolution
Choose a template

Copy the template for the chosen base class. It supplies the override, control ports, evaluation timing, and access appropriate to that type of node.

Build a discoverable node class​

After you copy a template into the project, its assembly, registration metadata, ports, and context together determine whether the node can be discovered, how it appears, and what logic it runs

Place it in a runtime assembly​

The node assembly needs to reference only TinyGiants.GCS.Runtime, not TinyGiants.GCS.Editor; editor-only assemblies are excluded from player builds, so their nodes cannot execute at runtime

{
"name": "YourGame.Cards",
"references": [
"TinyGiants.GCS.Runtime"
]
}
tip

If the project scripts already belong to a runtime Assembly Definition, add the reference to that assembly; a project that does not use Assembly Definitions can place the node in a normal runtime script directory

Register a concrete node​

The node class must be serializable, discoverable, and executable

using System;
using TinyGiants.GCS.Runtime;

namespace YourGame.Cards
{
[Serializable]
[FlowNode("Action", "Restore Armor")]
public sealed class RestoreArmorNode : MutatorNode
{
public override void Execute(IExecutionContext ctx)
{
}
}
}

[Serializable] lets Unity save the node instance in a Behavior; [FlowNode(category, displayName)] provides the Add Node group and node title; FlowNodeRegistry automatically discovers public, concrete subclasses of the seven base classes, so no manual registration call is needed

tip

Class names, field names, and port names all become part of saved Behaviors; do not rename them after a node enters use, and preserve old fields, add a migration, or publish a new node type when the public contract must change

Turn fields into input ports​

Public instance fields follow fixed inference rules; a node implements INodePorts only when inference cannot express an output or branch

Field or declarationGenerated port
public int AmountAmount integer input
public float ScaleScale float input
public bool EnabledEnabled Boolean input
public string KeyKey text input
public UnitSource TargetSourceTarget unit input
public CardSource CardSourceCard card input
NodePort.ResultOut(...)Explicit data output
NodePort.ControlOut(...)Explicit control branch

A value connected to a port must be read through ctx.PullInputOr(this, nameof(Field), Field); reading the field directly returns only the Inspector fallback value; unit and card references must likewise be resolved through ResolveUnit, ResolveUnits, ResolveCard, or ResolveCards; see custom ports for the complete declaration model

Use the context for each family​

IEvaluationContext provides only read and evaluation capabilities for Operator, Source, and Selector; IExecutionContext adds Controller access, result storage, variables, Hooks, and the execution host for Mutator, Control, Pattern, and VFX

Route all state changes through ctx.Controller and publish all results through ctx.StoreResult; presentation nodes may read the model and find Views, but damage, healing, status changes, and turn advancement do not belong in Play; see execution context for context members and failure conventions

Seven complete implementations​

The following code preserves the fields, ports, boundary handling, and execution logic from Examples/, omitting only the XML comments from the included files

HealIfBelowHalfNode reads the target, healing amount, and filter condition, then heals living units through the Controller. It writes the actual HP restored, capped by maximum HP, to Healed. Make Mutator state changes through the Controller, and check whether downstream nodes still need a result on each early-return path.

using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;

namespace TinyGiants.GCS.Samples
{
[Serializable]
[FlowNode("Action", "Heal If Below Half")]
public sealed class HealIfBelowHalfNode : MutatorNode, INodePorts
{
private const string HealedPort = "Healed";

public UnitSource TargetSource = UnitSource.Self;
public int HealAmount = 5;
public bool OnlyBelowHalf = true;

public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut(HealedPort, PortDataType.Int);
}

public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(HealAmount), HealAmount);
bool onlyBelowHalf = ctx.PullInputOr(this, nameof(OnlyBelowHalf), OnlyBelowHalf);
int healed = 0;

if (amount > 0)
{
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
{
if (target == null || target.IsDead) continue;
if (onlyBelowHalf && target.CurrentHp * 2 > target.MaxHp) continue;

int before = target.CurrentHp;
ctx.Controller.GainHp(target, amount);
healed += target.CurrentHp - before;
}
}

ctx.StoreResult(this, HealedPort, healed);
}
}
}

When replacing fields and method bodies from an example, preserve the permission boundary of the chosen base class; compare built-in node fields, ports, and trigger timing in the node library

Turn a template into a project node​

  1. Copy the file from Templates/ whose responsibility matches the goal into a project runtime assembly
  2. Change namespace YourGame.Cards to the project namespace
  3. Rename the file and class together, keeping one public node type per file
  4. Change [FlowNode(category, displayName)], reusing an existing menu group for Category or choosing a stable project-specific group
  5. Replace the template fields with project fields, without redeclaring inputs that field inference can provide
  6. Keep only result outputs, named inputs, dropdown choices, and control branches in DeclarePorts()
  7. Implement Execute, Evaluate, DecideNext, or Play for the base class
  8. Define behavior for invalid values, null targets, dead units, missing Views, and empty candidate sets
  9. Keep class names, field names, port names, and port types stable after a Behavior starts using the node
tip

When a node needs higher-level battle control, queries, or event subscriptions, use GCSApi as the public entry point; do not create a parallel interface from editor assemblies or internal managers

Verify the node in FlowGraph​

Wait for Unity to finish compiling with no C# errors in the Console; open a compatible Card, Status, or Enemy Behavior in FlowGraph Editor, then search Create Node for the [FlowNode] Display Name; the node should appear under its Category with inferred inputs and explicit outputs

Connect its control and data ports, run the card, status, or enemy that owns the Behavior, then open the Flow tab in Monitor; a new record should show the trigger, total node count, and execution time

A trigger, node count, and execution time in the Monitor Flow tab

Troubleshoot common problems​

SymptomWhere to checkFix
Node is missing from Add NodeAssembly, class, and attributesPlace it in a runtime assembly, confirm the class is public and concrete, and keep [Serializable] and [FlowNode]
Code fails to compile after changing the base classOverride and port structureCopy the target family template again instead of replacing only the base-class name
Connected value is ignoredScalar field readUse ctx.PullInputOr(this, nameof(Field), Field)
Connected target is ignoredUnit or card resolutionUse the ResolveUnit(s) or ResolveCard(s) method that matches the field
Output cannot be connectedINodePorts.DeclarePorts()Declare a result output or control branch with the correct PortDataType
Downstream node cannot read a Mutator resultDeclared and stored namesUse one constant for both ResultOut and StoreResult
State change does not trigger events or HooksMutator method bodyChange battle state through ctx.Controller
Several enemies share Pattern progressPattern state locationUse GetOrCreatePatternState(NodeId) on the host enemy
VFX affects gameplay resultsVfxNode.PlayMove rule changes into a separate Mutator and treat a missing View as a safe skip
Connections in a saved Behavior are lostRenamed class, field, or portRestore the old name, add a migration, or publish a new node type