refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)
* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 * fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞 * feat(behavior-tree): 完善行为树核心功能并修复类型错误
This commit is contained in:
@@ -2,594 +2,391 @@
|
||||
|
||||
本文介绍行为树系统的高级功能和使用技巧。
|
||||
|
||||
## 子树系统
|
||||
|
||||
子树允许你将行为树的一部分抽取为独立的资产,实现复用和模块化。
|
||||
|
||||
### 创建子树
|
||||
|
||||
子树本质上就是一个独立的行为树资产:
|
||||
|
||||
```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';
|
||||
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取全局黑板服务
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
|
||||
// 设置全局变量
|
||||
GlobalBlackboard.setValue('gameState', 'playing');
|
||||
GlobalBlackboard.setValue('playerCount', 4);
|
||||
GlobalBlackboard.setValue('difficulty', 'hard');
|
||||
globalBlackboard.setValue('gameState', 'playing');
|
||||
globalBlackboard.setValue('playerCount', 4);
|
||||
globalBlackboard.setValue('difficulty', 'hard');
|
||||
|
||||
// 读取全局变量
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
const playerCount = globalBlackboard.getValue<number>('playerCount');
|
||||
```
|
||||
|
||||
### 在行为树中访问全局黑板
|
||||
### 在自定义执行器中访问全局黑板
|
||||
|
||||
```typescript
|
||||
.action('CheckGameState', (entity, blackboard) => {
|
||||
const gameState = GlobalBlackboard.getValue('gameState');
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '@esengine/behavior-tree';
|
||||
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
if (gameState === 'paused') {
|
||||
return TaskStatus.Failure;
|
||||
export class CheckGameState implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
|
||||
if (gameState === 'paused') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 全局黑板监听
|
||||
|
||||
监听全局变量变化:
|
||||
|
||||
```typescript
|
||||
const unsubscribe = GlobalBlackboard.subscribe('difficulty', (newValue, oldValue) => {
|
||||
console.log(`难度从 ${oldValue} 变为 ${newValue}`);
|
||||
// 调整AI行为
|
||||
});
|
||||
|
||||
// 取消监听
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 使用对象池
|
||||
### 1. 降低更新频率
|
||||
|
||||
复用行为树实体以减少GC压力:
|
||||
对于不需要每帧更新的AI,可以使用冷却装饰器:
|
||||
|
||||
```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逻辑
|
||||
// 每0.1秒执行一次
|
||||
const ai = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.1, 'ThrottleRoot')
|
||||
.selector('MainLogic')
|
||||
// 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
|
||||
### 2. 条件缓存
|
||||
|
||||
在自定义执行器中缓存昂贵的条件检查结果:
|
||||
|
||||
```typescript
|
||||
export class CachedCheck implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime, totalTime } = context;
|
||||
const cacheTime = state.lastCheckTime || 0;
|
||||
|
||||
// 如果缓存未过期(1秒内),直接使用缓存结果
|
||||
if (totalTime - cacheTime < 1.0) {
|
||||
return state.cachedResult || TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行昂贵的检查
|
||||
const result = performExpensiveCheck();
|
||||
const status = result ? TaskStatus.Success : TaskStatus.Failure;
|
||||
|
||||
// 缓存结果
|
||||
state.cachedResult = status;
|
||||
state.lastCheckTime = totalTime;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.cachedResult = undefined;
|
||||
context.state.lastCheckTime = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 分帧执行
|
||||
|
||||
将大量计算分散到多帧:
|
||||
|
||||
```typescript
|
||||
export class ProcessLargeDataset implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime } = context;
|
||||
|
||||
const data = runtime.getBlackboardValue<any[]>('dataset') || [];
|
||||
let processedIndex = state.processedIndex || 0;
|
||||
|
||||
const batchSize = 100; // 每帧处理100个
|
||||
const endIndex = Math.min(processedIndex + batchSize, data.length);
|
||||
|
||||
for (let i = processedIndex; i < endIndex; i++) {
|
||||
processItem(data[i]);
|
||||
}
|
||||
|
||||
state.processedIndex = endIndex;
|
||||
|
||||
if (endIndex >= data.length) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
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;
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.processedIndex = 0;
|
||||
}
|
||||
|
||||
// 执行昂贵的检查
|
||||
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. 日志节点
|
||||
### 1. 使用日志节点
|
||||
|
||||
在关键位置添加日志:
|
||||
在关键位置添加日志:
|
||||
|
||||
```typescript
|
||||
.log('开始战斗序列', 'info')
|
||||
.sequence('Combat')
|
||||
.log('检查生命值', 'debug')
|
||||
.compareBlackboardValue('health', CompareOperator.Greater, 0)
|
||||
.log('执行攻击', 'info')
|
||||
.action('Attack', () => TaskStatus.Success)
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('Debug')
|
||||
.log('开始战斗序列', 'StartCombat')
|
||||
.sequence('Combat')
|
||||
.log('检查生命值', 'CheckHealth')
|
||||
.blackboardCompare('health', 0, 'greater')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 黑板快照
|
||||
|
||||
定期输出黑板状态:
|
||||
### 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;
|
||||
})
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 输出所有黑板变量
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
|
||||
// 输出活动节点
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 3. 条件断言
|
||||
|
||||
验证重要条件:
|
||||
### 3. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
.action('AssertPlayerExists', (entity, blackboard) => {
|
||||
const player = blackboard?.getValue('player');
|
||||
export class DebugAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (!player) {
|
||||
console.error('断言失败: 玩家不存在');
|
||||
return TaskStatus.Failure;
|
||||
console.log(`[${nodeData.name}] 开始执行`);
|
||||
console.log('配置:', nodeData.config);
|
||||
console.log('状态:', state);
|
||||
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||
|
||||
// 执行逻辑...
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能分析
|
||||
|
||||
测量节点执行时间:
|
||||
测量节点执行时间:
|
||||
|
||||
```typescript
|
||||
.action('ProfiledAction', (entity, blackboard) => {
|
||||
const startTime = performance.now();
|
||||
export class ProfiledAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 执行操作
|
||||
doSomething();
|
||||
// 执行操作
|
||||
doSomething();
|
||||
|
||||
const elapsed = performance.now() - startTime;
|
||||
console.log(`操作耗时: ${elapsed.toFixed(2)}ms`);
|
||||
const elapsed = performance.now() - startTime;
|
||||
console.log(`[${context.nodeData.name}] 耗时: ${elapsed.toFixed(2)}ms`);
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
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()
|
||||
const fsm = BehaviorTreeBuilder.create('StateMachine')
|
||||
.defineBlackboardVariable('currentState', 'idle')
|
||||
.selector('StateSwitch')
|
||||
// Idle状态
|
||||
.sequence('IdleState')
|
||||
.checkBlackboardValue('currentState', 'idle')
|
||||
.action('IdleBehavior', (e, bb) => {
|
||||
console.log('执行Idle行为');
|
||||
// 状态转换条件
|
||||
if (shouldTransitionToMove()) {
|
||||
bb?.setValue('currentState', 'move');
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.blackboardCompare('currentState', 'idle', 'equals')
|
||||
.log('执行Idle行为', 'IdleBehavior')
|
||||
.end()
|
||||
// Move状态
|
||||
.sequence('MoveState')
|
||||
.checkBlackboardValue('currentState', 'move')
|
||||
.action('MoveBehavior', (e, bb) => {
|
||||
console.log('执行Move行为');
|
||||
if (shouldTransitionToAttack()) {
|
||||
bb?.setValue('currentState', 'attack');
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.blackboardCompare('currentState', 'move', 'equals')
|
||||
.log('执行Move行为', 'MoveBehavior')
|
||||
.end()
|
||||
// Attack状态
|
||||
.sequence('AttackState')
|
||||
.checkBlackboardValue('currentState', 'attack')
|
||||
.action('AttackBehavior', (e, bb) => {
|
||||
console.log('执行Attack行为');
|
||||
if (shouldTransitionToIdle()) {
|
||||
bb?.setValue('currentState', 'idle');
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.blackboardCompare('currentState', 'attack', 'equals')
|
||||
.log('执行Attack行为', 'AttackBehavior')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 优先级队列模式
|
||||
|
||||
按优先级执行任务:
|
||||
状态转换通过修改黑板变量实现:
|
||||
|
||||
```typescript
|
||||
.selector('PriorityQueue')
|
||||
// 最高优先级:生存
|
||||
.sequence('Survive')
|
||||
.compareBlackboardValue('health', CompareOperator.Less, 20)
|
||||
.action('Heal', () => TaskStatus.Success)
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('currentState', 'move');
|
||||
```
|
||||
|
||||
### 2. 优先级队列模式
|
||||
|
||||
按优先级执行任务:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('PriorityQueue')
|
||||
.selector('Priorities')
|
||||
// 最高优先级:生存
|
||||
.sequence('Survive')
|
||||
.blackboardCompare('health', 20, 'less')
|
||||
.log('治疗', 'Heal')
|
||||
.end()
|
||||
// 中优先级:战斗
|
||||
.sequence('Combat')
|
||||
.blackboardExists('nearbyEnemy')
|
||||
.log('战斗', 'Fight')
|
||||
.end()
|
||||
// 低优先级:收集资源
|
||||
.sequence('Gather')
|
||||
.log('收集资源', 'CollectResources')
|
||||
.end()
|
||||
.end()
|
||||
// 中优先级:战斗
|
||||
.sequence('Combat')
|
||||
.checkBlackboardExists('nearbyEnemy', true)
|
||||
.action('Fight', () => TaskStatus.Success)
|
||||
.end()
|
||||
// 低优先级:收集资源
|
||||
.sequence('Gather')
|
||||
.action('CollectResources', () => TaskStatus.Success)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 3. 并行任务模式
|
||||
|
||||
同时执行多个任务:
|
||||
同时执行多个任务:
|
||||
|
||||
```typescript
|
||||
.parallel(ParallelPolicy.RequireAll) // 所有任务都要成功
|
||||
.action('PlayAnimation', () => TaskStatus.Success)
|
||||
.action('PlaySound', () => TaskStatus.Success)
|
||||
.action('SpawnParticles', () => TaskStatus.Success)
|
||||
const tree = BehaviorTreeBuilder.create('ParallelTasks')
|
||||
.parallel('Effects', { successPolicy: 'all' })
|
||||
.log('播放动画', 'PlayAnimation')
|
||||
.log('播放音效', 'PlaySound')
|
||||
.log('生成粒子', 'SpawnParticles')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 4. 重试模式
|
||||
|
||||
失败时重试:
|
||||
|
||||
```typescript
|
||||
// 使用自定义重试装饰器(参见custom-actions.md中的RetryDecorator示例)
|
||||
// 或者使用UntilSuccess装饰器
|
||||
const tree = BehaviorTreeBuilder.create('Retry')
|
||||
.untilSuccess('RetryUntilSuccess')
|
||||
.log('尝试操作', 'TryOperation')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 5. 超时模式
|
||||
|
||||
限制任务执行时间:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Timeout')
|
||||
.timeout(5.0, 'TimeLimit')
|
||||
.log('长时间运行的任务', 'LongTask')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Cocos Creator集成
|
||||
|
||||
参见[Cocos Creator集成指南](./cocos-integration.md)
|
||||
|
||||
### LayaAir集成
|
||||
|
||||
参见[LayaAir集成指南](./laya-integration.md)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用黑板
|
||||
|
||||
```typescript
|
||||
// 好的做法:使用类型化的黑板访问
|
||||
const health = runtime.getBlackboardValue<number>('health');
|
||||
|
||||
// 好的做法:定义所有黑板变量
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('state', 'idle')
|
||||
// ...
|
||||
```
|
||||
|
||||
### 2. 避免过深的树结构
|
||||
|
||||
```typescript
|
||||
// 不好:嵌套过深
|
||||
.selector()
|
||||
.sequence()
|
||||
.selector()
|
||||
.sequence()
|
||||
.selector()
|
||||
// 太深了!
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 好:使用合理的深度
|
||||
.selector()
|
||||
.sequence()
|
||||
.log('Action1')
|
||||
.log('Action2')
|
||||
.end()
|
||||
.sequence()
|
||||
.log('Action3')
|
||||
.log('Action4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 3. 使用有意义的节点名称
|
||||
|
||||
```typescript
|
||||
// 好的做法
|
||||
.selector('CombatDecision')
|
||||
.sequence('AttackEnemy')
|
||||
.blackboardExists('target', 'HasTarget')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 不好的做法
|
||||
.selector('Node1')
|
||||
.sequence('Node2')
|
||||
.blackboardExists('target', 'Node3')
|
||||
.log('Attack', 'Node4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 4. 模块化设计
|
||||
|
||||
将复杂逻辑分解为多个独立的行为树,在需要时组合使用。
|
||||
|
||||
### 5. 性能考虑
|
||||
|
||||
- 避免在每帧执行昂贵的操作
|
||||
- 使用冷却装饰器控制执行频率
|
||||
- 缓存计算结果
|
||||
- 合理使用并行节点
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义动作](./custom-actions.md)学习如何创建自定义行为节点
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide.md)学习可视化编辑
|
||||
|
||||
506
docs/guide/behavior-tree/asset-management.md
Normal file
506
docs/guide/behavior-tree/asset-management.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# 资产管理
|
||||
|
||||
本文介绍如何加载、管理和复用行为树资产。
|
||||
|
||||
## 为什么需要资产管理?
|
||||
|
||||
在实际游戏开发中,你可能会遇到以下场景:
|
||||
|
||||
1. **多个实体共享同一个行为树** - 100个敌人使用同一套AI逻辑
|
||||
2. **动态加载行为树** - 从JSON文件加载行为树配置
|
||||
3. **子树复用** - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
|
||||
4. **运行时切换行为树** - 敌人在不同阶段使用不同的行为树
|
||||
|
||||
## BehaviorTreeAssetManager
|
||||
|
||||
框架提供了 `BehaviorTreeAssetManager` 服务来统一管理行为树资产。
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **BehaviorTreeData(行为树数据)**:行为树的定义,可以被多个实体共享
|
||||
- **BehaviorTreeRuntimeComponent(运行时组件)**:每个实体独立的运行时状态
|
||||
- **AssetManager(资产管理器)**:统一管理所有 BehaviorTreeData
|
||||
|
||||
### 基本使用
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 1. 获取资产管理器(插件已自动注册)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 2. 创建并注册行为树资产
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('MainBehavior')
|
||||
.log('攻击')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(enemyAI);
|
||||
|
||||
// 3. 为多个实体使用同一份资产
|
||||
const enemy1 = scene.createEntity('Enemy1');
|
||||
const enemy2 = scene.createEntity('Enemy2');
|
||||
const enemy3 = scene.createEntity('Enemy3');
|
||||
|
||||
// 获取共享的资产
|
||||
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||
|
||||
if (sharedTree) {
|
||||
BehaviorTreeStarter.start(enemy1, sharedTree);
|
||||
BehaviorTreeStarter.start(enemy2, sharedTree);
|
||||
BehaviorTreeStarter.start(enemy3, sharedTree);
|
||||
}
|
||||
```
|
||||
|
||||
### 资产管理器 API
|
||||
|
||||
```typescript
|
||||
// 加载资产
|
||||
assetManager.loadAsset(treeData);
|
||||
|
||||
// 获取资产
|
||||
const tree = assetManager.getAsset('TreeID');
|
||||
|
||||
// 检查资产是否存在
|
||||
if (assetManager.hasAsset('TreeID')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 卸载资产
|
||||
assetManager.unloadAsset('TreeID');
|
||||
|
||||
// 获取所有资产ID
|
||||
const allIds = assetManager.getAllAssetIds();
|
||||
|
||||
// 清空所有资产
|
||||
assetManager.clearAll();
|
||||
```
|
||||
|
||||
## 从文件加载行为树
|
||||
|
||||
### JSON 格式
|
||||
|
||||
行为树可以导出为 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": "EnemyAI",
|
||||
"description": "敌人AI行为树"
|
||||
},
|
||||
"rootNodeId": "root-1",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "root-1",
|
||||
"name": "RootSelector",
|
||||
"nodeType": "Composite",
|
||||
"data": {
|
||||
"compositeType": "Selector"
|
||||
},
|
||||
"children": ["combat-1", "patrol-1"]
|
||||
},
|
||||
{
|
||||
"id": "combat-1",
|
||||
"name": "Combat",
|
||||
"nodeType": "Action",
|
||||
"data": {
|
||||
"actionType": "LogAction",
|
||||
"message": "攻击敌人"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"blackboard": [
|
||||
{
|
||||
"name": "health",
|
||||
"type": "number",
|
||||
"defaultValue": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 加载 JSON 文件
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeAssetManager
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function loadTreeFromFile(filePath: string) {
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 1. 读取文件内容
|
||||
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||
|
||||
// 2. 反序列化
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||
|
||||
// 3. 加载到资产管理器
|
||||
assetManager.loadAsset(treeData);
|
||||
|
||||
return treeData;
|
||||
}
|
||||
|
||||
// 使用
|
||||
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
## 子树(SubTree)
|
||||
|
||||
子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。
|
||||
|
||||
### 为什么使用子树?
|
||||
|
||||
1. **代码复用** - 避免重复定义相同的行为
|
||||
2. **模块化** - 将复杂的行为树拆分成小的可管理单元
|
||||
3. **团队协作** - 不同成员可以独立开发不同的子树
|
||||
|
||||
### 创建子树
|
||||
|
||||
```typescript
|
||||
// 1. 创建巡逻子树
|
||||
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
|
||||
.sequence('Patrol')
|
||||
.log('选择巡逻点', 'PickWaypoint')
|
||||
.log('移动到巡逻点', 'MoveToWaypoint')
|
||||
.wait(2.0, 'WaitAtWaypoint')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 2. 创建追击子树
|
||||
const chaseTree = BehaviorTreeBuilder.create('ChaseBehavior')
|
||||
.sequence('Chase')
|
||||
.log('锁定目标', 'LockTarget')
|
||||
.log('追击目标', 'ChaseTarget')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 3. 注册子树到资产管理器
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
assetManager.loadAsset(patrolTree);
|
||||
assetManager.loadAsset(chaseTree);
|
||||
```
|
||||
|
||||
### 使用子树
|
||||
|
||||
```typescript
|
||||
// 在主行为树中使用子树
|
||||
const mainTree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('hasTarget', false)
|
||||
|
||||
.selector('MainBehavior')
|
||||
// 条件:发现目标时执行追击子树
|
||||
.sequence('CombatBranch')
|
||||
.blackboardExists('hasTarget')
|
||||
.subTree('ChaseBehavior', { shareBlackboard: true })
|
||||
.end()
|
||||
|
||||
// 默认:执行巡逻子树
|
||||
.subTree('PatrolBehavior', { shareBlackboard: true })
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(mainTree);
|
||||
|
||||
// 启动主行为树
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(enemy, mainTree);
|
||||
```
|
||||
|
||||
### SubTree 配置选项
|
||||
|
||||
```typescript
|
||||
.subTree('SubTreeID', {
|
||||
shareBlackboard: true, // 是否共享黑板(默认true)
|
||||
})
|
||||
```
|
||||
|
||||
- **shareBlackboard: true** - 子树和父树共享黑板变量
|
||||
- **shareBlackboard: false** - 子树使用独立的黑板
|
||||
|
||||
## 资源预加载
|
||||
|
||||
在游戏启动时预加载所有行为树资产:
|
||||
|
||||
```typescript
|
||||
class BehaviorTreePreloader {
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor() {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
async preloadAll() {
|
||||
// 定义所有行为树文件
|
||||
const treeFiles = [
|
||||
'/assets/ai/enemy-ai.btree.json',
|
||||
'/assets/ai/boss-ai.btree.json',
|
||||
'/assets/ai/patrol.btree.json',
|
||||
'/assets/ai/chase.btree.json'
|
||||
];
|
||||
|
||||
// 并行加载所有文件
|
||||
const loadPromises = treeFiles.map(file => this.loadTree(file));
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
console.log(`已加载 ${this.assetManager.getAssetCount()} 个行为树资产`);
|
||||
}
|
||||
|
||||
private async loadTree(filePath: string) {
|
||||
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||
this.assetManager.loadAsset(treeData);
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏启动时调用
|
||||
const preloader = new BehaviorTreePreloader();
|
||||
await preloader.preloadAll();
|
||||
```
|
||||
|
||||
## 运行时切换行为树
|
||||
|
||||
敌人在不同阶段使用不同的行为树:
|
||||
|
||||
```typescript
|
||||
class EnemyAI {
|
||||
private entity: Entity;
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor(entity: Entity) {
|
||||
this.entity = entity;
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
// 切换到巡逻AI
|
||||
switchToPatrol() {
|
||||
const tree = this.assetManager.getAsset('PatrolAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到战斗AI
|
||||
switchToCombat() {
|
||||
const tree = this.assetManager.getAsset('CombatAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到狂暴模式
|
||||
switchToBerserk() {
|
||||
const tree = this.assetManager.getAsset('BerserkAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const enemyAI = new EnemyAI(enemyEntity);
|
||||
|
||||
// Boss血量低于30%时进入狂暴
|
||||
const runtime = enemyEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
const health = runtime?.getBlackboardValue<number>('health');
|
||||
|
||||
if (health && health < 30) {
|
||||
enemyAI.switchToBerserk();
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 1. 共享行为树数据
|
||||
|
||||
```typescript
|
||||
// 好的做法:100个敌人共享1份BehaviorTreeData
|
||||
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, sharedTree!); // 共享数据
|
||||
}
|
||||
|
||||
// 不好的做法:每个敌人创建独立的BehaviorTreeData
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI') // 重复创建
|
||||
// ... 节点定义
|
||||
.build();
|
||||
BehaviorTreeStarter.start(enemy, tree);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 及时卸载不用的资产
|
||||
|
||||
```typescript
|
||||
// 关卡结束时卸载该关卡的AI
|
||||
function onLevelEnd() {
|
||||
assetManager.unloadAsset('Level1BossAI');
|
||||
assetManager.unloadAsset('Level1EnemyAI');
|
||||
}
|
||||
|
||||
// 加载新关卡的AI
|
||||
function onLevelStart() {
|
||||
const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
|
||||
assetManager.loadAsset(boss2AI);
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:多敌人类型的游戏
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function setupGame() {
|
||||
// 1. 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 2. 创建共享的子树
|
||||
const patrolTree = BehaviorTreeBuilder.create('Patrol')
|
||||
.sequence('PatrolLoop')
|
||||
.log('巡逻')
|
||||
.wait(1.0)
|
||||
.end()
|
||||
.build();
|
||||
|
||||
const combatTree = BehaviorTreeBuilder.create('Combat')
|
||||
.sequence('CombatLoop')
|
||||
.log('战斗')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(patrolTree);
|
||||
assetManager.loadAsset(combatTree);
|
||||
|
||||
// 3. 创建不同类型敌人的AI
|
||||
const meleeEnemyAI = BehaviorTreeBuilder.create('MeleeEnemyAI')
|
||||
.selector('MeleeBehavior')
|
||||
.sequence('Attack')
|
||||
.blackboardExists('target')
|
||||
.log('近战攻击')
|
||||
.end()
|
||||
.subTree('Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
const rangedEnemyAI = BehaviorTreeBuilder.create('RangedEnemyAI')
|
||||
.selector('RangedBehavior')
|
||||
.sequence('Attack')
|
||||
.blackboardExists('target')
|
||||
.log('远程攻击')
|
||||
.end()
|
||||
.subTree('Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(meleeEnemyAI);
|
||||
assetManager.loadAsset(rangedEnemyAI);
|
||||
|
||||
// 4. 创建多个敌人实体
|
||||
// 10个近战敌人共享同一份AI
|
||||
const meleeAI = assetManager.getAsset('MeleeEnemyAI')!;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const enemy = scene.createEntity(`MeleeEnemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, meleeAI);
|
||||
}
|
||||
|
||||
// 5个远程敌人共享同一份AI
|
||||
const rangedAI = assetManager.getAsset('RangedEnemyAI')!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = scene.createEntity(`RangedEnemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, rangedAI);
|
||||
}
|
||||
|
||||
console.log(`已创建 15 个敌人,使用 ${assetManager.getAssetCount()} 个行为树资产`);
|
||||
|
||||
// 5. 游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.016);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
setupGame();
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何检查资产是否已加载?
|
||||
|
||||
```typescript
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
if (!assetManager.hasAsset('EnemyAI')) {
|
||||
// 加载资产
|
||||
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||
assetManager.loadAsset(tree);
|
||||
}
|
||||
```
|
||||
|
||||
### 子树找不到怎么办?
|
||||
|
||||
确保子树已经加载到资产管理器中:
|
||||
|
||||
```typescript
|
||||
// 1. 先加载子树
|
||||
const subTree = BehaviorTreeBuilder.create('SubTreeID')
|
||||
// ...
|
||||
.build();
|
||||
assetManager.loadAsset(subTree);
|
||||
|
||||
// 2. 再加载使用子树的主树
|
||||
const mainTree = BehaviorTreeBuilder.create('MainTree')
|
||||
.subTree('SubTreeID')
|
||||
.build();
|
||||
```
|
||||
|
||||
### 如何导出行为树为 JSON?
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||
|
||||
const tree = BehaviorTreeBuilder.create('MyTree')
|
||||
// ... 节点定义
|
||||
.build();
|
||||
|
||||
// 序列化为JSON字符串
|
||||
const json = BehaviorTreeAssetSerializer.serialize(tree);
|
||||
|
||||
// 保存到文件或发送到服务器
|
||||
console.log(json);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计
|
||||
@@ -1,22 +1,22 @@
|
||||
# 最佳实践
|
||||
|
||||
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||
|
||||
## 行为树设计原则
|
||||
|
||||
### 1. 保持树的层次清晰
|
||||
|
||||
将复杂行为分解成清晰的层次结构:
|
||||
将复杂行为分解成清晰的层次结构:
|
||||
|
||||
```
|
||||
Root Selector
|
||||
├── Emergency (高优先级:紧急情况)
|
||||
├── Emergency (高优先级:紧急情况)
|
||||
│ ├── FleeFromDanger
|
||||
│ └── CallForHelp
|
||||
├── Combat (中优先级:战斗)
|
||||
├── Combat (中优先级:战斗)
|
||||
│ ├── Attack
|
||||
│ └── Defend
|
||||
└── Idle (低优先级:空闲)
|
||||
└── Idle (低优先级:空闲)
|
||||
├── Patrol
|
||||
└── Rest
|
||||
```
|
||||
@@ -24,309 +24,272 @@ Root Selector
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事:
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
```typescript
|
||||
// 好的设计
|
||||
// 好的设计 - 使用内置节点
|
||||
.sequence('AttackSequence')
|
||||
.condition(hasTarget, 'CheckTarget')
|
||||
.action(aim, 'Aim')
|
||||
.action(fire, 'Fire')
|
||||
.blackboardExists('target', 'CheckTarget')
|
||||
.log('瞄准', 'Aim')
|
||||
.log('开火', 'Fire')
|
||||
.end()
|
||||
|
||||
// 不好的设计 - 一个动作做太多事
|
||||
.action('AttackPlayer', () => {
|
||||
checkTarget();
|
||||
aim();
|
||||
fire();
|
||||
playAnimation();
|
||||
playSound();
|
||||
// 太多职责了!
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 使用描述性名称
|
||||
|
||||
节点名称应该清楚地表达其功能:
|
||||
节点名称应该清楚地表达其功能:
|
||||
|
||||
```typescript
|
||||
// 好的命名
|
||||
.condition(isHealthLow, 'CheckHealthLow')
|
||||
.action(findNearestHealthPack, 'FindHealthPack')
|
||||
.action(moveToHealthPack, 'MoveToHealthPack')
|
||||
.blackboardCompare('health', 20, 'less', 'CheckHealthLow')
|
||||
.log('寻找最近的医疗包', 'FindHealthPack')
|
||||
.log('移动到医疗包', 'MoveToHealthPack')
|
||||
|
||||
// 不好的命名
|
||||
.condition(check1, 'C1')
|
||||
.action(doSomething, 'Action1')
|
||||
.action(move, 'A2')
|
||||
.blackboardCompare('health', 20, 'less', 'C1')
|
||||
.log('Do something', 'Action1')
|
||||
.log('Move', 'A2')
|
||||
```
|
||||
|
||||
## 黑板变量管理
|
||||
|
||||
### 1. 变量命名规范
|
||||
|
||||
使用清晰的命名约定:
|
||||
使用清晰的命名约定:
|
||||
|
||||
```typescript
|
||||
.blackboard()
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
// 状态变量
|
||||
.defineVariable('currentState', BlackboardValueType.String, 'idle')
|
||||
.defineVariable('isMoving', BlackboardValueType.Boolean, false)
|
||||
.defineBlackboardVariable('currentState', 'idle')
|
||||
.defineBlackboardVariable('isMoving', false)
|
||||
|
||||
// 目标和引用
|
||||
.defineVariable('targetEnemy', BlackboardValueType.Object, null)
|
||||
.defineVariable('patrolPoints', BlackboardValueType.Array, [])
|
||||
.defineBlackboardVariable('targetEnemy', null)
|
||||
.defineBlackboardVariable('patrolPoints', [])
|
||||
|
||||
// 配置参数
|
||||
.defineVariable('attackRange', BlackboardValueType.Number, 5.0)
|
||||
.defineVariable('moveSpeed', BlackboardValueType.Number, 10.0)
|
||||
.defineBlackboardVariable('attackRange', 5.0)
|
||||
.defineBlackboardVariable('moveSpeed', 10.0)
|
||||
|
||||
// 临时数据
|
||||
.defineVariable('lastAttackTime', BlackboardValueType.Number, 0)
|
||||
.defineVariable('searchAttempts', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
.defineBlackboardVariable('lastAttackTime', 0)
|
||||
.defineBlackboardVariable('searchAttempts', 0)
|
||||
// ...
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 避免过度使用黑板
|
||||
|
||||
只在需要跨节点共享的数据才放入黑板:
|
||||
只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量:
|
||||
|
||||
```typescript
|
||||
// 不好的做法 - 局部变量放黑板
|
||||
.action('Calculate', (e, bb) => {
|
||||
bb?.setValue('temp1', 10); // 不需要
|
||||
bb?.setValue('temp2', 20); // 不需要
|
||||
bb?.setValue('result', 30); // 如果其他节点需要,这个可以
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
|
||||
// 好的做法 - 使用局部变量
|
||||
.action('Calculate', (e, bb) => {
|
||||
const temp1 = 10;
|
||||
const temp2 = 20;
|
||||
const result = temp1 + temp2;
|
||||
bb?.setValue('calculationResult', result); // 只保存需要共享的结果
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
export class CalculateAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
// 局部计算
|
||||
const temp1 = 10;
|
||||
const temp2 = 20;
|
||||
const result = temp1 + temp2;
|
||||
|
||||
// 只保存需要共享的结果
|
||||
context.runtime.setBlackboardValue('calculationResult', result);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用类型安全的访问
|
||||
|
||||
```typescript
|
||||
// 定义类型接口
|
||||
interface EnemyBlackboard {
|
||||
health: number;
|
||||
target: Entity | null;
|
||||
state: 'idle' | 'patrol' | 'chase' | 'attack';
|
||||
}
|
||||
export class TypeSafeAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
|
||||
// 使用时进行类型检查
|
||||
.action('UseBlackboard', (e, bb) => {
|
||||
const health = bb?.getValue<number>('health');
|
||||
const target = bb?.getValue<Entity | null>('target');
|
||||
const state = bb?.getValue<string>('state');
|
||||
// 使用泛型进行类型安全访问
|
||||
const health = runtime.getBlackboardValue<number>('health');
|
||||
const target = runtime.getBlackboardValue<Entity | null>('target');
|
||||
const state = runtime.getBlackboardValue<string>('currentState');
|
||||
|
||||
if (health !== undefined && health < 30) {
|
||||
bb?.setValue('state', 'flee');
|
||||
if (health !== undefined && health < 30) {
|
||||
runtime.setBlackboardValue('currentState', 'flee');
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 条件节点设计
|
||||
## 执行器设计
|
||||
|
||||
### 1. 条件应该是无副作用的
|
||||
### 1. 保持执行器无状态
|
||||
|
||||
条件检查不应该修改状态:
|
||||
状态必须存储在`context.state`中,而不是执行器实例:
|
||||
|
||||
```typescript
|
||||
// 正确的做法
|
||||
export class TimedAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
if (!context.state.startTime) {
|
||||
context.state.startTime = context.totalTime;
|
||||
}
|
||||
|
||||
const elapsed = context.totalTime - context.state.startTime;
|
||||
|
||||
if (elapsed >= 3.0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.startTime = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 条件应该是无副作用的
|
||||
|
||||
条件检查不应该修改状态:
|
||||
|
||||
```typescript
|
||||
// 好的做法 - 只读检查
|
||||
.condition((e, bb) => {
|
||||
const health = bb?.getValue('health') || 0;
|
||||
return health < 30;
|
||||
}, 'IsHealthLow')
|
||||
|
||||
// 不好的做法 - 条件中修改状态
|
||||
.condition((e, bb) => {
|
||||
const health = bb?.getValue('health') || 0;
|
||||
if (health < 30) {
|
||||
bb?.setValue('needsHealing', true); // 不应该在条件中修改
|
||||
return true;
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'IsHealthLow',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '检查生命值低',
|
||||
category: '条件',
|
||||
configSchema: {
|
||||
threshold: {
|
||||
type: 'number',
|
||||
default: 30,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
```
|
||||
export class IsHealthLow implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const threshold = BindingHelper.getValue<number>(context, 'threshold', 30);
|
||||
const health = context.runtime.getBlackboardValue<number>('health') || 0;
|
||||
|
||||
### 2. 复杂条件拆分
|
||||
|
||||
将复杂条件拆分为多个简单条件:
|
||||
|
||||
```typescript
|
||||
// 不好的做法
|
||||
.condition((e, bb) => {
|
||||
const health = bb?.getValue('health');
|
||||
const ammo = bb?.getValue('ammo');
|
||||
const enemy = bb?.getValue('enemy');
|
||||
const distance = calculateDistance(e, enemy);
|
||||
|
||||
return health > 50 && ammo > 0 && enemy && distance < 10;
|
||||
}, 'ComplexCheck')
|
||||
|
||||
// 好的做法
|
||||
.sequence('CanAttack')
|
||||
.condition((e, bb) => (bb?.getValue('health') || 0) > 50, 'HasEnoughHealth')
|
||||
.condition((e, bb) => (bb?.getValue('ammo') || 0) > 0, 'HasAmmo')
|
||||
.condition((e, bb) => bb?.getValue('enemy') != null, 'HasTarget')
|
||||
.condition((e, bb) => {
|
||||
const enemy = bb?.getValue('enemy');
|
||||
return calculateDistance(e, enemy) < 10;
|
||||
}, 'InRange')
|
||||
.end()
|
||||
```
|
||||
|
||||
## 动作节点设计
|
||||
|
||||
### 1. 使用状态机模式处理长时间动作
|
||||
|
||||
```typescript
|
||||
.action('ChargeLaser', (e, bb, dt) => {
|
||||
// 初始化状态
|
||||
if (!bb?.hasVariable('chargeState')) {
|
||||
bb?.setValue('chargeState', 'charging');
|
||||
bb?.setValue('chargeTime', 0);
|
||||
return health < threshold ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
const state = bb?.getValue('chargeState');
|
||||
const chargeTime = bb?.getValue('chargeTime') || 0;
|
||||
### 3. 错误处理
|
||||
|
||||
switch (state) {
|
||||
case 'charging':
|
||||
bb?.setValue('chargeTime', chargeTime + dt);
|
||||
```typescript
|
||||
export class SafeAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
try {
|
||||
const resourceId = context.runtime.getBlackboardValue('resourceId');
|
||||
|
||||
if (chargeTime >= 3.0) {
|
||||
bb?.setValue('chargeState', 'ready');
|
||||
if (!resourceId) {
|
||||
console.error('[SafeAction] 资源ID未设置');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
return TaskStatus.Running;
|
||||
|
||||
case 'ready':
|
||||
// 发射激光
|
||||
fireLaser();
|
||||
bb?.setValue('chargeState', null);
|
||||
bb?.setValue('chargeTime', 0);
|
||||
// 执行操作...
|
||||
|
||||
return TaskStatus.Success;
|
||||
|
||||
default:
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```typescript
|
||||
.action('LoadResource', async (e, bb) => {
|
||||
try {
|
||||
const resourceId = bb?.getValue('resourceId');
|
||||
if (!resourceId) {
|
||||
console.error('资源ID未设置');
|
||||
} catch (error) {
|
||||
console.error('[SafeAction] 执行失败:', error);
|
||||
context.runtime.setBlackboardValue('lastError', error.message);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const resource = await loadResource(resourceId);
|
||||
bb?.setValue('loadedResource', resource);
|
||||
return TaskStatus.Success;
|
||||
|
||||
} catch (error) {
|
||||
console.error('资源加载失败:', error);
|
||||
bb?.setValue('loadError', error.message);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化技巧
|
||||
|
||||
### 1. 使用冷却装饰器
|
||||
|
||||
避免高频执行昂贵操作:
|
||||
避免高频执行昂贵操作:
|
||||
|
||||
```typescript
|
||||
.cooldown(1.0) // 最多每秒执行一次
|
||||
.action('ExpensiveSearch', () => {
|
||||
// 昂贵的搜索操作
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(1.0, 'ThrottleSearch') // 最多每秒执行一次
|
||||
.log('昂贵的搜索操作', 'ExpensiveSearch')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 缓存计算结果
|
||||
|
||||
```typescript
|
||||
.action('FindNearestEnemy', (e, bb) => {
|
||||
// 检查缓存是否有效
|
||||
const cacheTime = bb?.getValue('enemyCacheTime') || 0;
|
||||
const currentTime = Date.now();
|
||||
export class CachedFindNearest implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime, totalTime } = context;
|
||||
|
||||
if (currentTime - cacheTime < 500) { // 缓存500ms
|
||||
// 使用缓存结果
|
||||
return bb?.getValue('nearestEnemy') ? TaskStatus.Success : TaskStatus.Failure;
|
||||
// 检查缓存是否有效
|
||||
const cacheTime = state.enemyCacheTime || 0;
|
||||
|
||||
if (totalTime - cacheTime < 0.5) { // 缓存0.5秒
|
||||
const cached = runtime.getBlackboardValue('nearestEnemy');
|
||||
return cached ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
const nearest = findNearestEnemy();
|
||||
runtime.setBlackboardValue('nearestEnemy', nearest);
|
||||
state.enemyCacheTime = totalTime;
|
||||
|
||||
return nearest ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
const nearest = findNearestEnemy();
|
||||
bb?.setValue('nearestEnemy', nearest);
|
||||
bb?.setValue('enemyCacheTime', currentTime);
|
||||
|
||||
return nearest ? TaskStatus.Success : TaskStatus.Failure;
|
||||
})
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.enemyCacheTime = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用早期退出
|
||||
|
||||
```typescript
|
||||
.selector('FindTarget')
|
||||
// 先检查缓存的目标
|
||||
.condition((e, bb) => bb?.hasVariable('cachedTarget'), 'HasCachedTarget')
|
||||
const tree = BehaviorTreeBuilder.create('EarlyExit')
|
||||
.selector('FindTarget')
|
||||
// 先检查缓存的目标
|
||||
.blackboardExists('cachedTarget', 'HasCachedTarget')
|
||||
|
||||
// 没有缓存才进行搜索
|
||||
.action('SearchNewTarget', (e, bb) => {
|
||||
const target = performExpensiveSearch();
|
||||
bb?.setValue('cachedTarget', target);
|
||||
return target ? TaskStatus.Success : TaskStatus.Failure;
|
||||
})
|
||||
.end()
|
||||
// 没有缓存才进行搜索(需要自定义执行器)
|
||||
.log('执行昂贵的搜索', 'SearchNewTarget')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 可维护性
|
||||
|
||||
### 1. 使用子树模块化
|
||||
|
||||
将可复用的行为提取为子树:
|
||||
### 1. 使用有意义的节点名称
|
||||
|
||||
```typescript
|
||||
// 巡逻子树
|
||||
const patrolBehavior = BehaviorTreeBuilder.create(scene, 'Patrol')
|
||||
.sequence()
|
||||
.action('MoveToNextWaypoint', () => TaskStatus.Success)
|
||||
.wait(2.0)
|
||||
// 好的做法
|
||||
const tree = BehaviorTreeBuilder.create('CombatAI')
|
||||
.selector('CombatDecision')
|
||||
.sequence('AttackEnemy')
|
||||
.blackboardExists('target', 'HasTarget')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 主树中引用
|
||||
const mainTree = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.selector()
|
||||
.sequence('Combat')
|
||||
// 战斗逻辑
|
||||
// 不好的做法
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
.selector('Node1')
|
||||
.sequence('Node2')
|
||||
.blackboardExists('target', 'Node3')
|
||||
.log('Attack', 'Node4')
|
||||
.end()
|
||||
.subTree(patrolBehavior) // 复用巡逻行为
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 使用编辑器创建复杂树
|
||||
|
||||
对于复杂的AI,使用可视化编辑器:
|
||||
对于复杂的AI,使用可视化编辑器:
|
||||
|
||||
- 更直观的结构
|
||||
- 方便非程序员调整
|
||||
@@ -337,27 +300,26 @@ const mainTree = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
### 3. 添加注释和文档
|
||||
|
||||
```typescript
|
||||
const ai = BehaviorTreeBuilder.create(scene, 'BossAI')
|
||||
.blackboard()
|
||||
.defineVariable('phase', BlackboardValueType.Number, 1) // 1=正常, 2=狂暴, 3=濒死
|
||||
.endBlackboard()
|
||||
// 为行为树添加清晰的注释
|
||||
const bossAI = BehaviorTreeBuilder.create('BossAI')
|
||||
.defineBlackboardVariable('phase', 1) // 1=正常, 2=狂暴, 3=濒死
|
||||
|
||||
.selector('MainBehavior')
|
||||
// 阶段3:生命值<20%,使用终极技能
|
||||
// 阶段3: 生命值<20%,使用终极技能
|
||||
.sequence('Phase3')
|
||||
.compareBlackboardValue('phase', CompareOperator.Equal, 3)
|
||||
.action('UltimateAbility', () => TaskStatus.Success)
|
||||
.blackboardCompare('phase', 3, 'equals')
|
||||
.log('使用终极技能', 'UltimateAbility')
|
||||
.end()
|
||||
|
||||
// 阶段2:生命值<50%,进入狂暴
|
||||
// 阶段2: 生命值<50%,进入狂暴
|
||||
.sequence('Phase2')
|
||||
.compareBlackboardValue('phase', CompareOperator.Equal, 2)
|
||||
.action('BerserkMode', () => TaskStatus.Success)
|
||||
.blackboardCompare('phase', 2, 'equals')
|
||||
.log('进入狂暴模式', 'BerserkMode')
|
||||
.end()
|
||||
|
||||
// 阶段1:正常战斗
|
||||
// 阶段1: 正常战斗
|
||||
.sequence('Phase1')
|
||||
.action('NormalAttack', () => TaskStatus.Success)
|
||||
.log('普通攻击', 'NormalAttack')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
@@ -368,44 +330,52 @@ const ai = BehaviorTreeBuilder.create(scene, 'BossAI')
|
||||
### 1. 使用日志节点
|
||||
|
||||
```typescript
|
||||
.log('开始攻击序列', 'info')
|
||||
.sequence('Attack')
|
||||
.log('检查目标', 'debug')
|
||||
.condition(hasTarget)
|
||||
.log('执行攻击', 'info')
|
||||
.action(attack)
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('Debug')
|
||||
.log('开始攻击序列', 'StartAttack')
|
||||
.sequence('Attack')
|
||||
.log('检查目标', 'CheckTarget')
|
||||
.blackboardExists('target')
|
||||
.log('执行攻击', 'DoAttack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 添加断言
|
||||
### 2. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
.action('ValidateState', (e, bb) => {
|
||||
const health = bb?.getValue('health');
|
||||
const maxHealth = bb?.getValue('maxHealth');
|
||||
export class DebugAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
console.assert(health !== undefined, 'health不应为undefined');
|
||||
console.assert(maxHealth !== undefined, 'maxHealth不应为undefined');
|
||||
console.assert(health <= maxHealth, `health(${health})不应大于maxHealth(${maxHealth})`);
|
||||
console.group(`[${nodeData.name}]`);
|
||||
console.log('配置:', nodeData.config);
|
||||
console.log('状态:', state);
|
||||
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime.activeNodeIds));
|
||||
console.groupEnd();
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 状态可视化
|
||||
|
||||
```typescript
|
||||
.action('DebugState', (e, bb) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.group('AI State');
|
||||
console.log('Entity:', e.name);
|
||||
console.log('Health:', bb?.getValue('health'));
|
||||
console.log('State:', bb?.getValue('currentState'));
|
||||
console.log('Target:', bb?.getValue('target'));
|
||||
console.groupEnd();
|
||||
export class VisualizeState implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.group('AI State');
|
||||
console.log('Entity:', context.entity.name);
|
||||
console.log('Health:', context.runtime.getBlackboardValue('health'));
|
||||
console.log('State:', context.runtime.getBlackboardValue('currentState'));
|
||||
console.log('Target:', context.runtime.getBlackboardValue('target'));
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 常见反模式
|
||||
@@ -418,133 +388,81 @@ const ai = BehaviorTreeBuilder.create(scene, 'BossAI')
|
||||
.sequence()
|
||||
.sequence()
|
||||
.sequence()
|
||||
.action('DeepAction', () => TaskStatus.Success)
|
||||
.log('太深了', 'DeepAction')
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 好 - 使用子树扁平化
|
||||
const innerBehavior = BehaviorTreeBuilder.create(scene, 'Inner')
|
||||
.action('DeepAction', () => TaskStatus.Success)
|
||||
.build();
|
||||
|
||||
// 好 - 使用合理的深度
|
||||
.selector()
|
||||
.subTree(innerBehavior)
|
||||
.sequence()
|
||||
.log('Action1')
|
||||
.log('Action2')
|
||||
.end()
|
||||
.sequence()
|
||||
.log('Action3')
|
||||
.log('Action4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 2. 在行为树中实现游戏逻辑
|
||||
### 2. 在执行器中存储状态
|
||||
|
||||
```typescript
|
||||
// 不好 - 行为树不应包含具体游戏逻辑
|
||||
.action('Attack', (e, bb) => {
|
||||
const enemy = bb?.getValue('enemy');
|
||||
const damage = calculateDamage(e.getComponent(Weapon));
|
||||
enemy.health -= damage;
|
||||
// 错误 - 状态存储在执行器中
|
||||
export class BadAction implements INodeExecutor {
|
||||
private startTime = 0; // 错误!多个节点会共享这个值
|
||||
|
||||
if (enemy.health <= 0) {
|
||||
enemy.die();
|
||||
e.experience += enemy.expReward;
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
this.startTime = context.totalTime; // 错误!
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
playAttackAnimation();
|
||||
playAttackSound();
|
||||
// 太多细节了!
|
||||
})
|
||||
|
||||
// 好 - 行为树只负责决策,具体逻辑由系统处理
|
||||
.action('Attack', (e, bb) => {
|
||||
const enemy = bb?.getValue('enemy');
|
||||
// 发送攻击命令,具体逻辑由战斗系统处理
|
||||
Core.ecsAPI?.emit('combat:attack', { attacker: e, target: enemy });
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
// 正确 - 状态存储在context.state中
|
||||
export class GoodAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
if (!context.state.startTime) {
|
||||
context.state.startTime = context.totalTime; // 正确!
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 频繁修改黑板
|
||||
|
||||
```typescript
|
||||
// 不好 - 每帧都修改黑板
|
||||
.action('UpdatePosition', (e, bb, dt) => {
|
||||
const pos = getCurrentPosition();
|
||||
bb?.setValue('position', pos); // 每帧都set
|
||||
bb?.setValue('velocity', getVelocity());
|
||||
bb?.setValue('rotation', getRotation());
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
export class FrequentUpdate implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const pos = getCurrentPosition();
|
||||
context.runtime.setBlackboardValue('position', pos); // 每帧都set
|
||||
context.runtime.setBlackboardValue('velocity', getVelocity());
|
||||
context.runtime.setBlackboardValue('rotation', getRotation());
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
// 好 - 只在需要时修改
|
||||
.action('UpdatePosition', (e, bb, dt) => {
|
||||
const oldPos = bb?.getValue('position');
|
||||
const newPos = getCurrentPosition();
|
||||
export class SmartUpdate implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const oldPos = context.runtime.getBlackboardValue('position');
|
||||
const newPos = getCurrentPosition();
|
||||
|
||||
// 只在位置变化时更新
|
||||
if (!positionsEqual(oldPos, newPos)) {
|
||||
bb?.setValue('position', newPos);
|
||||
// 只在位置变化时更新
|
||||
if (!positionsEqual(oldPos, newPos)) {
|
||||
context.runtime.setBlackboardValue('position', newPos);
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 单元测试节点
|
||||
|
||||
```typescript
|
||||
describe('AttackAction', () => {
|
||||
it('应该在有目标时返回Success', () => {
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity('Test');
|
||||
const blackboard = entity.addComponent(new BlackboardComponent());
|
||||
|
||||
blackboard.setValue('target', mockEnemy);
|
||||
blackboard.setValue('ammo', 10);
|
||||
|
||||
const result = attackAction(entity, blackboard, 0);
|
||||
|
||||
expect(result).toBe(TaskStatus.Success);
|
||||
});
|
||||
|
||||
it('应该在没有弹药时返回Failure', () => {
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity('Test');
|
||||
const blackboard = entity.addComponent(new BlackboardComponent());
|
||||
|
||||
blackboard.setValue('target', mockEnemy);
|
||||
blackboard.setValue('ammo', 0);
|
||||
|
||||
const result = attackAction(entity, blackboard, 0);
|
||||
|
||||
expect(result).toBe(TaskStatus.Failure);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
|
||||
```typescript
|
||||
describe('EnemyAI', () => {
|
||||
it('应该在玩家接近时攻击', () => {
|
||||
const scene = new Scene();
|
||||
const ai = createEnemyAI(scene);
|
||||
const blackboard = ai.getComponent(BlackboardComponent);
|
||||
|
||||
// 设置玩家在攻击范围内
|
||||
blackboard?.setValue('player', mockPlayer);
|
||||
blackboard?.setValue('distanceToPlayer', 5);
|
||||
|
||||
BehaviorTreeStarter.start(ai);
|
||||
scene.update();
|
||||
|
||||
// 验证进入了攻击状态
|
||||
expect(blackboard?.getValue('currentState')).toBe('attacking');
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[自定义动作](./custom-actions.md)扩展行为树功能
|
||||
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts.md)深入理解原理
|
||||
|
||||
@@ -112,28 +112,23 @@ export class Main extends Component {
|
||||
import { _decorator, Component, Node } from 'cc';
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeAssetLoader,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BlackboardComponent
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
import { resources } from 'cc';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('EnemyAIComponent')
|
||||
export class EnemyAIComponent extends Component {
|
||||
@property
|
||||
behaviorTreeAsset: string = 'behaviors/enemy-ai.btree';
|
||||
|
||||
private aiEntity: Entity | null = null;
|
||||
|
||||
async start() {
|
||||
// 加载行为树资产
|
||||
await this.loadBehaviorTree();
|
||||
// 创建行为树
|
||||
await this.createBehaviorTree();
|
||||
}
|
||||
|
||||
private async loadBehaviorTree() {
|
||||
private async createBehaviorTree() {
|
||||
try {
|
||||
// 获取Core管理的场景
|
||||
const scene = Core.scene;
|
||||
@@ -142,41 +137,32 @@ export class EnemyAIComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 resources 加载JSON资产
|
||||
resources.load(this.behaviorTreeAsset, (err, jsonAsset: any) => {
|
||||
if (err) {
|
||||
console.error('加载行为树失败:', err);
|
||||
return;
|
||||
}
|
||||
// 使用Builder API创建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('cocosNode', this.node)
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('playerNode', null)
|
||||
.defineBlackboardVariable('detectionRange', 10)
|
||||
.defineBlackboardVariable('attackRange', 2)
|
||||
.selector('MainBehavior')
|
||||
.sequence('Combat')
|
||||
.blackboardExists('playerNode')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.log('攻击玩家', 'AttackPlayer')
|
||||
.end()
|
||||
.sequence('Flee')
|
||||
.blackboardCompare('health', 30, 'lessOrEqual')
|
||||
.log('逃跑', 'RunAway')
|
||||
.end()
|
||||
.log('巡逻', 'Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 获取JSON字符串
|
||||
const jsonString = jsonAsset.json ? JSON.stringify(jsonAsset.json) : jsonAsset.text;
|
||||
// 创建AI实体并启动
|
||||
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
|
||||
// 反序列化
|
||||
const btAsset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||||
|
||||
// 实例化
|
||||
this.aiEntity = BehaviorTreeAssetLoader.instantiate(
|
||||
btAsset,
|
||||
scene,
|
||||
{
|
||||
namePrefix: this.node.name
|
||||
}
|
||||
);
|
||||
|
||||
// 设置黑板初始值
|
||||
const blackboard = this.aiEntity.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
// 可以在这里设置引用到 Cocos 节点
|
||||
blackboard.setValue('cocosNode', this.node);
|
||||
blackboard.setValue('position', this.node.position.clone());
|
||||
}
|
||||
|
||||
// 启动 AI
|
||||
BehaviorTreeStarter.start(this.aiEntity);
|
||||
|
||||
console.log('敌人 AI 已启动');
|
||||
});
|
||||
console.log('敌人 AI 已启动');
|
||||
} catch (error) {
|
||||
console.error('初始化行为树失败:', error);
|
||||
}
|
||||
@@ -194,19 +180,50 @@ export class EnemyAIComponent extends Component {
|
||||
|
||||
## 与 Cocos 节点交互
|
||||
|
||||
### 在编辑器ExecuteAction节点中编写代码
|
||||
### 创建自定义执行器
|
||||
|
||||
在行为树编辑器中,可以使用 `Execute Action` 节点,并编写代码:
|
||||
要实现与Cocos节点的交互,需要创建自定义执行器:
|
||||
|
||||
```javascript
|
||||
// 获取 Cocos 节点
|
||||
const cocosNode = blackboard.getValue('cocosNode');
|
||||
```typescript
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
import { Animation } from 'cc';
|
||||
|
||||
// 播放攻击动画
|
||||
const animation = cocosNode.getComponent('Animation');
|
||||
animation.play('attack');
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'PlayAnimation',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '播放动画',
|
||||
description: '播放Cocos节点上的动画',
|
||||
category: 'Cocos',
|
||||
configSchema: {
|
||||
animationName: {
|
||||
type: 'string',
|
||||
default: 'attack'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class PlayAnimationAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const cocosNode = context.runtime.getBlackboardValue('cocosNode');
|
||||
const animationName = context.nodeData.config.animationName;
|
||||
|
||||
return TaskStatus.Success;
|
||||
if (!cocosNode) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const animation = cocosNode.getComponent(Animation);
|
||||
if (animation) {
|
||||
animation.play(animationName);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -250,7 +267,7 @@ RootSelector
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component, Node, Vec3 } from 'cc';
|
||||
import { BlackboardComponent } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -262,18 +279,18 @@ export class PlayerDetector extends Component {
|
||||
@property
|
||||
detectionRange: number = 10;
|
||||
|
||||
private blackboard: BlackboardComponent | null = null;
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
start() {
|
||||
// 假设AI组件在同一节点上
|
||||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||
if (aiComponent && aiComponent.aiEntity) {
|
||||
this.blackboard = aiComponent.aiEntity.getComponent(BlackboardComponent);
|
||||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (!this.blackboard || !this.player) {
|
||||
if (!this.runtime || !this.player) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -281,9 +298,9 @@ export class PlayerDetector extends Component {
|
||||
const distance = Vec3.distance(this.node.position, this.player.position);
|
||||
|
||||
// 更新黑板
|
||||
this.blackboard.setValue('playerNode', this.player);
|
||||
this.blackboard.setValue('playerInRange', distance <= this.detectionRange);
|
||||
this.blackboard.setValue('distanceToPlayer', distance);
|
||||
this.runtime.setBlackboardValue('playerNode', this.player);
|
||||
this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange);
|
||||
this.runtime.setBlackboardValue('distanceToPlayer', distance);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -291,52 +308,210 @@ export class PlayerDetector extends Component {
|
||||
|
||||
## 资源管理
|
||||
|
||||
### 预加载行为树资产
|
||||
### 使用 BehaviorTreeAssetManager
|
||||
|
||||
在游戏启动时预加载所有行为树资产:
|
||||
框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建:
|
||||
|
||||
```typescript
|
||||
import { resources } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function preloadBehaviorTrees() {
|
||||
const assets = [
|
||||
'behaviors/enemy-ai',
|
||||
'behaviors/boss-ai',
|
||||
'behaviors/patrol'
|
||||
];
|
||||
// 获取资产管理器(插件已自动注册)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
for (const path of assets) {
|
||||
await new Promise((resolve, reject) => {
|
||||
resources.preload(path, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve(null);
|
||||
// 创建并注册行为树(只创建一次)
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('MainBehavior')
|
||||
.log('攻击')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(enemyAI);
|
||||
|
||||
// 为多个敌人实体使用同一份资产
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
const tree = assetManager.getAsset('EnemyAI')!;
|
||||
BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据
|
||||
}
|
||||
```
|
||||
|
||||
### 从 Cocos Creator 资源加载
|
||||
|
||||
#### 1. 将行为树 JSON 放入 resources 目录
|
||||
|
||||
```
|
||||
assets/
|
||||
└── resources/
|
||||
└── behaviors/
|
||||
├── enemy-ai.btree.json
|
||||
└── boss-ai.btree.json
|
||||
```
|
||||
|
||||
#### 2. 创建资源加载器
|
||||
|
||||
创建 `assets/scripts/BehaviorTreeLoader.ts`:
|
||||
|
||||
```typescript
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeData
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
export class BehaviorTreeLoader {
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor() {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 resources 目录加载行为树
|
||||
* @param path 相对于 resources 的路径,不带扩展名
|
||||
* @example await loader.load('behaviors/enemy-ai')
|
||||
*/
|
||||
async load(path: string): Promise<BehaviorTreeData | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resources.load(path, JsonAsset, (err, jsonAsset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树失败: ${path}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 反序列化 JSON 为 BehaviorTreeData
|
||||
const jsonStr = JSON.stringify(jsonAsset.json);
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonStr);
|
||||
|
||||
// 加载到资产管理器
|
||||
this.assetManager.loadAsset(treeData);
|
||||
|
||||
console.log(`行为树已加载: ${treeData.name}`);
|
||||
resolve(treeData);
|
||||
} catch (error) {
|
||||
console.error(`解析行为树失败: ${path}`, error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('行为树资产预加载完成');
|
||||
/**
|
||||
* 预加载所有行为树
|
||||
*/
|
||||
async preloadAll(paths: string[]): Promise<void> {
|
||||
const promises = paths.map(path => this.load(path));
|
||||
await Promise.all(promises);
|
||||
console.log(`已预加载 ${paths.length} 个行为树`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 AssetManager
|
||||
#### 3. 在游戏启动时预加载
|
||||
|
||||
对于动态加载,可以使用 Cocos 的 AssetManager:
|
||||
修改 `Main.ts`:
|
||||
|
||||
```typescript
|
||||
import { assetManager } from 'cc';
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
|
||||
|
||||
assetManager.loadBundle('behaviors', (err, bundle) => {
|
||||
if (err) {
|
||||
console.error('加载 bundle 失败:', err);
|
||||
return;
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('Main')
|
||||
export class Main extends Component {
|
||||
private loader: BehaviorTreeLoader | null = null;
|
||||
|
||||
async onLoad() {
|
||||
// 初始化 ECS Core
|
||||
Core.create();
|
||||
|
||||
// 安装行为树插件
|
||||
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(behaviorTreePlugin);
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
behaviorTreePlugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 创建加载器并预加载所有行为树
|
||||
this.loader = new BehaviorTreeLoader();
|
||||
await this.loader.preloadAll([
|
||||
'behaviors/enemy-ai',
|
||||
'behaviors/boss-ai',
|
||||
'behaviors/patrol', // 子树
|
||||
'behaviors/chase' // 子树
|
||||
]);
|
||||
|
||||
console.log('游戏初始化完成');
|
||||
}
|
||||
|
||||
bundle.load('enemy-ai', (err, asset) => {
|
||||
if (!err) {
|
||||
// 使用资产
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 在敌人组件中使用
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('EnemyAIComponent')
|
||||
export class EnemyAIComponent extends Component {
|
||||
@property
|
||||
aiType: string = 'enemy-ai'; // 在编辑器中配置使用哪个AI
|
||||
|
||||
private aiEntity: Entity | null = null;
|
||||
|
||||
start() {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
// 从资产管理器获取已加载的行为树
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
const tree = assetManager.getAsset(this.aiType);
|
||||
|
||||
if (tree) {
|
||||
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
|
||||
// 设置黑板变量
|
||||
const runtime = this.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('cocosNode', this.node);
|
||||
} else {
|
||||
console.error(`找不到行为树资产: ${this.aiType}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (this.aiEntity) {
|
||||
BehaviorTreeStarter.stop(this.aiEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试
|
||||
@@ -347,7 +522,7 @@ assetManager.loadBundle('behaviors', (err, bundle) => {
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component, Label } from 'cc';
|
||||
import { BlackboardComponent } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -356,24 +531,24 @@ export class AIDebugger extends Component {
|
||||
@property(Label)
|
||||
debugLabel: Label = null;
|
||||
|
||||
private blackboard: BlackboardComponent | null = null;
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
start() {
|
||||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||
if (aiComponent && aiComponent.aiEntity) {
|
||||
this.blackboard = aiComponent.aiEntity.getComponent(BlackboardComponent);
|
||||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.blackboard || !this.debugLabel) {
|
||||
if (!this.runtime || !this.debugLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const health = this.blackboard.getValue('health');
|
||||
const state = this.blackboard.getValue('currentState');
|
||||
const health = this.runtime.getBlackboardValue('health');
|
||||
const playerNode = this.runtime.getBlackboardValue('playerNode');
|
||||
|
||||
this.debugLabel.string = `Health: ${health}\nState: ${state}`;
|
||||
this.debugLabel.string = `Health: ${health}\nHas Target: ${playerNode ? 'Yes' : 'No'}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -381,95 +556,100 @@ export class AIDebugger extends Component {
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 使用对象池
|
||||
### 1. 限制行为树数量
|
||||
|
||||
为 AI 实体使用对象池:
|
||||
合理控制同时运行的行为树数量:
|
||||
|
||||
```typescript
|
||||
class AIEntityPool {
|
||||
private pool: Entity[] = [];
|
||||
private scene: Scene;
|
||||
class AIManager {
|
||||
private activeAIs: Entity[] = [];
|
||||
private maxAIs: number = 20;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
acquire(behaviorTreeAsset: any): Entity {
|
||||
if (this.pool.length > 0) {
|
||||
const entity = this.pool.pop()!;
|
||||
BehaviorTreeStarter.restart(entity);
|
||||
return entity;
|
||||
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||||
if (this.activeAIs.length >= this.maxAIs) {
|
||||
// 移除最远的AI
|
||||
const furthest = this.findFurthestAI();
|
||||
if (furthest) {
|
||||
BehaviorTreeStarter.stop(furthest);
|
||||
this.activeAIs = this.activeAIs.filter(e => e !== furthest);
|
||||
}
|
||||
}
|
||||
|
||||
return BehaviorTreeAssetLoader.instantiate(behaviorTreeAsset, this.scene);
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
this.activeAIs.push(entity);
|
||||
}
|
||||
|
||||
release(entity: Entity) {
|
||||
removeAI(entity: Entity) {
|
||||
BehaviorTreeStarter.stop(entity);
|
||||
this.pool.push(entity);
|
||||
this.activeAIs = this.activeAIs.filter(e => e !== entity);
|
||||
}
|
||||
|
||||
private findFurthestAI(): Entity | null {
|
||||
// 根据距离找到最远的AI
|
||||
// 实现细节略
|
||||
return this.activeAIs[0];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 限制更新频率
|
||||
### 2. 使用冷却装饰器
|
||||
|
||||
对于远离相机的敌人,可以在行为树内部使用节流机制:
|
||||
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||
|
||||
```typescript
|
||||
// 在行为树的Action节点中实现节流
|
||||
function throttledAction(entity, blackboard, deltaTime) {
|
||||
let lastUpdate = blackboard?.getValue('lastUpdateTime') || 0;
|
||||
const currentTime = Date.now();
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||
.selector('MainBehavior')
|
||||
// AI逻辑...
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
// 根据距离决定更新间隔
|
||||
const distance = getDistanceToCamera();
|
||||
const updateInterval = distance < 10 ? 0 : 200; // 远处敌人200ms更新一次
|
||||
### 3. 缓存计算结果
|
||||
|
||||
if (currentTime - lastUpdate < updateInterval) {
|
||||
return TaskStatus.Running;
|
||||
在自定义执行器中缓存昂贵的计算:
|
||||
|
||||
```typescript
|
||||
export class CachedFindTarget implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime, totalTime } = context;
|
||||
const cacheTime = state.lastFindTime || 0;
|
||||
|
||||
if (totalTime - cacheTime < 1.0) {
|
||||
const cached = runtime.getBlackboardValue('target');
|
||||
return cached ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const target = findNearestTarget();
|
||||
runtime.setBlackboardValue('target', target);
|
||||
state.lastFindTime = totalTime;
|
||||
|
||||
return target ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
blackboard?.setValue('lastUpdateTime', currentTime);
|
||||
|
||||
// 执行实际逻辑
|
||||
performAILogic();
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用二进制格式
|
||||
## 多平台注意事项
|
||||
|
||||
在构建时将 JSON 转换为二进制格式以减小包体:
|
||||
### 性能考虑
|
||||
|
||||
```bash
|
||||
# 在构建脚本中
|
||||
node scripts/convert-bt-to-binary.js
|
||||
```
|
||||
不同平台的性能差异:
|
||||
|
||||
## 多平台发布
|
||||
- **Web平台**: 受浏览器性能限制,建议减少同时运行的AI数量
|
||||
- **原生平台**: 性能较好,可以运行更多AI
|
||||
- **小游戏平台**: 内存受限,注意控制行为树数量和复杂度
|
||||
|
||||
### Web 平台
|
||||
|
||||
在 Web 平台,确保资源路径正确:
|
||||
### 平台适配
|
||||
|
||||
```typescript
|
||||
// 使用相对路径
|
||||
const assetPath = 'behaviors/enemy-ai';
|
||||
```
|
||||
import { sys } from 'cc';
|
||||
|
||||
### 原生平台
|
||||
// 根据平台调整AI数量
|
||||
const maxAIs = sys.isNative ? 50 : (sys.isBrowser ? 20 : 30);
|
||||
|
||||
原生平台可以使用二进制格式以获得更好的性能:
|
||||
|
||||
```typescript
|
||||
// 检测平台
|
||||
if (sys.isNative) {
|
||||
// 加载二进制格式
|
||||
assetPath = 'behaviors/enemy-ai.btree.bin';
|
||||
} else {
|
||||
// 加载 JSON 格式
|
||||
assetPath = 'behaviors/enemy-ai.btree.json';
|
||||
}
|
||||
// 根据平台调整更新频率
|
||||
const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
@@ -493,9 +673,11 @@ if (sys.isNative) {
|
||||
检查:
|
||||
1. 变量名拼写是否正确
|
||||
2. 是否在正确的时机更新变量
|
||||
3. 使用 `BlackboardComponent.getValue()` 和 `setValue()` 方法
|
||||
3. 使用 `BehaviorTreeRuntimeComponent.getBlackboardValue()` 和 `setBlackboardValue()` 方法
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[高级用法](./advanced-usage.md)了解子树和异步加载
|
||||
- 学习[最佳实践](./best-practices.md)优化你的 AI
|
||||
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
本文介绍行为树系统的核心概念和工作原理。
|
||||
|
||||
## 什么是行为树?
|
||||
## 什么是行为树?
|
||||
|
||||
行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。
|
||||
行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。
|
||||
|
||||
### 与状态机的对比
|
||||
|
||||
传统状态机:
|
||||
传统状态机:
|
||||
- 基于状态和转换
|
||||
- 状态之间的转换复杂
|
||||
- 难以扩展和维护
|
||||
- 不便于复用
|
||||
|
||||
行为树:
|
||||
行为树:
|
||||
- 基于任务和层次结构
|
||||
- 模块化、易于复用
|
||||
- 可视化编辑
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## 树结构
|
||||
|
||||
行为树由节点组成,形成树状结构:
|
||||
行为树由节点组成,形成树状结构:
|
||||
|
||||
```
|
||||
Root (根节点)
|
||||
@@ -37,8 +37,8 @@ Root (根节点)
|
||||
└── Wait (等待)
|
||||
```
|
||||
|
||||
每个节点都有:
|
||||
- 父节点(除了根节点)
|
||||
每个节点都有:
|
||||
- 父节点(除了根节点)
|
||||
- 零个或多个子节点
|
||||
- 执行状态
|
||||
- 返回结果
|
||||
@@ -46,307 +46,321 @@ Root (根节点)
|
||||
|
||||
## 节点类型
|
||||
|
||||
### 复合节点(Composite)
|
||||
### 复合节点(Composite)
|
||||
|
||||
复合节点有多个子节点,按特定规则执行它们。
|
||||
复合节点有多个子节点,按特定规则执行它们。
|
||||
|
||||
#### Selector(选择器)
|
||||
#### Selector(选择器)
|
||||
|
||||
按顺序尝试执行子节点,直到某个子节点成功。
|
||||
按顺序尝试执行子节点,直到某个子节点成功。
|
||||
|
||||
```typescript
|
||||
.selector('FindFood')
|
||||
.action('EatNearbyFood', () => {
|
||||
if (hasNearbyFood()) {
|
||||
eat();
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
return TaskStatus.Failure;
|
||||
})
|
||||
.action('SearchFood', () => {
|
||||
searchForFood();
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
.action('GiveUp', () => {
|
||||
return TaskStatus.Failure;
|
||||
})
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('FindFood')
|
||||
.selector('FindFoodSelector')
|
||||
.log('尝试吃附近的食物', 'EatNearby')
|
||||
.log('搜索食物', 'SearchFood')
|
||||
.log('放弃', 'GiveUp')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
执行逻辑:
|
||||
执行逻辑:
|
||||
1. 尝试第一个子节点
|
||||
2. 如果返回Success,选择器成功
|
||||
3. 如果返回Failure,尝试下一个子节点
|
||||
4. 如果返回Running,选择器返回Running
|
||||
5. 所有子节点都失败时,选择器失败
|
||||
2. 如果返回Success,选择器成功
|
||||
3. 如果返回Failure,尝试下一个子节点
|
||||
4. 如果返回Running,选择器返回Running
|
||||
5. 所有子节点都失败时,选择器失败
|
||||
|
||||
|
||||
#### Sequence(序列)
|
||||
#### Sequence(序列)
|
||||
|
||||
按顺序执行所有子节点,直到某个子节点失败。
|
||||
按顺序执行所有子节点,直到某个子节点失败。
|
||||
|
||||
```typescript
|
||||
.sequence('AttackSequence')
|
||||
.condition((e, bb) => hasTarget()) // 检查是否有目标
|
||||
.action('Aim', () => TaskStatus.Success) // 瞄准
|
||||
.action('Fire', () => TaskStatus.Success) // 开火
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('Attack')
|
||||
.sequence('AttackSequence')
|
||||
.blackboardExists('target') // 检查是否有目标
|
||||
.log('瞄准', 'Aim')
|
||||
.log('开火', 'Fire')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
执行逻辑:
|
||||
执行逻辑:
|
||||
1. 依次执行子节点
|
||||
2. 如果子节点返回Failure,序列失败
|
||||
3. 如果子节点返回Running,序列返回Running
|
||||
4. 如果子节点返回Success,继续下一个子节点
|
||||
5. 所有子节点都成功时,序列成功
|
||||
2. 如果子节点返回Failure,序列失败
|
||||
3. 如果子节点返回Running,序列返回Running
|
||||
4. 如果子节点返回Success,继续下一个子节点
|
||||
5. 所有子节点都成功时,序列成功
|
||||
|
||||
|
||||
#### Parallel(并行)
|
||||
#### Parallel(并行)
|
||||
|
||||
同时执行多个子节点。
|
||||
|
||||
```typescript
|
||||
import { ParallelPolicy } from '@esengine/behavior-tree';
|
||||
|
||||
.parallel(ParallelPolicy.RequireAll) // 所有任务都要成功
|
||||
.action('PlayAnimation', () => TaskStatus.Success)
|
||||
.action('PlaySound', () => TaskStatus.Success)
|
||||
.action('SpawnEffect', () => TaskStatus.Success)
|
||||
.end()
|
||||
```
|
||||
|
||||
策略类型:
|
||||
- `RequireAll`:所有子节点都成功才成功
|
||||
- `RequireOne`:任意一个子节点成功就成功
|
||||
|
||||
|
||||
### 装饰器节点(Decorator)
|
||||
|
||||
装饰器节点只有一个子节点,用于修改子节点的行为或结果。
|
||||
|
||||
#### Inverter(反转)
|
||||
|
||||
反转子节点的结果:
|
||||
|
||||
```typescript
|
||||
.inverter()
|
||||
.condition((e, bb) => isEnemyNearby()) // 检查敌人是否附近
|
||||
.end()
|
||||
// 如果有敌人返回false,没有敌人返回true
|
||||
```
|
||||
|
||||
#### Repeater(重复)
|
||||
|
||||
重复执行子节点:
|
||||
|
||||
```typescript
|
||||
.repeat(3) // 重复3次
|
||||
.action('Jump', () => TaskStatus.Success)
|
||||
.end()
|
||||
```
|
||||
|
||||
#### Cooldown(冷却)
|
||||
|
||||
限制子节点的执行频率:
|
||||
|
||||
```typescript
|
||||
.cooldown(5.0) // 5秒冷却
|
||||
.action('UseSpecialAbility', () => {
|
||||
console.log('使用特殊技能');
|
||||
return TaskStatus.Success;
|
||||
const tree = BehaviorTreeBuilder.create('PlayEffects')
|
||||
.parallel('Effects', {
|
||||
successPolicy: 'all', // 所有任务都要成功
|
||||
failurePolicy: 'one' // 任一失败则失败
|
||||
})
|
||||
.end()
|
||||
.log('播放动画', 'PlayAnimation')
|
||||
.log('播放音效', 'PlaySound')
|
||||
.log('生成粒子', 'SpawnEffect')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Timeout(超时)
|
||||
策略类型:
|
||||
- `successPolicy: 'all'`: 所有子节点都成功才成功
|
||||
- `successPolicy: 'one'`: 任意一个子节点成功就成功
|
||||
- `failurePolicy: 'all'`: 所有子节点都失败才失败
|
||||
- `failurePolicy: 'one'`: 任意一个子节点失败就失败
|
||||
|
||||
限制子节点的执行时间:
|
||||
|
||||
### 装饰器节点(Decorator)
|
||||
|
||||
装饰器节点只有一个子节点,用于修改子节点的行为或结果。
|
||||
|
||||
#### Inverter(反转)
|
||||
|
||||
反转子节点的结果:
|
||||
|
||||
```typescript
|
||||
.timeout(10.0) // 10秒超时
|
||||
.action('ComplexTask', () => {
|
||||
// 长时间运行的任务
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
.end()
|
||||
const tree = BehaviorTreeBuilder.create('CheckSafe')
|
||||
.inverter('NotHasEnemy')
|
||||
.blackboardExists('enemy')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Repeater(重复)
|
||||
|
||||
重复执行子节点:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Jump3Times')
|
||||
.repeater(3, 'RepeatJump')
|
||||
.log('跳跃', 'Jump')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Cooldown(冷却)
|
||||
|
||||
限制子节点的执行频率:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('UseSkill')
|
||||
.cooldown(5.0, 'SkillCooldown')
|
||||
.log('使用特殊技能', 'UseSpecialAbility')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Timeout(超时)
|
||||
|
||||
限制子节点的执行时间:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('TimedTask')
|
||||
.timeout(10.0, 'TaskTimeout')
|
||||
.log('长时间运行的任务', 'ComplexTask')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
|
||||
### 叶节点(Leaf)
|
||||
### 叶节点(Leaf)
|
||||
|
||||
叶节点没有子节点,执行具体的任务。
|
||||
叶节点没有子节点,执行具体的任务。
|
||||
|
||||
#### Action(动作)
|
||||
#### Action(动作)
|
||||
|
||||
执行具体操作:
|
||||
执行具体操作。内置动作节点包括:
|
||||
|
||||
```typescript
|
||||
.action('Move', (entity, blackboard, deltaTime) => {
|
||||
const target = blackboard?.getValue('targetPosition');
|
||||
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 移动逻辑
|
||||
const moved = moveTowards(target, deltaTime);
|
||||
|
||||
if (moved) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
const tree = BehaviorTreeBuilder.create('Actions')
|
||||
.sequence()
|
||||
.wait(2.0) // 等待2秒
|
||||
.log('Hello', 'LogAction') // 输出日志
|
||||
.setBlackboardValue('score', 100) // 设置黑板值
|
||||
.modifyBlackboardValue('score', 'add', 10) // 修改黑板值
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Condition(条件)
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
检查条件:
|
||||
#### Condition(条件)
|
||||
|
||||
检查条件。内置条件节点包括:
|
||||
|
||||
```typescript
|
||||
.condition((entity, blackboard) => {
|
||||
const health = blackboard?.getValue('health');
|
||||
return health > 50;
|
||||
}, 'CheckHealthHigh')
|
||||
const tree = BehaviorTreeBuilder.create('Conditions')
|
||||
.selector()
|
||||
.blackboardExists('player') // 检查变量是否存在
|
||||
.blackboardCompare('health', 50, 'greater') // 比较变量值
|
||||
.randomProbability(0.5) // 50%概率
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Wait(等待)
|
||||
#### Wait(等待)
|
||||
|
||||
等待指定时间:
|
||||
等待指定时间:
|
||||
|
||||
```typescript
|
||||
.wait(2.0) // 等待2秒
|
||||
const tree = BehaviorTreeBuilder.create('WaitExample')
|
||||
.wait(2.0, 'Wait2Seconds')
|
||||
.build();
|
||||
```
|
||||
|
||||
|
||||
## 任务状态
|
||||
|
||||
每个节点执行后返回以下状态之一:
|
||||
每个节点执行后返回以下状态之一:
|
||||
|
||||
### Success(成功)
|
||||
### Success(成功)
|
||||
|
||||
任务成功完成。
|
||||
|
||||
```typescript
|
||||
.action('CollectCoin', () => {
|
||||
coin.collect();
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
// 内置节点会根据逻辑自动返回Success
|
||||
.log('任务完成') // 总是返回Success
|
||||
.blackboardCompare('score', 100, 'greater') // 条件满足时返回Success
|
||||
```
|
||||
|
||||
### Failure(失败)
|
||||
### Failure(失败)
|
||||
|
||||
任务执行失败。
|
||||
|
||||
```typescript
|
||||
.condition((e, bb) => {
|
||||
const hasKey = bb?.getValue('hasKey');
|
||||
return hasKey ? TaskStatus.Success : TaskStatus.Failure;
|
||||
})
|
||||
.blackboardCompare('score', 100, 'greater') // 条件不满足返回Failure
|
||||
.blackboardExists('nonExistent') // 变量不存在返回Failure
|
||||
```
|
||||
|
||||
### Running(运行中)
|
||||
### Running(运行中)
|
||||
|
||||
任务需要多帧完成,仍在执行中。
|
||||
任务需要多帧完成,仍在执行中。
|
||||
|
||||
```typescript
|
||||
.action('ChargeLaser', (entity, blackboard, deltaTime) => {
|
||||
let chargeTime = blackboard?.getValue('chargeTime') || 0;
|
||||
chargeTime += deltaTime;
|
||||
blackboard?.setValue('chargeTime', chargeTime);
|
||||
|
||||
if (chargeTime >= 3.0) {
|
||||
// 充能完成
|
||||
blackboard?.setValue('chargeTime', 0);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running; // 继续充能
|
||||
})
|
||||
.wait(3.0) // 等待过程中返回Running,3秒后返回Success
|
||||
```
|
||||
|
||||
### Invalid(无效)
|
||||
### Invalid(无效)
|
||||
|
||||
节点未初始化或已重置。通常不需要手动返回此状态。
|
||||
节点未初始化或已重置。通常不需要手动处理此状态。
|
||||
|
||||
|
||||
## 黑板系统
|
||||
|
||||
黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。
|
||||
黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。
|
||||
|
||||
### 本地黑板
|
||||
|
||||
每个行为树实例都有自己的本地黑板:
|
||||
每个行为树实例都有自己的本地黑板:
|
||||
|
||||
```typescript
|
||||
const ai = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('target', BlackboardValueType.Object, null)
|
||||
.defineVariable('state', BlackboardValueType.String, 'idle')
|
||||
.endBlackboard()
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('state', 'idle')
|
||||
// ...
|
||||
.build();
|
||||
```
|
||||
|
||||
### 支持的数据类型
|
||||
|
||||
```typescript
|
||||
import { BlackboardValueType } from '@esengine/behavior-tree';
|
||||
黑板支持以下数据类型:
|
||||
- String:字符串
|
||||
- Number:数字
|
||||
- Boolean:布尔值
|
||||
- Vector2:二维向量
|
||||
- Vector3:三维向量
|
||||
- Object:对象引用
|
||||
- Array:数组
|
||||
|
||||
.blackboard()
|
||||
.defineVariable('count', BlackboardValueType.Number, 0)
|
||||
.defineVariable('name', BlackboardValueType.String, 'Enemy')
|
||||
.defineVariable('isActive', BlackboardValueType.Boolean, true)
|
||||
.defineVariable('position', BlackboardValueType.Vector2, { x: 0, y: 0 })
|
||||
.defineVariable('direction', BlackboardValueType.Vector3, { x: 0, y: 0, z: 0 })
|
||||
.defineVariable('data', BlackboardValueType.Object, {})
|
||||
.defineVariable('items', BlackboardValueType.Array, [])
|
||||
.endBlackboard()
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Variables')
|
||||
.defineBlackboardVariable('name', 'Enemy') // 字符串
|
||||
.defineBlackboardVariable('count', 0) // 数字
|
||||
.defineBlackboardVariable('isActive', true) // 布尔值
|
||||
.defineBlackboardVariable('position', { x: 0, y: 0 }) // 对象(也可用于Vector2)
|
||||
.defineBlackboardVariable('velocity', { x: 0, y: 0, z: 0 }) // 对象(也可用于Vector3)
|
||||
.defineBlackboardVariable('items', []) // 数组
|
||||
.build();
|
||||
```
|
||||
|
||||
### 读写变量
|
||||
|
||||
通过`BehaviorTreeRuntimeComponent`访问黑板:
|
||||
|
||||
```typescript
|
||||
.action('UseBlackboard', (entity, blackboard) => {
|
||||
// 读取变量
|
||||
const health = blackboard?.getValue('health');
|
||||
const target = blackboard?.getValue('target');
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 写入变量
|
||||
blackboard?.setValue('health', health - 10);
|
||||
blackboard?.setValue('lastAttackTime', Date.now());
|
||||
// 读取变量
|
||||
const health = runtime?.getBlackboardValue('health');
|
||||
const target = runtime?.getBlackboardValue('target');
|
||||
|
||||
// 检查变量是否存在
|
||||
if (blackboard?.hasVariable('powerup')) {
|
||||
const powerup = blackboard.getValue('powerup');
|
||||
console.log('已获得强化:', powerup);
|
||||
}
|
||||
// 写入变量
|
||||
runtime?.setBlackboardValue('health', 50);
|
||||
runtime?.setBlackboardValue('lastAttackTime', Date.now());
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
// 获取所有变量
|
||||
const allVars = runtime?.getAllBlackboardVariables();
|
||||
```
|
||||
|
||||
也可以使用内置节点操作黑板:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('BlackboardOps')
|
||||
.sequence()
|
||||
.setBlackboardValue('score', 100) // 设置值
|
||||
.modifyBlackboardValue('score', 'add', 10) // 增加10
|
||||
.blackboardCompare('score', 110, 'equals') // 检查是否等于110
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 全局黑板
|
||||
|
||||
所有行为树实例共享的黑板:
|
||||
所有行为树实例共享的黑板,通过`GlobalBlackboardService`访问:
|
||||
|
||||
```typescript
|
||||
import { GlobalBlackboard } from '@esengine/behavior-tree';
|
||||
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
|
||||
// 设置全局变量
|
||||
GlobalBlackboard.setValue('gameState', 'playing');
|
||||
GlobalBlackboard.setValue('difficulty', 5);
|
||||
globalBlackboard.setValue('gameState', 'playing');
|
||||
globalBlackboard.setValue('difficulty', 5);
|
||||
|
||||
// 在任何行为树中访问
|
||||
.action('CheckGlobalState', () => {
|
||||
const gameState = GlobalBlackboard.getValue('gameState');
|
||||
// 读取全局变量
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
```
|
||||
|
||||
if (gameState === 'paused') {
|
||||
return TaskStatus.Failure;
|
||||
在自定义执行器中访问全局黑板:
|
||||
|
||||
```typescript
|
||||
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
export class CheckGameState implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
|
||||
if (gameState === 'paused') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -355,18 +369,24 @@ GlobalBlackboard.setValue('difficulty', 5);
|
||||
### 初始化
|
||||
|
||||
```typescript
|
||||
// 1. 初始化Core和场景
|
||||
// 1. 初始化Core和插件
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 2. 创建场景
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 2. 构建行为树
|
||||
const ai = BehaviorTreeBuilder.create(scene, 'AI')
|
||||
// 3. 构建行为树
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
// ... 定义节点
|
||||
.build();
|
||||
|
||||
// 3. 启动
|
||||
BehaviorTreeStarter.start(ai);
|
||||
// 4. 创建实体并启动
|
||||
const entity = scene.createEntity('AIEntity');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
### 更新循环
|
||||
@@ -383,7 +403,7 @@ gameLoop(() => {
|
||||
|
||||
```
|
||||
1. 从根节点开始
|
||||
2. 根节点执行其逻辑(通常是Selector或Sequence)
|
||||
2. 根节点执行其逻辑(通常是Selector或Sequence)
|
||||
3. 根节点的子节点按顺序执行
|
||||
4. 每个子节点可能有自己的子节点
|
||||
5. 叶节点执行具体操作并返回状态
|
||||
@@ -395,90 +415,77 @@ gameLoop(() => {
|
||||
### 执行示例
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create(scene, 'Example')
|
||||
const tree = BehaviorTreeBuilder.create('Example')
|
||||
.selector('Root') // 1. 执行选择器
|
||||
.sequence('Branch1') // 2. 尝试第一个分支
|
||||
.condition(() => false) // 3. 条件失败
|
||||
.end() // 4. 序列失败,选择器继续下一个分支
|
||||
.blackboardCompare('ready', true, 'equals', 'CheckReady') // 3. 条件失败
|
||||
.end() // 4. 序列失败,选择器继续下一个分支
|
||||
.sequence('Branch2') // 5. 尝试第二个分支
|
||||
.condition(() => true) // 6. 条件成功
|
||||
.action(() => TaskStatus.Success) // 7. 动作成功
|
||||
.end() // 8. 序列成功,选择器成功
|
||||
.blackboardCompare('active', true, 'equals', 'CheckActive') // 6. 条件成功
|
||||
.log('执行动作', 'DoAction') // 7. 动作成功
|
||||
.end() // 8. 序列成功,选择器成功
|
||||
.end() // 9. 整个树成功
|
||||
.build();
|
||||
```
|
||||
|
||||
执行流程图:
|
||||
执行流程图:
|
||||
|
||||
```
|
||||
Root(Selector)
|
||||
→ Branch1(Sequence)
|
||||
→ Condition: Failure
|
||||
→ CheckReady: Failure
|
||||
→ Branch1 fails
|
||||
→ Branch2(Sequence)
|
||||
→ Condition: Success
|
||||
→ Action: Success
|
||||
→ CheckActive: Success
|
||||
→ DoAction: Success
|
||||
→ Branch2 succeeds
|
||||
→ Root succeeds
|
||||
```
|
||||
|
||||
|
||||
## ECS集成
|
||||
## Runtime架构
|
||||
|
||||
本框架的行为树完全基于ECS架构:
|
||||
本框架的行为树采用Runtime执行器架构:
|
||||
|
||||
### 节点即实体
|
||||
### 核心组件
|
||||
|
||||
每个行为树节点都是一个Entity:
|
||||
- **BehaviorTreeData**: 纯数据结构,描述行为树的结构和配置
|
||||
- **BehaviorTreeRuntimeComponent**: 运行时组件,管理执行状态和黑板
|
||||
- **BehaviorTreeExecutionSystem**: 执行系统,驱动行为树运行
|
||||
- **INodeExecutor**: 节点执行器接口,定义节点的执行逻辑
|
||||
- **NodeExecutionContext**: 执行上下文,包含执行所需的所有信息
|
||||
|
||||
### 架构特点
|
||||
|
||||
1. **数据与逻辑分离**: BehaviorTreeData是纯数据,执行逻辑在执行器中
|
||||
2. **无状态执行器**: 执行器实例可以在多个节点间共享,状态存储在Runtime中
|
||||
3. **类型安全**: 通过TypeScript类型系统保证类型安全
|
||||
4. **高性能**: 避免不必要的对象创建,优化内存使用
|
||||
|
||||
### 数据流
|
||||
|
||||
```typescript
|
||||
// 行为树节点在内部被表示为:
|
||||
const nodeEntity = scene.createEntity('SelectorNode');
|
||||
nodeEntity.addComponent(new SelectorComponent());
|
||||
nodeEntity.addComponent(new ParentComponent(parentEntity));
|
||||
```
|
||||
|
||||
### 组件存储数据
|
||||
|
||||
节点属性存储在组件中:
|
||||
|
||||
```typescript
|
||||
// Action节点的数据组件
|
||||
class ActionComponent extends Component {
|
||||
actionFunc: ActionFunction;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Blackboard组件
|
||||
class BlackboardComponent extends Component {
|
||||
private variables: Map<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
### 系统驱动行为
|
||||
|
||||
行为树系统负责更新所有节点:
|
||||
|
||||
```typescript
|
||||
class BehaviorTreeSystem extends EntitySystem {
|
||||
update() {
|
||||
// 更新所有活跃的行为树
|
||||
for (const entity of this.entities) {
|
||||
const root = entity.getComponent(BehaviorTreeRootComponent);
|
||||
if (root && root.isActive) {
|
||||
this.updateNode(root.rootEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BehaviorTreeBuilder
|
||||
↓ (构建)
|
||||
BehaviorTreeData
|
||||
↓ (加载到)
|
||||
BehaviorTreeAssetManager
|
||||
↓ (读取)
|
||||
BehaviorTreeExecutionSystem
|
||||
↓ (执行)
|
||||
INodeExecutor.execute(context)
|
||||
↓ (返回)
|
||||
TaskStatus
|
||||
↓ (更新)
|
||||
NodeRuntimeState
|
||||
```
|
||||
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
|
||||
- 查看[快速开始](./getting-started.md)创建第一个行为树
|
||||
- 学习[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices.md)学习设计模式
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,10 +55,13 @@ npm run tauri:dev
|
||||
3. 在节点中通过变量名引用黑板变量
|
||||
|
||||
支持的变量类型:
|
||||
- Number:数字
|
||||
- String:字符串
|
||||
- Number:数字
|
||||
- Boolean:布尔值
|
||||
- Vector2:二维向量
|
||||
- Vector3:三维向量
|
||||
- Object:对象引用
|
||||
- Array:数组
|
||||
|
||||
## 导出运行时资产
|
||||
|
||||
@@ -77,24 +80,30 @@ npm run tauri:dev
|
||||
|
||||
### 加载运行时资产
|
||||
|
||||
`deserialize`方法会自动识别数据格式(JSON或二进制):
|
||||
编辑器导出的文件是编辑器格式,包含UI布局信息。当前版本中,从编辑器导出的资产可以使用Builder API在代码中重新构建,或者等待资产加载系统的完善。
|
||||
|
||||
推荐使用Builder API创建行为树:
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeAssetSerializer, BehaviorTreeAssetLoader } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 加载二进制格式
|
||||
const binaryData = await loadFile('enemy-ai.btree.bin'); // Uint8Array
|
||||
const asset = BehaviorTreeAssetSerializer.deserialize(binaryData);
|
||||
const aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
```
|
||||
// 使用Builder创建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.selector('MainBehavior')
|
||||
.sequence('AttackBranch')
|
||||
.blackboardCompare('health', 50, 'greater')
|
||||
.log('攻击玩家', 'Attack')
|
||||
.end()
|
||||
.log('逃离战斗', 'Flee')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeAssetSerializer, BehaviorTreeAssetLoader } from '@esengine/behavior-tree';
|
||||
|
||||
// 加载JSON格式
|
||||
const jsonString = await loadFile('enemy-ai.btree.json'); // string
|
||||
const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||||
const aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
// 启动行为树
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
## 支持的操作
|
||||
@@ -107,4 +116,4 @@ const aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
## 下一步
|
||||
|
||||
- 查看[编辑器工作流](./editor-workflow.md)了解完整的开发流程
|
||||
- 查看[自定义动作](./custom-actions.md)学习如何扩展节点
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何扩展节点
|
||||
|
||||
@@ -55,17 +55,19 @@ Root: Selector
|
||||
└── Idle (Action)
|
||||
```
|
||||
|
||||
## 在游戏中加载
|
||||
## 在游戏中使用
|
||||
|
||||
### 加载JSON资产
|
||||
### 使用Builder API创建
|
||||
|
||||
推荐使用Builder API在代码中创建行为树:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeAssetLoader,
|
||||
BehaviorTreeStarter
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 初始化
|
||||
@@ -77,18 +79,28 @@ const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 加载行为树
|
||||
const jsonString = await loadJsonFromFile('enemy-ai.btree.json');
|
||||
const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||||
const aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
// 使用Builder创建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('moveSpeed', 5.0)
|
||||
.selector('MainBehavior')
|
||||
.sequence('AttackBranch')
|
||||
.blackboardExists('target')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.log('攻击目标', 'Attack')
|
||||
.end()
|
||||
.log('巡逻', 'Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 设置黑板初始值
|
||||
const blackboard = aiEntity.getComponent(BlackboardComponent);
|
||||
blackboard?.setValue('health', 100);
|
||||
blackboard?.setValue('moveSpeed', 5.0);
|
||||
// 创建实体并启动行为树
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
// 启动AI
|
||||
BehaviorTreeStarter.start(aiEntity);
|
||||
// 访问和修改黑板
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('target', someTarget);
|
||||
|
||||
// 游戏循环
|
||||
setInterval(() => {
|
||||
@@ -96,103 +108,104 @@ setInterval(() => {
|
||||
}, 16);
|
||||
```
|
||||
|
||||
## 实现自定义动作
|
||||
## 实现自定义执行器
|
||||
|
||||
编辑器中的ExecuteAction节点需要在代码中提供实际逻辑。有两种方式:
|
||||
|
||||
### 方式1:通过事件系统(推荐)
|
||||
|
||||
在Action节点中触发事件,在游戏代码中监听:
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)):
|
||||
|
||||
```typescript
|
||||
// 在编辑器的ExecuteAction节点中
|
||||
entity.scene?.eventSystem.emit('ai:attack', {
|
||||
attacker: entity,
|
||||
target: blackboard?.getValue('target')
|
||||
});
|
||||
return TaskStatus.Success;
|
||||
```
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
BindingHelper,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
|
||||
```typescript
|
||||
// 在游戏代码中监听
|
||||
Core.scene.eventSystem.on('ai:attack', (data) => {
|
||||
const { attacker, target } = data;
|
||||
// 执行实际的攻击逻辑
|
||||
performAttack(attacker, target);
|
||||
});
|
||||
```
|
||||
|
||||
### 方式2:创建自定义组件
|
||||
|
||||
创建专用的Action组件(详见[自定义动作](./custom-actions.md)):
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { BehaviorNode, BehaviorProperty, NodeType, TaskStatus } from '@esengine/behavior-tree';
|
||||
|
||||
@BehaviorNode({
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '攻击目标',
|
||||
description: '对目标造成伤害',
|
||||
category: '战斗',
|
||||
type: NodeType.Action,
|
||||
description: '对目标造成伤害'
|
||||
configSchema: {
|
||||
damage: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@ECSComponent('AttackAction')
|
||||
export class AttackAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '伤害值',
|
||||
type: 'number'
|
||||
})
|
||||
damage: number = 10;
|
||||
export class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
const target = context.runtime.getBlackboardValue('target');
|
||||
|
||||
execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus {
|
||||
const target = blackboard?.getValue('target');
|
||||
if (!target) return TaskStatus.Failure;
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行攻击逻辑
|
||||
performAttack(entity, target, this.damage);
|
||||
performAttack(context.entity, target, damage);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
// 清理状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用日志
|
||||
### 1. 使用日志节点
|
||||
|
||||
在编辑器中添加Log节点输出调试信息:
|
||||
在行为树中添加Log节点输出调试信息:
|
||||
|
||||
```typescript
|
||||
.log('进入战斗分支', 'info')
|
||||
.action('Attack', (entity, blackboard) => {
|
||||
console.log('目标:', blackboard?.getValue('target'));
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
const tree = BehaviorTreeBuilder.create('DebugAI')
|
||||
.log('开始战斗序列', 'StartCombat')
|
||||
.sequence('Combat')
|
||||
.blackboardCompare('health', 0, 'greater')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 监控黑板
|
||||
### 2. 监控黑板状态
|
||||
|
||||
```typescript
|
||||
const blackboard = aiEntity.getComponent(BlackboardComponent);
|
||||
console.log('黑板状态:', blackboard?.getAllVariables());
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 3. 检查节点状态
|
||||
### 3. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
const node = aiEntity.getComponent(BehaviorTreeNode);
|
||||
console.log('节点状态:', node?.status);
|
||||
export class DebugAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
console.group(`[${nodeData.name}]`);
|
||||
console.log('配置:', nodeData.config);
|
||||
console.log('状态:', state);
|
||||
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||
console.groupEnd();
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BlackboardValueType,
|
||||
TaskStatus
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 初始化
|
||||
@@ -204,28 +217,28 @@ const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 直接用代码构建(不用编辑器)
|
||||
const aiEntity = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('hasTarget', BlackboardValueType.Boolean, false)
|
||||
.endBlackboard()
|
||||
// 使用Builder API构建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasTarget', false)
|
||||
.selector('Root')
|
||||
.sequence('Combat')
|
||||
.condition((e, bb) => bb?.getValue('hasTarget') === true, 'CheckTarget')
|
||||
.action('Attack', (e, bb) => {
|
||||
console.log('攻击!');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.blackboardCompare('hasTarget', true, 'equals')
|
||||
.log('攻击玩家', 'Attack')
|
||||
.end()
|
||||
.action('Idle', () => {
|
||||
console.log('空闲');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.log('空闲', 'Idle')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
BehaviorTreeStarter.start(aiEntity);
|
||||
// 创建实体并启动
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
// 模拟发现目标
|
||||
setTimeout(() => {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasTarget', true);
|
||||
}, 2000);
|
||||
|
||||
// 游戏循环
|
||||
setInterval(() => {
|
||||
@@ -235,6 +248,6 @@ setInterval(() => {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义动作](./custom-actions.md)学习如何创建专用的Action组件
|
||||
- 查看[高级用法](./advanced-usage.md)了解子树、异步操作等高级特性
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage.md)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices.md)优化你的AI设计
|
||||
|
||||
@@ -10,23 +10,20 @@ npm install @esengine/behavior-tree
|
||||
|
||||
## 第一个行为树
|
||||
|
||||
让我们创建一个简单的AI行为树,实现"巡逻-发现敌人-攻击"的逻辑。
|
||||
让我们创建一个简单的AI行为树,实现"巡逻-发现敌人-攻击"的逻辑。
|
||||
|
||||
### 步骤1:导入依赖
|
||||
### 步骤1: 导入依赖
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin,
|
||||
BlackboardValueType,
|
||||
TaskStatus,
|
||||
CompareOperator
|
||||
BehaviorTreePlugin
|
||||
} from '@esengine/behavior-tree';
|
||||
```
|
||||
|
||||
### 步骤2:安装插件
|
||||
### 步骤2: 初始化Core并安装插件
|
||||
|
||||
```typescript
|
||||
Core.create();
|
||||
@@ -34,7 +31,7 @@ const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
### 步骤3:创建场景并设置行为树系统
|
||||
### 步骤3: 创建场景并设置行为树系统
|
||||
|
||||
```typescript
|
||||
const scene = new Scene();
|
||||
@@ -42,63 +39,51 @@ plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
```
|
||||
|
||||
### 步骤4:构建行为树
|
||||
### 步骤4: 构建行为树数据
|
||||
|
||||
```typescript
|
||||
const guardAI = BehaviorTreeBuilder.create(scene, 'GuardAI')
|
||||
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||
// 定义黑板变量
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('hasEnemy', BlackboardValueType.Boolean, false)
|
||||
.defineVariable('patrolPoint', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
|
||||
// 根选择器
|
||||
.selector('RootSelector')
|
||||
// 分支1:如果发现敌人且生命值高,则攻击
|
||||
.sequence('CombatBranch')
|
||||
.checkBlackboardExists('hasEnemy', true, 'CheckEnemy')
|
||||
.compareBlackboardValue('health', CompareOperator.Greater, 30, 'CheckHealth')
|
||||
.action('Attack', (entity, blackboard) => {
|
||||
console.log('守卫正在攻击敌人');
|
||||
// 模拟攻击逻辑
|
||||
const health = blackboard?.getValue<number>('health') || 100;
|
||||
blackboard?.setValue('health', health - 10);
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
// 分支1: 如果发现敌人且生命值高,则攻击
|
||||
.selector('CombatBranch')
|
||||
.blackboardExists('hasEnemy', 'CheckEnemy')
|
||||
.blackboardCompare('health', 30, 'greater', 'CheckHealth')
|
||||
.log('守卫正在攻击敌人', 'Attack')
|
||||
.end()
|
||||
|
||||
// 分支2:如果生命值低,则逃跑
|
||||
.sequence('FleeBranch')
|
||||
.compareBlackboardValue('health', CompareOperator.LessOrEqual, 30)
|
||||
.action('Flee', (entity) => {
|
||||
console.log('守卫生命值过低,正在逃跑');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
// 分支2: 如果生命值低,则逃跑
|
||||
.selector('FleeBranch')
|
||||
.blackboardCompare('health', 30, 'lessOrEqual', 'CheckLowHealth')
|
||||
.log('守卫生命值过低,正在逃跑', 'Flee')
|
||||
.end()
|
||||
|
||||
// 分支3:默认巡逻
|
||||
.sequence('PatrolBranch')
|
||||
.action('MoveToNextPoint', (entity, blackboard) => {
|
||||
const point = blackboard?.getValue<number>('patrolPoint') || 0;
|
||||
const nextPoint = (point + 1) % 4;
|
||||
blackboard?.setValue('patrolPoint', nextPoint);
|
||||
console.log(`守卫移动到巡逻点 ${nextPoint}`);
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.wait(2.0, 'WaitAtPoint') // 在巡逻点等待2秒
|
||||
// 分支3: 默认巡逻
|
||||
.selector('PatrolBranch')
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1, 'IncrementPatrol')
|
||||
.log('守卫正在巡逻', 'Patrol')
|
||||
.wait(2.0, 'WaitAtPoint')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 步骤5:启动行为树
|
||||
### 步骤5: 创建实体并启动行为树
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.start(guardAI);
|
||||
// 创建守卫实体
|
||||
const guardEntity = scene.createEntity('Guard');
|
||||
|
||||
// 启动行为树
|
||||
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||
```
|
||||
|
||||
### 步骤6:运行游戏循环
|
||||
### 步骤6: 运行游戏循环
|
||||
|
||||
```typescript
|
||||
// 模拟游戏循环
|
||||
@@ -107,6 +92,24 @@ setInterval(() => {
|
||||
}, 100); // 每100ms更新一次
|
||||
```
|
||||
|
||||
### 步骤7: 模拟游戏事件
|
||||
|
||||
```typescript
|
||||
// 5秒后模拟发现敌人
|
||||
setTimeout(() => {
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasEnemy', true);
|
||||
console.log('发现敌人!');
|
||||
}, 5000);
|
||||
|
||||
// 10秒后模拟受伤
|
||||
setTimeout(() => {
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('health', 20);
|
||||
console.log('守卫受伤!');
|
||||
}, 10000);
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```typescript
|
||||
@@ -115,73 +118,64 @@ import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin,
|
||||
BlackboardValueType,
|
||||
TaskStatus,
|
||||
CompareOperator
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function main() {
|
||||
// 创建核心和场景
|
||||
// 1. 创建核心并安装插件
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 2. 创建场景
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 构建行为树
|
||||
const guardAI = BehaviorTreeBuilder.create(scene, 'GuardAI')
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('hasEnemy', BlackboardValueType.Boolean, false)
|
||||
.defineVariable('patrolPoint', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
// 3. 构建行为树数据
|
||||
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
.selector('RootSelector')
|
||||
.sequence('CombatBranch')
|
||||
.checkBlackboardExists('hasEnemy', true)
|
||||
.compareBlackboardValue('health', CompareOperator.Greater, 30)
|
||||
.action('Attack', (entity, blackboard) => {
|
||||
console.log('守卫正在攻击敌人');
|
||||
const health = blackboard?.getValue<number>('health') || 100;
|
||||
blackboard?.setValue('health', health - 10);
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.selector('CombatBranch')
|
||||
.blackboardExists('hasEnemy')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.log('守卫正在攻击敌人')
|
||||
.end()
|
||||
.sequence('FleeBranch')
|
||||
.compareBlackboardValue('health', CompareOperator.LessOrEqual, 30)
|
||||
.action('Flee', () => {
|
||||
console.log('守卫生命值过低,正在逃跑');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.selector('FleeBranch')
|
||||
.blackboardCompare('health', 30, 'lessOrEqual')
|
||||
.log('守卫生命值过低,正在逃跑')
|
||||
.end()
|
||||
.sequence('PatrolBranch')
|
||||
.action('MoveToNextPoint', (entity, blackboard) => {
|
||||
const point = blackboard?.getValue<number>('patrolPoint') || 0;
|
||||
const nextPoint = (point + 1) % 4;
|
||||
blackboard?.setValue('patrolPoint', nextPoint);
|
||||
console.log(`守卫移动到巡逻点 ${nextPoint}`);
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.selector('PatrolBranch')
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1)
|
||||
.log('守卫正在巡逻')
|
||||
.wait(2.0)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 启动AI
|
||||
BehaviorTreeStarter.start(guardAI);
|
||||
// 4. 创建守卫实体并启动行为树
|
||||
const guardEntity = scene.createEntity('Guard');
|
||||
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||
|
||||
// 运行游戏循环
|
||||
// 5. 运行游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.1); // 传入deltaTime(秒)
|
||||
Core.update(0.1);
|
||||
}, 100);
|
||||
|
||||
// 5秒后模拟发现敌人
|
||||
// 6. 模拟游戏事件
|
||||
setTimeout(() => {
|
||||
const blackboard = guardAI.getComponent(BlackboardComponent);
|
||||
blackboard?.setValue('hasEnemy', true);
|
||||
console.log('发现敌人!');
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasEnemy', true);
|
||||
console.log('发现敌人!');
|
||||
}, 5000);
|
||||
|
||||
setTimeout(() => {
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('health', 20);
|
||||
console.log('守卫受伤!');
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -189,18 +183,17 @@ main();
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你会看到类似的输出:
|
||||
运行程序后,你会看到类似的输出:
|
||||
|
||||
```
|
||||
守卫移动到巡逻点 1
|
||||
守卫移动到巡逻点 2
|
||||
守卫移动到巡逻点 3
|
||||
发现敌人!
|
||||
守卫正在巡逻
|
||||
守卫正在巡逻
|
||||
守卫正在巡逻
|
||||
发现敌人!
|
||||
守卫正在攻击敌人
|
||||
守卫正在攻击敌人
|
||||
守卫正在攻击敌人
|
||||
...
|
||||
守卫生命值过低,正在逃跑
|
||||
守卫受伤!
|
||||
守卫生命值过低,正在逃跑
|
||||
```
|
||||
|
||||
## 理解代码
|
||||
@@ -208,17 +201,15 @@ main();
|
||||
### 黑板变量
|
||||
|
||||
```typescript
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('hasEnemy', BlackboardValueType.Boolean, false)
|
||||
.defineVariable('patrolPoint', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
```
|
||||
|
||||
黑板用于在节点之间共享数据。这里定义了三个变量:
|
||||
- `health`:守卫的生命值
|
||||
- `hasEnemy`:是否发现敌人
|
||||
- `patrolPoint`:当前巡逻点编号
|
||||
黑板用于在节点之间共享数据。这里定义了三个变量:
|
||||
- `health`: 守卫的生命值
|
||||
- `hasEnemy`: 是否发现敌人
|
||||
- `patrolPoint`: 当前巡逻点编号
|
||||
|
||||
### 选择器节点
|
||||
|
||||
@@ -230,84 +221,165 @@ main();
|
||||
.end()
|
||||
```
|
||||
|
||||
选择器按顺序尝试执行子节点,直到某个子节点返回成功。类似于编程中的 `if-else if-else`。
|
||||
选择器按顺序尝试执行子节点,直到某个子节点返回成功。类似于编程中的 `if-else if-else`。
|
||||
|
||||
### 序列节点
|
||||
### 条件节点
|
||||
|
||||
```typescript
|
||||
.sequence('CombatBranch')
|
||||
.checkBlackboardExists('hasEnemy', true)
|
||||
.compareBlackboardValue('health', CompareOperator.Greater, 30)
|
||||
.action('Attack', ...)
|
||||
.end()
|
||||
.blackboardExists('hasEnemy') // 检查变量是否存在
|
||||
.blackboardCompare('health', 30, 'greater') // 比较变量值
|
||||
```
|
||||
|
||||
序列节点按顺序执行所有子节点,如果任何一个失败则整个序列失败。类似于编程中的 `&&` 运算符。
|
||||
条件节点用于检查黑板变量的值。
|
||||
|
||||
### 自定义动作
|
||||
### 动作节点
|
||||
|
||||
```typescript
|
||||
.action('Attack', (entity, blackboard, deltaTime) => {
|
||||
// 你的自定义逻辑
|
||||
console.log('执行攻击');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.log('守卫正在攻击敌人') // 输出日志
|
||||
.wait(2.0) // 等待2秒
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1) // 修改黑板值
|
||||
```
|
||||
|
||||
动作节点执行具体的操作并返回状态:
|
||||
- `TaskStatus.Success`:成功完成
|
||||
- `TaskStatus.Failure`:执行失败
|
||||
- `TaskStatus.Running`:正在执行(需要多帧完成)
|
||||
动作节点执行具体的操作。
|
||||
|
||||
### Runtime组件
|
||||
|
||||
```typescript
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasEnemy', true);
|
||||
runtime?.getBlackboardValue('health');
|
||||
```
|
||||
|
||||
通过`BehaviorTreeRuntimeComponent`访问和修改黑板变量。
|
||||
|
||||
## 常见任务状态
|
||||
|
||||
行为树的每个节点返回以下状态之一:
|
||||
行为树的每个节点返回以下状态之一:
|
||||
|
||||
- **Success**:任务成功完成
|
||||
- **Failure**:任务执行失败
|
||||
- **Running**:任务正在执行,需要在后续帧继续
|
||||
- **Invalid**:无效状态(未初始化或已重置)
|
||||
- **Success**: 任务成功完成
|
||||
- **Failure**: 任务执行失败
|
||||
- **Running**: 任务正在执行,需要在后续帧继续
|
||||
- **Invalid**: 无效状态(未初始化或已重置)
|
||||
|
||||
## 内置节点
|
||||
|
||||
### 复合节点
|
||||
|
||||
- `sequence()` - 序列节点,按顺序执行所有子节点
|
||||
- `selector()` - 选择器节点,按顺序尝试子节点直到成功
|
||||
- `parallel()` - 并行节点,同时执行多个子节点
|
||||
- `parallelSelector()` - 并行选择器
|
||||
- `randomSequence()` - 随机序列
|
||||
- `randomSelector()` - 随机选择器
|
||||
|
||||
### 装饰器节点
|
||||
|
||||
- `inverter()` - 反转子节点结果
|
||||
- `repeater(count)` - 重复执行子节点
|
||||
- `alwaysSucceed()` - 总是返回成功
|
||||
- `alwaysFail()` - 总是返回失败
|
||||
- `untilSuccess()` - 重复直到成功
|
||||
- `untilFail()` - 重复直到失败
|
||||
- `conditional(key, value, operator)` - 条件装饰器
|
||||
- `cooldown(time)` - 冷却装饰器
|
||||
- `timeout(time)` - 超时装饰器
|
||||
|
||||
### 动作节点
|
||||
|
||||
- `wait(duration)` - 等待指定时间
|
||||
- `log(message)` - 输出日志
|
||||
- `setBlackboardValue(key, value)` - 设置黑板值
|
||||
- `modifyBlackboardValue(key, operation, value)` - 修改黑板值
|
||||
- `executeAction(actionName)` - 执行自定义动作
|
||||
|
||||
### 条件节点
|
||||
|
||||
- `blackboardExists(key)` - 检查变量是否存在
|
||||
- `blackboardCompare(key, value, operator)` - 比较黑板值
|
||||
- `randomProbability(probability)` - 随机概率
|
||||
- `executeCondition(conditionName)` - 执行自定义条件
|
||||
|
||||
## 控制行为树
|
||||
|
||||
### 启动
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.start(entity, treeData);
|
||||
```
|
||||
|
||||
### 停止
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.stop(entity);
|
||||
```
|
||||
|
||||
### 暂停和恢复
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.pause(entity);
|
||||
// ... 一段时间后
|
||||
BehaviorTreeStarter.resume(entity);
|
||||
```
|
||||
|
||||
### 重启
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.restart(entity);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经创建了第一个行为树,接下来可以:
|
||||
现在你已经创建了第一个行为树,接下来可以:
|
||||
|
||||
1. 学习[核心概念](./core-concepts.md)深入理解行为树原理
|
||||
2. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||
3. 查看[高级用法](./advanced-usage.md)了解更多功能
|
||||
2. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration.md) 或 [Node.js](./nodejs-usage.md)
|
||||
5. 查看[高级用法](./advanced-usage.md)了解更多功能
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么行为树不执行?
|
||||
### 为什么行为树不执行?
|
||||
|
||||
确保:
|
||||
确保:
|
||||
1. 已经安装了 `BehaviorTreePlugin`
|
||||
2. 调用了 `plugin.setupScene(scene)`
|
||||
3. 调用了 `BehaviorTreeStarter.start(aiRoot)`
|
||||
4. 场景的 `update()` 方法在游戏循环中被调用
|
||||
3. 调用了 `BehaviorTreeStarter.start(entity, treeData)`
|
||||
4. 在游戏循环中调用了 `Core.update(deltaTime)`
|
||||
|
||||
### 如何调试行为树?
|
||||
|
||||
使用日志动作和控制台输出:
|
||||
### 如何访问黑板变量?
|
||||
|
||||
```typescript
|
||||
.log('到达这个节点', 'info')
|
||||
.action('MyAction', (entity, blackboard) => {
|
||||
console.log('blackboard:', blackboard?.getAllVariables());
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 读取
|
||||
const health = runtime?.getBlackboardValue('health');
|
||||
|
||||
// 写入
|
||||
runtime?.setBlackboardValue('health', 50);
|
||||
|
||||
// 获取所有变量
|
||||
const allVars = runtime?.getAllBlackboardVariables();
|
||||
```
|
||||
|
||||
### 如何停止行为树?
|
||||
### 如何调试行为树?
|
||||
|
||||
使用日志节点:
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.stop(aiRoot);
|
||||
.log('到达这个节点', 'DebugLog')
|
||||
```
|
||||
|
||||
或暂停后恢复:
|
||||
或者在代码中监控黑板:
|
||||
|
||||
```typescript
|
||||
BehaviorTreeStarter.pause(aiRoot);
|
||||
// ... 一段时间后
|
||||
BehaviorTreeStarter.resume(aiRoot);
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 如何使用自定义逻辑?
|
||||
|
||||
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||
|
||||
参见[自定义节点执行器](./custom-actions.md)学习如何创建。
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
# 行为树系统
|
||||
|
||||
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了完全ECS化的行为树系统,所有节点都是实体和组件,充分利用了ECS的性能优势。
|
||||
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。
|
||||
|
||||
## 什么是行为树?
|
||||
## 什么是行为树?
|
||||
|
||||
行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于:
|
||||
行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于:
|
||||
|
||||
- 游戏AI(敌人、NPC行为)
|
||||
- 游戏AI(敌人、NPC行为)
|
||||
- 状态机的替代方案
|
||||
- 复杂的决策逻辑
|
||||
- 可视化的行为设计
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 完全ECS化
|
||||
- 所有节点都是实体(Entity)
|
||||
- 节点属性存储在组件(Component)中
|
||||
- 利用ECS的缓存友好特性
|
||||
- 支持大规模AI实例
|
||||
### Runtime执行器架构
|
||||
- 数据与逻辑分离
|
||||
- 无状态执行器设计
|
||||
- 高性能执行
|
||||
- 类型安全
|
||||
|
||||
### 可视化编辑器
|
||||
- 图形化节点编辑
|
||||
- 实时预览和调试
|
||||
- 拖拽式节点创建
|
||||
- 支持子树复用
|
||||
- 属性连接和绑定
|
||||
|
||||
### 灵活的黑板系统
|
||||
- 本地黑板(单个行为树)
|
||||
- 全局黑板(所有行为树共享)
|
||||
- 本地黑板(单个行为树)
|
||||
- 全局黑板(所有行为树共享)
|
||||
- 类型安全的变量访问
|
||||
- 支持多种数据类型
|
||||
- 支持属性绑定
|
||||
|
||||
### 强大的序列化
|
||||
- JSON格式(可读性好)
|
||||
- 二进制格式(体积小60-70%)
|
||||
- 跨平台兼容
|
||||
- 支持格式转换
|
||||
|
||||
### 引擎集成
|
||||
- Cocos Creator 支持
|
||||
- Laya 引擎支持
|
||||
- 纯TypeScript实现
|
||||
- 易于扩展到其他引擎
|
||||
### 插件系统
|
||||
- 自动注册机制
|
||||
- 装饰器声明元数据
|
||||
- 支持多语言
|
||||
- 易于扩展
|
||||
|
||||
## 文档导航
|
||||
|
||||
@@ -55,87 +49,149 @@
|
||||
- **[编辑器使用指南](./editor-guide.md)** - 可视化创建行为树
|
||||
- **[编辑器工作流](./editor-workflow.md)** - 完整的开发流程
|
||||
|
||||
### 资源管理
|
||||
|
||||
- **[资产管理](./asset-management.md)** - 加载、管理和复用行为树资产、使用子树
|
||||
|
||||
### 引擎集成
|
||||
|
||||
- **[Cocos Creator 集成](./cocos-integration.md)** - 在 Cocos Creator 中使用行为树
|
||||
- **[Laya 引擎集成](./laya-integration.md)** - 在 Laya 中使用行为树
|
||||
- **[Node.js 服务端使用](./nodejs-usage.md)** - 在服务器、聊天机器人等场景中使用行为树
|
||||
|
||||
### 高级主题
|
||||
|
||||
- **[高级用法](./advanced-usage.md)** - 子树、异步加载、性能优化
|
||||
- **[自定义动作](./custom-actions.md)** - 创建自定义行为节点
|
||||
- **[高级用法](./advanced-usage.md)** - 性能优化、调试技巧
|
||||
- **[自定义节点执行器](./custom-actions.md)** - 创建自定义行为节点
|
||||
- **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧
|
||||
|
||||
## 快速示例
|
||||
|
||||
### 代码方式创建
|
||||
### 使用Builder创建
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BlackboardValueType,
|
||||
TaskStatus
|
||||
BehaviorTreePlugin
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
const scene = new Scene();
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 创建敌人AI
|
||||
const enemyAI = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.blackboard()
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('target', BlackboardValueType.Object, null)
|
||||
.endBlackboard()
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 创建行为树
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.selector('MainBehavior')
|
||||
// 如果生命值高,则攻击
|
||||
// 如果生命值高,则攻击
|
||||
.sequence('AttackBranch')
|
||||
.compareBlackboardValue('health', CompareOperator.Greater, 50)
|
||||
.action('Attack', () => {
|
||||
console.log('Attacking player');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.blackboardCompare('health', 50, 'greater')
|
||||
.log('攻击玩家', 'Attack')
|
||||
.end()
|
||||
// 否则逃跑
|
||||
.action('Flee', () => {
|
||||
console.log('Fleeing from battle');
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.log('逃离战斗', 'Flee')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 启动AI
|
||||
BehaviorTreeStarter.start(enemyAI);
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, enemyAI);
|
||||
```
|
||||
|
||||
### 编辑器方式创建
|
||||
### 使用编辑器创建
|
||||
|
||||
1. 打开行为树编辑器
|
||||
2. 创建新的行为树资产
|
||||
3. 拖拽节点到画布
|
||||
4. 配置节点属性
|
||||
5. 保存并导出
|
||||
6. 在代码中加载使用
|
||||
4. 配置节点属性和连接
|
||||
5. 保存并在代码中使用
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Runtime执行器架构
|
||||
|
||||
本框架采用Runtime执行器架构,将节点定义和执行逻辑分离:
|
||||
|
||||
**核心组件:**
|
||||
- `BehaviorTreeData`: 纯数据结构,描述行为树
|
||||
- `BehaviorTreeRuntimeComponent`: 运行时组件,管理状态和黑板
|
||||
- `BehaviorTreeExecutionSystem`: 执行系统,驱动行为树运行
|
||||
- `INodeExecutor`: 节点执行器接口
|
||||
- `NodeExecutionContext`: 执行上下文
|
||||
|
||||
**优势:**
|
||||
- 数据与逻辑分离,易于序列化
|
||||
- 执行器无状态,可复用
|
||||
- 类型安全,编译时检查
|
||||
- 高性能执行
|
||||
|
||||
### 自定义执行器
|
||||
|
||||
创建自定义节点非常简单:
|
||||
|
||||
```typescript
|
||||
// 加载编辑器创建的行为树
|
||||
const asset = await loadBehaviorTree('enemy-ai.btree.json');
|
||||
const ai = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
BehaviorTreeStarter.start(ai);
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
BindingHelper,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '攻击',
|
||||
description: '攻击目标',
|
||||
category: '战斗',
|
||||
configSchema: {
|
||||
damage: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
const target = context.runtime.getBlackboardValue('target');
|
||||
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
console.log(`造成 ${damage} 点伤害`);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
详细说明请参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
## 下一步
|
||||
|
||||
建议按照以下顺序学习:
|
||||
建议按照以下顺序学习:
|
||||
|
||||
1. 阅读[快速开始](./getting-started.md)了解基础用法
|
||||
2. 学习[核心概念](./core-concepts.md)理解行为树原理
|
||||
3. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||
4. 根据你的引擎查看集成教程([Cocos](./cocos-integration.md) 或 [Laya](./laya-integration.md))
|
||||
5. 探索[高级用法](./advanced-usage.md)提升技能
|
||||
3. 学习[资产管理](./asset-management.md)了解如何加载和复用行为树、使用子树
|
||||
4. 根据你的场景查看集成教程:
|
||||
- 客户端游戏:[Cocos Creator](./cocos-integration.md) 或 [Laya](./laya-integration.md)
|
||||
- 服务端应用:[Node.js 服务端使用](./nodejs-usage.md)
|
||||
5. 尝试[编辑器使用指南](./editor-guide.md)可视化创建行为树
|
||||
6. 探索[高级用法](./advanced-usage.md)和[自定义节点执行器](./custom-actions.md)提升技能
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 查看 [示例项目](https://github.com/esengine/ecs-framework/tree/master/examples)
|
||||
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
|
||||
- 加入社区讨论
|
||||
- 参考文档中的完整代码示例
|
||||
|
||||
@@ -87,22 +87,19 @@ new Main();
|
||||
```typescript
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeAssetLoader,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BlackboardComponent
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
export class EnemyAI extends Laya.Script {
|
||||
behaviorTreePath: string = "resources/behaviors/enemy.btree";
|
||||
|
||||
private aiEntity: Entity;
|
||||
|
||||
onEnable() {
|
||||
this.loadBehaviorTree();
|
||||
this.createBehaviorTree();
|
||||
}
|
||||
|
||||
private async loadBehaviorTree() {
|
||||
private createBehaviorTree() {
|
||||
// 获取Core管理的场景
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
@@ -110,30 +107,25 @@ export class EnemyAI extends Laya.Script {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载JSON资产
|
||||
const jsonData = await Laya.loader.load(this.behaviorTreePath, Laya.Loader.JSON);
|
||||
const sprite = this.owner as Laya.Sprite;
|
||||
|
||||
// 转换为JSON字符串
|
||||
const jsonString = typeof jsonData === 'string' ? jsonData : JSON.stringify(jsonData);
|
||||
// 使用Builder API创建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('layaSprite', sprite)
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('position', { x: sprite.x, y: sprite.y })
|
||||
.selector('MainBehavior')
|
||||
.sequence('Combat')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.log('攻击', 'Attack')
|
||||
.end()
|
||||
.log('巡逻', 'Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 反序列化
|
||||
const asset = BehaviorTreeAssetSerializer.deserialize(jsonString);
|
||||
|
||||
// 实例化
|
||||
this.aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene, {
|
||||
namePrefix: (this.owner as Laya.Sprite).name
|
||||
});
|
||||
|
||||
// 设置黑板变量
|
||||
const blackboard = this.aiEntity.getComponent(BlackboardComponent);
|
||||
blackboard?.setValue('layaSprite', this.owner);
|
||||
blackboard?.setValue('position', {
|
||||
x: (this.owner as Laya.Sprite).x,
|
||||
y: (this.owner as Laya.Sprite).y
|
||||
});
|
||||
|
||||
// 启动AI
|
||||
BehaviorTreeStarter.start(this.aiEntity);
|
||||
// 创建AI实体并启动
|
||||
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
@@ -148,19 +140,65 @@ export class EnemyAI extends Laya.Script {
|
||||
|
||||
## 与Laya节点交互
|
||||
|
||||
在BehaviorTreeBuilder的action方法中,可以直接操作Laya节点。下面的完整示例展示了如何实现。
|
||||
要实现与Laya节点的交互,需要创建自定义执行器。下面展示一个完整示例。
|
||||
|
||||
## 完整示例
|
||||
|
||||
创建一个完整的敌人AI系统:
|
||||
创建一个使用自定义执行器的敌人AI系统:
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeBuilder, BehaviorTreeStarter, BlackboardValueType, TaskStatus } from '@esengine/behavior-tree';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
NodeExecutorMetadata,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 自定义移动执行器
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'MoveToTarget',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '移动到目标',
|
||||
category: 'Laya',
|
||||
configSchema: {
|
||||
speed: {
|
||||
type: 'number',
|
||||
default: 50,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class MoveToTargetAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const sprite = context.runtime.getBlackboardValue('layaSprite');
|
||||
const targetPos = context.runtime.getBlackboardValue('targetPosition');
|
||||
const speed = context.nodeData.config.speed;
|
||||
|
||||
if (!sprite || !targetPos) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const dx = targetPos.x - sprite.x;
|
||||
const dy = targetPos.y - sprite.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 10) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
sprite.x += (dx / distance) * speed * context.deltaTime;
|
||||
sprite.y += (dy / distance) * speed * context.deltaTime;
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleEnemyAI extends Laya.Script {
|
||||
public player: Laya.Sprite;
|
||||
public patrolPoints: Array<{x: number, y: number}> = [];
|
||||
|
||||
private aiEntity: Entity;
|
||||
|
||||
@@ -177,68 +215,40 @@ export class SimpleEnemyAI extends Laya.Script {
|
||||
|
||||
const sprite = this.owner as Laya.Sprite;
|
||||
|
||||
this.aiEntity = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.blackboard()
|
||||
.defineVariable('sprite', BlackboardValueType.Object, sprite)
|
||||
.defineVariable('health', BlackboardValueType.Number, 100)
|
||||
.defineVariable('player', BlackboardValueType.Object, this.player)
|
||||
.defineVariable('patrolIndex', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
.selector()
|
||||
// 攻击玩家
|
||||
.sequence()
|
||||
.condition((e, bb) => {
|
||||
const player = bb?.getValue('player');
|
||||
if (!player) return false;
|
||||
|
||||
const dx = player.x - sprite.x;
|
||||
const dy = player.y - sprite.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
return distance < 200; // 检测范围
|
||||
}, 'CheckPlayerInRange')
|
||||
.action('Attack', (e, bb) => {
|
||||
console.log('攻击玩家');
|
||||
// 攻击逻辑
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
.end()
|
||||
// 巡逻
|
||||
.sequence()
|
||||
.action('Patrol', (e, bb, dt) => {
|
||||
const index = bb?.getValue('patrolIndex') || 0;
|
||||
const point = this.patrolPoints[index];
|
||||
|
||||
const dx = point.x - sprite.x;
|
||||
const dy = point.y - sprite.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 10) {
|
||||
// 到达路点
|
||||
const nextIndex = (index + 1) % this.patrolPoints.length;
|
||||
bb?.setValue('patrolIndex', nextIndex);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
// 移动
|
||||
const speed = 50;
|
||||
sprite.x += (dx / distance) * speed * dt;
|
||||
sprite.y += (dy / distance) * speed * dt;
|
||||
|
||||
return TaskStatus.Running;
|
||||
})
|
||||
.wait(2.0)
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('layaSprite', sprite)
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('player', this.player)
|
||||
.defineBlackboardVariable('targetPosition', { x: 0, y: 0 })
|
||||
.selector('MainBehavior')
|
||||
.sequence('Attack')
|
||||
.blackboardExists('player')
|
||||
.log('攻击玩家', 'DoAttack')
|
||||
.end()
|
||||
.log('巡逻', 'Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
BehaviorTreeStarter.start(this.aiEntity);
|
||||
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
|
||||
// 可以在帧更新中修改黑板
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const runtime = this.aiEntity?.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime && this.player) {
|
||||
runtime.setBlackboardValue('targetPosition', {
|
||||
x: this.player.x,
|
||||
y: this.player.y
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
// 停止AI
|
||||
if (this.aiEntity) {
|
||||
BehaviorTreeStarter.stop(this.aiEntity);
|
||||
}
|
||||
Laya.timer.clearAll(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -246,36 +256,37 @@ export class SimpleEnemyAI extends Laya.Script {
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 使用对象池
|
||||
### 使用冷却装饰器
|
||||
|
||||
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||
|
||||
```typescript
|
||||
class AIPool {
|
||||
private pool: Entity[] = [];
|
||||
|
||||
get(asset: any, scene: Scene): Entity {
|
||||
return this.pool.pop() ||
|
||||
BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
}
|
||||
|
||||
release(entity: Entity) {
|
||||
BehaviorTreeStarter.stop(entity);
|
||||
this.pool.push(entity);
|
||||
}
|
||||
}
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||
.selector('MainBehavior')
|
||||
// AI逻辑...
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 降低更新频率
|
||||
### 限制同时运行的AI数量
|
||||
|
||||
```typescript
|
||||
private updateInterval: number = 0.1; // 每0.1秒更新
|
||||
private timer: number = 0;
|
||||
class AIManager {
|
||||
private activeAIs: Entity[] = [];
|
||||
private maxAIs: number = 20;
|
||||
|
||||
onUpdate() {
|
||||
this.timer += Laya.timer.delta / 1000;
|
||||
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||||
if (this.activeAIs.length >= this.maxAIs) {
|
||||
const furthest = this.activeAIs.shift();
|
||||
if (furthest) {
|
||||
BehaviorTreeStarter.stop(furthest);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.timer >= this.updateInterval) {
|
||||
this.scene?.update();
|
||||
this.timer = 0;
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
this.activeAIs.push(entity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
@@ -0,0 +1,580 @@
|
||||
# Node.js 服务端使用
|
||||
|
||||
本文介绍如何在 Node.js 服务端环境(如游戏服务器、机器人、自动化工具)中使用行为树系统。
|
||||
|
||||
## 使用场景
|
||||
|
||||
行为树不仅适用于游戏客户端AI,在服务端也有广泛应用:
|
||||
|
||||
1. **游戏服务器** - NPC AI逻辑、副本关卡脚本
|
||||
2. **聊天机器人** - 对话流程控制、智能回复
|
||||
3. **自动化测试** - 测试用例执行流程
|
||||
4. **工作流引擎** - 业务流程自动化
|
||||
5. **爬虫系统** - 数据采集流程控制
|
||||
|
||||
## 基础设置
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||
```
|
||||
|
||||
### TypeScript 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 简单的游戏服务器 NPC
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function startServer() {
|
||||
// 1. 初始化 ECS Core
|
||||
Core.create();
|
||||
|
||||
// 2. 安装行为树插件
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 3. 创建场景
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 4. 创建 NPC 行为树
|
||||
const npcAI = BehaviorTreeBuilder.create('MerchantNPC')
|
||||
.defineBlackboardVariable('mood', 'friendly')
|
||||
.defineBlackboardVariable('goldAmount', 1000)
|
||||
|
||||
.selector('NPCBehavior')
|
||||
// 如果玩家触发对话
|
||||
.sequence('Dialogue')
|
||||
.blackboardExists('playerRequest')
|
||||
.log('NPC: 欢迎光临!')
|
||||
.end()
|
||||
|
||||
// 默认行为:闲置
|
||||
.sequence('Idle')
|
||||
.log('NPC: 正在整理商品...')
|
||||
.wait(5.0)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 5. 创建 NPC 实体
|
||||
const npc = scene.createEntity('Merchant');
|
||||
BehaviorTreeStarter.start(npc, npcAI);
|
||||
|
||||
// 6. 启动游戏循环(20 TPS)
|
||||
setInterval(() => {
|
||||
Core.update(0.05); // 50ms = 1/20秒
|
||||
}, 50);
|
||||
|
||||
// 7. 模拟玩家交互
|
||||
setTimeout(() => {
|
||||
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('playerRequest', 'buy_sword');
|
||||
console.log('玩家发起交易请求');
|
||||
}, 3000);
|
||||
|
||||
console.log('游戏服务器已启动');
|
||||
}
|
||||
|
||||
startServer();
|
||||
```
|
||||
|
||||
## 实战示例:聊天机器人
|
||||
|
||||
创建一个基于行为树的智能聊天机器人:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent,
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
TaskStatus,
|
||||
NodeType,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 1. 创建自定义节点:回复消息
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SendMessage',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '发送消息',
|
||||
configSchema: {
|
||||
message: { type: 'string', default: '' }
|
||||
}
|
||||
})
|
||||
class SendMessageAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const message = context.nodeData.config['message'] as string;
|
||||
const userMessage = context.runtime.getBlackboardValue<string>('userMessage');
|
||||
|
||||
console.log(`[机器人回复]: ${message}`);
|
||||
console.log(` 回复给: ${userMessage}`);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建自定义节点:匹配关键词
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'MatchKeyword',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '匹配关键词',
|
||||
configSchema: {
|
||||
keyword: { type: 'string', default: '' }
|
||||
}
|
||||
})
|
||||
class MatchKeywordCondition implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const keyword = context.nodeData.config['keyword'] as string;
|
||||
const userMessage = context.runtime.getBlackboardValue<string>('userMessage') || '';
|
||||
|
||||
return userMessage.includes(keyword) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建聊天机器人类
|
||||
class ChatBot {
|
||||
private botEntity: Entity;
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
// 创建机器人行为树
|
||||
const botBehavior = BehaviorTreeBuilder.create('ChatBotAI')
|
||||
.defineBlackboardVariable('userMessage', '')
|
||||
.defineBlackboardVariable('userName', 'Guest')
|
||||
|
||||
.selector('ResponseSelector')
|
||||
// 问候语
|
||||
.sequence('Greeting')
|
||||
.executeCondition('MatchKeyword', { keyword: '你好' })
|
||||
.executeAction('SendMessage', { message: '你好!我是智能助手,有什么可以帮你的吗?' })
|
||||
.end()
|
||||
|
||||
// 帮助请求
|
||||
.sequence('Help')
|
||||
.executeCondition('MatchKeyword', { keyword: '帮助' })
|
||||
.executeAction('SendMessage', { message: '我可以帮你回答问题、查询信息。试试问我一些问题吧!' })
|
||||
.end()
|
||||
|
||||
// 查询天气
|
||||
.sequence('Weather')
|
||||
.executeCondition('MatchKeyword', { keyword: '天气' })
|
||||
.executeAction('SendMessage', { message: '今天天气不错,晴天,温度适宜。' })
|
||||
.end()
|
||||
|
||||
// 查询时间
|
||||
.sequence('Time')
|
||||
.executeCondition('MatchKeyword', { keyword: '时间' })
|
||||
.executeAction('SendMessage', { message: `现在时间是 ${new Date().toLocaleString()}` })
|
||||
.end()
|
||||
|
||||
// 默认回复
|
||||
.executeAction('SendMessage', { message: '抱歉,我还不太理解你的意思。可以换个方式问我吗?' })
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 创建实体并启动
|
||||
this.botEntity = scene.createEntity('ChatBot');
|
||||
BehaviorTreeStarter.start(this.botEntity, botBehavior);
|
||||
this.runtime = this.botEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
// 处理用户消息
|
||||
async handleMessage(userName: string, message: string) {
|
||||
if (this.runtime) {
|
||||
this.runtime.setBlackboardValue('userName', userName);
|
||||
this.runtime.setBlackboardValue('userMessage', message);
|
||||
}
|
||||
|
||||
// 等待一帧让行为树执行
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 主程序
|
||||
async function main() {
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 注册自定义节点
|
||||
const system = scene.getSystem(BehaviorTreeExecutionSystem);
|
||||
if (system) {
|
||||
const registry = system.getExecutorRegistry();
|
||||
registry.register('SendMessage', new SendMessageAction());
|
||||
registry.register('MatchKeyword', new MatchKeywordCondition());
|
||||
}
|
||||
|
||||
// 创建聊天机器人
|
||||
const bot = new ChatBot(scene);
|
||||
|
||||
// 启动更新循环
|
||||
setInterval(() => {
|
||||
Core.update(0.1);
|
||||
}, 100);
|
||||
|
||||
// 模拟用户对话
|
||||
console.log('\n=== 聊天机器人测试 ===\n');
|
||||
|
||||
await bot.handleMessage('Alice', '你好');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Bob', '现在几点了?');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Charlie', '今天天气怎么样');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('David', '你能帮我做什么');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Eve', '你好吗?');
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## 实战示例:多人游戏服务器
|
||||
|
||||
### 房间管理系统
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeAssetManager
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 游戏房间
|
||||
class GameRoom {
|
||||
private scene: Scene;
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
private monsters: Entity[] = [];
|
||||
|
||||
constructor(roomId: string) {
|
||||
// 创建房间场景
|
||||
this.scene = new Scene();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
plugin.setupScene(this.scene);
|
||||
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 初始化房间
|
||||
this.spawnMonsters();
|
||||
console.log(`房间 ${roomId} 已创建,怪物数量: ${this.monsters.length}`);
|
||||
}
|
||||
|
||||
private spawnMonsters() {
|
||||
// 从资产管理器获取怪物AI(所有房间共享)
|
||||
const monsterAI = this.assetManager.getAsset('MonsterAI');
|
||||
if (!monsterAI) return;
|
||||
|
||||
// 生成10个怪物
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const monster = this.scene.createEntity(`Monster_${i}`);
|
||||
BehaviorTreeStarter.start(monster, monsterAI);
|
||||
this.monsters.push(monster);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
this.scene.update(deltaTime);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.monsters.forEach(m => m.destroy());
|
||||
this.monsters = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private rooms: Map<string, GameRoom> = new Map();
|
||||
|
||||
createRoom(roomId: string): GameRoom {
|
||||
const room = new GameRoom(roomId);
|
||||
this.rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
getRoom(roomId: string): GameRoom | undefined {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
destroyRoom(roomId: string) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (room) {
|
||||
room.destroy();
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
this.rooms.forEach(room => room.update(deltaTime));
|
||||
}
|
||||
}
|
||||
|
||||
// 主程序
|
||||
async function startGameServer() {
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 预加载怪物AI(所有房间共享)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
const monsterAI = BehaviorTreeBuilder.create('MonsterAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('Behavior')
|
||||
.log('攻击玩家')
|
||||
.end()
|
||||
.build();
|
||||
assetManager.loadAsset(monsterAI);
|
||||
|
||||
// 创建房间管理器
|
||||
const roomManager = new RoomManager();
|
||||
|
||||
// 模拟房间创建
|
||||
roomManager.createRoom('room_1');
|
||||
roomManager.createRoom('room_2');
|
||||
|
||||
// 服务器主循环(60 TPS)
|
||||
setInterval(() => {
|
||||
roomManager.update(1/60);
|
||||
}, 1000 / 60);
|
||||
|
||||
console.log('游戏服务器已启动');
|
||||
}
|
||||
|
||||
startGameServer();
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 控制更新频率
|
||||
|
||||
```typescript
|
||||
// 不同类型的AI使用不同的更新频率
|
||||
class AIManager {
|
||||
private importantAIs: Entity[] = []; // Boss等重要AI,60 TPS
|
||||
private normalAIs: Entity[] = []; // 普通敌人,20 TPS
|
||||
private backgroundAIs: Entity[] = []; // 背景NPC,5 TPS
|
||||
|
||||
update() {
|
||||
// 重要AI每帧更新
|
||||
this.updateAIs(this.importantAIs, 1/60);
|
||||
|
||||
// 普通AI每3帧更新一次
|
||||
if (frameCount % 3 === 0) {
|
||||
this.updateAIs(this.normalAIs, 3/60);
|
||||
}
|
||||
|
||||
// 背景AI每12帧更新一次
|
||||
if (frameCount % 12 === 0) {
|
||||
this.updateAIs(this.backgroundAIs, 12/60);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 资源管理
|
||||
|
||||
```typescript
|
||||
// 使用资产管理器避免重复创建
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 预加载所有AI
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI').build();
|
||||
const bossAI = BehaviorTreeBuilder.create('BossAI').build();
|
||||
|
||||
assetManager.loadAsset(enemyAI);
|
||||
assetManager.loadAsset(bossAI);
|
||||
|
||||
// 创建1000个敌人,但只使用1份BehaviorTreeData
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
const ai = assetManager.getAsset('EnemyAI')!;
|
||||
BehaviorTreeStarter.start(enemy, ai);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用对象池
|
||||
|
||||
```typescript
|
||||
class EntityPool {
|
||||
private pool: Entity[] = [];
|
||||
private active: Entity[] = [];
|
||||
|
||||
spawn(scene: Scene, treeId: string): Entity {
|
||||
let entity = this.pool.pop();
|
||||
|
||||
if (!entity) {
|
||||
entity = scene.createEntity();
|
||||
const tree = assetManager.getAsset(treeId)!;
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
} else {
|
||||
BehaviorTreeStarter.restart(entity);
|
||||
}
|
||||
|
||||
this.active.push(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
recycle(entity: Entity) {
|
||||
BehaviorTreeStarter.pause(entity);
|
||||
const index = this.active.indexOf(entity);
|
||||
if (index >= 0) {
|
||||
this.active.splice(index, 1);
|
||||
this.pool.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用环境变量控制调试
|
||||
|
||||
```typescript
|
||||
const DEBUG = process.env.NODE_ENV === 'development';
|
||||
|
||||
const aiTree = BehaviorTreeBuilder.create('AI')
|
||||
.selector('Main')
|
||||
.when(DEBUG, builder =>
|
||||
builder.log('调试信息:开始AI逻辑')
|
||||
)
|
||||
// AI 逻辑...
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
// ... 构建逻辑
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(tree);
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
} catch (error) {
|
||||
console.error('启动AI失败:', error);
|
||||
// 使用默认AI或进行降级处理
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 监控和日志
|
||||
|
||||
```typescript
|
||||
// 定期输出AI状态
|
||||
setInterval(() => {
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
const count = assetManager.getAssetCount();
|
||||
const entities = scene.getEntitiesFor(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||
|
||||
console.log(`[AI监控] 行为树资产: ${count}, 活跃实体: ${entities.length}`);
|
||||
}, 10000);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何与 Express/Koa 等框架集成?
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const app = express();
|
||||
const scene = new Scene();
|
||||
|
||||
// 在单独的循环中更新ECS
|
||||
setInterval(() => {
|
||||
Core.update(0.016);
|
||||
}, 16);
|
||||
|
||||
app.post('/npc/:id/interact', (req, res) => {
|
||||
const npcId = req.params.id;
|
||||
const npc = scene.findEntity(npcId);
|
||||
|
||||
if (npc) {
|
||||
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('playerRequest', req.body);
|
||||
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ error: 'NPC not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### 如何持久化行为树状态?
|
||||
|
||||
```typescript
|
||||
// 保存状态
|
||||
function saveAIState(entity: Entity) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
return {
|
||||
treeId: runtime.treeId,
|
||||
blackboard: runtime.getAllBlackboardVariables(),
|
||||
activeNodes: Array.from(runtime.activeNodeIds)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复状态
|
||||
function loadAIState(entity: Entity, savedState: any) {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
if (runtime) {
|
||||
// 恢复黑板变量
|
||||
Object.entries(savedState.blackboard).forEach(([key, value]) => {
|
||||
runtime.setBlackboardValue(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management.md)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的服务端AI
|
||||
Reference in New Issue
Block a user