高级用法
本文介绍行为树系统的高级功能和使用技巧。
子树系统
子树允许你将行为树的一部分抽取为独立的资产,实现复用和模块化。
创建子树
子树本质上就是一个独立的行为树资产:
typescript
import { BehaviorTreeBuilder, BlackboardValueType, TaskStatus } from '@esengine/behavior-tree';
// 创建一个巡逻子树
const patrolSubtree = BehaviorTreeBuilder.create(scene, 'PatrolBehavior')
.blackboard()
.defineVariable('patrolPoints', BlackboardValueType.Array, [])
.defineVariable('currentIndex', BlackboardValueType.Number, 0)
.endBlackboard()
.sequence('PatrolSequence')
.action('MoveToNextPoint', (entity, blackboard) => {
const points = blackboard?.getValue('patrolPoints') || [];
const index = blackboard?.getValue('currentIndex') || 0;
if (points.length === 0) return TaskStatus.Failure;
const target = points[index];
console.log(`移动到巡逻点 ${index}:`, target);
// 移动逻辑...
return TaskStatus.Success;
})
.action('UpdateIndex', (entity, blackboard) => {
const points = blackboard?.getValue('patrolPoints') || [];
const index = blackboard?.getValue('currentIndex') || 0;
const nextIndex = (index + 1) % points.length;
blackboard?.setValue('currentIndex', nextIndex);
return TaskStatus.Success;
})
.wait(1.0)
.end()
.build();使用SubTree节点
在主树中引用子树:
typescript
const mainTree = BehaviorTreeBuilder.create(scene, 'EnemyAI')
.blackboard()
.defineVariable('health', BlackboardValueType.Number, 100)
.endBlackboard()
.selector('Root')
.sequence('Combat')
.compareBlackboardValue('health', CompareOperator.Greater, 50)
.action('Attack', () => TaskStatus.Success)
.end()
// 使用子树
.subTree(patrolSubtree, {
inheritParentBlackboard: true, // 继承父黑板
propagateFailure: true // 传播失败状态
})
.end()
.build();从资产加载子树
使用编辑器创建的子树资产:
typescript
import {
BehaviorTreeAssetSerializer,
BehaviorTreeAssetLoader,
BehaviorTreeBuilder
} from '@esengine/behavior-tree';
// 加载子树资产
const subtreeJson = await loadFile('patrol-behavior.btree.json');
const subtreeAsset = BehaviorTreeAssetSerializer.deserialize(subtreeJson);
// 在主树中使用
const mainTree = BehaviorTreeBuilder.create(scene, 'MainAI')
.selector('Root')
.subTreeFromAsset(subtreeAsset, scene, {
namePrefix: 'Patrol',
inheritParentBlackboard: true
})
.end()
.build();子树黑板继承
当启用 inheritParentBlackboard 时,子树可以访问父树的黑板变量:
typescript
// 父树定义的变量
const parent = BehaviorTreeBuilder.create(scene, 'Parent')
.blackboard()
.defineVariable('playerPosition', BlackboardValueType.Vector3, { x: 0, y: 0, z: 0 })
.endBlackboard()
.subTree(childTree, { inheritParentBlackboard: true })
.build();
// 子树可以访问父树的 playerPosition 变量
const child = BehaviorTreeBuilder.create(scene, 'Child')
.action('UseParentData', (entity, blackboard) => {
const playerPos = blackboard?.getValue('playerPosition');
console.log('玩家位置:', playerPos);
return TaskStatus.Success;
})
.build();异步操作
异步动作
对于需要多帧完成的操作,返回 TaskStatus.Running:
typescript
.action('LoadResource', (entity, blackboard, deltaTime) => {
// 检查是否已开始加载
let loadingState = blackboard?.getValue('loadingState');
if (!loadingState) {
// 第一次执行,开始加载
startAsyncLoad().then(result => {
blackboard?.setValue('loadingState', 'completed');
blackboard?.setValue('loadedData', result);
});
blackboard?.setValue('loadingState', 'loading');
return TaskStatus.Running; // 继续等待
}
if (loadingState === 'loading') {
return TaskStatus.Running; // 仍在加载中
}
if (loadingState === 'completed') {
// 加载完成
blackboard?.setValue('loadingState', null);
return TaskStatus.Success;
}
return TaskStatus.Failure;
})超时控制
使用装饰器实现超时:
typescript
.timeout(5.0, 'LoadTimeout') // 5秒超时
.action('SlowOperation', () => {
// 长时间运行的操作
return TaskStatus.Running;
})
.end()全局黑板
全局黑板在所有行为树实例之间共享数据。
创建全局黑板
typescript
import { GlobalBlackboard } from '@esengine/behavior-tree';
// 设置全局变量
GlobalBlackboard.setValue('gameState', 'playing');
GlobalBlackboard.setValue('playerCount', 4);
GlobalBlackboard.setValue('difficulty', 'hard');在行为树中访问全局黑板
typescript
.action('CheckGameState', (entity, blackboard) => {
const gameState = GlobalBlackboard.getValue('gameState');
if (gameState === 'paused') {
return TaskStatus.Failure;
}
return TaskStatus.Success;
})全局黑板监听
监听全局变量变化:
typescript
const unsubscribe = GlobalBlackboard.subscribe('difficulty', (newValue, oldValue) => {
console.log(`难度从 ${oldValue} 变为 ${newValue}`);
// 调整AI行为
});
// 取消监听
unsubscribe();性能优化
1. 使用对象池
复用行为树实体以减少GC压力:
typescript
class BehaviorTreePool {
private pool: Map<string, Entity[]> = new Map();
private scene: Scene;
constructor(scene: Scene) {
this.scene = scene;
}
acquire(asset: BehaviorTreeAsset, poolKey: string): Entity {
let pool = this.pool.get(poolKey);
if (!pool) {
pool = [];
this.pool.set(poolKey, pool);
}
if (pool.length > 0) {
const entity = pool.pop()!;
BehaviorTreeStarter.restart(entity);
return entity;
}
return BehaviorTreeAssetLoader.instantiate(asset, this.scene);
}
release(entity: Entity, poolKey: string) {
BehaviorTreeStarter.stop(entity);
const pool = this.pool.get(poolKey) || [];
pool.push(entity);
this.pool.set(poolKey, pool);
}
clear() {
for (const [key, pool] of this.pool) {
for (const entity of pool) {
entity.destroy();
}
}
this.pool.clear();
}
}
// 使用示例
const pool = new BehaviorTreePool(scene);
// 获取AI实例
const enemyAI = pool.acquire(enemyAsset, 'enemy');
// 释放回池
pool.release(enemyAI, 'enemy');2. 降低更新频率
对于不需要每帧更新的AI,可以在行为树内部使用节流逻辑:
typescript
// 方法1: 在行为树根节点使用Cooldown装饰器
const ai = BehaviorTreeBuilder.create(scene, 'ThrottledAI')
.cooldown(0.1) // 每0.1秒执行一次
.selector()
// AI逻辑
.end()
.end()
.build();
// 方法2: 在Action中实现自定义节流
.action('ThrottledAction', (entity, blackboard, deltaTime) => {
const lastUpdate = blackboard?.getValue('lastUpdateTime') || 0;
const currentTime = Date.now();
const updateInterval = 100; // 100ms
if (currentTime - lastUpdate < updateInterval) {
return TaskStatus.Running;
}
blackboard?.setValue('lastUpdateTime', currentTime);
// 执行实际逻辑
performAILogic();
return TaskStatus.Success;
})3. 条件缓存
缓存昂贵的条件检查结果:
typescript
.action('CacheExpensiveCheck', (entity, blackboard) => {
const cacheKey = 'expensiveCheckResult';
const cacheTime = blackboard?.getValue('expensiveCheckTime') || 0;
const currentTime = Date.now();
// 如果缓存未过期(1秒内),直接使用缓存结果
if (currentTime - cacheTime < 1000) {
const cachedResult = blackboard?.getValue(cacheKey);
return cachedResult ? TaskStatus.Success : TaskStatus.Failure;
}
// 执行昂贵的检查
const result = performExpensiveCheck();
// 缓存结果
blackboard?.setValue(cacheKey, result);
blackboard?.setValue('expensiveCheckTime', currentTime);
return result ? TaskStatus.Success : TaskStatus.Failure;
})4. 分帧执行
将大量计算分散到多帧:
typescript
.action('ProcessLargeDataset', (entity, blackboard, deltaTime) => {
const data = blackboard?.getValue('dataset') || [];
let processedIndex = blackboard?.getValue('processedIndex') || 0;
const batchSize = 100; // 每帧处理100个
const endIndex = Math.min(processedIndex + batchSize, data.length);
for (let i = processedIndex; i < endIndex; i++) {
// 处理单个数据项
processItem(data[i]);
}
blackboard?.setValue('processedIndex', endIndex);
if (endIndex >= data.length) {
// 全部处理完成
blackboard?.setValue('processedIndex', 0);
return TaskStatus.Success;
}
return TaskStatus.Running; // 继续下一帧
})序列化和反序列化
JSON格式
标准的可读格式:
typescript
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
// 序列化为JSON
const asset = createBehaviorTreeAsset();
const json = BehaviorTreeAssetSerializer.serialize(asset);
// 保存到文件
await saveFile('ai-behavior.btree.json', json);
// 从JSON加载
const loadedJson = await loadFile('ai-behavior.btree.json');
const loadedAsset = BehaviorTreeAssetSerializer.deserialize(loadedJson);二进制格式
体积更小的二进制格式(通常比JSON小60-70%):
typescript
// 序列化为二进制
const binary = BehaviorTreeAssetSerializer.serializeToBinary(asset);
// 保存二进制文件
await saveFile('ai-behavior.btree.bin', binary);
// 从二进制加载
const loadedBinary = await loadFile('ai-behavior.btree.bin');
const loadedAsset = BehaviorTreeAssetSerializer.deserializeFromBinary(loadedBinary);格式转换
在JSON和二进制之间转换:
typescript
// JSON转二进制
const jsonData = await loadFile('tree.btree.json');
const asset = BehaviorTreeAssetSerializer.deserialize(jsonData);
const binary = BehaviorTreeAssetSerializer.serializeToBinary(asset);
await saveFile('tree.btree.bin', binary);
// 二进制转JSON
const binaryData = await loadFile('tree.btree.bin');
const asset2 = BehaviorTreeAssetSerializer.deserializeFromBinary(binaryData);
const json = BehaviorTreeAssetSerializer.serialize(asset2);
await saveFile('tree.btree.json', json);调试技巧
1. 日志节点
在关键位置添加日志:
typescript
.log('开始战斗序列', 'info')
.sequence('Combat')
.log('检查生命值', 'debug')
.compareBlackboardValue('health', CompareOperator.Greater, 0)
.log('执行攻击', 'info')
.action('Attack', () => TaskStatus.Success)
.end()2. 黑板快照
定期输出黑板状态:
typescript
.action('DebugBlackboard', (entity, blackboard) => {
console.log('=== 黑板快照 ===');
const vars = blackboard?.getAllVariables();
for (const [key, value] of Object.entries(vars || {})) {
console.log(`${key}:`, value);
}
return TaskStatus.Success;
})3. 条件断言
验证重要条件:
typescript
.action('AssertPlayerExists', (entity, blackboard) => {
const player = blackboard?.getValue('player');
if (!player) {
console.error('断言失败: 玩家不存在');
return TaskStatus.Failure;
}
return TaskStatus.Success;
})4. 性能分析
测量节点执行时间:
typescript
.action('ProfiledAction', (entity, blackboard) => {
const startTime = performance.now();
// 执行操作
doSomething();
const elapsed = performance.now() - startTime;
console.log(`操作耗时: ${elapsed.toFixed(2)}ms`);
return TaskStatus.Success;
})5. 可视化调试
在编辑器中运行行为树并观察节点状态:
typescript
import { BehaviorTreeDebugger } from '@esengine/behavior-tree';
// 启用调试模式
BehaviorTreeDebugger.enable(aiEntity);
// 获取当前执行路径
const executionPath = BehaviorTreeDebugger.getExecutionPath(aiEntity);
console.log('执行路径:', executionPath);
// 获取节点状态
const nodeStatus = BehaviorTreeDebugger.getNodeStatus(aiEntity, nodeId);
console.log('节点状态:', nodeStatus);常见模式
1. 状态机模式
使用行为树实现状态机:
typescript
const fsm = BehaviorTreeBuilder.create(scene, 'StateMachine')
.blackboard()
.defineVariable('currentState', BlackboardValueType.String, 'idle')
.endBlackboard()
.selector('StateSwitch')
// Idle状态
.sequence('IdleState')
.checkBlackboardValue('currentState', 'idle')
.action('IdleBehavior', (e, bb) => {
console.log('执行Idle行为');
// 状态转换条件
if (shouldTransitionToMove()) {
bb?.setValue('currentState', 'move');
}
return TaskStatus.Success;
})
.end()
// Move状态
.sequence('MoveState')
.checkBlackboardValue('currentState', 'move')
.action('MoveBehavior', (e, bb) => {
console.log('执行Move行为');
if (shouldTransitionToAttack()) {
bb?.setValue('currentState', 'attack');
}
return TaskStatus.Success;
})
.end()
// Attack状态
.sequence('AttackState')
.checkBlackboardValue('currentState', 'attack')
.action('AttackBehavior', (e, bb) => {
console.log('执行Attack行为');
if (shouldTransitionToIdle()) {
bb?.setValue('currentState', 'idle');
}
return TaskStatus.Success;
})
.end()
.end()
.build();2. 优先级队列模式
按优先级执行任务:
typescript
.selector('PriorityQueue')
// 最高优先级:生存
.sequence('Survive')
.compareBlackboardValue('health', CompareOperator.Less, 20)
.action('Heal', () => TaskStatus.Success)
.end()
// 中优先级:战斗
.sequence('Combat')
.checkBlackboardExists('nearbyEnemy', true)
.action('Fight', () => TaskStatus.Success)
.end()
// 低优先级:收集资源
.sequence('Gather')
.action('CollectResources', () => TaskStatus.Success)
.end()
.end()3. 并行任务模式
同时执行多个任务:
typescript
.parallel(ParallelPolicy.RequireAll) // 所有任务都要成功
.action('PlayAnimation', () => TaskStatus.Success)
.action('PlaySound', () => TaskStatus.Success)
.action('SpawnParticles', () => TaskStatus.Success)
.end()