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