跳到主要内容

自定义节点

Guide

自定义节点就是一个普通的 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 classOverride适用场景
MutatorNodeExecute(IExecutionContext ctx)改战斗状态:伤害、治疗、护甲、状态、卡牌移动、能量、变量
OperatorNodeEvaluate(IEvaluationContext ctx, string outPort)纯值:数学、比较、转换、字符串或数字结果
SourceNodeEvaluate(IEvaluationContext ctx, string outPort)读上下文数据:当前回合、来源单位、打出的卡、状态层数
SelectorNodeEvaluate(IEvaluationContext ctx, string outPort)产出供其他节点消费的单位或卡牌选择
ControlNodeDecideNext(IExecutionContext ctx)把流程路由到一个或多个命名分支
PatternNodeDecideNext(IExecutionContext ctx)敌人目标模式,决定下一个意图分支
VfxNodePlay(IExecutionContext ctx)纯表现:特效、相机冲击、UI 节拍

大多数 gameplay 节点最终是 MutatorNodeOperatorNode,如果节点改变战斗状态,就是 mutator;如果节点只是为其他节点提供一个值,就是 operator

Demo 内容使用相同的节点家族,Hunter 场景中的 Mire Reaper 由 Pattern 节点选择意图分支,每个分支再连接 Action 和 FX 节点,自定义 MutatorNodeVfxNode 会出现在对应的 Add Node 分类中:

Mire Reaper 的敌人行为图:pattern 节点选择意图分支,每个分支运行 action 和 FX 节点

按玩法结果命名

[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 的 intfloatboolstring 字段都可以暴露成 scalar 输入端口
  • 名为 <Name>SourceUnitSource 字段暴露一个名为 <Name> 的单位输入,所以 TargetSource 给出 Target 端口
  • 名为 <Name>SourceCardSource 字段以同样方式暴露卡牌输入
  • 其他 public 字段类型(枚举、prefab、颜色)显示为节点上的配置行,不是端口

字段是否暴露为端口由 FlowGraph 中的节点配置决定,字段未暴露或端口未连接时,节点使用行内填写的值

scalar 字段一律通过 ctx.PullInputOr(this, nameof(Field), Field) 读取,端口接线后使用连接值,未接线时使用节点字段值

所有改动走 controller

MutatorNode.Execute 里,每一处战斗改动都要经过 ctx.Controller,绝不直接改 UnitStateCardInstance、牌堆、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) 传入的名字一致

拼写错误会出现在 Console 里

在编辑器下,StoreResult 的端口名匹配不到任何已声明输出时会打一条 warning,名字拼错会变成一条 Console 消息,而不是一个无提示丢失的值

把名字当成已保存内容的契约

一旦真实的图用上了你的节点,它的 class 名、字段名和已声明端口名就是写进已保存内容的契约,只有连同迁移引用它们的 FlowGraph 资产时,才去重命名

好的自定义节点小而直白:

  • 一个节点只做一件做卡的事
  • 字段名描述可见的选择:HealAmountTargetSourceOnlyBelowHalf
  • 输出名描述那个值:HealedDamageDealtCount
  • 容易误解的规则应写进字段名或 tooltip,让第一次使用时就能确认语义

编译并找到它

  1. 让 Unity 无错误编译完成
  2. 打开任意一张 GCS FlowGraph
  3. 打开 Add Node 菜单,搜索你在 [FlowNode] 里写的 display name
  4. 加上节点,暴露需要的端口,开着 Monitor 跑一场战斗

节点没出现时,检查 class 是否为具体类(不能是 abstract)、带 [Serializable]、带 [FlowNode],并且编译在引用了 TinyGiants.GCS.Runtime 的 runtime assembly 里