2025-10-28 11:45:35 +08:00
|
|
|
# 最佳实践
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
## 行为树设计原则
|
|
|
|
|
|
|
|
|
|
### 1. 保持树的层次清晰
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
将复杂行为分解成清晰的层次结构:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Root Selector
|
2025-10-31 17:27:38 +08:00
|
|
|
├── Emergency (高优先级:紧急情况)
|
2025-10-28 11:45:35 +08:00
|
|
|
│ ├── FleeFromDanger
|
|
|
|
|
│ └── CallForHelp
|
2025-10-31 17:27:38 +08:00
|
|
|
├── Combat (中优先级:战斗)
|
2025-10-28 11:45:35 +08:00
|
|
|
│ ├── Attack
|
|
|
|
|
│ └── Defend
|
2025-10-31 17:27:38 +08:00
|
|
|
└── Idle (低优先级:空闲)
|
2025-10-28 11:45:35 +08:00
|
|
|
├── Patrol
|
|
|
|
|
└── Rest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 单一职责原则
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 好的设计 - 使用内置节点
|
2025-10-28 11:45:35 +08:00
|
|
|
.sequence('AttackSequence')
|
2025-10-31 17:27:38 +08:00
|
|
|
.blackboardExists('target', 'CheckTarget')
|
|
|
|
|
.log('瞄准', 'Aim')
|
|
|
|
|
.log('开火', 'Fire')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 使用描述性名称
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
节点名称应该清楚地表达其功能:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 好的命名
|
2025-10-31 17:27:38 +08:00
|
|
|
.blackboardCompare('health', 20, 'less', 'CheckHealthLow')
|
|
|
|
|
.log('寻找最近的医疗包', 'FindHealthPack')
|
|
|
|
|
.log('移动到医疗包', 'MoveToHealthPack')
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
// 不好的命名
|
2025-10-31 17:27:38 +08:00
|
|
|
.blackboardCompare('health', 20, 'less', 'C1')
|
|
|
|
|
.log('Do something', 'Action1')
|
|
|
|
|
.log('Move', 'A2')
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 黑板变量管理
|
|
|
|
|
|
|
|
|
|
### 1. 变量命名规范
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
使用清晰的命名约定:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
const tree = BehaviorTreeBuilder.create('AI')
|
2025-10-28 11:45:35 +08:00
|
|
|
// 状态变量
|
2025-10-31 17:27:38 +08:00
|
|
|
.defineBlackboardVariable('currentState', 'idle')
|
|
|
|
|
.defineBlackboardVariable('isMoving', false)
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
// 目标和引用
|
2025-10-31 17:27:38 +08:00
|
|
|
.defineBlackboardVariable('targetEnemy', null)
|
|
|
|
|
.defineBlackboardVariable('patrolPoints', [])
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
// 配置参数
|
2025-10-31 17:27:38 +08:00
|
|
|
.defineBlackboardVariable('attackRange', 5.0)
|
|
|
|
|
.defineBlackboardVariable('moveSpeed', 10.0)
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
// 临时数据
|
2025-10-31 17:27:38 +08:00
|
|
|
.defineBlackboardVariable('lastAttackTime', 0)
|
|
|
|
|
.defineBlackboardVariable('searchAttempts', 0)
|
|
|
|
|
// ...
|
|
|
|
|
.build();
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 避免过度使用黑板
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 好的做法 - 使用局部变量
|
2025-10-31 17:27:38 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 使用类型安全的访问
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
export class TypeSafeAction implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
const { runtime } = context;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 使用泛型进行类型安全访问
|
|
|
|
|
const health = runtime.getBlackboardValue<number>('health');
|
|
|
|
|
const target = runtime.getBlackboardValue<Entity | null>('target');
|
|
|
|
|
const state = runtime.getBlackboardValue<string>('currentState');
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
if (health !== undefined && health < 30) {
|
|
|
|
|
runtime.setBlackboardValue('currentState', 'flee');
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return TaskStatus.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
## 执行器设计
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 1. 保持执行器无状态
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
状态必须存储在`context.state`中,而不是执行器实例:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 正确的做法
|
|
|
|
|
export class TimedAction implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
if (!context.state.startTime) {
|
|
|
|
|
context.state.startTime = context.totalTime;
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
const elapsed = context.totalTime - context.state.startTime;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
if (elapsed >= 3.0) {
|
|
|
|
|
return TaskStatus.Success;
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return TaskStatus.Running;
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
reset(context: NodeExecutionContext): void {
|
|
|
|
|
context.state.startTime = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 2. 条件应该是无副作用的
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
条件检查不应该修改状态:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 好的做法 - 只读检查
|
|
|
|
|
@NodeExecutorMetadata({
|
|
|
|
|
implementationType: 'IsHealthLow',
|
|
|
|
|
nodeType: NodeType.Condition,
|
|
|
|
|
displayName: '检查生命值低',
|
|
|
|
|
category: '条件',
|
|
|
|
|
configSchema: {
|
|
|
|
|
threshold: {
|
|
|
|
|
type: 'number',
|
|
|
|
|
default: 30,
|
|
|
|
|
supportBinding: true
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
}
|
2025-10-31 17:27:38 +08:00
|
|
|
})
|
|
|
|
|
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;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return health < threshold ? TaskStatus.Success : TaskStatus.Failure;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 3. 错误处理
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
```typescript
|
|
|
|
|
export class SafeAction implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
try {
|
|
|
|
|
const resourceId = context.runtime.getBlackboardValue('resourceId');
|
|
|
|
|
|
|
|
|
|
if (!resourceId) {
|
|
|
|
|
console.error('[SafeAction] 资源ID未设置');
|
|
|
|
|
return TaskStatus.Failure;
|
2025-10-28 11:45:35 +08:00
|
|
|
}
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 执行操作...
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return TaskStatus.Success;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[SafeAction] 执行失败:', error);
|
|
|
|
|
context.runtime.setBlackboardValue('lastError', error.message);
|
2025-10-28 11:45:35 +08:00
|
|
|
return TaskStatus.Failure;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-31 17:27:38 +08:00
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 性能优化技巧
|
|
|
|
|
|
|
|
|
|
### 1. 使用冷却装饰器
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
避免高频执行昂贵操作:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
|
|
|
|
.cooldown(1.0, 'ThrottleSearch') // 最多每秒执行一次
|
|
|
|
|
.log('昂贵的搜索操作', 'ExpensiveSearch')
|
|
|
|
|
.end()
|
|
|
|
|
.build();
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 缓存计算结果
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
export class CachedFindNearest implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
const { state, runtime, totalTime } = context;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 检查缓存是否有效
|
|
|
|
|
const cacheTime = state.enemyCacheTime || 0;
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset(context: NodeExecutionContext): void {
|
|
|
|
|
context.state.enemyCacheTime = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 使用早期退出
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
const tree = BehaviorTreeBuilder.create('EarlyExit')
|
|
|
|
|
.selector('FindTarget')
|
|
|
|
|
// 先检查缓存的目标
|
|
|
|
|
.blackboardExists('cachedTarget', 'HasCachedTarget')
|
|
|
|
|
|
|
|
|
|
// 没有缓存才进行搜索(需要自定义执行器)
|
|
|
|
|
.log('执行昂贵的搜索', 'SearchNewTarget')
|
|
|
|
|
.end()
|
|
|
|
|
.build();
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 可维护性
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 1. 使用有意义的节点名称
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 好的做法
|
|
|
|
|
const tree = BehaviorTreeBuilder.create('CombatAI')
|
|
|
|
|
.selector('CombatDecision')
|
|
|
|
|
.sequence('AttackEnemy')
|
|
|
|
|
.blackboardExists('target', 'HasTarget')
|
|
|
|
|
.log('执行攻击', 'Attack')
|
|
|
|
|
.end()
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
.build();
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 不好的做法
|
|
|
|
|
const tree = BehaviorTreeBuilder.create('AI')
|
|
|
|
|
.selector('Node1')
|
|
|
|
|
.sequence('Node2')
|
|
|
|
|
.blackboardExists('target', 'Node3')
|
|
|
|
|
.log('Attack', 'Node4')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
.end()
|
|
|
|
|
.build();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 使用编辑器创建复杂树
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
对于复杂的AI,使用可视化编辑器:
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
- 更直观的结构
|
|
|
|
|
- 方便非程序员调整
|
|
|
|
|
- 易于版本控制
|
|
|
|
|
- 支持实时调试
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 添加注释和文档
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 为行为树添加清晰的注释
|
|
|
|
|
const bossAI = BehaviorTreeBuilder.create('BossAI')
|
|
|
|
|
.defineBlackboardVariable('phase', 1) // 1=正常, 2=狂暴, 3=濒死
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
.selector('MainBehavior')
|
2025-10-31 17:27:38 +08:00
|
|
|
// 阶段3: 生命值<20%,使用终极技能
|
2025-10-28 11:45:35 +08:00
|
|
|
.sequence('Phase3')
|
2025-10-31 17:27:38 +08:00
|
|
|
.blackboardCompare('phase', 3, 'equals')
|
|
|
|
|
.log('使用终极技能', 'UltimateAbility')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 阶段2: 生命值<50%,进入狂暴
|
2025-10-28 11:45:35 +08:00
|
|
|
.sequence('Phase2')
|
2025-10-31 17:27:38 +08:00
|
|
|
.blackboardCompare('phase', 2, 'equals')
|
|
|
|
|
.log('进入狂暴模式', 'BerserkMode')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 阶段1: 正常战斗
|
2025-10-28 11:45:35 +08:00
|
|
|
.sequence('Phase1')
|
2025-10-31 17:27:38 +08:00
|
|
|
.log('普通攻击', 'NormalAttack')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
.end()
|
|
|
|
|
.build();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 调试技巧
|
|
|
|
|
|
|
|
|
|
### 1. 使用日志节点
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
const tree = BehaviorTreeBuilder.create('Debug')
|
|
|
|
|
.log('开始攻击序列', 'StartAttack')
|
|
|
|
|
.sequence('Attack')
|
|
|
|
|
.log('检查目标', 'CheckTarget')
|
|
|
|
|
.blackboardExists('target')
|
|
|
|
|
.log('执行攻击', 'DoAttack')
|
|
|
|
|
.end()
|
|
|
|
|
.build();
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 2. 在自定义执行器中调试
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
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.log('活动节点:', Array.from(runtime.activeNodeIds));
|
|
|
|
|
console.groupEnd();
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return TaskStatus.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 状态可视化
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
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;
|
2025-10-28 11:45:35 +08:00
|
|
|
}
|
2025-10-31 17:27:38 +08:00
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 常见反模式
|
|
|
|
|
|
|
|
|
|
### 1. 过深的嵌套
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 不好 - 太深的嵌套
|
|
|
|
|
.selector()
|
|
|
|
|
.sequence()
|
|
|
|
|
.sequence()
|
|
|
|
|
.sequence()
|
2025-10-31 17:27:38 +08:00
|
|
|
.log('太深了', 'DeepAction')
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
.end()
|
|
|
|
|
.end()
|
|
|
|
|
.end()
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 好 - 使用合理的深度
|
2025-10-28 11:45:35 +08:00
|
|
|
.selector()
|
2025-10-31 17:27:38 +08:00
|
|
|
.sequence()
|
|
|
|
|
.log('Action1')
|
|
|
|
|
.log('Action2')
|
|
|
|
|
.end()
|
|
|
|
|
.sequence()
|
|
|
|
|
.log('Action3')
|
|
|
|
|
.log('Action4')
|
|
|
|
|
.end()
|
2025-10-28 11:45:35 +08:00
|
|
|
.end()
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
### 2. 在执行器中存储状态
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
```typescript
|
2025-10-31 17:27:38 +08:00
|
|
|
// 错误 - 状态存储在执行器中
|
|
|
|
|
export class BadAction implements INodeExecutor {
|
|
|
|
|
private startTime = 0; // 错误!多个节点会共享这个值
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
this.startTime = context.totalTime; // 错误!
|
|
|
|
|
return TaskStatus.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
// 正确 - 状态存储在context.state中
|
|
|
|
|
export class GoodAction implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
if (!context.state.startTime) {
|
|
|
|
|
context.state.startTime = context.totalTime; // 正确!
|
|
|
|
|
}
|
|
|
|
|
return TaskStatus.Success;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 频繁修改黑板
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// 不好 - 每帧都修改黑板
|
2025-10-31 17:27:38 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
|
|
|
|
// 好 - 只在需要时修改
|
2025-10-31 17:27:38 +08:00
|
|
|
export class SmartUpdate implements INodeExecutor {
|
|
|
|
|
execute(context: NodeExecutionContext): TaskStatus {
|
|
|
|
|
const oldPos = context.runtime.getBlackboardValue('position');
|
|
|
|
|
const newPos = getCurrentPosition();
|
|
|
|
|
|
|
|
|
|
// 只在位置变化时更新
|
|
|
|
|
if (!positionsEqual(oldPos, newPos)) {
|
|
|
|
|
context.runtime.setBlackboardValue('position', newPos);
|
|
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
return TaskStatus.Running;
|
2025-10-28 11:45:35 +08:00
|
|
|
}
|
2025-10-31 17:27:38 +08:00
|
|
|
}
|
2025-10-28 11:45:35 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 下一步
|
|
|
|
|
|
2025-10-31 17:27:38 +08:00
|
|
|
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
|
2025-10-28 11:45:35 +08:00
|
|
|
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
2025-10-31 17:27:38 +08:00
|
|
|
- 参考[核心概念](./core-concepts.md)深入理解原理
|