fix(behavior-tree): 修复插件节点执行问题并完善文档
This commit is contained in:
551
docs/guide/behavior-tree/best-practices.md
Normal file
551
docs/guide/behavior-tree/best-practices.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# 最佳实践
|
||||
|
||||
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||
|
||||
## 行为树设计原则
|
||||
|
||||
### 1. 保持树的层次清晰
|
||||
|
||||
将复杂行为分解成清晰的层次结构:
|
||||
|
||||
```
|
||||
Root Selector
|
||||
├── Emergency (高优先级:紧急情况)
|
||||
│ ├── FleeFromDanger
|
||||
│ └── CallForHelp
|
||||
├── Combat (中优先级:战斗)
|
||||
│ ├── Attack
|
||||
│ └── Defend
|
||||
└── Idle (低优先级:空闲)
|
||||
├── Patrol
|
||||
└── Rest
|
||||
```
|
||||
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事:
|
||||
|
||||
```typescript
|
||||
// 好的设计
|
||||
.sequence('AttackSequence')
|
||||
.condition(hasTarget, 'CheckTarget')
|
||||
.action(aim, 'Aim')
|
||||
.action(fire, 'Fire')
|
||||
.end()
|
||||
|
||||
// 不好的设计 - 一个动作做太多事
|
||||
.action('AttackPlayer', () => {
|
||||
checkTarget();
|
||||
aim();
|
||||
fire();
|
||||
playAnimation();
|
||||
playSound();
|
||||
// 太多职责了!
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 使用描述性名称
|
||||
|
||||
节点名称应该清楚地表达其功能:
|
||||
|
||||
```typescript
|
||||
// 好的命名
|
||||
.condition(isHealthLow, 'CheckHealthLow')
|
||||
.action(findNearestHealthPack, 'FindHealthPack')
|
||||
.action(moveToHealthPack, 'MoveToHealthPack')
|
||||
|
||||
// 不好的命名
|
||||
.condition(check1, 'C1')
|
||||
.action(doSomething, 'Action1')
|
||||
.action(move, 'A2')
|
||||
```
|
||||
|
||||
## 黑板变量管理
|
||||
|
||||
### 1. 变量命名规范
|
||||
|
||||
使用清晰的命名约定:
|
||||
|
||||
```typescript
|
||||
.blackboard()
|
||||
// 状态变量
|
||||
.defineVariable('currentState', BlackboardValueType.String, 'idle')
|
||||
.defineVariable('isMoving', BlackboardValueType.Boolean, false)
|
||||
|
||||
// 目标和引用
|
||||
.defineVariable('targetEnemy', BlackboardValueType.Object, null)
|
||||
.defineVariable('patrolPoints', BlackboardValueType.Array, [])
|
||||
|
||||
// 配置参数
|
||||
.defineVariable('attackRange', BlackboardValueType.Number, 5.0)
|
||||
.defineVariable('moveSpeed', BlackboardValueType.Number, 10.0)
|
||||
|
||||
// 临时数据
|
||||
.defineVariable('lastAttackTime', BlackboardValueType.Number, 0)
|
||||
.defineVariable('searchAttempts', BlackboardValueType.Number, 0)
|
||||
.endBlackboard()
|
||||
```
|
||||
|
||||
### 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;
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 使用类型安全的访问
|
||||
|
||||
```typescript
|
||||
// 定义类型接口
|
||||
interface EnemyBlackboard {
|
||||
health: number;
|
||||
target: Entity | null;
|
||||
state: 'idle' | 'patrol' | 'chase' | 'attack';
|
||||
}
|
||||
|
||||
// 使用时进行类型检查
|
||||
.action('UseBlackboard', (e, bb) => {
|
||||
const health = bb?.getValue<number>('health');
|
||||
const target = bb?.getValue<Entity | null>('target');
|
||||
const state = bb?.getValue<string>('state');
|
||||
|
||||
if (health !== undefined && health < 30) {
|
||||
bb?.setValue('state', 'flee');
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
```
|
||||
|
||||
## 条件节点设计
|
||||
|
||||
### 1. 条件应该是无副作用的
|
||||
|
||||
条件检查不应该修改状态:
|
||||
|
||||
```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;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
```
|
||||
|
||||
### 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);
|
||||
}
|
||||
|
||||
const state = bb?.getValue('chargeState');
|
||||
const chargeTime = bb?.getValue('chargeTime') || 0;
|
||||
|
||||
switch (state) {
|
||||
case 'charging':
|
||||
bb?.setValue('chargeTime', chargeTime + dt);
|
||||
|
||||
if (chargeTime >= 3.0) {
|
||||
bb?.setValue('chargeState', 'ready');
|
||||
}
|
||||
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未设置');
|
||||
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()
|
||||
```
|
||||
|
||||
### 2. 缓存计算结果
|
||||
|
||||
```typescript
|
||||
.action('FindNearestEnemy', (e, bb) => {
|
||||
// 检查缓存是否有效
|
||||
const cacheTime = bb?.getValue('enemyCacheTime') || 0;
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - cacheTime < 500) { // 缓存500ms
|
||||
// 使用缓存结果
|
||||
return bb?.getValue('nearestEnemy') ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
const nearest = findNearestEnemy();
|
||||
bb?.setValue('nearestEnemy', nearest);
|
||||
bb?.setValue('enemyCacheTime', currentTime);
|
||||
|
||||
return nearest ? TaskStatus.Success : TaskStatus.Failure;
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 使用早期退出
|
||||
|
||||
```typescript
|
||||
.selector('FindTarget')
|
||||
// 先检查缓存的目标
|
||||
.condition((e, bb) => bb?.hasVariable('cachedTarget'), 'HasCachedTarget')
|
||||
|
||||
// 没有缓存才进行搜索
|
||||
.action('SearchNewTarget', (e, bb) => {
|
||||
const target = performExpensiveSearch();
|
||||
bb?.setValue('cachedTarget', target);
|
||||
return target ? TaskStatus.Success : TaskStatus.Failure;
|
||||
})
|
||||
.end()
|
||||
```
|
||||
|
||||
## 可维护性
|
||||
|
||||
### 1. 使用子树模块化
|
||||
|
||||
将可复用的行为提取为子树:
|
||||
|
||||
```typescript
|
||||
// 巡逻子树
|
||||
const patrolBehavior = BehaviorTreeBuilder.create(scene, 'Patrol')
|
||||
.sequence()
|
||||
.action('MoveToNextWaypoint', () => TaskStatus.Success)
|
||||
.wait(2.0)
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 主树中引用
|
||||
const mainTree = BehaviorTreeBuilder.create(scene, 'EnemyAI')
|
||||
.selector()
|
||||
.sequence('Combat')
|
||||
// 战斗逻辑
|
||||
.end()
|
||||
.subTree(patrolBehavior) // 复用巡逻行为
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 使用编辑器创建复杂树
|
||||
|
||||
对于复杂的AI,使用可视化编辑器:
|
||||
|
||||
- 更直观的结构
|
||||
- 方便非程序员调整
|
||||
- 易于版本控制
|
||||
- 支持实时调试
|
||||
|
||||
|
||||
### 3. 添加注释和文档
|
||||
|
||||
```typescript
|
||||
const ai = BehaviorTreeBuilder.create(scene, 'BossAI')
|
||||
.blackboard()
|
||||
.defineVariable('phase', BlackboardValueType.Number, 1) // 1=正常, 2=狂暴, 3=濒死
|
||||
.endBlackboard()
|
||||
|
||||
.selector('MainBehavior')
|
||||
// 阶段3:生命值<20%,使用终极技能
|
||||
.sequence('Phase3')
|
||||
.compareBlackboardValue('phase', CompareOperator.Equal, 3)
|
||||
.action('UltimateAbility', () => TaskStatus.Success)
|
||||
.end()
|
||||
|
||||
// 阶段2:生命值<50%,进入狂暴
|
||||
.sequence('Phase2')
|
||||
.compareBlackboardValue('phase', CompareOperator.Equal, 2)
|
||||
.action('BerserkMode', () => TaskStatus.Success)
|
||||
.end()
|
||||
|
||||
// 阶段1:正常战斗
|
||||
.sequence('Phase1')
|
||||
.action('NormalAttack', () => TaskStatus.Success)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用日志节点
|
||||
|
||||
```typescript
|
||||
.log('开始攻击序列', 'info')
|
||||
.sequence('Attack')
|
||||
.log('检查目标', 'debug')
|
||||
.condition(hasTarget)
|
||||
.log('执行攻击', 'info')
|
||||
.action(attack)
|
||||
.end()
|
||||
```
|
||||
|
||||
### 2. 添加断言
|
||||
|
||||
```typescript
|
||||
.action('ValidateState', (e, bb) => {
|
||||
const health = bb?.getValue('health');
|
||||
const maxHealth = bb?.getValue('maxHealth');
|
||||
|
||||
console.assert(health !== undefined, 'health不应为undefined');
|
||||
console.assert(maxHealth !== undefined, 'maxHealth不应为undefined');
|
||||
console.assert(health <= maxHealth, `health(${health})不应大于maxHealth(${maxHealth})`);
|
||||
|
||||
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();
|
||||
}
|
||||
return TaskStatus.Success;
|
||||
})
|
||||
```
|
||||
|
||||
## 常见反模式
|
||||
|
||||
### 1. 过深的嵌套
|
||||
|
||||
```typescript
|
||||
// 不好 - 太深的嵌套
|
||||
.selector()
|
||||
.sequence()
|
||||
.sequence()
|
||||
.sequence()
|
||||
.action('DeepAction', () => TaskStatus.Success)
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 好 - 使用子树扁平化
|
||||
const innerBehavior = BehaviorTreeBuilder.create(scene, 'Inner')
|
||||
.action('DeepAction', () => TaskStatus.Success)
|
||||
.build();
|
||||
|
||||
.selector()
|
||||
.subTree(innerBehavior)
|
||||
.end()
|
||||
```
|
||||
|
||||
### 2. 在行为树中实现游戏逻辑
|
||||
|
||||
```typescript
|
||||
// 不好 - 行为树不应包含具体游戏逻辑
|
||||
.action('Attack', (e, bb) => {
|
||||
const enemy = bb?.getValue('enemy');
|
||||
const damage = calculateDamage(e.getComponent(Weapon));
|
||||
enemy.health -= damage;
|
||||
|
||||
if (enemy.health <= 0) {
|
||||
enemy.die();
|
||||
e.experience += enemy.expReward;
|
||||
}
|
||||
|
||||
playAttackAnimation();
|
||||
playAttackSound();
|
||||
// 太多细节了!
|
||||
})
|
||||
|
||||
// 好 - 行为树只负责决策,具体逻辑由系统处理
|
||||
.action('Attack', (e, bb) => {
|
||||
const enemy = bb?.getValue('enemy');
|
||||
// 发送攻击命令,具体逻辑由战斗系统处理
|
||||
Core.ecsAPI?.emit('combat:attack', { attacker: e, target: enemy });
|
||||
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;
|
||||
})
|
||||
|
||||
// 好 - 只在需要时修改
|
||||
.action('UpdatePosition', (e, bb, dt) => {
|
||||
const oldPos = bb?.getValue('position');
|
||||
const newPos = getCurrentPosition();
|
||||
|
||||
// 只在位置变化时更新
|
||||
if (!positionsEqual(oldPos, newPos)) {
|
||||
bb?.setValue('position', newPos);
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[节点参考](./node-reference.md)了解所有内置节点
|
||||
- 学习[自定义节点](./custom-nodes.md)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
||||
Reference in New Issue
Block a user