行为树 BT
可视化编辑行为树 → 导出为不可变 Blob → 按 TreeId 注册 → 由 ECS 系统对实体逐帧驱动。
编辑器能跑 ≠ 真机能跑:真机必须导出 Blob 并在引导期读回。
全链路
BTTreeAsset(画布编辑)
↓ BTBlobBuilder.Build(编辑器:进 Play 由 BTAutoRegisterOnPlay 自动全量注册)
BTRootBlob(纯数据树,Blob)
↓ BTManager.Register(treeId, blobRef)
注册完成
↓ BTInterpreterSystem(ISystem)对挂 RunningBT 的实体
逐帧 Tick → BTInterpreter 指针遍历
─── 打包(真机)另有一条路 ───
BTTreeAsset
↓ BTBlobExporter.ExportAll()(菜单 HYC Framework/BT/Export Blob (For Build))
StreamingAssets/BTBlob/<treeId>.blob + manifest.txt
↓ 运行时 BTBlobLoader.LoadAll()(TryRead → BTManager.Register)
注册完成 → 同上逐帧 Tick
两种驱动方式
| 方式 | 入口 | 适用 |
|---|---|---|
| ECS(推荐) | BTInterpreterSystem(ISystem),自动对挂 RunningBT{TreeId} 的实体 Tick | DOTS / ECS 项目 |
| 非 ECS | BTManager + BTEventBus + BTNodeRuntimeRegistry 手动驱动 | 纯 MonoBehaviour 场景 |
两者共用同一份 BTRootBlob 与同一个 BTInterpreter,差别只在「谁调用 Tick、状态存在哪」。
数据模型:BTTreeAsset
位于 Editor/BT/BTTreeAsset.cs(编辑器侧资产):
| 字段 | 说明 |
|---|---|
TreeId (long) | 树唯一 ID,运行期按它注册与查找 |
Kind (BTTreeKind) | Skill / AI / Other,用于节点语义分区 |
Nodes (List<BTNodeData>) | 节点:NodeId / Type / Position / Note + 三个参数池 |
Connections | 连线:SourceNodeId → TargetNodeId + PortIndex(决定子节点顺序) |
Blackboard | 黑板定义:Key / ValueType / DefaultValue |
节点参数用三个定长池 FloatParams / LongParams / StringParams,每个节点类型占用的参数槽位有明确约定(见枚举注释)。
创建行为树资产(BTAssetFactory)
BTTreeAsset 的唯一程序化创建入口是 BTAssetFactory,与 ConfigAssetFactory 平行:
// 自动在 Assets/BTTrees 下建资产并派发唯一 TreeId
BTAssetFactory.Create("EnemyAI");
BTAssetFactory.CreateMany("Mob", 10);
⚠
禁止
ScriptableObject.CreateInstance<BTTreeAsset>() + 手填 TreeId:原 DateTime.Now.Ticks % 100000 同毫秒会撞车、[CreateAssetMenu] 默认 TreeId=0 与「(无)」哨兵撞;子树引用按 TreeId 选树,撞车会选错树。TreeId 由工厂以本机指纹(MAC)派生派发唯一值:高 48 位取机器 MAC、低 15 位为本机段内序号,跨机天然隔离、无需服务器也不会撞(行为树是独立 long 空间,不接 ConfigIdService)。节点 ID(BTNodeData.NodeId)同样由工厂统管:BTAssetFactory.NextNodeId(tree) 派发树内最小未占用正整数,外部不得手填。
节点类型全景
| 分类 | 节点 |
|---|---|
| 入口 / 出口 | Root(唯一入口)· End(显式终止,返回 Success) |
| 组合(有子节点) | Sequence · Selector · Parallel · RandomSelector / RandomSequence · FlipSelector · UtilitySelector · ProbabilitySelector · StepSequencer · Switch · BinarySelector |
| 装饰(包 1~2 子节点) | Invert · Repeat · UntilSuccess · UntilFail · AlwaysSuccess · AlwaysFail · CooldownGate · Conditional · TimeLimit · Timeout · WaitUntil · Guard · Interruptor · Filter · Iterator · Monitor · Optional · Remapper |
| 条件 / 动作(叶子) | 条件:CheckDistance · CheckBlackboard · CheckEvent;动作:Wait · NoOp · SendEvent |
| 特殊 / 扩展 | SubTree(执行另一棵树,Long[0] = 目标树 ID)· GameCustom = 128(游戏层注册) |
参数槽位约定(节选)
| 节点 | 参数 |
|---|---|
Sequence / Selector | Long[0] bit0 = dynamic(每帧从 0 重估高优先级子节点) |
Parallel | Long[0] = policy;Long[1] = dynamic |
Repeat | Long[0] = 次数(≤0 无限) |
Switch | Long[0] = case 黑板键;Long[2] = dynamic;Long[3] = 越界模式 |
SubTree | Long[0] = 目标树 ID |
SendEvent / CheckEvent | Long[0] = 事件 key 哈希;Long[1] = 范围(0 本实体 / 1 全局) |
完整的槽位约定以 BTNodeType.cs 的注释为准 —— 改节点参数前先读那里的注释。
编辑器入口
| 菜单 | 用途 |
|---|---|
HYC Framework / BT / BT Editor | 打开行为树编辑器画布 |
HYC Framework / BT / Export Blob (For Build) | 把全部树导出到 Assets/StreamingAssets/BTBlob(真机必做) |
✕
出包前必须做两件事:① 跑
HYC Framework/BT/Export Blob (For Build) 导出文件;② 在引导期调用 BTBlobLoader.LoadAll() 注册。只改了树却忘了导出,真机会静默跑旧版本。常见坑
Root必须唯一且必须连线,否则BTBlobBuilder.Build直接失败并打 LogError。BTNodeType是byte枚举,新增类型必须追加在末尾,插在中间会让旧资产序列化错位。- 节点跨帧状态靠
BTNodeRuntimeState持久化,不是存在节点上。 GameCustom节点走托管路径(委托 + 反射),不参与 Burst 编译;纯内建节点的树才是指针遍历、无托管分配。- 移动 / 改名
BTTreeAsset不影响引用(运行期按TreeId查找);但改TreeId会断引用。 - 编辑器靠
BTAutoRegisterOnPlay自动注册,真机没有这回事,必须导出 +LoadAll()。