596 lines
15 KiB
Markdown
596 lines
15 KiB
Markdown
# 高级用法
|
||
|
||
本文介绍行为树系统的高级功能和使用技巧。
|
||
|
||
## 子树系统
|
||
|
||
子树允许你将行为树的一部分抽取为独立的资产,实现复用和模块化。
|
||
|
||
### 创建子树
|
||
|
||
子树本质上就是一个独立的行为树资产:
|
||
|
||
```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()
|
||
```
|
||
|
||
|
||
## 下一步
|
||
|
||
- 查看[自定义动作](./custom-actions.md)学习如何创建自定义行为节点
|
||
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
|