Skip to main content

Custom nodes

Guide

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

When the built-in nodes cannot fully express a project rule, you can follow the existing GCS node workflow and implement your gameplay logic as a custom node that works directly in FlowGraph

Choose the public base class that matches the node's responsibility from the seven included templates, then use its complete example to configure registration, ports, and execution logic; after Unity compiles, the node appears in the Add Node menu and is saved with 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
  • Group is an editing structure that FlowGraph Editor uses to organize the canvas and reuse subgraphs; it is not a registrable runtime node family, and a menu Category is not the same thing as a base class
  • [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 included templates

GCS keeps the extension source under Assets/TinyGiants/GameCardSystem/Samples~/CustomNodes/; open this directory in Rider or a file browser to find Templates and Examples organized by responsibility, with all seven node skeletons visible under Templates in the screenshot

The CustomNodes template and example directories in Rider

The screenshot places the template and example entry points in the same directory; every file currently under Templates/ covers the family purpose, assembly requirements, port inference, context use, state boundaries, extension approach, serialization stability, and common mistakes, while Examples/ provides one directly compilable finished node for each of the seven contracts

Assets/TinyGiants/GameCardSystem/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

Unity ignores directories whose names end in ~, so these files do not compile in place or add template nodes to the normal Add Node menu; the copy destination must be one of your project's runtime assemblies

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

Choose the base class by runtime responsibility before copying the matching family template; do not copy MutatorNodeTemplate.cs and replace only its base class, because each family has a different override, control-port structure, evaluation timing, and state permission

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, heal amount, and filter condition; heals living units through the Controller; and writes the actual HP restored after the maximum-HP cap to Healed

A Mutator may change rule state but must not write UnitState fields directly; every early-return path must also account for whether downstream nodes will still read its result

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