自定义节点
自定义节点就是一个普通的 runtime C# class:继承一个节点家族基类,挂上 [FlowNode],Unity 编译完成后它就出现在 Add Node 菜单里,没有注册调用,不写编辑器代码
整个扩展面都在 TinyGiants.GCS.Runtime 里,你的 class 永远不引用 TinyGiants.GCS.Editor,GCS 会通过反射,从任何引用了 runtime 的已加载程序集里发现它:
文件与程序集位置
节点文件应放进项目自己的 runtime assembly,并引用 TinyGiants.GCS.Runtime;editor-only assembly 不会进入运行时构建,因此放在其中的节点无法在玩家构建中执行,FlowGraph 编辑器则会通过 public runtime API 读取正确位置的 runtime 节点
最快的起点是随包的样例文件夹:
Assets/TinyGiants/GameCardSystem/Samples~/CustomNodes/
复制一个样例进你的项目,然后重命名 namespace、class、category、display name 和字段,自定义节点示例一页会逐个文件展开
一个可用节点需要三项内容:
| 要求 | 为什么重要 |
|---|---|
[Serializable] | Unity 把节点实例序列化在拥有者的 FlowGraph 里 |
[FlowNode(category, displayName)] | 把节点注册到 Add Node 菜单的某个分组下 |
| 一个 public 节点基类 | 决定图运行到或读取这个节点时发生什么 |
using System;
using TinyGiants.GCS.Runtime;
namespace YourGame.Cards
{
[Serializable]
[FlowNode("Action", "Gain Armor To Self")]
public sealed class GainArmorToSelfNode : MutatorNode
{
public int Amount = 1;
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(Amount), Amount);
if (amount <= 0 || ctx.Host == null) return;
ctx.Controller.GainArmor(ctx.Host, amount);
}
}
}
选节点家族
按节点在 FlowGraph 中承担的任务选择基类,让分类与实际用途一致
| Base class | Override | 适用场景 |
|---|---|---|
MutatorNode | Execute(IExecutionContext ctx) | 改战斗状态:伤害、治疗、护甲、状态、卡牌移动、能量、变量 |
OperatorNode | Evaluate(IEvaluationContext ctx, string outPort) | 纯值:数学、比较、转换、字符串或数字结果 |
SourceNode | Evaluate(IEvaluationContext ctx, string outPort) | 读上下文数据:当前回合、来源单位、打出的卡、状态层数 |
SelectorNode | Evaluate(IEvaluationContext ctx, string outPort) | 产出供其他节点消费的单位或卡牌选择 |
ControlNode | DecideNext(IExecutionContext ctx) | 把流程路由到一个或多个命名分支 |
PatternNode | DecideNext(IExecutionContext ctx) | 敌人目标模式,决定下一个意图分支 |
VfxNode | Play(IExecutionContext ctx) | 纯表现:特效、相机冲击、UI 节拍 |
大多数 gameplay 节点最终是 MutatorNode 或 OperatorNode,如果节点改变战斗状态,就是 mutator;如果节点只是为其他节点提供一个值,就是 operator
Demo 内容使用相同的节点家族,Hunter 场景中的 Mire Reaper 由 Pattern 节点选择意图分支,每个分支再连接 Action 和 FX 节点,自定义 MutatorNode 或 VfxNode 会出现在对应的 Add Node 分类中:

按玩法结果命名
[FlowNode] 的 category 就是 Add Node 菜单分组,节点适合排在内置行为旁边时,复用内置分组:Action 放改状态的效果,Operator 放计算值,Get 放上下文读取和选择,Flow 放分支,FX 放表现,Intent 放敌人意图模式,写其他任意字符串则新开一个分组,一整族游戏专属节点正该这么做
display name 应该描述节点产生的玩法结果,例如 Heal If Below Half,不要使用 ConditionalHpRestoreUtil 这类实现名称,Add Node 菜单需要直接说明这个节点能做什么
字段就是创作 UI
public 字段会直接成为你在节点上看到并编辑的控件:
public UnitSource TargetSource = UnitSource.Self;
public int HealAmount = 5;
public bool OnlyBelowHalf = true;
GCS 会从字段推断端口:
- 每个 public 的
int、float、bool、string字段都可以暴露成 scalar 输入端口 - 名为
<Name>Source的UnitSource字段暴露一个名为<Name>的单位输入,所以TargetSource给出Target端口 - 名为
<Name>Source的CardSource字段以同样方式暴露卡牌输入 - 其他 public 字段类型(枚举、prefab、颜色)显示为节点上的配置行,不是端口
字段是否暴露为端口由 FlowGraph 中的节点配置决定,字段未暴露或端口未连接时,节点使用行内填写的值
scalar 字段一律通过 ctx.PullInputOr(this, nameof(Field), Field) 读取,端口接线后使用连接值,未接线时使用节点字段值
所有改动走 controller
在 MutatorNode.Execute 里,每一处战斗改动都要经过 ctx.Controller,绝不直接改 UnitState、CardInstance、牌堆、HP、状态或能量
public override void Execute(IExecutionContext ctx)
{
int amount = ctx.PullInputOr(this, nameof(HealAmount), HealAmount);
if (amount <= 0) return;
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
{
if (target == null || target.IsDead) continue;
ctx.Controller.GainHp(target, amount);
}
}
controller 调用是 runtime 事件、状态 Hook、Monitor 输出和表现层保持同步的前提,直接写字段会绕过这一切,结果就是一张数字上"生效"、却不发事件、没有视觉反馈、也不触发任何反应的卡
发布可被其他节点读取的结果
如果下游节点需要读你的节点产出的值,实现 INodePorts 并声明一个 result 输出
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
[Serializable]
[FlowNode("Action", "Count Living Targets")]
public sealed class CountLivingTargetsNode : MutatorNode, INodePorts
{
public UnitSource TargetSource = UnitSource.AllEnemies;
public IEnumerable<NodePort> DeclarePorts()
{
yield return NodePort.ResultOut("Count", PortDataType.Int);
}
public override void Execute(IExecutionContext ctx)
{
int count = 0;
foreach (var target in ctx.ResolveUnits(this, TargetSource, nameof(TargetSource)))
if (target != null && !target.IsDead) count++;
ctx.StoreResult(this, "Count", count);
}
}
NodePort.ResultOut("Count", ...) 里的名字必须和 ctx.StoreResult(this, "Count", value) 传入的名字一致
在编辑器下,StoreResult 的端口名匹配不到任何已声明输出时会打一条 warning,名字拼错会变成一条 Console 消息,而不是一个无提示丢失的值
把名字当成已保存内容的契约
一旦真实的图用上了你的节点,它的 class 名、字段名和已声明端口名就是写进已保存内容的契约,只有连同迁移引用它们的 FlowGraph 资产时,才去重命名
好的自定义节点小而直白:
- 一个节点只做一件做卡的事
- 字段名描述可见的选择:
HealAmount、TargetSource、OnlyBelowHalf - 输出名描述那个值:
Healed、DamageDealt、Count - 容易误解的规则应写进字段名或 tooltip,让第一次使用时就能确认语义
编译并找到它
- 让 Unity 无错误编译完成
- 打开任意一张 GCS FlowGraph
- 打开 Add Node 菜单,搜索你在
[FlowNode]里写的 display name - 加上节点,暴露需要的端口,开着 Monitor 跑一场战斗
节点没出现时,检查 class 是否为具体类(不能是 abstract)、带 [Serializable]、带 [FlowNode],并且编译在引用了 TinyGiants.GCS.Runtime 的 runtime assembly 里