本APP,是仿造 behavior3editor 编写的;
这个版本的行为树编辑器,导出的行为树结构如下:
顶层结构如下:
{
"version": "0.3.0",
// 可能的值:node, project, tree
"scope": "project",
"selectedTree": "a7bd4b69-6915-438d-84b0-59ec4277fc06",
// tree 列表
"trees": [],
"custom_nodes": []
}tree 结构如下:
{
"version": "0.3.0",
// 如上
"scope": "tree",
// guid, 创建时,自动生成
"id": "a7bd4b69-6915-438d-84b0-59ec4277fc06",
// bt名;手动填写
"title": "TemplateBehaviorTree",
"description": "",
// 根节点guid值
"root": "065166b4-9a8c-4b3a-8329-4be5a2721382",
"properties": {},
// node map, key为guid, value为node
// * key 值等于 node.id
"nodes": {
"$guid": {
/*...*/
},
},
"display": {
"camera_x": 94,
"camera_y": 247.5,
"camera_z": 1,
"x": 0,
"y": 0
}
}node 结构如下:
{
"id": "065166b4-9a8c-4b3a-8329-4be5a2721382",
// 模板节点名
"name": "Priority",
// 节点标题——默认值等于节点模板名
"title": "Priority",
"description": "",
"properties": {
// 自定义属性,以及默认值
"State": 2
},
// 节点坐标
"display": {
"x": 96,
"y": 0
},
"children": [
"f6092c66-d5e7-4d86-9578-90fe0b178425",
"0dd72798-e40f-4714-8f8e-04fa4b2b2b8f"
],
// 对于出度为1的结点,"children"字段将被替换为 "child"字段,其类型为 string
}模板节点定义如下(顶层结构的
custom_nodes
属性下)
{
// ...
"custom_nodes": [
{
"version": "0.3.0",
"scope": "node",
// 模板节点名称 (唯一)
"name": "Parallel",
"category": "composite|condition|action|decorator",
// 模板节点类型
"title": null,
"description": null,
// 自定义属性 map
"properties": {
"Weight": 0,
// 当前模板的自定义属性名,以及其默认值(自动识别类型为数字或字符串)
"ContrastSymbols": "<=",
}
}
// {...}
]
}也就是说,其导出文件是以project(多个tree)为单位进行保存的。这不方便进行团队协作和svn管理。
比如,我们项目用到了上百个行为树,为了进行团队协作,一个文件的project里,就放了一个tree;这使得每一个json导出文件中,都有各自版本的
"custom_nodes";这造成了一个额外的问题就是,当需要往已有的
"custom_node"添加自定义属性的时候,将非常困难,使整个项目变得难于维护;
另外一个问题就是,策划需要调试(至少监控)行为树的运行;
因此,这边对上述js版本的bt编辑器,进行了仿写;目的就是在可以读取原本行为树导出文件数据时,并解决上述问题;
仿写版本编辑器的基本设计要求如下:
- 多tab页编辑,一个tab对应一个行为树;tab之间,可以互相黏贴结点,以及结点之间的连线
- 同一个项目中的行为树,共享同一份
"custom_node"配置 - 对
"custom_node"配置的修改,可以快速应用到已有的行为树导出文件中 - 与服务器进行通信,以监控行为树的执行
本App依赖于两个配置文件
./App.json5./Config.json5
App.json5 用于控制程序行为;Config.json5 用于配置结点,以及配置结点属性会用到的枚举类型
结点分为如下5种,除root外的其他几种类型的结点,允许自定义;
| 类型 | 入度 | 出度 |
|---|---|---|
| root | 0 | 1 |
| composite | 1 | + |
| decorator | 1 | 1 |
| action | 1 | 0 |
| condition | 1 | 0 |
说明 在保存时,如果某结点的入度、出度未满足,则会报错;
{
"custom_enums": [
// 转向仇恨目标方式
{
"name": "TurnType",
"values": [
"Directly",
"WithRotation",
"WithRotationAndMove"
]
},
//...
],
"custom_nodes": [
{
"version": "0.3.0",
"scope": "node",
"name": "TurnToTarget",
"category": "action",
"title": null,
// 描述字段,在行为树编辑区实例化之后,可以被编辑
// 此处相当于定义了该字段的默认值
"description": "转向仇恨目标",
"properties": {
"TurnType": "##Directly",
"AnimRotationSpeed": 0,
"Distance": 0,
"IsTargetInView": false,
}
},
// ...
]
}以上,自定义了一个值连续的,从0开始的枚举类型 TurnType;其字面量定义在"values"
如果值不连续,可额外定义
"numbers"属性;形如:"numbers": [1, 3, 5, 7]需要注意,该属性长度,得等于
values的长度不定义;长度不一致,会报错行为树序列化为json时,通过
"IsEnumAsString": true,字段,控制该枚举类型序列化成是字符串还是数字
custom_nodes 部分,则定义了一个分类为action,名叫 TurnToTarget 的自定义结点;
其 properties 属性,定义了该结点所使用的参数;支持如下运行时类型:
stringlongbooldouble
属性的bnf定义形式为:
name ':' defaultValue
name 为属性名,defaultValue 为为上述支持类型的字面量; 特别的有 long和double的区别,仅在于是否包含小数。
那么,如何定义一个使用预定义枚举类型的结点属性呢?
"TurnType": "##Directly",
包含了两个#,并且以 # 开头,即表示该属性使用了预定义的枚举类型。
# 符号,将 defaultValue 分为两部分部分:
'#' enumName? '#' enumDefaultValue
如果,enumName 被省略了,则以属性名,在预定义的枚举类型集合中去查找;
enumDefaultValue 则为找到的枚举类型中定义好的字面量;
- 运行演示用ws-server
ws-server-managed/ws-server-managed.exe -config ws-server-managed/config.json -custom BehaviorTreeEditorApp/Custom.json5 -btsFolder ./bts
- 运行本App,并编辑一个行为树(若是新建的行为树,得保存到约定的文件夹中;比如
./bts)
本编辑器,默认会读取其运行目录下的
App.json5,Custom.json5两个配置文件;
-
Editor中,点击上方的 红色圆形按钮(或者执行菜单 File -> Monitor B-Tree Execution Start)
-
在监听弹窗对话框中,点击确定即可
监听的效果如上面的视频
如果需要对行为树运行时行为进行监听,则需要先进行如下配置
App.json5
{
"monitor": {
// 监听请求的form字段列表
"startup_fields": [
{
"Default": "templateId",
"Options": [
"templateId",
"entityUid"
],
"Name": "MonitorType",
"Type": "string"
},
{
"Default": "regex:\\d+$",
"Options": null,
"Name": "MonitorId",
"Type": "int"
}
],
// 消息栈大小
"stack_size": 100,
// 监听的服务器地址模板
"ws_url": "ws://127.0.0.1:8999/api/v1/btMonitor/{title}/{MonitorType}/{MonitorId}",
// 监听会话的title模板
"id_format": "{title}.{MonitorType}.{MonitorId}"
}
}上面为监听启动 form,定义了两个字段:
MonitorId字段是一个long类型, 其默认值("Default")"regex:\\d+$"表示,他将从当前行为树title中提取末尾数字作为当前字段的默认值;此处的默认值,你也可以简单写为
0MonitorType则是string类型,其默认值是"templateId";非空的Options属性表示,该字段UI为一个下拉列表,其选项分布为"templateId", "entityUid"下拉选项也可以是整数或者是浮点数字面量,如果你需要对应类型的选项的话;
ws_url/id_format 两个配置项,都是插值字符串模板;在运行时,会被替换为当时的BTree-title,或者form中,用户对应字段的输入值。
插值字符串,求值时,忽略大小写
服务器发送给编辑器的消息是json格式;对应的C#结构体为:
public class MonitorMessage
{
// 结点的运行时状态:Running, Success, Failure,
// 其中,Running 表示正在运行,常亮;
// Success 表示成功
// Failure 表示失败
// 后两者均会短时间`熄灭`
public MonitorState State { get; set; } = MonitorState.None;
// node 唯一id,以标记处于当前状态的结点
public string? Content { get; set; }
// 进入退出某结点期间,所采集到的服务器日志列表
public List<string>? Messages { get; set; }
}Batch Refresh B-tree files
在调整了 Config.json5 进行了属性的添加和删除之后,就需要批量更新行为树文件。
- 新增字段直接选中行为树文件,执行即可;
- 如果是删除字段,则需要额外填写涉及到的自定义结点类型,以及属性名;然后再执行,否则批量转换时,程序发觉丢失字段,就会报错;
因为这里有一个工具的基本设计要求,那就是不丢失用户的编辑输入信息——除非用户手动确认
行为树结点的拷贝、黏贴,都发生在进程内部,并不会使用系统剪贴板
终止监听(点击监听按钮,或者使用 MonitorMessageHistory菜单)后,可以点击MonitorMessageHistory,以查看采集到的结点运行日志;
点击focus按钮,就会重播一次当前消息
使用菜单 Edit -> Find 即可进入查找
其顶部的输入框,支持简单的查找语法
fuzzySearch := text (' ' text )*;
nodeNameSearch := nodeName '[]';
propertyNameSearch := '[' propertyName ']'
propertyNameAndValueSearch := '[' propertyName '=' propertyValue ']'
elementSearch := text | nodeNameSearch | propertyNameSearch | propertyNameAndValueSearch
complexSearch := elementSearch (' ' elementSearch)*
注意: 空格间隔多个查找语素的话,表示 and 语意
范例:
10086: 查找任意字段值、属性值包含10086字样的节点ReleaseSkill[]: 查找所有ReleaseSkill的custom 节点[SkillCid=1001]: 查找任意类型节点的属性包含,SkillCid属性,并且对应属性值为1001的节点。
- Network
https://github.com/Wouterdek/NodeNetwork 这是原库地址
这个 fork是其一个优化pr
本app对 NodeNetwork 库的修改有:
- 结点、连线变化的消息通知,以支持
redo/undo - 连线线型,添加直线,以优化加载速度
- viewModel添加
isReadOnly属性,以支持只读状态 - 更新其依赖库的版本,以避免多版本依赖问题
App用到的图标,均来自 https://www.flaticon.com/
因为感觉行为树编辑器还有一定的使用场景,特别将特定游戏业务相关的代码修改调整为可配制化后,分享到github,以方便后来者;
最后,如果你觉得对你有帮助,请不吝您的money;也希望您的money能为我带来好运。


