跳到主要内容

自定义节点

Guide

掌握自定义节点的创建与使用方法,并基于七类随包模板和完整示例实现符合项目需求的 FlowGraph 节点

当内置节点无法完整表达项目规则时,可以沿用 GCS 已有的节点工作流,把自己的玩法逻辑实现为可在 FlowGraph 中直接使用的自定义节点

从随包提供的七类模板中选择与节点职责匹配的公开基类,再对照对应的完整示例配置注册信息、端口和执行逻辑,Unity 编译后,节点就会进入 Add Node 菜单并随 Behavior 保存

从节点类别判断扩展入口

九个菜单类别中,Action、Event、Operator、Get、Flow、FX 与 Intent 可以通过七个公开基类扩展,Entry 与 Hook 由 GCS 的生命周期和结算点驱动,没有面向项目节点开放的通用创作基类:

Add Node 类别FlowGraph 中的职责公开扩展基类运行入口
Entry在战斗、回合、卡牌、状态与单位事件发生后启动 Behavior不开放通用扩展由 GCS 注册并触发
Hook在数值或规则提交前进入 Hook 路径不开放通用扩展由 Hook 系统注册并触发
Action修改 HP、护甲、能量、卡牌、状态、变量与单位MutatorNodeExecute(IExecutionContext)
Event向 GCS 或 GES 发布事件MutatorNodeExecute(IExecutionContext)
Operator计算、比较、转换与组合数据OperatorNodeEvaluate(IEvaluationContext, string)
Get读取数据或选择单位与卡牌引用SourceNodeSelectorNodeEvaluate(IEvaluationContext, string)
Flow根据条件组织执行路径ControlNodeDecideNext(IExecutionContext)
FX请求 Prefab、声音、动画、相机与 UI 表现VfxNodePlay(IExecutionContext)
Intent为 Enemy 选择下一项意图PatternNodeDecideNext(IExecutionContext)
提示
  • Group 是 FlowGraph Editor 用于整理画布和复用子图的编辑结构,不是可注册的运行时节点家族,菜单 Category 也不等于基类
  • [FlowNode("Event", ...)] 可以把一个 MutatorNode 放进 Event 分组,却不会改变它通过 Execute 修改状态的运行契约
扩展边界

只从 MutatorNodeOperatorNodeSourceNodeSelectorNodeControlNodePatternNodeVfxNode 开始编写项目节点,不要直接继承 EffectFlowNode,也不要把 Entry、Hook 或 Group 当作自定义节点家族

从随包模板开始

GCS 把扩展源码放在 Assets/TinyGiants/GameCardSystem/Samples~/CustomNodes/,在 Rider 或文件浏览器中打开该目录,可以看到按职责分开的 Templates 与 Examples,截图中的 Templates 已完整列出七类节点骨架

Rider 中的 CustomNodes 模板与样例目录

截图把模板入口和示例入口放在同一目录中,当前 Templates/ 的每个文件都包含家族用途、程序集要求、端口推断、上下文用法、状态边界、扩展方式、序列化稳定性与常见错误,Examples/ 也为七类契约各提供一个可以直接编译的成品节点

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 会忽略名称以 ~ 结尾的目录,因此这些文件不会在原位置编译,也不会把模板节点加入正常的 Add Node 菜单,复制目标必须是项目自己的运行时程序集

模板对应样例适合表达的规则
MutatorNodeTemplate.csHealIfBelowHalfNode.cs修改战斗状态并发布实际结果
OperatorNodeTemplate.csAddValuesNode.cs生成供其他节点拉取的计算值
SourceNodeTemplate.csUnitHealthNode.cs读取上下文中的对象与数据
SelectorNodeTemplate.csLowestHealthUnitNode.cs从候选集合返回单位或卡牌引用
ControlNodeTemplate.csBranchByHealthNode.cs根据条件选择一个或多个执行分支
PatternNodeTemplate.csPeriodicSpecialIntentNode.cs为敌人选择并推进跨回合意图模式
VfxNodeTemplate.csSpawnTargetEffectNode.cs播放不影响规则结算的表现
选择模板

先按运行职责选择基类,再复制同名家族模板,不要复制 MutatorNodeTemplate.cs 后只替换基类,不同家族的重写方法、控制端口、求值时机与状态权限并不相同

建立可发现的节点类

模板复制到项目目录后,程序集、注册信息、端口和上下文会共同决定节点能否被发现、怎样显示以及运行时执行什么逻辑

放进运行时程序集

节点程序集只需要引用 TinyGiants.GCS.Runtime,不要引用 TinyGiants.GCS.Editor,Editor-only 程序集不会进入玩家构建,其中的节点也无法在运行时执行

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

当项目脚本已经位于一个运行时 Assembly Definition 时,只需给该程序集增加引用,尚未使用 Assembly Definition 的项目也可以把节点放进普通运行时脚本目录

注册具体节点

节点类需要同时满足可序列化、可发现与可执行三项条件

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] 让 Unity 把节点实例保存在 Behavior 中,[FlowNode(category, displayName)] 提供 Add Node 分组与节点标题,公开且非抽象的七类基类子类会由 FlowNodeRegistry 自动发现,不需要手工注册调用

提示

类名、字段名与端口名都会进入已保存的 Behavior,节点投入使用后不要直接改名,需要调整公开契约时保留旧字段、增加迁移,或发布新的节点类型

让字段成为输入端口

公共实例字段会按固定规则推断端口,节点只在推断无法表达输出或分支时实现 INodePorts

字段或声明生成的端口
public int AmountAmount 整数输入
public float ScaleScale 小数输入
public bool EnabledEnabled 布尔输入
public string KeyKey 文本输入
public UnitSource TargetSourceTarget 单位输入
public CardSource CardSourceCard 卡牌输入
NodePort.ResultOut(...)显式数据输出
NodePort.ControlOut(...)显式控制分支

连接到端口的值必须通过 ctx.PullInputOr(this, nameof(Field), Field) 读取,直接读取字段只会得到 Inspector 回落值,单位与卡牌引用同样要通过 ResolveUnitResolveUnitsResolveCardResolveCards 解析,完整声明方式见自定义端口

按家族使用上下文

IEvaluationContext 只提供读取与求值能力,适用于 Operator、Source 与 Selector,IExecutionContext 在此基础上提供 Controller、结果存储、变量、Hook 与执行宿主,适用于 Mutator、Control、Pattern 与 VFX

状态修改统一经过 ctx.Controller,结果统一通过 ctx.StoreResult 保存,表现节点允许读取模型和查找 View,却不能把伤害、治疗、状态或回合推进放进 Play,上下文成员与失败约定见执行上下文

七类完整实现

以下代码保留 Examples/ 的字段、端口、边界处理与执行逻辑,只省略随包文件中的 XML 注释

HealIfBelowHalfNode 读取目标、治疗量与过滤条件,通过 Controller 治疗存活单位,并把最大 HP 限制后的实际恢复量写入 Healed

Mutator 可以修改规则状态,但不能直接写 UnitState 字段,所有提前返回路径也要考虑下游是否仍会读取结果

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);
}
}
}

替换样例中的字段和方法体时,仍须保留所选基类的权限边界,内置节点的字段、端口与触发时机可以在节点库中对照

从模板改成项目节点

  1. Templates/ 复制与目标职责一致的文件到项目运行时程序集
  2. namespace YourGame.Cards 改成项目命名空间
  3. 同时重命名文件与 Class,保持一份文件对应一个公开节点类型
  4. 修改 [FlowNode(category, displayName)],Category 复用已有菜单分组或使用项目自己的稳定分组
  5. 用项目字段替换模板字段,能由字段推断的输入不重复声明
  6. DeclarePorts() 中只保留结果输出、命名输入、下拉选项与控制分支
  7. 按基类实现 ExecuteEvaluateDecideNextPlay
  8. 对无效数值、空目标、死亡单位、缺失 View 与空候选集合给出明确行为
  9. Behavior 开始使用节点后保持类名、字段名、端口名与端口类型稳定
提示

节点需要调用更高层的战斗控制、查询或事件订阅时,以 GCSApi 作为公开入口,不要从编辑器程序集或内部管理器补一套平行接口

在 FlowGraph 中验证节点

等待 Unity 编译完成且 Console 没有 C# 错误,在 FlowGraph Editor 中打开兼容的 Card、Status 或 Enemy Behavior,通过 Create Node 搜索 [FlowNode] 的 Display Name,节点应当出现在指定 Category 下,并显示推断输入与显式输出

连接控制端口与数据端口,运行拥有该 Behavior 的卡牌、状态或敌人,随后打开 Monitor 的 Flow 标签页,新增记录应显示本次触发、节点总数与执行耗时

Monitor Flow 标签页中的触发、节点数与执行耗时

定位常见问题

现象检查位置修正方式
Add Node 中没有节点程序集、Class 与属性放进运行时程序集,确认 Class 公开且非抽象,并保留 [Serializable][FlowNode]
修改基类后无法编译重写方法与端口结构重新复制目标家族模板,不只替换基类名称
连线数值被忽略标量字段读取使用 ctx.PullInputOr(this, nameof(Field), Field)
连线目标被忽略单位或卡牌解析使用与字段对应的 ResolveUnit(s)ResolveCard(s)
输出无法连接INodePorts.DeclarePorts()声明结果输出或控制分支,并使用正确 PortDataType
下游读不到 Mutator 结果声明名与存储名用同一个常量连接 ResultOutStoreResult
状态变化没有触发事件或 HookMutator 方法体通过 ctx.Controller 修改战斗状态
多个敌人共享 Pattern 进度Pattern 状态位置使用宿主敌人的 GetOrCreatePatternState(NodeId)
VFX 影响玩法结果VfxNode.Play把规则修改移到独立 Mutator,并让缺失 View 成为安全跳过
已保存 Behavior 的连接丢失Class、字段或端口改名恢复旧名称,增加迁移,或发布新的节点类型