refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)
* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 * fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞 * feat(behavior-tree): 完善行为树核心功能并修复类型错误
This commit is contained in:
@@ -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)学习如何创建。
|
||||
|
||||
Reference in New Issue
Block a user