关于 msw-behaviourtree
MSW BehaviourTree 是用于创建 MSW `.behaviourtree` 文件的技能。它从项目文件生成项目专属的节点与类型规范,并据此结合固定图规则指导行为树构建和验证。
主要功能
- 01生成项目专属的 BT 规范
- 02从项目文件发现自定义节点
- 03映射节点与属性类型
- 04规定图结构和子节点约束
- 05指导 Blackboard 与节点属性配置
适用场景
- 01为 MSW 项目创建行为树
- 02编写前核对自定义节点 ID 和属性
- 03组织复合节点、装饰器与动作节点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ msw-git/ msw-ai-coding-plugins-official --skill msw-behaviourtree安装后在 Claude Code 中输入/msw-behaviourtree确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/node-catalog.mdskeleton-full.jsonskeleton-minimal.jsonscripts/build-spec.cjs详细介绍
MSW BehaviourTree
用于编写 MSW .behaviourtree 文件的端到端技能。涵盖项目专属编写规范(<ProjectRoot>/.behaviourDocs/bt-spec.md)和树的生成。固定图规则和骨架位于此技能的 references/ 中;项目专属规范由此技能的本地 scripts/build-spec.cjs(重新)构建。
🚦 执行顺序(请按此顺序操作)
0. 构建/刷新项目规范(bt-spec.md)
该规范是所有项目特定数据的真实来源:每个自定义动作/装饰器/复合节点的 definitionId、btNodeType、可见的 propertyKey 名称,以及标记为本项目 CoreVersion 的序列化 Type.type 字符串。
何时(重新)构建:
- 首次在项目中处理 BT(尚无
.behaviourDocs/bt-spec.md)。 - 任何会影响 BT 节点范围的更改之后:
- 新增/重命名/移除
.codeblock,且其配对的.mlua扩展了ActionNode/DecoratorNode/CompositeNode - 在此类
.mlua中新增/移除/重命名property行 Environment/config中的CoreVersion升级(序列化类型字符串带有版本标记)。
- 新增/重命名/移除
- 用户表示最近新增/更改了 BT 代码块或
.mlua属性——过期 UUID/缺失属性会悄无声息地导致行为树损坏。 - 下游验证(步骤 7)指出
definitionId、propertyKey或版本不匹配。
运行方式——调用此技能的本地脚本:
node "scripts/build-spec.cjs" --projectRoot "<MSW project root>"
如果当前工作目录已是 MSW 项目根目录,可以省略 --projectRoot。需要 PATH 中有 Node.js(无其他依赖——仅使用标准库 fs/path)。
可选覆盖项(长标志,不区分大小写):
| 标志 | 默认值 | 说明 |
|---|---|---|
--projectRoot | 当前工作目录 | 要扫描的 MSW 项目根目录 |
--outputPath | <ProjectRoot>/.behaviourDocs/bt-spec.md | 如果文件夹不存在,则创建 |
--coreVersion | 从 <ProjectRoot>/Environment/config 读取(CoreVersion 字段) | 如果配置文件缺失,则必需 |
覆盖项示例:
node "scripts/build-spec.cjs" --projectRoot "C:/path/to/project" --coreVersion <CoreVersion>
如果 Environment/config 不存在且未传入 --coreVersion,脚本会抛出异常——没有备用默认值。
规范包含的内容:
- 项目元数据——项目根目录、
CoreVersion、生成时间、发现的节点数量。 - 复合节点——具有固定
definitionId/btNodeType的内置名称,以及发现的自定义复合节点(.mlua声明了extends CompositeNode)。 - 自定义动作节点——
Name、definitionId、btNodeType、可见属性名称。 - 自定义装饰器节点——结构与动作节点相同。
- 类型映射——mlua 类型到序列化
MODNativeType.type的映射,以及 BlackboardObjectValue结构。
UUID 来自项目中实际的 .codeblock 文件——规范绝不会凭空生成 UUID。@HideFromInspector 属性会自动过滤。固定的创作规则、文件骨架和验证清单位于此技能的 references/ 中,而非生成的规范中。
(重新)构建后,读取刚写入的 <ProjectRoot>/.behaviourDocs/bt-spec.md,然后继续执行以下步骤。精简规范只列出属性名称;构造 nodeProperties 时,需从配对的 .mlua 文件中确定每个属性的 mlua 类型/默认值,然后使用 bt-spec.md §4 中的类型映射填写 propertyType.type。
还需阅读 references/skeleton-minimal.json,了解最精简的有效行为树;阅读 references/skeleton-full.json,了解所有可选字段均已填充的 Composite+Decorator+Action+Blackboard 示例;阅读 references/node-catalog.md,了解固定的图规则;并参考项目中任何现有的 .behaviourtree(**/*.behaviourtree)以遵循惯例。将骨架中的 {CORE_VERSION} 替换为 bt-spec.md 中的 CoreVersion——顶层以及 Blackboard 变量和 nodeProperties 中的每个 MOD.Core.* 类型字符串都要替换。
1. 收集用户输入
通过上下文确认;如果有任何不明确之处,请使用 AskUserQuestion 询问:
| 项目 | 描述 | 示例 |
|---|---|---|
name | 树的显示名称 | "PatrolAndChase" |
| 保存路径 | .behaviourtree 的位置(相对于项目根目录) | RootDesk/MyDesk/PatrolAndChase.behaviourtree |
| 树结构 | 计划使用的节点图(根复合节点 + 子节点) | Sequence → [Chase, MoveTo] |
| 自定义节点 | 树引用的动作/装饰器/复合节点代码块 | Chase、MoveTo、Jump |
| 黑板变量 | 变量名称 + 类型 + 初始值 | TargetEntity: Entity、MoveSpeed: number = 10.0 |
| 节点属性 | 对于每个自定义节点,哪个属性映射到哪个黑板变量 | Chase.TargetEntityKey = "TargetEntity" |
**自定义节点存在性检查(必须):**用户提到的每个自定义动作/装饰器/复合节点名称都必须出现在 bt-spec.md §1 / §2 / §3 中。如果引用的节点不在规格说明中,请停止并询问用户——不要编造 UUID,不要假定同名节点存在,也不要跳过重新运行 Step 0。
2. 生成 UUID
你需要:
- 文件 UUID 一个 → 写入
EntryKey和ContentProto.Json.id(两者相同,且都以behaviourtree://为前缀)。 Nodes中的每个节点各需要一个 UUID(nodeId)。
node -e "console.log(require('node:crypto').randomUUID())"
提前生成 UUID,写入临时表,然后再组装。不要将文件 UUID 用作 nodeId。
3. 解析每个 definitionId
| 节点类别 | definitionId 值 | btNodeType |
|---|---|---|
内置复合节点(SequenceNode、SelectorNode、ParallelNode) | 与 nodeName 相同的字符串 | 1 |
自定义复合节点(extends CompositeNode) | 来自 bt-spec.md §1 的值 | 1 |
| 自定义动作节点 | 来自 bt-spec.md §2 的值 | 0 |
| 自定义装饰器节点 | 来自 bt-spec.md §3 的值 | 2 |
自定义节点 UUID 来自 bt-spec.md(该文件从真实的 .codeblock 文件中读取这些 UUID)——不得使用任何其他来源。
4. 构建黑板
对于每个变量,照抄 bt-spec.md §4 中的 Type.type 字符串和 ObjectValue 结构。带版本标记的子字符串(Version=<CoreVersion>)必须完全匹配——拼写错误会导致反序列化静默失败。
Variables 是一个有序数组;每个条目为 { Name, Type: { "$type": "MODNativeType", type: "<来自规格说明>" }, ObjectValue: <来自规格说明> }。ObjectValue 不包含 $type 区分符(与 .model 文件中的 Value 不同)。
对于 Component / ComponentRef,ComponentId 的格式为 <entity-uuid>:<ComponentName>(引擎组件)或 <entity-uuid>:<scriptCodeblockUuid>:<ScriptComponentName>(脚本组件)。请参照项目中已有的序列化示例。
数值型 ObjectValue 使用浮点数字面量形式(3.0,而不是 3)。
**运行时注意事项:**黑板存储在条目所关联的树上。如果任何脚本之后在该组件上调用
AIComponent:SetRootNode(...),条目树和此黑板都会被丢弃——BlackBoard会读取到nil,且*Key属性将停止解析。不要将运行时根节点切换与依赖黑板的树混用。
4.5 解析节点属性值
对于每个需要 nodeProperties 的自定义节点:
- 确认
propertyKey在该节点对应的bt-spec.md§1 / §2 / §3 中存在。 - 在项目中搜索
script <NodeName> extends ActionNode、extends DecoratorNode或extends CompositeNode,找到配对的.mlua。如果有多个文件匹配,优先选择其同级.codeblock中包含与bt-spec.md完全一致的definitionIdUUID 的文件;如果仍有歧义,请询问用户。 - 阅读
.mlua中可见的property声明,忽略带有@HideFromInspector的属性。由此可确定 mlua 类型和默认值。 - 仅当用户提供了值、行为需要非默认值,或
*Key属性必须指向 Blackboard 变量时,才添加nodeProperties条目。可以安全使用.mlua默认值的可选属性应省略。 - 对于
*Key字符串属性,将propertyValue设置为 Blackboard 变量名。如果变量一目了然,可根据名称和 getter 用法推断(MoveSpeedKey->MoveSpeed、TargetEntityKey->TargetEntity)。如果可能匹配多个 Blackboard 变量,请询问用户。 - 对于字面值属性,使用用户提供的值。如果用户未提供值,且
.mlua默认值有实际意义,则省略该属性,而不要序列化猜测的值。 - 如果
OnBehave会检查某个属性是否为nil、空字符串或无效枚举值,且无法推断其值,请在编写树之前询问用户。
nodeProperties 条目格式:
{
"propertyKey": "<property name>",
"propertyType": { "$type": "MODNativeType", "type": "<type from bt-spec.md §4>" },
"propertyValue": <value>
}
5. 组装节点
图结构硬性约束(编写前验证):
- RootNode 不是父节点。 它不能有
childNodes。它只存储startNodeId,且startNodeId必须指向Nodes中的一个节点。 - 如果树需要多个顶层行为,请使用一个 Composite 作为唯一的
startNodeId,或使用一个 Decorator 作为唯一的startNodeId,并让其decoChildNodes包装一个 Composite,或包装一条最终包含 Composite 的 Decorator 链。将多个行为放在该 Composite 的childNodes下。 Nodes中恰好只能有一个节点的nodeParentId: "":即RootNode.startNodeId所引用的节点。不要创建多个根级 Action/Composite/Decorator 节点。- Composite(
btNodeType: 1)是唯一可以通过childNodes拥有多个子节点的节点类别。 - Decorator(
btNodeType: 2)仅用作包装/父节点,且只能包含一个 Action、Composite 或 Decorator 节点。它也可以是另一个 Decorator 的子节点,因此 Decorator 到 Decorator 的链是有效的。它必须使用单数形式的decoChildNodes(一个nodeId字符串)指向该子节点,而不是使用childNodes;被包装的子节点也必须在其nodeParentId中记录 Decorator 的 id。 - 作用于同一个 Action 的 Decorator 必须串联,绝不能作为同级节点平铺。 每个 Decorator 只能拥有一个下游子树。如果两个或更多 Decorator 要对同一个 Action 进行门控/修改,则应构建一条链:
Composite → ADeco → BDeco → CDeco → Action,其中每个 Decorator 的decoChildNodes都指向下一个 Decorator(最后指向 Action)。具体来说:在通向单个 Action 的同一条链中,任意两个 Decorator 都不能共用同一个nodeParentId——每个 Decorator 的父节点都是前一个 Decorator,只有最顶层 Decorator 的父节点是 Composite。当每个 Decorator 包装不同的下游子树时,它们作为同一个 Composite 下的同级节点仍然有效。✅Composite → ADeco → BDeco → CDeco → Action(链式结构——链中每个 Decorator 都有唯一的父节点)。❌Composite → [ADeco→Action, BDeco→Action, CDeco→Action](通过重复 Action 来绕过串联)。❌Composite → [ADeco, BDeco, CDeco, Action](Decorator 被平铺——它们没有包装 Action,实际上成了孤立节点)。 - Action(
btNodeType: 0)是叶节点——绝不能有子节点。
节点写入不变量:
- 每个
nodeId在文件中都是唯一的。 - 每个非根节点的
nodeParentId都指向一个真实的nodeId,且该节点必须是 Composite 或 Decorator。绝不能指向RootNode,因为RootNode不作为节点显示在Nodes中。 - 如果节点的父节点是 Composite,该 Composite 必须在
childNodes中包含该节点的 id。 - 如果节点的父节点是 Decorator,该 Decorator 的
decoChildNodes必须等于该节点的nodeId。即使父节点和子节点都是 Decorator,这样设置也有效。 - Composite 的
childNodes与子节点的nodeParentId必须双向一致。 - Action 节点省略
childNodes。Decorator 节点省略childNodes,并使用且仅使用一个decoChildNodes(单个字符串nodeId)。 - 绝不要写入
probability。 编辑器在往返保存时会移除此字段,且受支持的 Composite(SequenceNode、SelectorNode、ParallelNode)不会使用每个子节点的权重。项目中较早生成的树可能仍在每个节点上带有"probability": 1.0;读取时将其视为旧版字段,但新建节点时不要写入。 - Decorator 节点(
btNodeType: 2)省略nodePosition。 编辑器会根据 Decorator 包裹的子节点自动定位,并在保存时不写入nodePosition字段。只有 Composite 和 Action 节点带有nodePosition。RootNode块也带有自己的nodePosition(与起始节点分开)。 - 空集合字段应省略,而非序列化为
[]。 尚无子节点的 Composite 应完全省略childNodes;没有覆盖项的节点应完全省略nodeProperties。空数组是编辑器草稿产生的内容,不要手动编写。 - Decorator 的子节点字段是
decoChildNodes(规范字段——这是编辑器保存时保留的字段;ChildNodeId在往返保存时会被静默移除)。它是一个保存被包裹子节点nodeId的单个字符串(不是数组)。读取旧版文件时,手动编写的 Decorator 中可能仍会出现ChildNodeId;应将其视为同一字段。写入时,始终输出decoChildNodes。 RootNode.startNodeId引用一个nodeId——该节点可以是 Action、Composite 或 Decorator——且该节点是唯一一个nodeParentId: ""的节点。
*Key 后缀的 String 属性保存 Blackboard 变量的名称(运行时通过 BlackBoard:GetXxx 解析)。不带 Key 的属性保存字面值。
6. nodePosition 格式
nodePosition 是一个包含数值 x / y 的 JSON 对象:
"nodePosition": { "x": 0.0, "y": 0.0 }
请使用浮点数字面量(0.0,而非 0)。旧版字符串格式 "(0.000, 0.000)" 仍可能出现在较早手动编写的树中——读取时将其视为等效格式,但始终写入对象格式(BT 编辑器会在保存时将其规范化为这种格式,因此首次打开文件时,字符串格式重新序列化会产生大量无关差异)。
编辑器坐标轴:BT 编辑器使用数学惯例的画布——+x 向右,+y 向上(右上象限为正)。因此,放置在比父节点 更高 y 坐标处的子节点,在屏幕上会显示在父节点上方。
布局规则——向下绘制树:深度沿 −y 方向增长(子节点位于父节点下方),同级节点围绕父节点的 x 坐标沿 ±x 方向展开。典型间距:深度层级之间相隔 200 个单位,同级节点之间相隔 200 个单位。
RootNode 块与起始节点——不要将它们放在同一位置。 RootNode.nodePosition 是画布锚点,保持为 { "x": 0.0, "y": 0.0 }。起始节点(由 startNodeId 引用的节点)必须位于该锚点下方一个层级——将其放在 (0, 0) 会导致它与编辑器画布上的 RootNode 标记视觉重叠。将 RootNode 锚点视为深度 0,起始节点视为深度 1。
RootNode.nodePosition:{ "x": 0.0, "y": 0.0 }(固定锚点——永不移动)- 起始节点(深度 1,由
startNodeId引用):{ "x": 0.0, "y": -200.0 } - 起始节点的单个子节点(深度 2):
{ "x": 0.0, "y": -400.0 } - 起始节点的两个子节点(深度 2):
{ "x": -100.0, "y": -400.0 }和{ "x": 100.0, "y": -400.0 } - 每增加一个层级:parent.y − 200
切勿将子节点放在 y 大于或等于父节点 y 的位置——这会使树向上绘制,并在视觉上与父节点重叠。RootNode 与起始节点之间也适用此规则:起始节点必须位于 y ≤ -200(严格低于锚点)。
装饰器节点不包含 nodePosition。 编辑器会根据被包装的子节点自动布局。请从所有 btNodeType: 2 节点中省略该字段;该字段仅出现在 RootNode、组合节点(btNodeType: 1)和动作节点(btNodeType: 0)上。
7. 编写并验证
写入 JSON 文件,然后运行此检查清单。请特别注意:
-
EntryKey为behaviourtree://{uuid},且与ContentProto.Json.id完全一致。 - 顶层
Id、GameId、Content均为""。Usage、UseService、DynamicLoading均为0。UsePublish为1。CoreVersion与项目一致(Environment/config)。StudioVersion为0.1.0.0。ContentType为x-mod/behaviourtree。ContentProto.Use为Json。 -
RootNode没有childNodes;RootNode.startNodeId与Nodes中恰好一个nodeId完全一致;该起始节点的nodeParentId: "";且没有其他节点的nodeParentId: ""。 - 每个
nodeParentId均为""或现有的nodeId。 - 所有
nodeId值均唯一。 - 对于每个 Composite,
childNodesID 集合等于nodeParentId为该 Composite 的节点集合。 - 每个 Action 都没有
childNodes。每个 Decorator 都没有childNodes,且恰好有一个decoChildNodes(单个nodeId字符串——ChildNodeId是旧版变体;编辑器在往返转换时会将其移除),并且该 ID 指向恰好一个 Action、Composite 或 Decorator 子节点,且该子节点的nodeParentId指回此 Decorator。Decorator 到 Decorator 的父子链有效,必须使用相同的decoChildNodes↔nodeParentId规则进行检查。 - Decorator 链规则: 多个 Decorator 作用于同一个 Action 时,必须形成一条链(
Composite → ADeco → BDeco → … → Action)。沿每个 Action 向上遍历至其所属 Composite,确认该路径上遇到的 Decorator 的nodeParentId值均唯一(即每个 Decorator 的父节点都是前一个 Decorator,而不是链中已出现过的另一个 Decorator)。同一链中的两个 Decorator 共享一个nodeParentId属于无效情况。同一 Composite 下、包装不同下游子树的同级 Decorator 没有问题——唯一性按链检查,而非全局检查。 - 没有节点序列化
"probability"。(读取时可能出现旧版1.0值,但绝不能在写入时生成。) - 每个 Composite 和 Action 都以对象形式携带
nodePosition:{ "x": <num>, "y": <num> },且使用浮点数字面量——写入时不能使用旧版"(x.xxx, y.yyy)"字符串。Decorator 节点完全不携带nodePosition。 - 起始节点不能与 RootNode 锚点重叠。
RootNode.nodePosition为{ "x": 0.0, "y": 0.0 },且startNodeId引用的节点的y ≤ -200.0(通常为{ "x": 0.0, "y": -200.0 })。如果起始节点是 Decorator(没有nodePosition),则链中向下的第一个 Composite/Action 必须满足此偏移要求。 - 没有节点序列化空数组——没有子节点的 Composite 应省略
childNodes;没有覆盖项的节点应省略nodeProperties。不要写入"childNodes": []或"nodeProperties": []。 - 每个自定义节点的
definitionId均从bt-spec.md复制(绝不能自行编造)。 - 每个
nodeProperties[].propertyKey均与该节点在bt-spec.md中的属性匹配。 - 每个
*Key属性的propertyValue均与正确类型的Blackboard.Variables[].Name匹配。 - 每个类型字符串均从
bt-spec.md§4 原样复制——带版本标签,且极易因拼写错误而失效。 - 版本交叉检查: 每个
MOD.Core.*类型字符串中的Version=X.Y.Z.Z子串(位于Blackboard.Variables[].Type.type和Nodes[].nodeProperties[].propertyType.type中)必须与文件顶层的CoreVersion一致。不匹配会导致反序列化静默失败——当bt-spec.md相对于项目当前的CoreVersion已过时,这种情况很常见。如果不一致,写入前请重新运行步骤 0。(System.*类型使用不可变的Version=4.0.0.0,不受此规则限制。) - JSON 可解析:
node -e "JSON.parse(require('node:fs').readFileSync(process.argv[1],'utf8'))" "<path>"
如果任何检查未通过,请先修复,再报告完成。
📂 此技能中的文件/使用的文件
scripts/build-spec.cjs— 扫描项目并生成<ProjectRoot>/.behaviourDocs/bt-spec.md的 Node.js 脚本。在步骤 0 中调用。<ProjectRoot>/.behaviourDocs/bt-spec.md— 生成的精简目录。它是节点名称、definitionId、btNodeType、属性名称和类型字符串的唯一真实来源。由上述脚本写入;供步骤 1–7 使用。references/skeleton-minimal.json— 最小有效树(空 Blackboard,单个没有子节点的 Composite 根节点)。references/skeleton-full.json— Composite → Decorator → Action,包含nodeProperties(字面值 +Key后缀)和已填充的Blackboard。只要树并非简单结构,就将其用作结构参考。references/node-catalog.md— 对btNodeType值、有效图结构、Key后缀约定以及规范构建器如何发现节点的说明(仅供参考;运行时目录本身位于bt-spec.md中)。
🔁 编辑工作流(现有文件)
- 阅读整个文件——切勿盲目编辑。UUID 和父子图必须保持一致。
- 切勿更改文件的包装 UUID(
EntryKey/ContentProto.Json.id)——否则外部引用会失效。 - 添加节点:生成一个全新的
nodeId,将其追加到Nodes,更新父 Composite 的childNodes,并设置新节点的nodeParentId。 - 移除节点:从
Nodes中移除,并从任何 Composite 的childNodes中移除其 ID。如果它是 Composite,则决定重新指定其子节点的父节点,还是一并移除子节点——切勿留下悬空的nodeParentId引用。 - 如果编辑涉及自定义节点名称、属性或类型,且这些内容可能在上次构建规范后已在项目中发生变化,请先重新运行步骤 0。
- 每次编辑后,重新运行步骤 7 的验证清单。