refactor(behavior-tree)!: 迁移到 Runtime 执行器架构 (#196)

* refactor(behavior-tree)!: 迁移到 Runtime 执行器架构

* fix(behavior-tree): 修复LogAction中的ReDoS安全漏洞

* feat(behavior-tree): 完善行为树核心功能并修复类型错误
This commit is contained in:
YHH
2025-10-31 17:27:38 +08:00
committed by GitHub
parent c58e3411fd
commit 61813e67b6
113 changed files with 7795 additions and 10564 deletions

View File

@@ -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)学习可视化编辑

View 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)优化你的行为树设计

View File

@@ -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)深入理解原理

View File

@@ -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)创建自定义行为

View File

@@ -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

View File

@@ -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)学习如何扩展节点

View File

@@ -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设计

View File

@@ -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)学习如何创建。

View File

@@ -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)
- 加入社区讨论
- 参考文档中的完整代码示例

View File

@@ -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);
}
}
```

View 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等重要AI60 TPS
private normalAIs: Entity[] = []; // 普通敌人20 TPS
private backgroundAIs: Entity[] = []; // 背景NPC5 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

View File

@@ -1,547 +1,357 @@
import { Entity, IScene } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from './Components/BehaviorTreeNode';
import { CompositeNodeComponent } from './Components/CompositeNodeComponent';
import { DecoratorNodeComponent } from './Components/DecoratorNodeComponent';
import { BlackboardComponent } from './Components/BlackboardComponent';
import { NodeType, CompositeType, DecoratorType, BlackboardValueType } from './Types/TaskStatus';
// 导入动作组件
import { WaitAction } from './Components/Actions/WaitAction';
import { LogAction } from './Components/Actions/LogAction';
import { SetBlackboardValueAction } from './Components/Actions/SetBlackboardValueAction';
import { ModifyBlackboardValueAction, ModifyOperation } from './Components/Actions/ModifyBlackboardValueAction';
import { ExecuteAction, CustomActionFunction } from './Components/Actions/ExecuteAction';
// 导入条件组件
import { BlackboardCompareCondition, CompareOperator } from './Components/Conditions/BlackboardCompareCondition';
import { BlackboardExistsCondition } from './Components/Conditions/BlackboardExistsCondition';
import { RandomProbabilityCondition } from './Components/Conditions/RandomProbabilityCondition';
import { ExecuteCondition, CustomConditionFunction } from './Components/Conditions/ExecuteCondition';
// 导入装饰器组件
import { RepeaterNode } from './Components/Decorators/RepeaterNode';
import { InverterNode } from './Components/Decorators/InverterNode';
import { UntilSuccessNode } from './Components/Decorators/UntilSuccessNode';
import { UntilFailNode } from './Components/Decorators/UntilFailNode';
import { AlwaysSucceedNode } from './Components/Decorators/AlwaysSucceedNode';
import { AlwaysFailNode } from './Components/Decorators/AlwaysFailNode';
import { ConditionalNode } from './Components/Decorators/ConditionalNode';
import { CooldownNode } from './Components/Decorators/CooldownNode';
import { TimeoutNode } from './Components/Decorators/TimeoutNode';
import { BehaviorTreeData, BehaviorNodeData } from './Runtime/BehaviorTreeData';
import { NodeType } from './Types/TaskStatus';
/**
* 行为树构建器
*
* 提供流式 API构建行为树结构
*
* @example
* ```typescript
* const aiRoot = BehaviorTreeBuilder.create(scene, 'AI')
* .blackboard()
* .defineVariable('health', BlackboardValueType.Number, 100)
* .defineVariable('target', BlackboardValueType.Object, null)
* .endBlackboard()
* .selector('MainSelector')
* .sequence('AttackSequence')
* .condition((entity, blackboard) => {
* return blackboard?.getValue('health') > 50;
* })
* .action('Attack', (entity) => TaskStatus.Success)
* .end()
* .action('Flee', (entity) => TaskStatus.Success)
* .end()
* .build();
* ```
* 提供流式API构建行为树数据结构
*/
export class BehaviorTreeBuilder {
private scene: IScene;
private currentEntity: Entity;
private entityStack: Entity[] = [];
private blackboardEntity?: Entity;
private treeData: BehaviorTreeData;
private nodeStack: string[] = [];
private nodeIdCounter: number = 0;
private constructor(scene: IScene, rootName: string) {
this.scene = scene;
this.currentEntity = scene.createEntity(rootName);
private constructor(treeName: string) {
this.treeData = {
id: `tree_${Date.now()}`,
name: treeName,
rootNodeId: '',
nodes: new Map(),
blackboardVariables: new Map()
};
}
/**
* 创建行为树构建器
*
* @param scene 场景实例
* @param rootName 根节点名称
* @returns 构建器实例
* 创建构建器
*/
static create(scene: IScene, rootName: string = 'BehaviorTreeRoot'): BehaviorTreeBuilder {
return new BehaviorTreeBuilder(scene, rootName);
}
/**
* 添加黑板组件到根节点
*/
blackboard(): BehaviorTreeBuilder {
this.blackboardEntity = this.currentEntity;
this.currentEntity.addComponent(new BlackboardComponent());
return this;
static create(treeName: string = 'BehaviorTree'): BehaviorTreeBuilder {
return new BehaviorTreeBuilder(treeName);
}
/**
* 定义黑板变量
*/
defineVariable(
name: string,
type: BlackboardValueType,
initialValue: any,
options?: { readonly?: boolean; description?: string }
): BehaviorTreeBuilder {
if (!this.blackboardEntity) {
throw new Error('Must call blackboard() first');
defineBlackboardVariable(key: string, initialValue: any): BehaviorTreeBuilder {
if (!this.treeData.blackboardVariables) {
this.treeData.blackboardVariables = new Map();
}
const blackboard = this.blackboardEntity.getComponent(BlackboardComponent);
if (blackboard) {
blackboard.defineVariable(name, type, initialValue, options);
}
this.treeData.blackboardVariables.set(key, initialValue);
return this;
}
/**
* 结束黑板定义
* 添加序列节点
*/
endBlackboard(): BehaviorTreeBuilder {
this.blackboardEntity = undefined;
return this;
sequence(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('Sequence', name || 'Sequence');
}
/**
* 创建序列节点
* 添加选择器节点
*/
sequence(name: string = 'Sequence'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.Sequence);
selector(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('Selector', name || 'Selector');
}
/**
* 创建选择器节点
* 添加并行节点
*/
selector(name: string = 'Selector'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.Selector);
parallel(name?: string, config?: { successPolicy?: string; failurePolicy?: string }): BehaviorTreeBuilder {
return this.addCompositeNode('Parallel', name || 'Parallel', config);
}
/**
* 创建并行节点
* 添加并行选择器节点
*/
parallel(name: string = 'Parallel'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.Parallel);
parallelSelector(name?: string, config?: { failurePolicy?: string }): BehaviorTreeBuilder {
return this.addCompositeNode('ParallelSelector', name || 'ParallelSelector', config);
}
/**
* 创建并行选择器节点
* 添加随机序列节点
*/
parallelSelector(name: string = 'ParallelSelector'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.ParallelSelector);
randomSequence(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('RandomSequence', name || 'RandomSequence');
}
/**
* 创建随机序列节点
* 添加随机选择器节点
*/
randomSequence(name: string = 'RandomSequence'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.RandomSequence);
randomSelector(name?: string): BehaviorTreeBuilder {
return this.addCompositeNode('RandomSelector', name || 'RandomSelector');
}
/**
* 创建随机选择器节点
* 添加反转装饰器
*/
randomSelector(name: string = 'RandomSelector'): BehaviorTreeBuilder {
return this.composite(name, CompositeType.RandomSelector);
inverter(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Inverter', name || 'Inverter');
}
/**
* 创建复合节点
* 添加重复装饰器
*/
private composite(name: string, type: CompositeType): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Composite;
node.nodeName = name;
const composite = entity.addComponent(new CompositeNodeComponent());
composite.compositeType = type;
this.currentEntity = entity;
return this;
repeater(repeatCount: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Repeater', name || 'Repeater', { repeatCount });
}
/**
* 创建反转装饰器
* 添加总是成功装饰器
*/
inverter(name: string = 'Inverter'): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
entity.addComponent(new InverterNode());
this.currentEntity = entity;
return this;
alwaysSucceed(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('AlwaysSucceed', name || 'AlwaysSucceed');
}
/**
* 创建重复装饰器
* 添加总是失败装饰器
*/
repeater(name: string = 'Repeater', count: number = -1, endOnFailure: boolean = false): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
const decorator = entity.addComponent(new RepeaterNode());
decorator.repeatCount = count;
decorator.endOnFailure = endOnFailure;
this.currentEntity = entity;
return this;
alwaysFail(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('AlwaysFail', name || 'AlwaysFail');
}
/**
* 创建直到成功装饰器
* 添加直到成功装饰器
*/
untilSuccess(name: string = 'UntilSuccess'): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
entity.addComponent(new UntilSuccessNode());
this.currentEntity = entity;
return this;
untilSuccess(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('UntilSuccess', name || 'UntilSuccess');
}
/**
* 创建直到失败装饰器
* 添加直到失败装饰器
*/
untilFail(name: string = 'UntilFail'): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
entity.addComponent(new UntilFailNode());
this.currentEntity = entity;
return this;
untilFail(name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('UntilFail', name || 'UntilFail');
}
/**
* 创建总是成功装饰器
* 添加条件装饰器
*/
alwaysSucceed(name: string = 'AlwaysSucceed'): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
entity.addComponent(new AlwaysSucceedNode());
this.currentEntity = entity;
return this;
conditional(blackboardKey: string, expectedValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Conditional', name || 'Conditional', {
blackboardKey,
expectedValue,
operator: operator || 'equals'
});
}
/**
* 创建总是失败装饰器
* 添加冷却装饰器
*/
alwaysFail(name: string = 'AlwaysFail'): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
entity.addComponent(new AlwaysFailNode());
this.currentEntity = entity;
return this;
cooldown(cooldownTime: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Cooldown', name || 'Cooldown', { cooldownTime });
}
/**
* 创建条件装饰器
* 添加超时装饰器
*/
conditional(name: string, conditionCode: string): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
const decorator = entity.addComponent(new ConditionalNode());
decorator.conditionCode = conditionCode;
this.currentEntity = entity;
return this;
timeout(timeout: number, name?: string): BehaviorTreeBuilder {
return this.addDecoratorNode('Timeout', name || 'Timeout', { timeout });
}
/**
* 创建冷却装饰器
* 添加等待动作
*/
cooldown(name: string = 'Cooldown', cooldownTime: number = 1.0): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
const decorator = entity.addComponent(new CooldownNode());
decorator.cooldownTime = cooldownTime;
this.currentEntity = entity;
return this;
wait(duration: number, name?: string): BehaviorTreeBuilder {
return this.addActionNode('Wait', name || 'Wait', { duration });
}
/**
* 创建超时装饰器
* 添加日志动作
*/
timeout(name: string = 'Timeout', timeoutDuration: number = 5.0): BehaviorTreeBuilder {
this.entityStack.push(this.currentEntity);
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Decorator;
node.nodeName = name;
const decorator = entity.addComponent(new TimeoutNode());
decorator.timeoutDuration = timeoutDuration;
this.currentEntity = entity;
return this;
log(message: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('Log', name || 'Log', { message });
}
/**
* 创建等待动作
* 添加设置黑板值动作
*/
wait(waitTime: number, name: string = 'Wait'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Action;
node.nodeName = name;
const action = entity.addComponent(new WaitAction());
action.waitTime = waitTime;
return this;
setBlackboardValue(key: string, value: any, name?: string): BehaviorTreeBuilder {
return this.addActionNode('SetBlackboardValue', name || 'SetBlackboardValue', { key, value });
}
/**
* 创建日志动作
* 添加修改黑板值动作
*/
log(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log', name: string = 'Log'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Action;
node.nodeName = name;
const action = entity.addComponent(new LogAction());
action.message = message;
action.level = level;
return this;
modifyBlackboardValue(key: string, operation: string, value: number, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ModifyBlackboardValue', name || 'ModifyBlackboardValue', {
key,
operation,
value
});
}
/**
* 创建设置黑板值动作
* 添加执行动作
*/
setBlackboardValue(variableName: string, value: any, name: string = 'SetValue'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Action;
node.nodeName = name;
const action = entity.addComponent(new SetBlackboardValueAction());
action.variableName = variableName;
action.value = value;
return this;
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
}
/**
* 创建修改黑板值动作
* 添加黑板比较条件
*/
modifyBlackboardValue(
variableName: string,
operation: ModifyOperation,
operand: any,
name: string = 'ModifyValue'
): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Action;
node.nodeName = name;
const action = entity.addComponent(new ModifyBlackboardValueAction());
action.variableName = variableName;
action.operation = operation;
action.operand = operand;
return this;
blackboardCompare(key: string, compareValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('BlackboardCompare', name || 'BlackboardCompare', {
key,
compareValue,
operator: operator || 'equals'
});
}
/**
* 创建自定义动作
* 添加黑板存在检查条件
*/
action(name: string, func: CustomActionFunction): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Action;
node.nodeName = name;
const action = entity.addComponent(new ExecuteAction());
action.setFunction(func);
return this;
blackboardExists(key: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('BlackboardExists', name || 'BlackboardExists', { key });
}
/**
* 创建黑板比较条件
* 添加随机概率条件
*/
compareBlackboardValue(
variableName: string,
operator: CompareOperator,
compareValue: any,
name: string = 'Compare'
): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Condition;
node.nodeName = name;
const condition = entity.addComponent(new BlackboardCompareCondition());
condition.variableName = variableName;
condition.operator = operator;
condition.compareValue = compareValue;
return this;
randomProbability(probability: number, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('RandomProbability', name || 'RandomProbability', { probability });
}
/**
* 创建黑板变量存在条件
* 添加执行条件
*/
checkBlackboardExists(variableName: string, checkNotNull: boolean = false, name: string = 'Exists'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Condition;
node.nodeName = name;
const condition = entity.addComponent(new BlackboardExistsCondition());
condition.variableName = variableName;
condition.checkNotNull = checkNotNull;
return this;
}
/**
* 创建随机概率条件
*/
randomProbability(probability: number, name: string = 'Random'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Condition;
node.nodeName = name;
const condition = entity.addComponent(new RandomProbabilityCondition());
condition.probability = probability;
return this;
}
/**
* 创建自定义条件
*/
condition(func: CustomConditionFunction, name: string = 'Condition'): BehaviorTreeBuilder {
const entity = this.scene.createEntity(name);
this.currentEntity.addChild(entity);
const node = entity.addComponent(new BehaviorTreeNode());
node.nodeType = NodeType.Condition;
node.nodeName = name;
const condition = entity.addComponent(new ExecuteCondition());
condition.setFunction(func);
return this;
executeCondition(conditionName: string, name?: string): BehaviorTreeBuilder {
return this.addConditionNode('ExecuteCondition', name || 'ExecuteCondition', { conditionName });
}
/**
* 结束当前节点,返回父节点
*/
end(): BehaviorTreeBuilder {
if (this.entityStack.length === 0) {
throw new Error('No parent node to return to');
if (this.nodeStack.length > 0) {
this.nodeStack.pop();
}
this.currentEntity = this.entityStack.pop()!;
return this;
}
/**
* 构建并返回根节点实体
* 构建行为树数据
*/
build(): Entity {
// 确保返回到根节点
while (this.entityStack.length > 0) {
this.currentEntity = this.entityStack.pop()!;
build(): BehaviorTreeData {
if (!this.treeData.rootNodeId) {
throw new Error('No root node defined. Add at least one node to the tree.');
}
return this.treeData;
}
private addCompositeNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Composite,
implementationType,
children: [],
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
return this.currentEntity;
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
this.nodeStack.push(nodeId);
return this;
}
private addDecoratorNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Decorator,
implementationType,
children: [],
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
this.nodeStack.push(nodeId);
return this;
}
private addActionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Action,
implementationType,
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
return this;
}
private addConditionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
const nodeId = this.generateNodeId();
const node: BehaviorNodeData = {
id: nodeId,
name,
nodeType: NodeType.Condition,
implementationType,
config
};
this.treeData.nodes.set(nodeId, node);
if (!this.treeData.rootNodeId) {
this.treeData.rootNodeId = nodeId;
}
if (this.nodeStack.length > 0) {
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
const parentNode = this.treeData.nodes.get(parentId);
if (parentNode && parentNode.children) {
parentNode.children.push(nodeId);
}
}
return this;
}
private generateNodeId(): string {
return `node_${this.nodeIdCounter++}`;
}
}

View File

@@ -1,11 +1,9 @@
import type { Core } from '@esengine/ecs-framework';
import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
import { WorldManager } from '@esengine/ecs-framework';
import { LeafExecutionSystem } from './Systems/LeafExecutionSystem';
import { DecoratorExecutionSystem } from './Systems/DecoratorExecutionSystem';
import { CompositeExecutionSystem } from './Systems/CompositeExecutionSystem';
import { SubTreeExecutionSystem } from './Systems/SubTreeExecutionSystem';
import { BehaviorTreeExecutionSystem } from './Runtime/BehaviorTreeExecutionSystem';
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
/**
* 行为树插件
@@ -33,11 +31,12 @@ export class BehaviorTreePlugin implements IPlugin {
/**
* 安装插件
*/
async install(core: Core, services: ServiceContainer): Promise<void> {
async install(_core: Core, services: ServiceContainer): Promise<void> {
this.services = services;
// 注册全局黑板服务
// 注册全局服务
services.registerSingleton(GlobalBlackboardService);
services.registerSingleton(BehaviorTreeAssetManager);
this.worldManager = services.resolve(WorldManager);
}
@@ -46,9 +45,9 @@ export class BehaviorTreePlugin implements IPlugin {
* 卸载插件
*/
async uninstall(): Promise<void> {
// 注销全局黑板服务
if (this.services) {
this.services.unregister(GlobalBlackboardService);
this.services.unregister(BehaviorTreeAssetManager);
}
this.worldManager = null;
@@ -58,11 +57,7 @@ export class BehaviorTreePlugin implements IPlugin {
/**
* 为场景设置行为树系统
*
* 向场景添加所有必需的行为树系统
* - LeafExecutionSystem (updateOrder: 100)
* - DecoratorExecutionSystem (updateOrder: 200)
* - CompositeExecutionSystem (updateOrder: 300)
* - SubTreeExecutionSystem (updateOrder: 300)
* 向场景添加行为树执行系统
*
* @param scene 目标场景
*
@@ -73,10 +68,7 @@ export class BehaviorTreePlugin implements IPlugin {
* ```
*/
public setupScene(scene: IScene): void {
scene.addSystem(new LeafExecutionSystem());
scene.addSystem(new DecoratorExecutionSystem());
scene.addSystem(new CompositeExecutionSystem());
scene.addSystem(new SubTreeExecutionSystem());
scene.addSystem(new BehaviorTreeExecutionSystem());
}
/**

View File

@@ -1,179 +1,92 @@
import { Entity } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from './Components/BehaviorTreeNode';
import { ActiveNode } from './Components/ActiveNode';
import { TaskStatus } from './Types/TaskStatus';
import { Entity, Core } from '@esengine/ecs-framework';
import { BehaviorTreeData } from './Runtime/BehaviorTreeData';
import { BehaviorTreeRuntimeComponent } from './Runtime/BehaviorTreeRuntimeComponent';
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
/**
* 行为树启动/停止辅助类
* 行为树启动辅助类
*
* 提供便捷方法来启动、停止和暂停行为树
* 提供便捷方法来启动、停止行为树
*/
export class BehaviorTreeStarter {
/**
* 启动行为树
*
* 给根节点添加 ActiveNode 组件,使行为树开始执行
*
* @param rootEntity 行为树根节点实体
*
* @example
* ```typescript
* const aiRoot = scene.createEntity('aiRoot');
* // ... 构建行为树结构
* BehaviorTreeStarter.start(aiRoot);
* ```
* @param entity 游戏实体
* @param treeData 行为树数据
* @param autoStart 是否自动开始执行
*/
static start(rootEntity: Entity): void {
if (!rootEntity.hasComponent(BehaviorTreeNode)) {
throw new Error('Entity must have BehaviorTreeNode component');
static start(entity: Entity, treeData: BehaviorTreeData, autoStart: boolean = true): void {
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
assetManager.loadAsset(treeData);
let runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (!runtime) {
runtime = new BehaviorTreeRuntimeComponent();
entity.addComponent(runtime);
}
if (!rootEntity.hasComponent(ActiveNode)) {
rootEntity.addComponent(new ActiveNode());
runtime.treeAssetId = treeData.id;
runtime.autoStart = autoStart;
if (treeData.blackboardVariables) {
for (const [key, value] of treeData.blackboardVariables.entries()) {
runtime.setBlackboardValue(key, value);
}
}
if (autoStart) {
runtime.isRunning = true;
}
}
/**
* 停止行为树
*
* 移除所有节点的 ActiveNode 组件,停止执行
*
* @param rootEntity 行为树根节点实体
*
* @example
* ```typescript
* BehaviorTreeStarter.stop(aiRoot);
* ```
* @param entity 游戏实体
*/
static stop(rootEntity: Entity): void {
this.stopRecursive(rootEntity);
}
/**
* 递归停止所有子节点
*/
private static stopRecursive(entity: Entity): void {
// 移除活跃标记
if (entity.hasComponent(ActiveNode)) {
entity.removeComponentByType(ActiveNode);
}
// 重置节点状态
const node = entity.getComponent(BehaviorTreeNode);
if (node) {
node.reset();
}
// 递归处理子节点
for (const child of entity.children) {
this.stopRecursive(child);
static stop(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = false;
runtime.resetAllStates();
}
}
/**
* 暂停行为树
*
* 移除 ActiveNode 但保留节点状态,可以恢复执行
*
* @param rootEntity 行为树根节点实体
*
* @example
* ```typescript
* // 暂停
* BehaviorTreeStarter.pause(aiRoot);
*
* // 恢复
* BehaviorTreeStarter.resume(aiRoot);
* ```
* @param entity 游戏实体
*/
static pause(rootEntity: Entity): void {
this.pauseRecursive(rootEntity);
}
/**
* 递归暂停所有子节点
*/
private static pauseRecursive(entity: Entity): void {
// 只移除活跃标记,不重置状态
if (entity.hasComponent(ActiveNode)) {
entity.removeComponentByType(ActiveNode);
}
// 递归处理子节点
for (const child of entity.children) {
this.pauseRecursive(child);
static pause(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = false;
}
}
/**
* 恢复行为树执行
* 恢复行为树
*
* 从暂停状态恢复,重新添加 ActiveNode 到之前正在执行的节点
*
* @param rootEntity 行为树根节点实体
*
* @example
* ```typescript
* BehaviorTreeStarter.resume(aiRoot);
* ```
* @param entity 游戏实体
*/
static resume(rootEntity: Entity): void {
this.resumeRecursive(rootEntity);
}
/**
* 递归恢复所有正在执行的节点
*/
private static resumeRecursive(entity: Entity): void {
const node = entity.getComponent(BehaviorTreeNode);
if (!node) {
return;
}
// 如果节点状态是 Running恢复活跃标记
if (node.status === TaskStatus.Running) {
if (!entity.hasComponent(ActiveNode)) {
entity.addComponent(new ActiveNode());
}
}
// 递归处理子节点
for (const child of entity.children) {
this.resumeRecursive(child);
static resume(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.isRunning = true;
}
}
/**
* 重启行为树
*
* 停止并重置所有节点,然后重新启动
*
* @param rootEntity 行为树根节点实体
*
* @example
* ```typescript
* BehaviorTreeStarter.restart(aiRoot);
* ```
* @param entity 游戏实体
*/
static restart(rootEntity: Entity): void {
this.stop(rootEntity);
this.start(rootEntity);
}
/**
* 检查行为树是否正在运行
*
* @param rootEntity 行为树根节点实体
* @returns 是否正在运行
*
* @example
* ```typescript
* if (BehaviorTreeStarter.isRunning(aiRoot)) {
* console.log('AI is active');
* }
* ```
*/
static isRunning(rootEntity: Entity): boolean {
return rootEntity.hasComponent(ActiveNode);
static restart(entity: Entity): void {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
if (runtime) {
runtime.resetAllStates();
runtime.isRunning = true;
}
}
}

View File

@@ -1,87 +0,0 @@
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { BlackboardComponent } from '../BlackboardComponent';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 自定义动作函数类型
*/
export type CustomActionFunction = (
entity: Entity,
blackboard?: BlackboardComponent,
deltaTime?: number
) => TaskStatus;
/**
* 执行自定义函数动作组件
*
* 允许用户提供自定义的动作执行函数
*/
@BehaviorNode({
displayName: '自定义动作',
category: '动作',
type: NodeType.Action,
icon: 'Code',
description: '执行自定义代码',
color: '#FFC107'
})
@ECSComponent('ExecuteAction')
@Serializable({ version: 1 })
export class ExecuteAction extends Component {
@BehaviorProperty({
label: '动作代码',
type: 'code',
description: 'JavaScript 代码,返回 TaskStatus',
required: true
})
@Serialize()
actionCode?: string = 'return TaskStatus.Success;';
@Serialize()
parameters: Record<string, any> = {};
/** 编译后的函数(不序列化) */
@IgnoreSerialization()
private compiledFunction?: CustomActionFunction;
/**
* 获取或编译执行函数
*/
getFunction(): CustomActionFunction | undefined {
if (!this.compiledFunction && this.actionCode) {
try {
const func = new Function(
'entity',
'blackboard',
'deltaTime',
'parameters',
'TaskStatus',
`
const { Success, Failure, Running, Invalid } = TaskStatus;
try {
${this.actionCode}
} catch (error) {
return TaskStatus.Failure;
}
`
);
this.compiledFunction = (entity, blackboard, deltaTime) => {
return func(entity, blackboard, deltaTime, this.parameters, TaskStatus) || TaskStatus.Success;
};
} catch (error) {
return undefined;
}
}
return this.compiledFunction;
}
/**
* 设置自定义函数(运行时使用)
*/
setFunction(func: CustomActionFunction): void {
this.compiledFunction = func;
}
}

View File

@@ -1,49 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 日志动作组件
*
* 输出日志信息
*/
@BehaviorNode({
displayName: '日志',
category: '动作',
type: NodeType.Action,
icon: 'FileText',
description: '输出日志消息',
color: '#673AB7'
})
@ECSComponent('LogAction')
@Serializable({ version: 1 })
export class LogAction extends Component {
@BehaviorProperty({
label: '消息',
type: 'string',
required: true
})
@Serialize()
message: string = 'Hello';
@BehaviorProperty({
label: '级别',
type: 'select',
options: [
{ label: 'Log', value: 'log' },
{ label: 'Info', value: 'info' },
{ label: 'Warn', value: 'warn' },
{ label: 'Error', value: 'error' }
]
})
@Serialize()
level: 'log' | 'info' | 'warn' | 'error' = 'log';
@BehaviorProperty({
label: '包含实体信息',
type: 'boolean'
})
@Serialize()
includeEntityInfo: boolean = false;
}

View File

@@ -1,76 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 修改操作类型
*/
export enum ModifyOperation {
/** 加法 */
Add = 'add',
/** 减法 */
Subtract = 'subtract',
/** 乘法 */
Multiply = 'multiply',
/** 除法 */
Divide = 'divide',
/** 取模 */
Modulo = 'modulo',
/** 追加(数组/字符串) */
Append = 'append',
/** 移除(数组) */
Remove = 'remove'
}
/**
* 修改黑板变量值动作组件
*
* 对黑板变量执行数学或逻辑操作
*/
@BehaviorNode({
displayName: '修改变量',
category: '动作',
type: NodeType.Action,
icon: 'Calculator',
description: '对黑板变量执行数学或逻辑操作',
color: '#FF9800'
})
@ECSComponent('ModifyBlackboardValueAction')
@Serializable({ version: 1 })
export class ModifyBlackboardValueAction extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '操作类型',
type: 'select',
options: [
{ label: '加法', value: 'add' },
{ label: '减法', value: 'subtract' },
{ label: '乘法', value: 'multiply' },
{ label: '除法', value: 'divide' },
{ label: '取模', value: 'modulo' },
{ label: '追加', value: 'append' },
{ label: '移除', value: 'remove' }
]
})
@Serialize()
operation: ModifyOperation = ModifyOperation.Add;
@BehaviorProperty({
label: '操作数',
type: 'string',
description: '可以是固定值或变量引用 {{varName}}'
})
@Serialize()
operand: any = 0;
@Serialize()
force: boolean = false;
}

View File

@@ -1,43 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 设置黑板变量值动作组件
*
* 将指定值或另一个黑板变量的值设置到目标变量
*/
@BehaviorNode({
displayName: '设置变量',
category: '动作',
type: NodeType.Action,
icon: 'Edit',
description: '设置黑板变量的值',
color: '#3F51B5'
})
@ECSComponent('SetBlackboardValueAction')
@Serializable({ version: 1 })
export class SetBlackboardValueAction extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '值',
type: 'string',
description: '可以使用 {{varName}} 引用其他变量'
})
@Serialize()
value: any = '';
@Serialize()
sourceVariable?: string;
@Serialize()
force: boolean = false;
}

View File

@@ -1,43 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 等待动作组件
*
* 等待指定时间后返回成功
*/
@BehaviorNode({
displayName: '等待',
category: '动作',
type: NodeType.Action,
icon: 'Clock',
description: '等待指定时间',
color: '#9E9E9E'
})
@ECSComponent('WaitAction')
@Serializable({ version: 1 })
export class WaitAction extends Component {
@BehaviorProperty({
label: '等待时间',
type: 'number',
min: 0,
step: 0.1,
description: '等待时间(秒)',
required: true
})
@Serialize()
waitTime: number = 1.0;
/** 已等待时间(秒) */
@IgnoreSerialization()
elapsedTime: number = 0;
/**
* 重置等待状态
*/
reset(): void {
this.elapsedTime = 0;
}
}

View File

@@ -1,20 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* 活跃节点标记组件
*
* 标记当前应该被执行的节点。
* 只有带有此组件的节点才会被各个执行系统处理。
*
* 这是一个标记组件Tag Component不包含数据只用于标识。
*
* 执行流程:
* 1. 初始时只有根节点带有 ActiveNode
* 2. 父节点决定激活哪个子节点时,为子节点添加 ActiveNode
* 3. 节点执行完成后移除 ActiveNode
* 4. 通过这种方式实现按需执行,避免每帧遍历整棵树
*/
@ECSComponent('ActiveNode')
export class ActiveNode extends Component {
// 标记组件,无需数据字段
}

View File

@@ -1,61 +0,0 @@
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* 资产元数据组件
*
* 附加到从资产实例化的行为树根节点上,
* 用于标记资产ID和版本信息便于循环引用检测和调试。
*
* @example
* ```typescript
* const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, scene);
*
* // 添加元数据
* const metadata = rootEntity.addComponent(new BehaviorTreeAssetMetadata());
* metadata.assetId = 'patrol';
* metadata.assetVersion = '1.0.0';
* ```
*/
@ECSComponent('BehaviorTreeAssetMetadata')
@Serializable({ version: 1 })
export class BehaviorTreeAssetMetadata extends Component {
/**
* 资产ID
*/
@Serialize()
assetId: string = '';
/**
* 资产版本
*/
@Serialize()
assetVersion: string = '';
/**
* 资产名称
*/
@Serialize()
assetName: string = '';
/**
* 加载时间
*/
@Serialize()
loadedAt: number = 0;
/**
* 资产描述
*/
@Serialize()
description: string = '';
/**
* 初始化
*/
initialize(assetId: string, assetVersion: string, assetName?: string): void {
this.assetId = assetId;
this.assetVersion = assetVersion;
this.assetName = assetName || assetId;
this.loadedAt = Date.now();
}
}

View File

@@ -1,44 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { TaskStatus, NodeType } from '../Types/TaskStatus';
/**
* 行为树节点基础组件
*
* 所有行为树节点都必须包含此组件
*/
@ECSComponent('BehaviorTreeNode')
@Serializable({ version: 1 })
export class BehaviorTreeNode extends Component {
/** 节点类型 */
@Serialize()
nodeType: NodeType = NodeType.Action;
/** 节点名称(用于调试) */
@Serialize()
nodeName: string = 'Node';
/** 当前执行状态 */
@IgnoreSerialization()
status: TaskStatus = TaskStatus.Invalid;
/** 当前执行的子节点索引(用于复合节点) */
@IgnoreSerialization()
currentChildIndex: number = 0;
/**
* 重置节点状态
*/
reset(): void {
this.status = TaskStatus.Invalid;
this.currentChildIndex = 0;
}
/**
* 标记节点为失效(递归重置子节点)
* 注意:此方法只重置当前节点,子节点需要在 System 中处理
*/
invalidate(): void {
this.reset();
}
}

View File

@@ -1,201 +0,0 @@
import { Component, ECSComponent, Core } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { BlackboardValueType } from '../Types/TaskStatus';
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
/**
* 黑板变量定义
*/
export interface BlackboardVariable {
name: string;
type: BlackboardValueType;
value: any;
readonly?: boolean;
description?: string;
}
/**
* 黑板组件 - 用于节点间共享数据
*
* 支持分层查找:
* 1. 先查找本地变量
* 2. 如果找不到,自动查找全局 Blackboard
*
* 通常附加到行为树的根节点上
*/
@ECSComponent('Blackboard')
@Serializable({ version: 1 })
export class BlackboardComponent extends Component {
/** 存储的本地变量 */
@Serialize()
private variables: Map<string, BlackboardVariable> = new Map();
/** 是否启用全局 Blackboard 查找 */
private useGlobalBlackboard: boolean = true;
/**
* 定义一个新变量
*/
defineVariable(
name: string,
type: BlackboardValueType,
initialValue: any,
options?: {
readonly?: boolean;
description?: string;
}
): void {
this.variables.set(name, {
name,
type,
value: initialValue,
readonly: options?.readonly ?? false,
description: options?.description
});
}
/**
* 获取变量值
* 先查找本地变量,找不到则查找全局变量
*/
getValue<T = any>(name: string): T | undefined {
const variable = this.variables.get(name);
if (variable !== undefined) {
return variable.value as T;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).getValue<T>(name);
}
return undefined;
}
/**
* 获取本地变量值(不查找全局)
*/
getLocalValue<T = any>(name: string): T | undefined {
const variable = this.variables.get(name);
return variable?.value as T;
}
/**
* 设置变量值
* 优先设置本地变量,如果本地不存在且全局存在,则设置全局变量
*/
setValue(name: string, value: any, force: boolean = false): boolean {
const variable = this.variables.get(name);
if (variable) {
if (variable.readonly && !force) {
return false;
}
variable.value = value;
return true;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).setValue(name, value, force);
}
return false;
}
/**
* 设置本地变量值(不影响全局)
*/
setLocalValue(name: string, value: any, force: boolean = false): boolean {
const variable = this.variables.get(name);
if (!variable) {
return false;
}
if (variable.readonly && !force) {
return false;
}
variable.value = value;
return true;
}
/**
* 检查变量是否存在(包括本地和全局)
*/
hasVariable(name: string): boolean {
if (this.variables.has(name)) {
return true;
}
if (this.useGlobalBlackboard) {
return Core.services.resolve(GlobalBlackboardService).hasVariable(name);
}
return false;
}
/**
* 检查本地变量是否存在
*/
hasLocalVariable(name: string): boolean {
return this.variables.has(name);
}
/**
* 删除变量
*/
removeVariable(name: string): boolean {
return this.variables.delete(name);
}
/**
* 获取所有变量名
*/
getVariableNames(): string[] {
return Array.from(this.variables.keys());
}
/**
* 清空所有本地变量
*/
clear(): void {
this.variables.clear();
}
/**
* 启用/禁用全局 Blackboard 查找
*/
setUseGlobalBlackboard(enabled: boolean): void {
this.useGlobalBlackboard = enabled;
}
/**
* 是否启用全局 Blackboard 查找
*/
isUsingGlobalBlackboard(): boolean {
return this.useGlobalBlackboard;
}
/**
* 获取所有变量(包括本地和全局)
*/
getAllVariables(): BlackboardVariable[] {
const locals = Array.from(this.variables.values());
if (this.useGlobalBlackboard) {
const globals = Core.services.resolve(GlobalBlackboardService).getAllVariables();
const localNames = new Set(this.variables.keys());
const filteredGlobals = globals.filter(v => !localNames.has(v.name));
return [...locals, ...filteredGlobals];
}
return locals;
}
/**
* 获取全局 Blackboard 服务的引用
*/
static getGlobalBlackboard(): GlobalBlackboardService {
return Core.services.resolve(GlobalBlackboardService);
}
}

View File

@@ -1,66 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { CompositeType } from '../Types/TaskStatus';
/**
* 复合节点组件
*
* 用于标识复合节点类型Sequence, Selector, Parallel等
*/
@ECSComponent('CompositeNode')
@Serializable({ version: 1 })
export class CompositeNodeComponent extends Component {
/** 复合节点类型 */
@Serialize()
compositeType: CompositeType = CompositeType.Sequence;
/** 随机化的子节点索引顺序 */
protected shuffledIndices: number[] = [];
/** 是否在重启时重新洗牌(子类可选) */
protected reshuffleOnRestart: boolean = true;
/**
* 获取下一个子节点索引
*/
getNextChildIndex(currentIndex: number, totalChildren: number): number {
// 对于随机类型,使用洗牌后的索引
if (this.compositeType === CompositeType.RandomSequence ||
this.compositeType === CompositeType.RandomSelector) {
// 首次执行或需要重新洗牌
if (this.shuffledIndices.length === 0 || currentIndex === 0 && this.reshuffleOnRestart) {
this.shuffleIndices(totalChildren);
}
if (currentIndex < this.shuffledIndices.length) {
return this.shuffledIndices[currentIndex];
}
return totalChildren; // 结束
}
// 普通顺序执行
return currentIndex;
}
/**
* 洗牌子节点索引
*/
private shuffleIndices(count: number): void {
this.shuffledIndices = Array.from({ length: count }, (_, i) => i);
// Fisher-Yates 洗牌算法
for (let i = this.shuffledIndices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.shuffledIndices[i], this.shuffledIndices[j]] =
[this.shuffledIndices[j], this.shuffledIndices[i]];
}
}
/**
* 重置洗牌状态
*/
resetShuffle(): void {
this.shuffledIndices = [];
}
}

View File

@@ -1,51 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 并行节点
*
* 同时执行所有子节点
*/
@BehaviorNode({
displayName: '并行',
category: '组合',
type: NodeType.Composite,
icon: 'Layers',
description: '同时执行所有子节点',
color: '#CDDC39'
})
@ECSComponent('ParallelNode')
@Serializable({ version: 1 })
export class ParallelNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '成功策略',
type: 'select',
description: '多少个子节点成功时整体成功',
options: [
{ label: '全部成功', value: 'all' },
{ label: '任意一个成功', value: 'one' }
]
})
@Serialize()
successPolicy: 'all' | 'one' = 'all';
@BehaviorProperty({
label: '失败策略',
type: 'select',
description: '多少个子节点失败时整体失败',
options: [
{ label: '任意一个失败', value: 'one' },
{ label: '全部失败', value: 'all' }
]
})
@Serialize()
failurePolicy: 'one' | 'all' = 'one';
constructor() {
super();
this.compositeType = CompositeType.Parallel;
}
}

View File

@@ -1,39 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 并行选择节点
*
* 并行执行子节点,任一成功则成功
*/
@BehaviorNode({
displayName: '并行选择',
category: '组合',
type: NodeType.Composite,
icon: 'Sparkles',
description: '并行执行子节点,任一成功则成功',
color: '#FFC107'
})
@ECSComponent('ParallelSelectorNode')
@Serializable({ version: 1 })
export class ParallelSelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '失败策略',
type: 'select',
description: '多少个子节点失败时整体失败',
options: [
{ label: '任意一个失败', value: 'one' },
{ label: '全部失败', value: 'all' }
]
})
@Serialize()
failurePolicy: 'one' | 'all' = 'all';
constructor() {
super();
this.compositeType = CompositeType.ParallelSelector;
}
}

View File

@@ -1,35 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 随机选择节点
*
* 随机顺序执行子节点选择
*/
@BehaviorNode({
displayName: '随机选择',
category: '组合',
type: NodeType.Composite,
icon: 'Dices',
description: '随机顺序执行子节点选择',
color: '#F44336'
})
@ECSComponent('RandomSelectorNode')
@Serializable({ version: 1 })
export class RandomSelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '重启时重新洗牌',
type: 'boolean',
description: '每次重启时是否重新随机子节点顺序'
})
@Serialize()
override reshuffleOnRestart: boolean = true;
constructor() {
super();
this.compositeType = CompositeType.RandomSelector;
}
}

View File

@@ -1,35 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 随机序列节点
*
* 随机顺序执行子节点序列
*/
@BehaviorNode({
displayName: '随机序列',
category: '组合',
type: NodeType.Composite,
icon: 'Shuffle',
description: '随机顺序执行子节点序列',
color: '#FF5722'
})
@ECSComponent('RandomSequenceNode')
@Serializable({ version: 1 })
export class RandomSequenceNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '重启时重新洗牌',
type: 'boolean',
description: '每次重启时是否重新随机子节点顺序'
})
@Serialize()
override reshuffleOnRestart: boolean = true;
constructor() {
super();
this.compositeType = CompositeType.RandomSequence;
}
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, CompositeType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 根节点
*
* 行为树的根节点,简单地激活第一个子节点
*/
@BehaviorNode({
displayName: '根节点',
category: '根节点',
type: NodeType.Composite,
icon: 'TreePine',
description: '行为树的根节点',
color: '#FFD700'
})
@ECSComponent('RootNode')
@Serializable({ version: 1 })
export class RootNode extends CompositeNodeComponent {
constructor() {
super();
this.compositeType = CompositeType.Sequence;
}
}

View File

@@ -1,41 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 选择节点
*
* 按顺序执行子节点,任一成功则成功
*/
@BehaviorNode({
displayName: '选择',
category: '组合',
type: NodeType.Composite,
icon: 'GitBranch',
description: '按顺序执行子节点,任一成功则成功',
color: '#8BC34A'
})
@ECSComponent('SelectorNode')
@Serializable({ version: 1 })
export class SelectorNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '中止类型',
type: 'select',
description: '条件变化时的中止行为',
options: [
{ label: '无', value: 'none' },
{ label: '自身', value: 'self' },
{ label: '低优先级', value: 'lower-priority' },
{ label: '两者', value: 'both' }
]
})
@Serialize()
abortType: AbortType = AbortType.None;
constructor() {
super();
this.compositeType = CompositeType.Selector;
}
}

View File

@@ -1,41 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
/**
* 序列节点
*
* 按顺序执行所有子节点,全部成功才成功
*/
@BehaviorNode({
displayName: '序列',
category: '组合',
type: NodeType.Composite,
icon: 'List',
description: '按顺序执行子节点,全部成功才成功',
color: '#4CAF50'
})
@ECSComponent('SequenceNode')
@Serializable({ version: 1 })
export class SequenceNode extends CompositeNodeComponent {
@BehaviorProperty({
label: '中止类型',
type: 'select',
description: '条件变化时的中止行为',
options: [
{ label: '无', value: 'none' },
{ label: '自身', value: 'self' },
{ label: '低优先级', value: 'lower-priority' },
{ label: '两者', value: 'both' }
]
})
@Serialize()
abortType: AbortType = AbortType.None;
constructor() {
super();
this.compositeType = CompositeType.Sequence;
}
}

View File

@@ -1,172 +0,0 @@
import { ECSComponent, Serializable, Serialize, Entity } from '@esengine/ecs-framework';
import { CompositeNodeComponent } from '../CompositeNodeComponent';
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* SubTree 节点 - 引用其他行为树作为子树
*
* 允许将其他行为树嵌入到当前树中,实现行为树的复用和模块化。
*
* 注意SubTreeNode 是一个特殊的叶子节点,它不会执行编辑器中静态连接的子节点,
* 只会执行从 assetId 动态加载的外部行为树文件。
*
* @example
* ```typescript
* const subTree = entity.addComponent(SubTreeNode);
* subTree.assetId = 'patrol';
* subTree.inheritParentBlackboard = true;
* ```
*/
@BehaviorNode({
displayName: '子树',
category: '组合',
type: NodeType.Composite,
icon: 'GitBranch',
description: '引用并执行外部行为树文件(不支持静态子节点)',
color: '#FF9800',
requiresChildren: false
})
@ECSComponent('SubTreeNode')
@Serializable({ version: 1 })
export class SubTreeNode extends CompositeNodeComponent {
/**
* 引用的子树资产ID
* 逻辑标识符,例如 'patrol' 或 'ai/patrol'
* 实际的文件路径由 AssetLoader 决定
*/
@BehaviorProperty({
label: '资产ID',
type: 'asset',
description: '要引用的行为树资产ID'
})
@Serialize()
assetId: string = '';
/**
* 是否将父黑板传递给子树
*
* - true: 子树可以访问和修改父树的黑板变量
* - false: 子树使用独立的黑板实例
*/
@BehaviorProperty({
label: '继承父黑板',
type: 'boolean',
description: '子树是否可以访问父树的黑板变量'
})
@Serialize()
inheritParentBlackboard: boolean = true;
/**
* 子树执行失败时是否传播失败状态
*
* - true: 子树失败时SubTree 节点返回 Failure
* - false: 子树失败时SubTree 节点返回 Success忽略失败
*/
@BehaviorProperty({
label: '传播失败',
type: 'boolean',
description: '子树失败时是否传播失败状态'
})
@Serialize()
propagateFailure: boolean = true;
/**
* 是否在行为树启动时预加载子树
*
* - true: 在根节点开始执行前预加载此子树,确保执行时子树已就绪
* - false: 运行时异步加载,执行到此节点时才开始加载(可能会有延迟)
*/
@BehaviorProperty({
label: '预加载',
type: 'boolean',
description: '在行为树启动时预加载子树,避免运行时加载延迟'
})
@Serialize()
preload: boolean = true;
/**
* 子树的根实体(运行时)
* 在执行时动态创建,执行结束后销毁
*/
private subTreeRoot?: Entity;
/**
* 子树是否已完成
*/
private subTreeCompleted: boolean = false;
/**
* 子树的最终状态
*/
private subTreeResult: TaskStatus = TaskStatus.Invalid;
/**
* 获取子树根实体
*/
getSubTreeRoot(): Entity | undefined {
return this.subTreeRoot;
}
/**
* 设置子树根实体(由执行系统调用)
*/
setSubTreeRoot(root: Entity | undefined): void {
this.subTreeRoot = root;
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 标记子树完成(由执行系统调用)
*/
markSubTreeCompleted(result: TaskStatus): void {
this.subTreeCompleted = true;
this.subTreeResult = result;
}
/**
* 检查子树是否已完成
*/
isSubTreeCompleted(): boolean {
return this.subTreeCompleted;
}
/**
* 获取子树执行结果
*/
getSubTreeResult(): TaskStatus {
return this.subTreeResult;
}
/**
* 重置子树状态
*/
reset(): void {
this.subTreeRoot = undefined;
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 重置完成状态(用于复用预加载的子树)
* 保留子树根引用,只重置完成标记
*/
resetCompletionState(): void {
this.subTreeCompleted = false;
this.subTreeResult = TaskStatus.Invalid;
}
/**
* 验证配置
*/
validate(): string[] {
const errors: string[] = [];
if (!this.assetId || this.assetId.trim() === '') {
errors.push('SubTree 节点必须指定资产ID');
}
return errors;
}
}

View File

@@ -1,83 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 比较运算符
*/
export enum CompareOperator {
/** 等于 */
Equal = 'equal',
/** 不等于 */
NotEqual = 'notEqual',
/** 大于 */
Greater = 'greater',
/** 大于等于 */
GreaterOrEqual = 'greaterOrEqual',
/** 小于 */
Less = 'less',
/** 小于等于 */
LessOrEqual = 'lessOrEqual',
/** 包含(字符串/数组) */
Contains = 'contains',
/** 正则匹配 */
Matches = 'matches'
}
/**
* 黑板变量比较条件组件
*
* 比较黑板变量与指定值或另一个变量
*/
@BehaviorNode({
displayName: '比较变量',
category: '条件',
type: NodeType.Condition,
icon: 'Scale',
description: '比较黑板变量与指定值',
color: '#2196F3'
})
@ECSComponent('BlackboardCompareCondition')
@Serializable({ version: 1 })
export class BlackboardCompareCondition extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '运算符',
type: 'select',
options: [
{ label: '等于', value: 'equal' },
{ label: '不等于', value: 'notEqual' },
{ label: '大于', value: 'greater' },
{ label: '大于等于', value: 'greaterOrEqual' },
{ label: '小于', value: 'less' },
{ label: '小于等于', value: 'lessOrEqual' },
{ label: '包含', value: 'contains' },
{ label: '正则匹配', value: 'matches' }
]
})
@Serialize()
operator: CompareOperator = CompareOperator.Equal;
@BehaviorProperty({
label: '比较值',
type: 'string',
description: '可以是固定值或变量引用 {{varName}}'
})
@Serialize()
compareValue: any = null;
@BehaviorProperty({
label: '反转结果',
type: 'boolean'
})
@Serialize()
invertResult: boolean = false;
}

View File

@@ -1,45 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 黑板变量存在性检查条件组件
*
* 检查黑板变量是否存在
*/
@BehaviorNode({
displayName: '检查变量存在',
category: '条件',
type: NodeType.Condition,
icon: 'Search',
description: '检查黑板变量是否存在',
color: '#00BCD4'
})
@ECSComponent('BlackboardExistsCondition')
@Serializable({ version: 1 })
export class BlackboardExistsCondition extends Component {
@BehaviorProperty({
label: '变量名',
type: 'variable',
required: true
})
@Serialize()
variableName: string = '';
@BehaviorProperty({
label: '检查非空',
type: 'boolean',
description: '检查值不为 null/undefined'
})
@Serialize()
checkNotNull: boolean = false;
@BehaviorProperty({
label: '反转结果',
type: 'boolean',
description: '检查不存在'
})
@Serialize()
invertResult: boolean = false;
}

View File

@@ -1,92 +0,0 @@
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { BlackboardComponent } from '../BlackboardComponent';
/**
* 自定义条件函数类型
*/
export type CustomConditionFunction = (
entity: Entity,
blackboard?: BlackboardComponent,
deltaTime?: number
) => boolean;
/**
* 执行自定义条件组件
*
* 允许用户提供自定义的条件检查函数
*/
@BehaviorNode({
displayName: '自定义条件',
category: '条件',
type: NodeType.Condition,
icon: 'Code',
description: '执行自定义条件代码',
color: '#9C27B0'
})
@ECSComponent('ExecuteCondition')
@Serializable({ version: 1 })
export class ExecuteCondition extends Component {
@BehaviorProperty({
label: '条件代码',
type: 'code',
description: 'JavaScript 代码,返回 boolean',
required: true
})
@Serialize()
conditionCode?: string;
@Serialize()
parameters: Record<string, any> = {};
@BehaviorProperty({
label: '反转结果',
type: 'boolean'
})
@Serialize()
invertResult: boolean = false;
/** 编译后的函数(不序列化) */
@IgnoreSerialization()
private compiledFunction?: CustomConditionFunction;
/**
* 获取或编译条件函数
*/
getFunction(): CustomConditionFunction | undefined {
if (!this.compiledFunction && this.conditionCode) {
try {
const func = new Function(
'entity',
'blackboard',
'deltaTime',
'parameters',
`
try {
${this.conditionCode}
} catch (error) {
return false;
}
`
);
this.compiledFunction = (entity, blackboard, deltaTime) => {
return Boolean(func(entity, blackboard, deltaTime, this.parameters));
};
} catch (error) {
return undefined;
}
}
return this.compiledFunction;
}
/**
* 设置自定义函数(运行时使用)
*/
setFunction(func: CustomConditionFunction): void {
this.compiledFunction = func;
}
}

View File

@@ -1,61 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { NodeType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
/**
* 随机概率条件组件
*
* 根据概率返回成功或失败
*/
@BehaviorNode({
displayName: '随机概率',
category: '条件',
type: NodeType.Condition,
icon: 'Dice',
description: '根据概率返回成功或失败',
color: '#E91E63'
})
@ECSComponent('RandomProbabilityCondition')
@Serializable({ version: 1 })
export class RandomProbabilityCondition extends Component {
@BehaviorProperty({
label: '成功概率',
type: 'number',
min: 0,
max: 1,
step: 0.1,
description: '0.0 - 1.0',
required: true
})
@Serialize()
probability: number = 0.5;
@BehaviorProperty({
label: '总是重新随机',
type: 'boolean',
description: 'false则第一次随机后固定结果'
})
@Serialize()
alwaysRandomize: boolean = true;
/** 缓存的随机结果(不序列化) */
private cachedResult?: boolean;
/**
* 评估随机概率
*/
evaluate(): boolean {
if (this.alwaysRandomize || this.cachedResult === undefined) {
this.cachedResult = Math.random() < this.probability;
}
return this.cachedResult;
}
/**
* 重置缓存
*/
reset(): void {
this.cachedResult = undefined;
}
}

View File

@@ -1,18 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize } from '@esengine/ecs-framework';
import { DecoratorType } from '../Types/TaskStatus';
/**
* 装饰器节点组件基类
*
* 只包含通用的装饰器类型标识
* 具体的属性由各个子类自己定义
*/
@ECSComponent('DecoratorNode')
@Serializable({ version: 1 })
export class DecoratorNodeComponent extends Component {
/** 装饰器类型 */
@Serialize()
decoratorType: DecoratorType = DecoratorType.Inverter;
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 总是失败节点
*
* 无论子节点结果如何都返回失败
*/
@BehaviorNode({
displayName: '总是失败',
category: '装饰器',
type: NodeType.Decorator,
icon: 'ThumbsDown',
description: '无论子节点结果如何都返回失败',
color: '#FF5722'
})
@ECSComponent('AlwaysFailNode')
@Serializable({ version: 1 })
export class AlwaysFailNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.AlwaysFail;
}
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 总是成功节点
*
* 无论子节点结果如何都返回成功
*/
@BehaviorNode({
displayName: '总是成功',
category: '装饰器',
type: NodeType.Decorator,
icon: 'ThumbsUp',
description: '无论子节点结果如何都返回成功',
color: '#8BC34A'
})
@ECSComponent('AlwaysSucceedNode')
@Serializable({ version: 1 })
export class AlwaysSucceedNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.AlwaysSucceed;
}
}

View File

@@ -1,89 +0,0 @@
import { ECSComponent, Entity } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
import { BlackboardComponent } from '../BlackboardComponent';
/**
* 条件装饰器节点
*
* 基于条件判断是否执行子节点
*/
@BehaviorNode({
displayName: '条件装饰器',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Filter',
description: '基于条件判断是否执行子节点',
color: '#3F51B5'
})
@ECSComponent('ConditionalNode')
@Serializable({ version: 1 })
export class ConditionalNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Conditional;
}
@BehaviorProperty({
label: '条件代码',
type: 'code',
description: 'JavaScript 代码,返回 boolean',
required: true
})
@Serialize()
conditionCode?: string;
@BehaviorProperty({
label: '重新评估条件',
type: 'boolean',
description: '每次执行时是否重新评估条件'
})
@Serialize()
shouldReevaluate: boolean = true;
/** 编译后的条件函数(不序列化) */
@IgnoreSerialization()
private compiledCondition?: (entity: Entity, blackboard?: BlackboardComponent) => boolean;
/**
* 评估条件
*/
evaluateCondition(entity: Entity, blackboard?: BlackboardComponent): boolean {
if (!this.conditionCode) {
return false;
}
if (!this.compiledCondition) {
try {
const func = new Function(
'entity',
'blackboard',
`
try {
return Boolean(${this.conditionCode});
} catch (error) {
return false;
}
`
);
this.compiledCondition = (entity, blackboard) => {
return Boolean(func(entity, blackboard));
};
} catch (error) {
return false;
}
}
return this.compiledCondition(entity, blackboard);
}
/**
* 设置条件函数(运行时使用)
*/
setConditionFunction(func: (entity: Entity, blackboard?: BlackboardComponent) => boolean): void {
this.compiledCondition = func;
}
}

View File

@@ -1,67 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 冷却节点
*
* 在冷却时间内阻止子节点执行
*/
@BehaviorNode({
displayName: '冷却',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Timer',
description: '在冷却时间内阻止子节点执行',
color: '#00BCD4'
})
@ECSComponent('CooldownNode')
@Serializable({ version: 1 })
export class CooldownNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Cooldown;
}
@BehaviorProperty({
label: '冷却时间',
type: 'number',
min: 0,
step: 0.1,
description: '冷却时间(秒)',
required: true
})
@Serialize()
cooldownTime: number = 1.0;
/** 上次执行时间 */
@IgnoreSerialization()
lastExecutionTime: number = 0;
/**
* 检查是否可以执行
*/
canExecute(currentTime: number): boolean {
// 如果从未执行过,允许执行
if (this.lastExecutionTime === 0) {
return true;
}
return currentTime - this.lastExecutionTime >= this.cooldownTime;
}
/**
* 记录执行时间
*/
recordExecution(currentTime: number): void {
this.lastExecutionTime = currentTime;
}
/**
* 重置状态
*/
reset(): void {
this.lastExecutionTime = 0;
}
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 反转节点
*
* 反转子节点的执行结果
*/
@BehaviorNode({
displayName: '反转',
category: '装饰器',
type: NodeType.Decorator,
icon: 'RotateCcw',
description: '反转子节点的执行结果',
color: '#607D8B'
})
@ECSComponent('InverterNode')
@Serializable({ version: 1 })
export class InverterNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Inverter;
}
}

View File

@@ -1,74 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 重复节点
*
* 重复执行子节点指定次数
*/
@BehaviorNode({
displayName: '重复',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Repeat',
description: '重复执行子节点指定次数',
color: '#9E9E9E'
})
@ECSComponent('RepeaterNode')
@Serializable({ version: 1 })
export class RepeaterNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Repeater;
}
@BehaviorProperty({
label: '重复次数',
type: 'number',
min: -1,
step: 1,
description: '-1表示无限重复',
required: true
})
@Serialize()
repeatCount: number = 1;
@BehaviorProperty({
label: '失败时停止',
type: 'boolean',
description: '子节点失败时是否停止重复'
})
@Serialize()
endOnFailure: boolean = false;
/** 当前已重复次数 */
@IgnoreSerialization()
currentRepeatCount: number = 0;
/**
* 增加重复计数
*/
incrementRepeat(): void {
this.currentRepeatCount++;
}
/**
* 检查是否应该继续重复
*/
shouldContinueRepeat(): boolean {
if (this.repeatCount === -1) {
return true;
}
return this.currentRepeatCount < this.repeatCount;
}
/**
* 重置状态
*/
reset(): void {
this.currentRepeatCount = 0;
}
}

View File

@@ -1,68 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 超时节点
*
* 子节点执行超时则返回失败
*/
@BehaviorNode({
displayName: '超时',
category: '装饰器',
type: NodeType.Decorator,
icon: 'Clock',
description: '子节点执行超时则返回失败',
color: '#FF9800'
})
@ECSComponent('TimeoutNode')
@Serializable({ version: 1 })
export class TimeoutNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.Timeout;
}
@BehaviorProperty({
label: '超时时间',
type: 'number',
min: 0,
step: 0.1,
description: '超时时间(秒)',
required: true
})
@Serialize()
timeoutDuration: number = 5.0;
/** 开始执行时间 */
@IgnoreSerialization()
startTime: number = 0;
/**
* 记录开始时间
*/
recordStartTime(currentTime: number): void {
if (this.startTime === 0) {
this.startTime = currentTime;
}
}
/**
* 检查是否超时
*/
isTimeout(currentTime: number): boolean {
if (this.startTime === 0) {
return false;
}
return currentTime - this.startTime >= this.timeoutDuration;
}
/**
* 重置状态
*/
reset(): void {
this.startTime = 0;
}
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 直到失败节点
*
* 重复执行子节点直到失败
*/
@BehaviorNode({
displayName: '直到失败',
category: '装饰器',
type: NodeType.Decorator,
icon: 'XCircle',
description: '重复执行子节点直到失败',
color: '#F44336'
})
@ECSComponent('UntilFailNode')
@Serializable({ version: 1 })
export class UntilFailNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.UntilFail;
}
}

View File

@@ -1,27 +0,0 @@
import { ECSComponent } from '@esengine/ecs-framework';
import { Serializable } from '@esengine/ecs-framework';
import { NodeType, DecoratorType } from '../../Types/TaskStatus';
import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator';
import { DecoratorNodeComponent } from '../DecoratorNodeComponent';
/**
* 直到成功节点
*
* 重复执行子节点直到成功
*/
@BehaviorNode({
displayName: '直到成功',
category: '装饰器',
type: NodeType.Decorator,
icon: 'CheckCircle',
description: '重复执行子节点直到成功',
color: '#4CAF50'
})
@ECSComponent('UntilSuccessNode')
@Serializable({ version: 1 })
export class UntilSuccessNode extends DecoratorNodeComponent {
constructor() {
super();
this.decoratorType = DecoratorType.UntilSuccess;
}
}

View File

@@ -1,36 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
/**
* 日志输出组件
*
* 存储运行时输出的日志信息用于在UI中显示
*/
@ECSComponent('LogOutput')
export class LogOutput extends Component {
/**
* 日志消息列表
*/
messages: Array<{
timestamp: number;
message: string;
level: 'log' | 'info' | 'warn' | 'error';
}> = [];
/**
* 添加日志消息
*/
addMessage(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log'): void {
this.messages.push({
timestamp: Date.now(),
message,
level
});
}
/**
* 清空日志
*/
clear(): void {
this.messages = [];
}
}

View File

@@ -1,42 +0,0 @@
import { Component } from '@esengine/ecs-framework';
/**
* 属性绑定组件
* 记录节点属性到黑板变量的绑定关系
*/
export class PropertyBindings extends Component {
/**
* 属性绑定映射
* key: 属性名称 (如 'message')
* value: 黑板变量名 (如 'test1')
*/
bindings: Map<string, string> = new Map();
/**
* 添加属性绑定
*/
addBinding(propertyName: string, blackboardKey: string): void {
this.bindings.set(propertyName, blackboardKey);
}
/**
* 获取属性绑定的黑板变量名
*/
getBinding(propertyName: string): string | undefined {
return this.bindings.get(propertyName);
}
/**
* 检查属性是否绑定到黑板变量
*/
hasBinding(propertyName: string): boolean {
return this.bindings.has(propertyName);
}
/**
* 清除所有绑定
*/
clearBindings(): void {
this.bindings.clear();
}
}

View File

@@ -1,189 +0,0 @@
import { NodeTemplate, PropertyDefinition } from '../Serialization/NodeTemplates';
import { NodeType } from '../Types/TaskStatus';
import { getComponentTypeName } from '@esengine/ecs-framework';
/**
* 行为树节点元数据
*/
export interface BehaviorNodeMetadata {
displayName: string;
category: string;
type: NodeType;
icon?: string;
description: string;
color?: string;
className?: string;
/**
* 是否需要子节点
* - true: 节点需要子节点(如 SequenceNode、DecoratorNode
* - false: 节点不需要子节点(如 ActionNode、SubTreeNode
* - undefined: 根据节点类型自动判断
*/
requiresChildren?: boolean;
}
/**
* 节点类注册表
*/
class NodeClassRegistry {
private static nodeClasses = new Map<string, {
metadata: BehaviorNodeMetadata;
constructor: any;
}>();
static registerNodeClass(constructor: any, metadata: BehaviorNodeMetadata): void {
const key = `${metadata.category}:${metadata.displayName}`;
this.nodeClasses.set(key, { metadata, constructor });
}
static getAllNodeClasses(): Array<{ metadata: BehaviorNodeMetadata; constructor: any }> {
return Array.from(this.nodeClasses.values());
}
static getNodeClass(category: string, displayName: string): any {
const key = `${category}:${displayName}`;
return this.nodeClasses.get(key)?.constructor;
}
static clear(): void {
this.nodeClasses.clear();
}
}
/**
* 行为树节点装饰器
*
* 用于标注一个类是可在编辑器中使用的行为树节点
*
* @example
* ```typescript
* @BehaviorNode({
* displayName: '等待',
* category: '动作',
* type: NodeType.Action,
* icon: 'Clock',
* description: '等待指定时间',
* color: '#9E9E9E'
* })
* class WaitNode extends Component {
* @BehaviorProperty({
* label: '持续时间',
* type: 'number',
* min: 0,
* step: 0.1,
* description: '等待时间(秒)'
* })
* duration: number = 1.0;
* }
* ```
*/
export function BehaviorNode(metadata: BehaviorNodeMetadata) {
return function <T extends { new (...args: any[]): any }>(constructor: T) {
const metadataWithClassName = {
...metadata,
className: getComponentTypeName(constructor as any)
};
NodeClassRegistry.registerNodeClass(constructor, metadataWithClassName);
return constructor;
};
}
/**
* 行为树属性装饰器
*
* 用于标注节点的可配置属性,这些属性会在编辑器中显示
*
* @example
* ```typescript
* @BehaviorNode({ ... })
* class MyNode {
* @BehaviorProperty({
* label: '速度',
* type: 'number',
* min: 0,
* max: 100,
* description: '移动速度'
* })
* speed: number = 10;
* }
* ```
*/
export function BehaviorProperty(config: Omit<PropertyDefinition, 'name' | 'defaultValue'>) {
return function (target: any, propertyKey: string) {
if (!target.constructor.__nodeProperties) {
target.constructor.__nodeProperties = [];
}
target.constructor.__nodeProperties.push({
name: propertyKey,
...config
});
};
}
/**
* @deprecated 使用 BehaviorProperty 代替
*/
export const NodeProperty = BehaviorProperty;
/**
* 获取所有注册的节点模板
*/
export function getRegisteredNodeTemplates(): NodeTemplate[] {
return NodeClassRegistry.getAllNodeClasses().map(({ metadata, constructor }) => {
const propertyDefs = constructor.__nodeProperties || [];
const defaultConfig: any = {
nodeType: metadata.type.toLowerCase()
};
const instance = new constructor();
const properties: PropertyDefinition[] = propertyDefs.map((prop: PropertyDefinition) => {
const defaultValue = instance[prop.name];
if (defaultValue !== undefined) {
defaultConfig[prop.name] = defaultValue;
}
return {
...prop,
defaultValue: defaultValue !== undefined ? defaultValue : prop.defaultValue
};
});
switch (metadata.type) {
case NodeType.Composite:
defaultConfig.compositeType = metadata.displayName;
break;
case NodeType.Decorator:
defaultConfig.decoratorType = metadata.displayName;
break;
case NodeType.Action:
defaultConfig.actionType = metadata.displayName;
break;
case NodeType.Condition:
defaultConfig.conditionType = metadata.displayName;
break;
}
return {
type: metadata.type,
displayName: metadata.displayName,
category: metadata.category,
icon: metadata.icon,
description: metadata.description,
color: metadata.color,
className: metadata.className,
componentClass: constructor,
requiresChildren: metadata.requiresChildren,
defaultConfig,
properties
};
});
}
/**
* 清空所有注册的节点类
*/
export function clearRegisteredNodes(): void {
NodeClassRegistry.clear();
}
export { NodeClassRegistry };

View File

@@ -1,45 +0,0 @@
/**
* 注册所有内置节点
*
* 导入所有节点类以确保装饰器被执行
*/
// Actions
import './Components/Actions/ExecuteAction';
import './Components/Actions/WaitAction';
import './Components/Actions/LogAction';
import './Components/Actions/SetBlackboardValueAction';
import './Components/Actions/ModifyBlackboardValueAction';
// Conditions
import './Components/Conditions/BlackboardCompareCondition';
import './Components/Conditions/BlackboardExistsCondition';
import './Components/Conditions/RandomProbabilityCondition';
import './Components/Conditions/ExecuteCondition';
// Composites
import './Components/Composites/SequenceNode';
import './Components/Composites/SelectorNode';
import './Components/Composites/ParallelNode';
import './Components/Composites/ParallelSelectorNode';
import './Components/Composites/RandomSequenceNode';
import './Components/Composites/RandomSelectorNode';
import './Components/Composites/SubTreeNode';
// Decorators
import './Components/Decorators/InverterNode';
import './Components/Decorators/RepeaterNode';
import './Components/Decorators/UntilSuccessNode';
import './Components/Decorators/UntilFailNode';
import './Components/Decorators/AlwaysSucceedNode';
import './Components/Decorators/AlwaysFailNode';
import './Components/Decorators/ConditionalNode';
import './Components/Decorators/CooldownNode';
import './Components/Decorators/TimeoutNode';
/**
* 确保所有节点已注册
*/
export function ensureAllNodesRegistered(): void {
// 这个函数的调用会确保上面的 import 被执行
}

View File

@@ -0,0 +1,91 @@
import { BehaviorTreeData } from './BehaviorTreeData';
import { createLogger, IService } from '@esengine/ecs-framework';
const logger = createLogger('BehaviorTreeAssetManager');
/**
* 行为树资产管理器(服务)
*
* 管理所有共享的BehaviorTreeData
* 多个实例可以引用同一份数据
*
* 使用方式:
* ```typescript
* // 注册服务
* Core.services.registerSingleton(BehaviorTreeAssetManager);
*
* // 使用服务
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
* ```
*/
export class BehaviorTreeAssetManager implements IService {
/**
* 已加载的行为树资产
*/
private assets: Map<string, BehaviorTreeData> = new Map();
/**
* 加载行为树资产
*/
loadAsset(asset: BehaviorTreeData): void {
if (this.assets.has(asset.id)) {
logger.warn(`行为树资产已存在,将被覆盖: ${asset.id}`);
}
this.assets.set(asset.id, asset);
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
}
/**
* 获取行为树资产
*/
getAsset(assetId: string): BehaviorTreeData | undefined {
return this.assets.get(assetId);
}
/**
* 检查资产是否存在
*/
hasAsset(assetId: string): boolean {
return this.assets.has(assetId);
}
/**
* 卸载行为树资产
*/
unloadAsset(assetId: string): boolean {
const result = this.assets.delete(assetId);
if (result) {
logger.info(`行为树资产已卸载: ${assetId}`);
}
return result;
}
/**
* 清空所有资产
*/
clearAll(): void {
this.assets.clear();
logger.info('所有行为树资产已清空');
}
/**
* 获取已加载资产数量
*/
getAssetCount(): number {
return this.assets.size;
}
/**
* 获取所有资产ID
*/
getAllAssetIds(): string[] {
return Array.from(this.assets.keys());
}
/**
* 释放资源实现IService接口
*/
dispose(): void {
this.clearAll();
}
}

View File

@@ -0,0 +1,99 @@
import { TaskStatus, NodeType, AbortType } from '../Types/TaskStatus';
/**
* 行为树节点定义(纯数据结构)
*
* 不依赖Entity可以被多个实例共享
*/
export interface BehaviorNodeData {
/** 节点唯一ID */
id: string;
/** 节点名称(用于调试) */
name: string;
/** 节点类型 */
nodeType: NodeType;
/** 节点实现类型对应Component类名 */
implementationType: string;
/** 子节点ID列表 */
children?: string[];
/** 节点特定配置数据 */
config: Record<string, any>;
/** 属性到黑板变量的绑定映射 */
bindings?: Record<string, string>;
/** 中止类型(条件装饰器使用) */
abortType?: AbortType;
}
/**
* 行为树定义可共享的Asset
*/
export interface BehaviorTreeData {
/** 树ID */
id: string;
/** 树名称 */
name: string;
/** 根节点ID */
rootNodeId: string;
/** 所有节点(扁平化存储) */
nodes: Map<string, BehaviorNodeData>;
/** 黑板变量定义 */
blackboardVariables?: Map<string, any>;
}
/**
* 节点运行时状态
*
* 每个BehaviorTreeRuntimeComponent实例独立维护
*/
export interface NodeRuntimeState {
/** 当前执行状态 */
status: TaskStatus;
/** 当前执行的子节点索引(复合节点使用) */
currentChildIndex: number;
/** 开始执行时间(某些节点需要) */
startTime?: number;
/** 上次执行时间(冷却节点使用) */
lastExecutionTime?: number;
/** 当前重复次数(重复节点使用) */
repeatCount?: number;
/** 缓存的结果(某些条件节点使用) */
cachedResult?: any;
/** 洗牌后的索引(随机节点使用) */
shuffledIndices?: number[];
/** 是否被中止 */
isAborted?: boolean;
/** 上次条件评估结果(条件装饰器使用) */
lastConditionResult?: boolean;
/** 正在观察的黑板键(条件装饰器使用) */
observedKeys?: string[];
}
/**
* 创建默认的运行时状态
*/
export function createDefaultRuntimeState(): NodeRuntimeState {
return {
status: TaskStatus.Invalid,
currentChildIndex: 0
};
}

View File

@@ -0,0 +1,223 @@
import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem } from '@esengine/ecs-framework';
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor';
import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData';
import { TaskStatus } from '../Types/TaskStatus';
import { NodeMetadataRegistry } from './NodeMetadata';
import './Executors';
/**
* 行为树执行系统
*
* 统一处理所有行为树的执行
*/
@ECSSystem('BehaviorTreeExecution')
export class BehaviorTreeExecutionSystem extends EntitySystem {
private assetManager: BehaviorTreeAssetManager;
private executorRegistry: NodeExecutorRegistry;
constructor() {
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
this.executorRegistry = new NodeExecutorRegistry();
this.registerBuiltInExecutors();
}
/**
* 注册所有执行器(包括内置和插件提供的)
*/
private registerBuiltInExecutors(): void {
const constructors = NodeMetadataRegistry.getAllExecutorConstructors();
for (const [implementationType, ExecutorClass] of constructors) {
try {
const instance = new ExecutorClass();
this.executorRegistry.register(implementationType, instance);
} catch (error) {
this.logger.error(`注册执行器失败: ${implementationType}`, error);
}
}
}
/**
* 获取执行器注册表
*/
getExecutorRegistry(): NodeExecutorRegistry {
return this.executorRegistry;
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent)!;
if (!runtime.isRunning) {
continue;
}
const treeData = this.assetManager.getAsset(runtime.treeAssetId);
if (!treeData) {
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
continue;
}
// 如果标记了需要重置,先重置状态
if (runtime.needsReset) {
runtime.resetAllStates();
runtime.needsReset = false;
}
this.executeTree(entity, runtime, treeData);
}
}
/**
* 执行整个行为树
*/
private executeTree(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
treeData: BehaviorTreeData
): void {
const rootNode = treeData.nodes.get(treeData.rootNodeId);
if (!rootNode) {
this.logger.error(`未找到根节点: ${treeData.rootNodeId}`);
return;
}
const status = this.executeNode(entity, runtime, rootNode, treeData);
// 如果树完成了标记在下一个tick时重置状态
// 这样UI可以看到节点的最终状态
if (status !== TaskStatus.Running) {
runtime.needsReset = true;
} else {
runtime.needsReset = false;
}
}
/**
* 执行单个节点
*/
private executeNode(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
nodeData: BehaviorNodeData,
treeData: BehaviorTreeData
): TaskStatus {
const state = runtime.getNodeState(nodeData.id);
if (runtime.shouldAbort(nodeData.id)) {
runtime.clearAbortRequest(nodeData.id);
state.isAborted = true;
const executor = this.executorRegistry.get(nodeData.implementationType);
if (executor && executor.reset) {
const context = this.createContext(entity, runtime, nodeData, treeData);
executor.reset(context);
}
runtime.activeNodeIds.delete(nodeData.id);
state.status = TaskStatus.Failure;
return TaskStatus.Failure;
}
runtime.activeNodeIds.add(nodeData.id);
state.isAborted = false;
const executor = this.executorRegistry.get(nodeData.implementationType);
if (!executor) {
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
state.status = TaskStatus.Failure;
return TaskStatus.Failure;
}
const context = this.createContext(entity, runtime, nodeData, treeData);
try {
const status = executor.execute(context);
state.status = status;
if (status !== TaskStatus.Running) {
runtime.activeNodeIds.delete(nodeData.id);
if (executor.reset) {
executor.reset(context);
}
}
return status;
} catch (error) {
this.logger.error(`执行节点时发生错误: ${nodeData.name}`, error);
state.status = TaskStatus.Failure;
runtime.activeNodeIds.delete(nodeData.id);
return TaskStatus.Failure;
}
}
/**
* 创建执行上下文
*/
private createContext(
entity: Entity,
runtime: BehaviorTreeRuntimeComponent,
nodeData: BehaviorNodeData,
treeData: BehaviorTreeData
): NodeExecutionContext {
return {
entity,
nodeData,
state: runtime.getNodeState(nodeData.id),
runtime,
treeData,
deltaTime: Time.deltaTime,
totalTime: Time.totalTime,
executeChild: (childId: string) => {
const childData = treeData.nodes.get(childId);
if (!childData) {
this.logger.warn(`未找到子节点: ${childId}`);
return TaskStatus.Failure;
}
return this.executeNode(entity, runtime, childData, treeData);
}
};
}
/**
* 执行子节点列表
*/
executeChildren(
context: NodeExecutionContext,
childIndices?: number[]
): TaskStatus[] {
const { nodeData, treeData, entity, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return [];
}
const results: TaskStatus[] = [];
const indicesToExecute = childIndices ||
Array.from({ length: nodeData.children.length }, (_, i) => i);
for (const index of indicesToExecute) {
if (index >= nodeData.children.length) {
continue;
}
const childId = nodeData.children[index]!;
const childData = treeData.nodes.get(childId);
if (!childData) {
this.logger.warn(`未找到子节点: ${childId}`);
results.push(TaskStatus.Failure);
continue;
}
const status = this.executeNode(entity, runtime, childData, treeData);
results.push(status);
}
return results;
}
}

View File

@@ -0,0 +1,269 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
import { TaskStatus } from '../Types/TaskStatus';
/**
* 黑板变化监听器
*/
export type BlackboardChangeListener = (key: string, newValue: any, oldValue: any) => void;
/**
* 黑板观察者信息
*/
interface BlackboardObserver {
nodeId: string;
keys: Set<string>;
callback: BlackboardChangeListener;
}
/**
* 行为树运行时组件
*
* 挂载到游戏Entity上引用共享的BehaviorTreeData
* 维护该Entity独立的运行时状态
*/
@ECSComponent('BehaviorTreeRuntime')
@Serializable({ version: 1 })
export class BehaviorTreeRuntimeComponent extends Component {
/**
* 引用的行为树资产ID可序列化
*/
@Serialize()
treeAssetId: string = '';
/**
* 是否自动启动
*/
@Serialize()
autoStart: boolean = true;
/**
* 是否正在运行
*/
@IgnoreSerialization()
isRunning: boolean = false;
/**
* 节点运行时状态(每个节点独立)
* 不序列化,每次加载时重新初始化
*/
@IgnoreSerialization()
private nodeStates: Map<string, NodeRuntimeState> = new Map();
/**
* 黑板数据该Entity独立的数据
* 不序列化,通过初始化设置
*/
@IgnoreSerialization()
private blackboard: Map<string, any> = new Map();
/**
* 黑板观察者列表
*/
@IgnoreSerialization()
private blackboardObservers: Map<string, BlackboardObserver[]> = new Map();
/**
* 当前激活的节点ID列表用于调试
*/
@IgnoreSerialization()
activeNodeIds: Set<string> = new Set();
/**
* 标记是否需要在下一个tick重置状态
*/
@IgnoreSerialization()
needsReset: boolean = false;
/**
* 需要中止的节点ID列表
*/
@IgnoreSerialization()
nodesToAbort: Set<string> = new Set();
/**
* 获取节点运行时状态
*/
getNodeState(nodeId: string): NodeRuntimeState {
if (!this.nodeStates.has(nodeId)) {
this.nodeStates.set(nodeId, createDefaultRuntimeState());
}
return this.nodeStates.get(nodeId)!;
}
/**
* 重置节点状态
*/
resetNodeState(nodeId: string): void {
const state = this.getNodeState(nodeId);
state.status = TaskStatus.Invalid;
state.currentChildIndex = 0;
delete state.startTime;
delete state.lastExecutionTime;
delete state.repeatCount;
delete state.cachedResult;
delete state.shuffledIndices;
delete state.isAborted;
delete state.lastConditionResult;
delete state.observedKeys;
}
/**
* 重置所有节点状态
*/
resetAllStates(): void {
this.nodeStates.clear();
this.activeNodeIds.clear();
}
/**
* 获取黑板值
*/
getBlackboardValue<T = any>(key: string): T | undefined {
return this.blackboard.get(key) as T;
}
/**
* 设置黑板值
*/
setBlackboardValue(key: string, value: any): void {
const oldValue = this.blackboard.get(key);
this.blackboard.set(key, value);
if (oldValue !== value) {
this.notifyBlackboardChange(key, value, oldValue);
}
}
/**
* 检查黑板是否有某个键
*/
hasBlackboardKey(key: string): boolean {
return this.blackboard.has(key);
}
/**
* 初始化黑板(从树定义的默认值)
*/
initializeBlackboard(variables?: Map<string, any>): void {
if (variables) {
variables.forEach((value, key) => {
if (!this.blackboard.has(key)) {
this.blackboard.set(key, value);
}
});
}
}
/**
* 清空黑板
*/
clearBlackboard(): void {
this.blackboard.clear();
}
/**
* 启动行为树
*/
start(): void {
this.isRunning = true;
this.resetAllStates();
}
/**
* 停止行为树
*/
stop(): void {
this.isRunning = false;
this.activeNodeIds.clear();
}
/**
* 暂停行为树
*/
pause(): void {
this.isRunning = false;
}
/**
* 恢复行为树
*/
resume(): void {
this.isRunning = true;
}
/**
* 注册黑板观察者
*/
observeBlackboard(nodeId: string, keys: string[], callback: BlackboardChangeListener): void {
const observer: BlackboardObserver = {
nodeId,
keys: new Set(keys),
callback
};
for (const key of keys) {
if (!this.blackboardObservers.has(key)) {
this.blackboardObservers.set(key, []);
}
this.blackboardObservers.get(key)!.push(observer);
}
}
/**
* 取消注册黑板观察者
*/
unobserveBlackboard(nodeId: string): void {
for (const observers of this.blackboardObservers.values()) {
const index = observers.findIndex(o => o.nodeId === nodeId);
if (index !== -1) {
observers.splice(index, 1);
}
}
}
/**
* 通知黑板变化
*/
private notifyBlackboardChange(key: string, newValue: any, oldValue: any): void {
const observers = this.blackboardObservers.get(key);
if (!observers) return;
for (const observer of observers) {
try {
observer.callback(key, newValue, oldValue);
} catch (error) {
console.error(`黑板观察者回调错误 (节点: ${observer.nodeId}):`, error);
}
}
}
/**
* 请求中止节点
*/
requestAbort(nodeId: string): void {
this.nodesToAbort.add(nodeId);
}
/**
* 检查节点是否需要中止
*/
shouldAbort(nodeId: string): boolean {
return this.nodesToAbort.has(nodeId);
}
/**
* 清除中止请求
*/
clearAbortRequest(nodeId: string): void {
this.nodesToAbort.delete(nodeId);
}
/**
* 清除所有中止请求
*/
clearAllAbortRequests(): void {
this.nodesToAbort.clear();
}
}

View File

@@ -0,0 +1,40 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 总是失败装饰器执行器
*
* 无论子节点结果如何都返回失败
*/
@NodeExecutorMetadata({
implementationType: 'AlwaysFail',
nodeType: NodeType.Decorator,
displayName: '总是失败',
description: '无论子节点结果如何都返回失败',
category: 'Decorator'
})
export class AlwaysFailExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,40 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 总是成功装饰器执行器
*
* 无论子节点结果如何都返回成功
*/
@NodeExecutorMetadata({
implementationType: 'AlwaysSucceed',
nodeType: NodeType.Decorator,
displayName: '总是成功',
description: '无论子节点结果如何都返回成功',
category: 'Decorator'
})
export class AlwaysSucceedExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
return TaskStatus.Success;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,73 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 黑板比较条件执行器
*
* 比较黑板中的值
*/
@NodeExecutorMetadata({
implementationType: 'BlackboardCompare',
nodeType: NodeType.Condition,
displayName: '黑板比较',
description: '比较黑板中的值',
category: 'Condition',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
compareValue: {
type: 'object',
description: '比较值',
supportBinding: true
},
operator: {
type: 'string',
default: 'equals',
description: '比较运算符',
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
}
}
})
export class BlackboardCompare implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const compareValue = BindingHelper.getValue(context, 'compareValue');
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
if (!key) {
return TaskStatus.Failure;
}
const actualValue = runtime.getBlackboardValue(key);
if (this.compare(actualValue, compareValue, operator)) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
private compare(actualValue: any, compareValue: any, operator: string): boolean {
switch (operator) {
case 'equals':
return actualValue === compareValue;
case 'notEquals':
return actualValue !== compareValue;
case 'greaterThan':
return actualValue > compareValue;
case 'lessThan':
return actualValue < compareValue;
case 'greaterOrEqual':
return actualValue >= compareValue;
case 'lessOrEqual':
return actualValue <= compareValue;
default:
return false;
}
}
}

View File

@@ -0,0 +1,51 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 黑板存在检查条件执行器
*
* 检查黑板中是否存在指定的键
*/
@NodeExecutorMetadata({
implementationType: 'BlackboardExists',
nodeType: NodeType.Condition,
displayName: '黑板存在',
description: '检查黑板中是否存在指定的键',
category: 'Condition',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
checkNull: {
type: 'boolean',
default: false,
description: '检查是否为null'
}
}
})
export class BlackboardExists implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const checkNull = BindingHelper.getValue<boolean>(context, 'checkNull', false);
if (!key) {
return TaskStatus.Failure;
}
const value = runtime.getBlackboardValue(key);
if (value === undefined) {
return TaskStatus.Failure;
}
if (checkNull && value === null) {
return TaskStatus.Failure;
}
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,182 @@
import { TaskStatus, NodeType, AbortType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 条件装饰器执行器
*
* 根据条件决定是否执行子节点
* 支持动态优先级和中止机制
*/
@NodeExecutorMetadata({
implementationType: 'Conditional',
nodeType: NodeType.Decorator,
displayName: '条件',
description: '根据条件决定是否执行子节点',
category: 'Decorator',
configSchema: {
blackboardKey: {
type: 'string',
default: '',
description: '黑板变量名'
},
expectedValue: {
type: 'object',
description: '期望值',
supportBinding: true
},
operator: {
type: 'string',
default: 'equals',
description: '比较运算符',
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
},
abortType: {
type: 'string',
default: 'none',
description: '中止类型',
options: ['none', 'self', 'lower-priority', 'both']
}
}
})
export class ConditionalExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const blackboardKey = BindingHelper.getValue<string>(context, 'blackboardKey', '');
const expectedValue = BindingHelper.getValue(context, 'expectedValue');
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
const abortType = (nodeData.abortType || AbortType.None) as AbortType;
if (!blackboardKey) {
return TaskStatus.Failure;
}
const actualValue = runtime.getBlackboardValue(blackboardKey);
const conditionMet = this.evaluateCondition(actualValue, expectedValue, operator);
const wasRunning = state.status === TaskStatus.Running;
if (abortType !== AbortType.None) {
if (!state.observedKeys || state.observedKeys.length === 0) {
state.observedKeys = [blackboardKey];
this.setupObserver(context, blackboardKey, expectedValue, operator, abortType);
}
if (state.lastConditionResult !== undefined && state.lastConditionResult !== conditionMet) {
if (conditionMet) {
this.handleConditionBecameTrue(context, abortType);
} else if (wasRunning) {
this.handleConditionBecameFalse(context, abortType);
}
}
}
state.lastConditionResult = conditionMet;
if (!conditionMet) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
return status;
}
private evaluateCondition(actualValue: any, expectedValue: any, operator: string): boolean {
switch (operator) {
case 'equals':
return actualValue === expectedValue;
case 'notEquals':
return actualValue !== expectedValue;
case 'greaterThan':
return actualValue > expectedValue;
case 'lessThan':
return actualValue < expectedValue;
case 'greaterOrEqual':
return actualValue >= expectedValue;
case 'lessOrEqual':
return actualValue <= expectedValue;
default:
return false;
}
}
/**
* 设置黑板观察者
*/
private setupObserver(
context: NodeExecutionContext,
blackboardKey: string,
expectedValue: any,
operator: string,
abortType: AbortType
): void {
const { nodeData, runtime } = context;
runtime.observeBlackboard(nodeData.id, [blackboardKey], (_key, newValue) => {
const conditionMet = this.evaluateCondition(newValue, expectedValue, operator);
const lastResult = context.state.lastConditionResult;
if (lastResult !== undefined && lastResult !== conditionMet) {
if (conditionMet) {
this.handleConditionBecameTrue(context, abortType);
} else {
this.handleConditionBecameFalse(context, abortType);
}
}
context.state.lastConditionResult = conditionMet;
});
}
/**
* 处理条件变为true
*/
private handleConditionBecameTrue(context: NodeExecutionContext, abortType: AbortType): void {
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
this.requestAbortLowerPriority(context);
}
}
/**
* 处理条件变为false
*/
private handleConditionBecameFalse(context: NodeExecutionContext, abortType: AbortType): void {
const { nodeData, runtime } = context;
if (abortType === AbortType.Self || abortType === AbortType.Both) {
if (nodeData.children && nodeData.children.length > 0) {
runtime.requestAbort(nodeData.children[0]!);
}
}
}
/**
* 请求中止低优先级节点
*/
private requestAbortLowerPriority(context: NodeExecutionContext): void {
const { runtime } = context;
runtime.requestAbort('__lower_priority__');
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime, state } = context;
if (state.observedKeys && state.observedKeys.length > 0) {
runtime.unobserveBlackboard(nodeData.id);
delete state.observedKeys;
}
delete state.lastConditionResult;
if (nodeData.children && nodeData.children.length > 0) {
runtime.resetNodeState(nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,64 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 冷却装饰器执行器
*
* 子节点执行成功后进入冷却时间
*/
@NodeExecutorMetadata({
implementationType: 'Cooldown',
nodeType: NodeType.Decorator,
displayName: '冷却',
description: '子节点执行成功后进入冷却时间',
category: 'Decorator',
configSchema: {
cooldownTime: {
type: 'number',
default: 1.0,
description: '冷却时间(秒)',
min: 0,
supportBinding: true
}
}
})
export class CooldownExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const cooldownTime = BindingHelper.getValue<number>(context, 'cooldownTime', 1.0);
if (state.lastExecutionTime !== undefined) {
const timeSinceLastExecution = totalTime - state.lastExecutionTime;
if (timeSinceLastExecution < cooldownTime) {
return TaskStatus.Failure;
}
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.lastExecutionTime = totalTime;
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
delete context.state.lastExecutionTime;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 执行动作执行器
*
* 执行自定义动作逻辑
*/
@NodeExecutorMetadata({
implementationType: 'ExecuteAction',
nodeType: NodeType.Action,
displayName: '执行动作',
description: '执行自定义动作逻辑',
category: 'Action',
configSchema: {
actionName: {
type: 'string',
default: '',
description: '动作名称黑板中action_前缀的函数'
}
}
})
export class ExecuteAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, entity } = context;
const actionName = BindingHelper.getValue<string>(context, 'actionName', '');
if (!actionName) {
return TaskStatus.Failure;
}
const actionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => TaskStatus>(`action_${actionName}`);
if (!actionFunction || typeof actionFunction !== 'function') {
return TaskStatus.Failure;
}
try {
return actionFunction(entity);
} catch (error) {
console.error(`ExecuteAction failed: ${error}`);
return TaskStatus.Failure;
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 执行条件执行器
*
* 执行自定义条件逻辑
*/
@NodeExecutorMetadata({
implementationType: 'ExecuteCondition',
nodeType: NodeType.Condition,
displayName: '执行条件',
description: '执行自定义条件逻辑',
category: 'Condition',
configSchema: {
conditionName: {
type: 'string',
default: '',
description: '条件名称黑板中condition_前缀的函数'
}
}
})
export class ExecuteCondition implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, entity } = context;
const conditionName = BindingHelper.getValue<string>(context, 'conditionName', '');
if (!conditionName) {
return TaskStatus.Failure;
}
const conditionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => boolean>(`condition_${conditionName}`);
if (!conditionFunction || typeof conditionFunction !== 'function') {
return TaskStatus.Failure;
}
try {
return conditionFunction(entity) ? TaskStatus.Success : TaskStatus.Failure;
} catch (error) {
console.error(`ExecuteCondition failed: ${error}`);
return TaskStatus.Failure;
}
}
}

View File

@@ -0,0 +1,48 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 反转装饰器执行器
*
* 反转子节点的执行结果
*/
@NodeExecutorMetadata({
implementationType: 'Inverter',
nodeType: NodeType.Decorator,
displayName: '反转',
description: '反转子节点的执行结果',
category: 'Decorator'
})
export class InverterExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
return TaskStatus.Failure;
}
if (status === TaskStatus.Failure) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,71 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 日志动作执行器
*
* 输出日志信息
*/
@NodeExecutorMetadata({
implementationType: 'Log',
nodeType: NodeType.Action,
displayName: '日志',
description: '输出日志信息',
category: 'Action',
configSchema: {
message: {
type: 'string',
default: '',
description: '日志消息,支持{key}占位符引用黑板变量',
supportBinding: true
},
logLevel: {
type: 'string',
default: 'info',
description: '日志级别',
options: ['info', 'warn', 'error']
}
}
})
export class LogAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const message = BindingHelper.getValue<string>(context, 'message', '');
const logLevel = BindingHelper.getValue<string>(context, 'logLevel', 'info');
const finalMessage = this.replaceBlackboardVariables(message, runtime);
this.log(finalMessage, logLevel);
return TaskStatus.Success;
}
private replaceBlackboardVariables(message: string, runtime: NodeExecutionContext['runtime']): string {
if (!message.includes('{') || !message.includes('}')) {
return message;
}
// 使用限制长度的正则表达式避免 ReDoS 攻击
// 限制占位符名称最多100个字符只允许字母、数字、下划线和点号
return message.replace(/\{([\w.]{1,100})\}/g, (_, key) => {
const value = runtime.getBlackboardValue(key.trim());
return value !== undefined ? String(value) : `{${key}}`;
});
}
private log(message: string, level: string): void {
switch (level) {
case 'error':
console.error(message);
break;
case 'warn':
console.warn(message);
break;
case 'info':
default:
console.log(message);
break;
}
}
}

View File

@@ -0,0 +1,74 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 修改黑板值动作执行器
*
* 对黑板中的数值进行运算
*/
@NodeExecutorMetadata({
implementationType: 'ModifyBlackboardValue',
nodeType: NodeType.Action,
displayName: '修改黑板值',
description: '对黑板中的数值进行运算',
category: 'Action',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
operation: {
type: 'string',
default: 'add',
description: '运算类型',
options: ['add', 'subtract', 'multiply', 'divide', 'set']
},
value: {
type: 'number',
default: 0,
description: '操作数',
supportBinding: true
}
}
})
export class ModifyBlackboardValue implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const operation = BindingHelper.getValue<string>(context, 'operation', 'add');
const value = BindingHelper.getValue<number>(context, 'value', 0);
if (!key) {
return TaskStatus.Failure;
}
const currentValue = runtime.getBlackboardValue<number>(key) || 0;
let newValue: number;
switch (operation) {
case 'add':
newValue = currentValue + value;
break;
case 'subtract':
newValue = currentValue - value;
break;
case 'multiply':
newValue = currentValue * value;
break;
case 'divide':
newValue = value !== 0 ? currentValue / value : currentValue;
break;
case 'set':
newValue = value;
break;
default:
return TaskStatus.Failure;
}
runtime.setBlackboardValue(key, newValue);
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,96 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 并行节点执行器
*
* 同时执行所有子节点
*/
@NodeExecutorMetadata({
implementationType: 'Parallel',
nodeType: NodeType.Composite,
displayName: '并行',
description: '同时执行所有子节点',
category: 'Composite',
configSchema: {
successPolicy: {
type: 'string',
default: 'all',
description: '成功策略',
options: ['all', 'one']
},
failurePolicy: {
type: 'string',
default: 'one',
description: '失败策略',
options: ['all', 'one']
}
}
})
export class ParallelExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
const successPolicy = BindingHelper.getValue<string>(context, 'successPolicy', 'all');
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'one');
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
let hasRunning = false;
let successCount = 0;
let failureCount = 0;
for (const childId of nodeData.children) {
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
hasRunning = true;
} else if (status === TaskStatus.Success) {
successCount++;
} else if (status === TaskStatus.Failure) {
failureCount++;
}
}
if (successPolicy === 'one' && successCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Success;
}
if (successPolicy === 'all' && successCount === nodeData.children.length) {
return TaskStatus.Success;
}
if (failurePolicy === 'one' && failureCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Failure;
}
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
return TaskStatus.Failure;
}
return hasRunning ? TaskStatus.Running : TaskStatus.Success;
}
private stopAllChildren(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.activeNodeIds.delete(childId);
runtime.resetNodeState(childId);
}
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.resetNodeState(childId);
}
}
}

View File

@@ -0,0 +1,85 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 并行选择器执行器
*
* 并行执行子节点,任一成功则成功
*/
@NodeExecutorMetadata({
implementationType: 'ParallelSelector',
nodeType: NodeType.Composite,
displayName: '并行选择器',
description: '并行执行子节点,任一成功则成功',
category: 'Composite',
configSchema: {
failurePolicy: {
type: 'string',
default: 'all',
description: '失败策略',
options: ['all', 'one']
}
}
})
export class ParallelSelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData } = context;
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'all');
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
let hasRunning = false;
let successCount = 0;
let failureCount = 0;
for (const childId of nodeData.children) {
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
hasRunning = true;
} else if (status === TaskStatus.Success) {
successCount++;
} else if (status === TaskStatus.Failure) {
failureCount++;
}
}
if (successCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Success;
}
if (failurePolicy === 'one' && failureCount > 0) {
this.stopAllChildren(context);
return TaskStatus.Failure;
}
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
return TaskStatus.Failure;
}
return hasRunning ? TaskStatus.Running : TaskStatus.Failure;
}
private stopAllChildren(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.activeNodeIds.delete(childId);
runtime.resetNodeState(childId);
}
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime } = context;
if (!nodeData.children) return;
for (const childId of nodeData.children) {
runtime.resetNodeState(childId);
}
}
}

View File

@@ -0,0 +1,39 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机概率条件执行器
*
* 根据概率返回成功或失败
*/
@NodeExecutorMetadata({
implementationType: 'RandomProbability',
nodeType: NodeType.Condition,
displayName: '随机概率',
description: '根据概率返回成功或失败',
category: 'Condition',
configSchema: {
probability: {
type: 'number',
default: 0.5,
description: '成功概率0-1',
min: 0,
max: 1,
supportBinding: true
}
}
})
export class RandomProbability implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const probability = BindingHelper.getValue<number>(context, 'probability', 0.5);
const clampedProbability = Math.max(0, Math.min(1, probability));
if (Math.random() < clampedProbability) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
}

View File

@@ -0,0 +1,67 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机选择器执行器
*
* 随机顺序执行子节点,任一成功则成功
*/
@NodeExecutorMetadata({
implementationType: 'RandomSelector',
nodeType: NodeType.Composite,
displayName: '随机选择器',
description: '随机顺序执行子节点,任一成功则成功',
category: 'Composite'
})
export class RandomSelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
}
while (state.currentChildIndex < state.shuffledIndices.length) {
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
const childId = nodeData.children[shuffledIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Success;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Failure;
}
private shuffleIndices(length: number): number[] {
const indices = Array.from({ length }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = indices[i]!;
indices[i] = indices[j]!;
indices[j] = temp;
}
return indices;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
delete context.state.shuffledIndices;
}
}

View File

@@ -0,0 +1,67 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 随机序列执行器
*
* 随机顺序执行子节点序列,全部成功才成功
*/
@NodeExecutorMetadata({
implementationType: 'RandomSequence',
nodeType: NodeType.Composite,
displayName: '随机序列',
description: '随机顺序执行子节点,全部成功才成功',
category: 'Composite'
})
export class RandomSequenceExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
if (!state.shuffledIndices || state.shuffledIndices.length === 0) {
state.shuffledIndices = this.shuffleIndices(nodeData.children.length);
}
while (state.currentChildIndex < state.shuffledIndices.length) {
const shuffledIndex = state.shuffledIndices[state.currentChildIndex]!;
const childId = nodeData.children[shuffledIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Failure;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
delete state.shuffledIndices;
return TaskStatus.Success;
}
private shuffleIndices(length: number): number[] {
const indices = Array.from({ length }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = indices[i]!;
indices[i] = indices[j]!;
indices[j] = temp;
}
return indices;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
delete context.state.shuffledIndices;
}
}

View File

@@ -0,0 +1,76 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 重复装饰器执行器
*
* 重复执行子节点指定次数
*/
@NodeExecutorMetadata({
implementationType: 'Repeater',
nodeType: NodeType.Decorator,
displayName: '重复',
description: '重复执行子节点指定次数',
category: 'Decorator',
configSchema: {
repeatCount: {
type: 'number',
default: 1,
description: '重复次数(-1表示无限循环',
supportBinding: true
},
endOnFailure: {
type: 'boolean',
default: false,
description: '子节点失败时是否结束'
}
}
})
export class RepeaterExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, runtime } = context;
const repeatCount = BindingHelper.getValue<number>(context, 'repeatCount', 1);
const endOnFailure = BindingHelper.getValue<boolean>(context, 'endOnFailure', false);
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
if (!state.repeatCount) {
state.repeatCount = 0;
}
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure && endOnFailure) {
state.repeatCount = 0;
return TaskStatus.Failure;
}
state.repeatCount++;
runtime.resetNodeState(childId);
const shouldContinue = (repeatCount === -1) || (state.repeatCount < repeatCount);
if (shouldContinue) {
return TaskStatus.Running;
} else {
state.repeatCount = 0;
return TaskStatus.Success;
}
}
reset(context: NodeExecutionContext): void {
delete context.state.repeatCount;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,48 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 选择器节点执行器
*
* 按顺序执行子节点,任一成功则成功,全部失败才失败
*/
@NodeExecutorMetadata({
implementationType: 'Selector',
nodeType: NodeType.Composite,
displayName: '选择器',
description: '按顺序执行子节点,任一成功则成功',
category: 'Composite'
})
export class SelectorExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
while (state.currentChildIndex < nodeData.children.length) {
const childId = nodeData.children[state.currentChildIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
state.currentChildIndex = 0;
return TaskStatus.Success;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
}
}

View File

@@ -0,0 +1,48 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 序列节点执行器
*
* 按顺序执行子节点,全部成功才成功,任一失败则失败
*/
@NodeExecutorMetadata({
implementationType: 'Sequence',
nodeType: NodeType.Composite,
displayName: '序列',
description: '按顺序执行子节点,全部成功才成功',
category: 'Composite'
})
export class SequenceExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
while (state.currentChildIndex < nodeData.children.length) {
const childId = nodeData.children[state.currentChildIndex]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
state.currentChildIndex++;
}
state.currentChildIndex = 0;
return TaskStatus.Success;
}
reset(context: NodeExecutionContext): void {
context.state.currentChildIndex = 0;
}
}

View File

@@ -0,0 +1,144 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* Service执行接口
*/
export interface IServiceExecutor {
/**
* Service开始执行
*/
onServiceStart?(context: NodeExecutionContext): void;
/**
* Service每帧更新
*/
onServiceTick(context: NodeExecutionContext): void;
/**
* Service结束执行
*/
onServiceEnd?(context: NodeExecutionContext): void;
}
/**
* Service注册表
*/
class ServiceRegistry {
private static services: Map<string, IServiceExecutor> = new Map();
static register(name: string, service: IServiceExecutor): void {
this.services.set(name, service);
}
static get(name: string): IServiceExecutor | undefined {
return this.services.get(name);
}
static has(name: string): boolean {
return this.services.has(name);
}
static unregister(name: string): boolean {
return this.services.delete(name);
}
}
/**
* Service装饰器执行器
*
* 在子节点执行期间持续运行后台逻辑
*/
@NodeExecutorMetadata({
implementationType: 'Service',
nodeType: NodeType.Decorator,
displayName: 'Service',
description: '在子节点执行期间持续运行后台逻辑',
category: 'Decorator',
configSchema: {
serviceName: {
type: 'string',
default: '',
description: 'Service名称'
},
tickInterval: {
type: 'number',
default: 0,
description: 'Service更新间隔0表示每帧更新',
supportBinding: true
}
}
})
export class ServiceDecorator implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
const tickInterval = BindingHelper.getValue<number>(context, 'tickInterval', 0);
if (!serviceName) {
return TaskStatus.Failure;
}
const service = ServiceRegistry.get(serviceName);
if (!service) {
console.warn(`未找到Service: ${serviceName}`);
return TaskStatus.Failure;
}
if (state.status !== TaskStatus.Running) {
state.startTime = totalTime;
state.lastExecutionTime = totalTime;
if (service.onServiceStart) {
service.onServiceStart(context);
}
}
const shouldTick = tickInterval === 0 ||
(state.lastExecutionTime !== undefined &&
(totalTime - state.lastExecutionTime) >= tickInterval);
if (shouldTick) {
service.onServiceTick(context);
state.lastExecutionTime = totalTime;
}
const childId = nodeData.children[0]!;
const childStatus = context.executeChild(childId);
if (childStatus !== TaskStatus.Running) {
if (service.onServiceEnd) {
service.onServiceEnd(context);
}
}
return childStatus;
}
reset(context: NodeExecutionContext): void {
const { nodeData, runtime, state } = context;
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
if (serviceName) {
const service = ServiceRegistry.get(serviceName);
if (service && service.onServiceEnd) {
service.onServiceEnd(context);
}
}
delete state.startTime;
delete state.lastExecutionTime;
if (nodeData.children && nodeData.children.length > 0) {
runtime.resetNodeState(nodeData.children[0]!);
}
}
}
export { ServiceRegistry };

View File

@@ -0,0 +1,43 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 设置黑板值动作执行器
*
* 设置黑板中的变量值
*/
@NodeExecutorMetadata({
implementationType: 'SetBlackboardValue',
nodeType: NodeType.Action,
displayName: '设置黑板值',
description: '设置黑板中的变量值',
category: 'Action',
configSchema: {
key: {
type: 'string',
default: '',
description: '黑板变量名'
},
value: {
type: 'object',
description: '要设置的值',
supportBinding: true
}
}
})
export class SetBlackboardValue implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { runtime } = context;
const key = BindingHelper.getValue<string>(context, 'key', '');
const value = BindingHelper.getValue(context, 'value');
if (!key) {
return TaskStatus.Failure;
}
runtime.setBlackboardValue(key, value);
return TaskStatus.Success;
}
}

View File

@@ -0,0 +1,161 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
import { BehaviorTreeAssetManager } from '../BehaviorTreeAssetManager';
import { Core } from '@esengine/ecs-framework';
/**
* SubTree执行器
*
* 引用并执行其他行为树,实现模块化和复用
*/
@NodeExecutorMetadata({
implementationType: 'SubTree',
nodeType: NodeType.Action,
displayName: '子树',
description: '引用并执行其他行为树',
category: 'Special',
configSchema: {
treeAssetId: {
type: 'string',
default: '',
description: '要执行的行为树资产ID',
supportBinding: true
},
shareBlackboard: {
type: 'boolean',
default: true,
description: '是否共享黑板数据'
}
}
})
export class SubTreeExecutor implements INodeExecutor {
private assetManager: BehaviorTreeAssetManager | null = null;
private getAssetManager(): BehaviorTreeAssetManager {
if (!this.assetManager) {
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
}
return this.assetManager;
}
execute(context: NodeExecutionContext): TaskStatus {
const { runtime, state, entity } = context;
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
const shareBlackboard = BindingHelper.getValue<boolean>(context, 'shareBlackboard', true);
if (!treeAssetId) {
return TaskStatus.Failure;
}
const assetManager = this.getAssetManager();
const subTreeData = assetManager.getAsset(treeAssetId);
if (!subTreeData) {
console.warn(`未找到子树资产: ${treeAssetId}`);
return TaskStatus.Failure;
}
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
if (!rootNode) {
console.warn(`子树根节点未找到: ${subTreeData.rootNodeId}`);
return TaskStatus.Failure;
}
if (!shareBlackboard && state.status !== TaskStatus.Running) {
if (subTreeData.blackboardVariables) {
for (const [key, value] of subTreeData.blackboardVariables.entries()) {
if (!runtime.hasBlackboardKey(key)) {
runtime.setBlackboardValue(key, value);
}
}
}
}
const subTreeContext: NodeExecutionContext = {
entity,
nodeData: rootNode,
state: runtime.getNodeState(rootNode.id),
runtime,
treeData: subTreeData,
deltaTime: context.deltaTime,
totalTime: context.totalTime,
executeChild: (childId: string) => {
const childData = subTreeData.nodes.get(childId);
if (!childData) {
console.warn(`子树节点未找到: ${childId}`);
return TaskStatus.Failure;
}
const childContext: NodeExecutionContext = {
entity,
nodeData: childData,
state: runtime.getNodeState(childId),
runtime,
treeData: subTreeData,
deltaTime: context.deltaTime,
totalTime: context.totalTime,
executeChild: subTreeContext.executeChild
};
return this.executeSubTreeNode(childContext);
}
};
return this.executeSubTreeNode(subTreeContext);
}
private executeSubTreeNode(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
const state = runtime.getNodeState(nodeData.id);
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[state.currentChildIndex]!;
const childStatus = context.executeChild(childId);
if (childStatus === TaskStatus.Running) {
return TaskStatus.Running;
}
if (childStatus === TaskStatus.Failure) {
state.currentChildIndex = 0;
return TaskStatus.Failure;
}
state.currentChildIndex++;
if (state.currentChildIndex >= nodeData.children.length) {
state.currentChildIndex = 0;
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
if (treeAssetId) {
const assetManager = this.getAssetManager();
const subTreeData = assetManager.getAsset(treeAssetId);
if (subTreeData) {
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
if (rootNode) {
context.runtime.resetNodeState(rootNode.id);
if (rootNode.children) {
for (const childId of rootNode.children) {
context.runtime.resetNodeState(childId);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 超时装饰器执行器
*
* 限制子节点的执行时间
*/
@NodeExecutorMetadata({
implementationType: 'Timeout',
nodeType: NodeType.Decorator,
displayName: '超时',
description: '限制子节点的执行时间',
category: 'Decorator',
configSchema: {
timeout: {
type: 'number',
default: 1.0,
description: '超时时间(秒)',
min: 0,
supportBinding: true
}
}
})
export class TimeoutExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, state, totalTime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const timeout = BindingHelper.getValue<number>(context, 'timeout', 1.0);
if (state.startTime === undefined) {
state.startTime = totalTime;
}
const elapsedTime = totalTime - state.startTime;
if (elapsedTime >= timeout) {
delete state.startTime;
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
delete state.startTime;
return status;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,45 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 直到失败装饰器执行器
*
* 重复执行子节点直到失败
*/
@NodeExecutorMetadata({
implementationType: 'UntilFail',
nodeType: NodeType.Decorator,
displayName: '直到失败',
description: '重复执行子节点直到失败',
category: 'Decorator'
})
export class UntilFailExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Success;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Failure) {
return TaskStatus.Failure;
}
runtime.resetNodeState(childId);
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,45 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 直到成功装饰器执行器
*
* 重复执行子节点直到成功
*/
@NodeExecutorMetadata({
implementationType: 'UntilSuccess',
nodeType: NodeType.Decorator,
displayName: '直到成功',
description: '重复执行子节点直到成功',
category: 'Decorator'
})
export class UntilSuccessExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { nodeData, runtime } = context;
if (!nodeData.children || nodeData.children.length === 0) {
return TaskStatus.Failure;
}
const childId = nodeData.children[0]!;
const status = context.executeChild(childId);
if (status === TaskStatus.Running) {
return TaskStatus.Running;
}
if (status === TaskStatus.Success) {
return TaskStatus.Success;
}
runtime.resetNodeState(childId);
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
if (context.nodeData.children && context.nodeData.children.length > 0) {
context.runtime.resetNodeState(context.nodeData.children[0]!);
}
}
}

View File

@@ -0,0 +1,46 @@
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
import { NodeExecutorMetadata } from '../NodeMetadata';
/**
* 等待动作执行器
*
* 等待指定时间后返回成功
*/
@NodeExecutorMetadata({
implementationType: 'Wait',
nodeType: NodeType.Action,
displayName: '等待',
description: '等待指定时间后返回成功',
category: 'Action',
configSchema: {
duration: {
type: 'number',
default: 1.0,
description: '等待时长(秒)',
min: 0,
supportBinding: true
}
}
})
export class WaitAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, totalTime } = context;
const duration = BindingHelper.getValue<number>(context, 'duration', 1.0);
if (!state.startTime) {
state.startTime = totalTime;
return TaskStatus.Running;
}
if (totalTime - state.startTime >= duration) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
}
}

View File

@@ -0,0 +1,29 @@
import { TaskStatus } from '../../Types/TaskStatus';
import { INodeExecutor, NodeExecutionContext } from '../NodeExecutor';
/**
* 等待动作执行器
*
* 等待指定时间后返回成功
*/
export class WaitActionExecutor implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const { state, nodeData, totalTime } = context;
const duration = nodeData.config['duration'] as number || 1.0;
if (!state.startTime) {
state.startTime = totalTime;
return TaskStatus.Running;
}
if (totalTime - state.startTime >= duration) {
return TaskStatus.Success;
}
return TaskStatus.Running;
}
reset(context: NodeExecutionContext): void {
delete context.state.startTime;
}
}

View File

@@ -0,0 +1,30 @@
export { SequenceExecutor } from './SequenceExecutor';
export { SelectorExecutor } from './SelectorExecutor';
export { ParallelExecutor } from './ParallelExecutor';
export { ParallelSelectorExecutor } from './ParallelSelectorExecutor';
export { RandomSequenceExecutor } from './RandomSequenceExecutor';
export { RandomSelectorExecutor } from './RandomSelectorExecutor';
export { InverterExecutor } from './InverterExecutor';
export { RepeaterExecutor } from './RepeaterExecutor';
export { AlwaysSucceedExecutor } from './AlwaysSucceedExecutor';
export { AlwaysFailExecutor } from './AlwaysFailExecutor';
export { UntilSuccessExecutor } from './UntilSuccessExecutor';
export { UntilFailExecutor } from './UntilFailExecutor';
export { ConditionalExecutor } from './ConditionalExecutor';
export { CooldownExecutor } from './CooldownExecutor';
export { TimeoutExecutor } from './TimeoutExecutor';
export { ServiceDecorator, ServiceRegistry } from './ServiceDecorator';
export type { IServiceExecutor } from './ServiceDecorator';
export { WaitAction } from './WaitAction';
export { LogAction } from './LogAction';
export { SetBlackboardValue } from './SetBlackboardValue';
export { ModifyBlackboardValue } from './ModifyBlackboardValue';
export { ExecuteAction } from './ExecuteAction';
export { SubTreeExecutor } from './SubTreeExecutor';
export { BlackboardCompare } from './BlackboardCompare';
export { BlackboardExists } from './BlackboardExists';
export { RandomProbability } from './RandomProbability';
export { ExecuteCondition } from './ExecuteCondition';

View File

@@ -0,0 +1,181 @@
import { Entity } from '@esengine/ecs-framework';
import { TaskStatus } from '../Types/TaskStatus';
import { BehaviorNodeData, BehaviorTreeData, NodeRuntimeState } from './BehaviorTreeData';
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
/**
* 节点执行上下文
*
* 包含执行节点所需的所有信息
*/
export interface NodeExecutionContext {
/** 游戏Entity行为树宿主 */
readonly entity: Entity;
/** 节点数据 */
readonly nodeData: BehaviorNodeData;
/** 节点运行时状态 */
readonly state: NodeRuntimeState;
/** 运行时组件(访问黑板等) */
readonly runtime: BehaviorTreeRuntimeComponent;
/** 行为树数据(访问子节点等) */
readonly treeData: BehaviorTreeData;
/** 当前帧增量时间 */
readonly deltaTime: number;
/** 总时间 */
readonly totalTime: number;
/** 执行子节点 */
executeChild(childId: string): TaskStatus;
}
/**
* 节点执行器接口
*
* 所有节点类型都需要实现对应的执行器
* 执行器是无状态的状态存储在NodeRuntimeState中
*/
export interface INodeExecutor {
/**
* 执行节点逻辑
*
* @param context 执行上下文
* @returns 执行结果状态
*/
execute(context: NodeExecutionContext): TaskStatus;
/**
* 重置节点状态(可选)
*
* 当节点完成或被中断时调用
*/
reset?(context: NodeExecutionContext): void;
}
/**
* 复合节点执行结果
*/
export interface CompositeExecutionResult {
/** 节点状态 */
status: TaskStatus;
/** 要激活的子节点索引列表undefined表示激活所有 */
activateChildren?: number[];
/** 是否停止所有子节点 */
stopAllChildren?: boolean;
}
/**
* 复合节点执行器接口
*/
export interface ICompositeExecutor extends INodeExecutor {
/**
* 执行复合节点逻辑
*
* @param context 执行上下文
* @returns 复合节点执行结果
*/
executeComposite(context: NodeExecutionContext): CompositeExecutionResult;
}
/**
* 绑定辅助工具
*
* 处理配置属性的黑板绑定
*/
export class BindingHelper {
/**
* 获取配置值(考虑黑板绑定)
*
* @param context 执行上下文
* @param configKey 配置键名
* @param defaultValue 默认值
* @returns 解析后的值
*/
static getValue<T = any>(
context: NodeExecutionContext,
configKey: string,
defaultValue?: T
): T {
const { nodeData, runtime } = context;
if (nodeData.bindings && nodeData.bindings[configKey]) {
const blackboardKey = nodeData.bindings[configKey];
const boundValue = runtime.getBlackboardValue<T>(blackboardKey);
return boundValue !== undefined ? boundValue : (defaultValue as T);
}
const configValue = nodeData.config[configKey];
return configValue !== undefined ? configValue : (defaultValue as T);
}
/**
* 检查配置是否绑定到黑板变量
*/
static hasBinding(context: NodeExecutionContext, configKey: string): boolean {
return !!(context.nodeData.bindings && context.nodeData.bindings[configKey]);
}
/**
* 获取绑定的黑板变量名
*/
static getBindingKey(context: NodeExecutionContext, configKey: string): string | undefined {
return context.nodeData.bindings?.[configKey];
}
}
/**
* 节点执行器注册表
*
* 管理所有节点类型的执行器
*/
export class NodeExecutorRegistry {
private executors: Map<string, INodeExecutor> = new Map();
/**
* 注册执行器
*
* @param implementationType 节点实现类型对应BehaviorNodeData.implementationType
* @param executor 执行器实例
*/
register(implementationType: string, executor: INodeExecutor): void {
if (this.executors.has(implementationType)) {
console.warn(`执行器已存在,将被覆盖: ${implementationType}`);
}
this.executors.set(implementationType, executor);
}
/**
* 获取执行器
*/
get(implementationType: string): INodeExecutor | undefined {
return this.executors.get(implementationType);
}
/**
* 检查是否有执行器
*/
has(implementationType: string): boolean {
return this.executors.has(implementationType);
}
/**
* 注销执行器
*/
unregister(implementationType: string): boolean {
return this.executors.delete(implementationType);
}
/**
* 清空所有执行器
*/
clear(): void {
this.executors.clear();
}
}

View File

@@ -0,0 +1,79 @@
import { NodeType } from '../Types/TaskStatus';
/**
* 配置参数定义
*/
export interface ConfigFieldDefinition {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
default?: any;
description?: string;
min?: number;
max?: number;
options?: string[];
supportBinding?: boolean;
allowMultipleConnections?: boolean;
}
/**
* 节点元数据
*/
export interface NodeMetadata {
implementationType: string;
nodeType: NodeType;
displayName: string;
description?: string;
category?: string;
configSchema?: Record<string, ConfigFieldDefinition>;
}
/**
* 节点元数据注册表
*/
export class NodeMetadataRegistry {
private static metadataMap: Map<string, NodeMetadata> = new Map();
private static executorClassMap: Map<Function, string> = new Map();
private static executorConstructors: Map<string, new () => any> = new Map();
static register(target: Function, metadata: NodeMetadata): void {
this.metadataMap.set(metadata.implementationType, metadata);
this.executorClassMap.set(target, metadata.implementationType);
this.executorConstructors.set(metadata.implementationType, target as new () => any);
}
static getMetadata(implementationType: string): NodeMetadata | undefined {
return this.metadataMap.get(implementationType);
}
static getAllMetadata(): NodeMetadata[] {
return Array.from(this.metadataMap.values());
}
static getByCategory(category: string): NodeMetadata[] {
return this.getAllMetadata().filter(m => m.category === category);
}
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
return this.getAllMetadata().filter(m => m.nodeType === nodeType);
}
static getImplementationType(executorClass: Function): string | undefined {
return this.executorClassMap.get(executorClass);
}
static getExecutorConstructor(implementationType: string): (new () => any) | undefined {
return this.executorConstructors.get(implementationType);
}
static getAllExecutorConstructors(): Map<string, new () => any> {
return new Map(this.executorConstructors);
}
}
/**
* 节点执行器元数据装饰器
*/
export function NodeExecutorMetadata(metadata: NodeMetadata) {
return function (target: Function) {
NodeMetadataRegistry.register(target, metadata);
};
}

View File

@@ -0,0 +1,8 @@
export { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
export { INodeExecutor, NodeExecutionContext, NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
export { NodeMetadata, ConfigFieldDefinition, NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
export * from './Executors';

View File

@@ -22,6 +22,14 @@ export interface BlackboardVariableDefinition {
description?: string;
}
/**
* 行为树节点配置数据
*/
export interface BehaviorNodeConfigData {
className?: string;
[key: string]: any;
}
/**
* 行为树节点数据(运行时格式)
*/
@@ -31,7 +39,7 @@ export interface BehaviorTreeNodeData {
nodeType: NodeType;
// 节点类型特定数据
data: Record<string, any>;
data: BehaviorNodeConfigData;
// 子节点ID列表
children: string[];
@@ -216,11 +224,19 @@ export class BehaviorTreeAssetValidator {
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
const result: AssetValidationResult = {
valid: errors.length === 0
};
if (errors.length > 0) {
result.errors = errors;
}
if (warnings.length > 0) {
result.warnings = warnings;
}
return result;
}
/**

View File

@@ -1,422 +0,0 @@
import { Entity, IScene, createLogger, ComponentRegistry, Component } from '@esengine/ecs-framework';
import type { BehaviorTreeAsset, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
import { BlackboardComponent } from '../Components/BlackboardComponent';
import { PropertyBindings } from '../Components/PropertyBindings';
import { NodeType } from '../Types/TaskStatus';
// 导入所有节点组件
import { RootNode } from '../Components/Composites/RootNode';
import { SequenceNode } from '../Components/Composites/SequenceNode';
import { SelectorNode } from '../Components/Composites/SelectorNode';
import { ParallelNode } from '../Components/Composites/ParallelNode';
import { ParallelSelectorNode } from '../Components/Composites/ParallelSelectorNode';
import { RandomSequenceNode } from '../Components/Composites/RandomSequenceNode';
import { RandomSelectorNode } from '../Components/Composites/RandomSelectorNode';
import { InverterNode } from '../Components/Decorators/InverterNode';
import { RepeaterNode } from '../Components/Decorators/RepeaterNode';
import { UntilSuccessNode } from '../Components/Decorators/UntilSuccessNode';
import { UntilFailNode } from '../Components/Decorators/UntilFailNode';
import { AlwaysSucceedNode } from '../Components/Decorators/AlwaysSucceedNode';
import { AlwaysFailNode } from '../Components/Decorators/AlwaysFailNode';
import { ConditionalNode } from '../Components/Decorators/ConditionalNode';
import { CooldownNode } from '../Components/Decorators/CooldownNode';
import { TimeoutNode } from '../Components/Decorators/TimeoutNode';
import { WaitAction } from '../Components/Actions/WaitAction';
import { LogAction } from '../Components/Actions/LogAction';
import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction';
import { ModifyBlackboardValueAction } from '../Components/Actions/ModifyBlackboardValueAction';
import { ExecuteAction } from '../Components/Actions/ExecuteAction';
import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition';
import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition';
import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition';
import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition';
import { AbortType } from '../Types/TaskStatus';
const logger = createLogger('BehaviorTreeAssetLoader');
/**
* 实例化选项
*/
export interface InstantiateOptions {
/**
* 实体名称前缀
*/
namePrefix?: string;
/**
* 是否共享黑板如果为true将使用全局黑板服务
*/
sharedBlackboard?: boolean;
/**
* 黑板变量覆盖(用于运行时动态设置初始值)
*/
blackboardOverrides?: Record<string, any>;
/**
* 是否作为子树实例化
* 如果为 true根节点不会添加 RootNode 组件,避免触发预加载逻辑
*/
asSubTree?: boolean;
}
/**
* 行为树资产加载器
*
* 将BehaviorTreeAsset实例化为可运行的Entity树
*/
export class BehaviorTreeAssetLoader {
/**
* 从资产实例化行为树
*
* @param asset 行为树资产
* @param scene 目标场景
* @param options 实例化选项
* @returns 根实体
*
* @example
* ```typescript
* const asset = await loadAssetFromFile('enemy-ai.btree.bin');
* const aiRoot = BehaviorTreeAssetLoader.instantiate(asset, scene);
* BehaviorTreeStarter.start(aiRoot);
* ```
*/
static instantiate(
asset: BehaviorTreeAsset,
scene: IScene,
options: InstantiateOptions = {}
): Entity {
logger.info(`开始实例化行为树: ${asset.metadata.name}`);
// 创建节点映射
const nodeMap = new Map<string, BehaviorTreeNodeData>();
for (const node of asset.nodes) {
nodeMap.set(node.id, node);
}
// 查找根节点
const rootNodeData = nodeMap.get(asset.rootNodeId);
if (!rootNodeData) {
throw new Error(`未找到根节点: ${asset.rootNodeId}`);
}
// 创建实体映射
const entityMap = new Map<string, Entity>();
// 递归创建实体树
const rootEntity = this.createEntityTree(
rootNodeData,
nodeMap,
entityMap,
scene,
options.namePrefix,
options.asSubTree
);
// 添加黑板
this.setupBlackboard(rootEntity, asset.blackboard, options.blackboardOverrides);
// 设置属性绑定
if (asset.propertyBindings && asset.propertyBindings.length > 0) {
this.setupPropertyBindings(asset.propertyBindings, entityMap);
}
logger.info(`行为树实例化完成: ${asset.nodes.length} 个节点`);
return rootEntity;
}
/**
* 递归创建实体树
*/
private static createEntityTree(
nodeData: BehaviorTreeNodeData,
nodeMap: Map<string, BehaviorTreeNodeData>,
entityMap: Map<string, Entity>,
scene: IScene,
namePrefix?: string,
asSubTree?: boolean,
isRootOfSubTree: boolean = true
): Entity {
const entityName = namePrefix ? `${namePrefix}_${nodeData.name}` : nodeData.name;
const entity = scene.createEntity(entityName);
// 记录实体
entityMap.set(nodeData.id, entity);
// 添加BehaviorTreeNode组件
const btNode = entity.addComponent(new BehaviorTreeNode());
btNode.nodeType = nodeData.nodeType;
btNode.nodeName = nodeData.name;
// 添加节点特定组件(如果是子树的根节点,跳过 RootNode
this.addNodeComponents(entity, nodeData, asSubTree && isRootOfSubTree);
// 递归创建子节点
for (const childId of nodeData.children) {
const childData = nodeMap.get(childId);
if (!childData) {
logger.warn(`子节点未找到: ${childId}`);
continue;
}
const childEntity = this.createEntityTree(
childData,
nodeMap,
entityMap,
scene,
namePrefix,
asSubTree,
false // 子节点不是根节点
);
entity.addChild(childEntity);
}
return entity;
}
/**
* 添加节点特定组件
* @param skipRootNode 是否跳过添加 RootNode 组件(用于子树)
*/
private static addNodeComponents(entity: Entity, nodeData: BehaviorTreeNodeData, skipRootNode: boolean = false): void {
const { nodeType, data, name } = nodeData;
logger.debug(`addNodeComponents: name=${name}, data.nodeType=${data.nodeType}, skipRootNode=${skipRootNode}`);
// 根据节点类型和名称添加对应组件
if (data.nodeType === 'root' || name === '根节点' || name === 'Root') {
if (!skipRootNode) {
logger.debug(`添加 RootNode 组件: ${name}`);
entity.addComponent(new RootNode());
} else {
// 子树的根节点,使用第一个子节点的类型(通常是 SequenceNode
logger.debug(`跳过为子树根节点添加 RootNode: ${name}`);
// 添加一个默认的 SequenceNode 作为子树的根
this.addCompositeComponent(entity, '序列', data);
}
}
// 组合节点
else if (nodeType === NodeType.Composite) {
this.addCompositeComponent(entity, name, data);
}
// 装饰器节点
else if (nodeType === NodeType.Decorator) {
this.addDecoratorComponent(entity, name, data);
}
// 动作节点
else if (nodeType === NodeType.Action) {
this.addActionComponent(entity, name, data);
}
// 条件节点
else if (nodeType === NodeType.Condition) {
this.addConditionComponent(entity, name, data);
}
}
/**
* 添加组合节点组件
*/
private static addCompositeComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('sequence') || nameLower.includes('序列')) {
const node = entity.addComponent(new SequenceNode());
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
} else if (nameLower.includes('selector') || nameLower.includes('选择')) {
const node = entity.addComponent(new SelectorNode());
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
} else if (nameLower.includes('parallelselector') || nameLower.includes('并行选择')) {
const node = entity.addComponent(new ParallelSelectorNode());
node.failurePolicy = data.failurePolicy ?? 'one';
} else if (nameLower.includes('parallel') || nameLower.includes('并行')) {
const node = entity.addComponent(new ParallelNode());
node.successPolicy = data.successPolicy ?? 'all';
node.failurePolicy = data.failurePolicy ?? 'one';
} else if (nameLower.includes('randomsequence') || nameLower.includes('随机序列')) {
entity.addComponent(new RandomSequenceNode());
} else if (nameLower.includes('randomselector') || nameLower.includes('随机选择')) {
entity.addComponent(new RandomSelectorNode());
} else {
logger.warn(`未知的组合节点类型: ${name}`);
}
}
/**
* 添加装饰器组件
*/
private static addDecoratorComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('inverter') || nameLower.includes('反转')) {
entity.addComponent(new InverterNode());
} else if (nameLower.includes('repeater') || nameLower.includes('重复')) {
const node = entity.addComponent(new RepeaterNode());
node.repeatCount = data.repeatCount ?? -1;
node.endOnFailure = data.endOnFailure ?? false;
} else if (nameLower.includes('untilsuccess') || nameLower.includes('直到成功')) {
entity.addComponent(new UntilSuccessNode());
} else if (nameLower.includes('untilfail') || nameLower.includes('直到失败')) {
entity.addComponent(new UntilFailNode());
} else if (nameLower.includes('alwayssucceed') || nameLower.includes('总是成功')) {
entity.addComponent(new AlwaysSucceedNode());
} else if (nameLower.includes('alwaysfail') || nameLower.includes('总是失败')) {
entity.addComponent(new AlwaysFailNode());
} else if (nameLower.includes('conditional') || nameLower.includes('条件装饰')) {
const node = entity.addComponent(new ConditionalNode());
node.conditionCode = data.conditionCode ?? '';
node.shouldReevaluate = data.shouldReevaluate ?? true;
} else if (nameLower.includes('cooldown') || nameLower.includes('冷却')) {
const node = entity.addComponent(new CooldownNode());
node.cooldownTime = data.cooldownTime ?? 1.0;
} else if (nameLower.includes('timeout') || nameLower.includes('超时')) {
const node = entity.addComponent(new TimeoutNode());
node.timeoutDuration = data.timeoutDuration ?? 1.0;
} else {
logger.warn(`未知的装饰器类型: ${name}`);
}
}
/**
* 添加动作组件
*/
private static addActionComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('wait') || nameLower.includes('等待')) {
const action = entity.addComponent(new WaitAction());
action.waitTime = data.waitTime ?? 1.0;
} else if (nameLower.includes('log') || nameLower.includes('日志')) {
const action = entity.addComponent(new LogAction());
action.message = data.message ?? '';
action.level = data.level ?? 'log';
} else if (nameLower.includes('setblackboard') || nameLower.includes('setvalue') || nameLower.includes('设置变量')) {
const action = entity.addComponent(new SetBlackboardValueAction());
action.variableName = data.variableName ?? '';
action.value = data.value;
} else if (nameLower.includes('modifyblackboard') || nameLower.includes('modifyvalue') || nameLower.includes('修改变量')) {
const action = entity.addComponent(new ModifyBlackboardValueAction());
action.variableName = data.variableName ?? '';
action.operation = data.operation ?? 'add';
action.operand = data.operand ?? 0;
} else if (nameLower.includes('execute') || nameLower.includes('自定义')) {
const action = entity.addComponent(new ExecuteAction());
action.actionCode = data.actionCode ?? 'return TaskStatus.Success;';
} else if (data.className) {
const ComponentClass = ComponentRegistry.getComponentType(data.className);
if (ComponentClass) {
try {
const component = new (ComponentClass as any)();
Object.assign(component, data);
entity.addComponent(component as Component);
} catch (error) {
logger.error(`创建动作组件失败: ${data.className}, error: ${error}`);
}
} else {
logger.warn(`未找到动作组件类: ${data.className}`);
}
} else {
logger.warn(`未知的动作类型: ${name}`);
}
}
/**
* 添加条件组件
*/
private static addConditionComponent(entity: Entity, name: string, data: Record<string, any>): void {
const nameLower = name.toLowerCase();
if (nameLower.includes('compare') || nameLower.includes('比较变量')) {
const condition = entity.addComponent(new BlackboardCompareCondition());
condition.variableName = data.variableName ?? '';
condition.operator = (data.operator as CompareOperator) ?? CompareOperator.Equal;
condition.compareValue = data.compareValue;
condition.invertResult = data.invertResult ?? false;
} else if (nameLower.includes('exists') || nameLower.includes('变量存在')) {
const condition = entity.addComponent(new BlackboardExistsCondition());
condition.variableName = data.variableName ?? '';
condition.checkNotNull = data.checkNotNull ?? false;
condition.invertResult = data.invertResult ?? false;
} else if (nameLower.includes('random') || nameLower.includes('概率')) {
const condition = entity.addComponent(new RandomProbabilityCondition());
condition.probability = data.probability ?? 0.5;
} else if (nameLower.includes('execute') || nameLower.includes('执行条件')) {
const condition = entity.addComponent(new ExecuteCondition());
condition.conditionCode = data.conditionCode ?? '';
condition.invertResult = data.invertResult ?? false;
} else if (data.className) {
const ComponentClass = ComponentRegistry.getComponentType(data.className);
if (ComponentClass) {
try {
const component = new (ComponentClass as any)();
Object.assign(component, data);
entity.addComponent(component as Component);
} catch (error) {
logger.error(`创建条件组件失败: ${data.className}, error: ${error}`);
}
} else {
logger.warn(`未找到条件组件类: ${data.className}`);
}
} else {
logger.warn(`未知的条件类型: ${name}`);
}
}
/**
* 设置黑板
*/
private static setupBlackboard(
rootEntity: Entity,
blackboardDef: BlackboardVariableDefinition[],
overrides?: Record<string, any>
): void {
const blackboard = rootEntity.addComponent(new BlackboardComponent());
for (const variable of blackboardDef) {
const value = overrides && overrides[variable.name] !== undefined
? overrides[variable.name]
: variable.defaultValue;
blackboard.defineVariable(
variable.name,
variable.type,
value,
{
readonly: variable.readonly,
description: variable.description
}
);
}
logger.info(`已设置黑板: ${blackboardDef.length} 个变量`);
}
/**
* 设置属性绑定
*/
private static setupPropertyBindings(
bindings: PropertyBinding[],
entityMap: Map<string, Entity>
): void {
for (const binding of bindings) {
const entity = entityMap.get(binding.nodeId);
if (!entity) {
logger.warn(`属性绑定引用的节点不存在: ${binding.nodeId}`);
continue;
}
let propertyBindings = entity.getComponent(PropertyBindings);
if (!propertyBindings) {
propertyBindings = entity.addComponent(new PropertyBindings());
}
propertyBindings.addBinding(binding.propertyName, binding.variableName);
}
logger.info(`已设置属性绑定: ${bindings.length} 个绑定`);
}
}

View File

@@ -1,189 +0,0 @@
import { Entity, IScene, SceneSerializer, SerializedScene, SerializedEntity } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
/**
* 行为树持久化工具
*
* 使用框架的序列化系统进行二进制/JSON序列化
*/
export class BehaviorTreePersistence {
/**
* 序列化行为树JSON格式
*
* @param rootEntity 行为树根实体
* @param pretty 是否格式化
* @returns 序列化数据JSON字符串或二进制
*
* @example
* ```typescript
* const data = BehaviorTreePersistence.serialize(aiRoot);
* ```
*/
static serialize(rootEntity: Entity, pretty: boolean = true): string | Uint8Array {
if (!rootEntity.hasComponent(BehaviorTreeNode)) {
throw new Error('Entity must have BehaviorTreeNode component');
}
if (!rootEntity.scene) {
throw new Error('Entity must be attached to a scene');
}
// 使用 SceneSerializer但只序列化这棵行为树
// 创建一个临时场景包含只这个实体树
return SceneSerializer.serialize(rootEntity.scene, {
format: 'json',
pretty: pretty,
includeMetadata: true
});
}
/**
* 从序列化数据加载行为树
*
* @param scene 场景实例
* @param data 序列化数据JSON字符串或二进制
*
* @example
* ```typescript
* // 从文件读取
* const json = await readFile('behavior-tree.json');
*
* // 恢复行为树到场景
* BehaviorTreePersistence.deserialize(scene, json);
* ```
*/
static deserialize(scene: IScene, data: string | Uint8Array): void {
SceneSerializer.deserialize(scene, data, {
strategy: 'merge'
});
}
/**
* 序列化为 JSON 字符串
*
* @param rootEntity 行为树根实体
* @param pretty 是否格式化
* @returns JSON 字符串
*/
static toJSON(rootEntity: Entity, pretty: boolean = true): string {
const data = this.serialize(rootEntity, pretty);
return JSON.stringify(data, null, pretty ? 2 : 0);
}
/**
* 从 JSON 字符串加载
*
* @param scene 场景实例
* @param json JSON 字符串
*/
static fromJSON(scene: IScene, json: string): void {
this.deserialize(scene, json);
}
/**
* 保存到文件(需要 Tauri 环境)
*
* @param rootEntity 行为树根实体
* @param filePath 文件路径
*
* @example
* ```typescript
* await BehaviorTreePersistence.saveToFile(aiRoot, 'ai-behavior.json');
* ```
*/
static async saveToFile(rootEntity: Entity, filePath: string): Promise<void> {
const json = this.toJSON(rootEntity, true);
// 需要在 Tauri 环境中使用
// const { writeTextFile } = await import('@tauri-apps/api/fs');
// await writeTextFile(filePath, json);
throw new Error('saveToFile requires Tauri environment. Use toJSON() for manual saving.');
}
/**
* 从文件加载(需要 Tauri 环境)
*
* @param scene 场景实例
* @param filePath 文件路径
* @returns 恢复的根实体
*
* @example
* ```typescript
* const aiRoot = await BehaviorTreePersistence.loadFromFile(scene, 'ai-behavior.json');
* ```
*/
static async loadFromFile(scene: IScene, filePath: string): Promise<Entity> {
// 需要在 Tauri 环境中使用
// const { readTextFile } = await import('@tauri-apps/api/fs');
// const json = await readTextFile(filePath);
// return this.fromJSON(scene, json);
throw new Error('loadFromFile requires Tauri environment. Use fromJSON() for manual loading.');
}
/**
* 验证是否为有效的行为树数据
*
* @param data 序列化数据(字符串格式)
* @returns 是否有效
*/
static validate(data: string): boolean {
try {
const parsed = JSON.parse(data) as SerializedScene;
if (!parsed || typeof parsed !== 'object') {
return false;
}
// 检查必要字段
if (!parsed.name ||
typeof parsed.version !== 'number' ||
!Array.isArray(parsed.entities) ||
!Array.isArray(parsed.componentTypeRegistry)) {
return false;
}
// 检查是否至少有一个实体包含 BehaviorTreeNode 组件
const hasBehaviorTreeNode = parsed.entities.some((entity: SerializedEntity) => {
return entity.components.some(
(comp: any) => comp.type === 'BehaviorTreeNode'
);
});
return hasBehaviorTreeNode;
} catch {
return false;
}
}
/**
* 克隆行为树
*
* @param scene 场景实例
* @param rootEntity 要克隆的行为树根实体
* @returns 克隆的新实体
*
* @example
* ```typescript
* const clonedAI = BehaviorTreePersistence.clone(scene, originalAI);
* ```
*/
static clone(scene: IScene, rootEntity: Entity): Entity {
const data = this.serialize(rootEntity);
const entityCountBefore = scene.entities.count;
this.deserialize(scene, data);
// 找到新添加的根实体(最后添加的实体)
const entities = Array.from(scene.entities.buffer);
for (let i = entities.length - 1; i >= entityCountBefore; i--) {
const entity = entities[i];
if (entity.hasComponent(BehaviorTreeNode) && !entity.parent) {
return entity;
}
}
throw new Error('Failed to find cloned root entity');
}
}

View File

@@ -7,15 +7,26 @@ const logger = createLogger('EditorFormatConverter');
/**
* 编辑器节点格式
*/
export interface EditorNodeTemplate {
displayName: string;
category: string;
type: NodeType;
className?: string;
[key: string]: any;
}
export interface EditorNodeData {
nodeType?: string;
className?: string;
variableName?: string;
name?: string;
[key: string]: any;
}
export interface EditorNode {
id: string;
template: {
displayName: string;
category: string;
type: NodeType;
[key: string]: any;
};
data: Record<string, any>;
template: EditorNodeTemplate;
data: EditorNodeData;
position: { x: number; y: number };
children: string[];
}
@@ -74,12 +85,24 @@ export class EditorFormatConverter {
const assetMetadata: AssetMetadata = {
name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree',
description: metadata?.description || editorData.metadata?.description,
version: metadata?.version || editorData.version || '1.0.0',
createdAt: metadata?.createdAt || editorData.metadata?.createdAt,
modifiedAt: metadata?.modifiedAt || new Date().toISOString()
version: metadata?.version || editorData.version || '1.0.0'
};
const description = metadata?.description || editorData.metadata?.description;
if (description) {
assetMetadata.description = description;
}
const createdAt = metadata?.createdAt || editorData.metadata?.createdAt;
if (createdAt) {
assetMetadata.createdAt = createdAt;
}
const modifiedAt = metadata?.modifiedAt || new Date().toISOString();
if (modifiedAt) {
assetMetadata.modifiedAt = modifiedAt;
}
const nodes = this.convertNodes(editorData.nodes);
const blackboard = this.convertBlackboard(editorData.blackboard);
@@ -95,10 +118,13 @@ export class EditorFormatConverter {
metadata: assetMetadata,
rootNodeId: rootNode.id,
nodes,
blackboard,
propertyBindings: propertyBindings.length > 0 ? propertyBindings : undefined
blackboard
};
if (propertyBindings.length > 0) {
asset.propertyBindings = propertyBindings;
}
logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`);
return asset;
@@ -243,21 +269,31 @@ export class EditorFormatConverter {
}
const connections = this.convertPropertyBindingsToConnections(
asset.propertyBindings || [],
asset.nodes
asset.propertyBindings || []
);
const nodeConnections = this.buildNodeConnections(asset.nodes);
connections.push(...nodeConnections);
const metadata: { name: string; description?: string; createdAt?: string; modifiedAt?: string } = {
name: asset.metadata.name
};
if (asset.metadata.description) {
metadata.description = asset.metadata.description;
}
if (asset.metadata.createdAt) {
metadata.createdAt = asset.metadata.createdAt;
}
if (asset.metadata.modifiedAt) {
metadata.modifiedAt = asset.metadata.modifiedAt;
}
const editorData: EditorFormat = {
version: asset.metadata.version,
metadata: {
name: asset.metadata.name,
description: asset.metadata.description,
createdAt: asset.metadata.createdAt,
modifiedAt: asset.metadata.modifiedAt
},
metadata,
nodes,
connections,
blackboard,
@@ -324,8 +360,7 @@ export class EditorFormatConverter {
* 将属性绑定转换为连接
*/
private static convertPropertyBindingsToConnections(
bindings: PropertyBinding[],
nodes: BehaviorTreeNodeData[]
bindings: PropertyBinding[]
): EditorConnection[] {
const connections: EditorConnection[] = [];

View File

@@ -1,5 +1,5 @@
import { NodeType } from '../Types/TaskStatus';
import { getRegisteredNodeTemplates } from '../Decorators/BehaviorNodeDecorator';
import { NodeMetadataRegistry, ConfigFieldDefinition } from '../Runtime/NodeMetadata';
/**
* 节点数据JSON格式
@@ -8,6 +8,8 @@ export interface NodeDataJSON {
nodeType: string;
compositeType?: string;
decoratorType?: string;
actionType?: string;
conditionType?: string;
[key: string]: any;
}
@@ -118,6 +120,12 @@ export interface PropertyDefinition {
/** 最大长度(字符串) */
maxLength?: number;
};
/**
* 是否允许多个连接
* 默认 false只允许一个黑板变量连接
*/
allowMultipleConnections?: boolean;
}
/**
@@ -138,16 +146,15 @@ export interface NodeTemplate {
}
/**
* 编辑器节点模板库
*
* 使用装饰器系统管理所有节点
* 节点模板库
*/
export class NodeTemplates {
/**
* 获取所有节点模板(通过装饰器注册)
* 获取所有节点模板
*/
static getAllTemplates(): NodeTemplate[] {
return getRegisteredNodeTemplates();
const allMetadata = NodeMetadataRegistry.getAllMetadata();
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata));
}
/**
@@ -172,4 +179,188 @@ export class NodeTemplates {
}
});
}
/**
* 将NodeMetadata转换为NodeTemplate
*/
private static convertMetadataToTemplate(metadata: any): NodeTemplate {
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
const defaultConfig: Partial<NodeDataJSON> = {
nodeType: this.nodeTypeToString(metadata.nodeType)
};
switch (metadata.nodeType) {
case NodeType.Composite:
defaultConfig.compositeType = metadata.implementationType;
break;
case NodeType.Decorator:
defaultConfig.decoratorType = metadata.implementationType;
break;
case NodeType.Action:
defaultConfig.actionType = metadata.implementationType;
break;
case NodeType.Condition:
defaultConfig.conditionType = metadata.implementationType;
break;
}
if (metadata.configSchema) {
for (const [key, field] of Object.entries(metadata.configSchema)) {
const fieldDef = field as ConfigFieldDefinition;
if (fieldDef.default !== undefined) {
defaultConfig[key] = fieldDef.default;
}
}
}
// 根据节点类型生成默认颜色和图标
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
return {
type: metadata.nodeType,
displayName: metadata.displayName,
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
description: metadata.description || '',
className: metadata.implementationType,
icon,
color,
defaultConfig,
properties
};
}
/**
* 将ConfigSchema转换为PropertyDefinition数组
*/
private static convertConfigSchemaToProperties(
configSchema: Record<string, ConfigFieldDefinition>
): PropertyDefinition[] {
const properties: PropertyDefinition[] = [];
for (const [name, field] of Object.entries(configSchema)) {
const property: PropertyDefinition = {
name,
type: this.mapFieldTypeToPropertyType(field),
label: name
};
if (field.description !== undefined) {
property.description = field.description;
}
if (field.default !== undefined) {
property.defaultValue = field.default;
}
if (field.min !== undefined) {
property.min = field.min;
}
if (field.max !== undefined) {
property.max = field.max;
}
if (field.allowMultipleConnections !== undefined) {
property.allowMultipleConnections = field.allowMultipleConnections;
}
if (field.options) {
property.options = field.options.map(opt => ({
label: opt,
value: opt
}));
}
if (field.supportBinding) {
property.renderConfig = {
component: 'BindableInput',
props: {
supportBinding: true
}
};
}
properties.push(property);
}
return properties;
}
/**
* 映射字段类型到属性类型
*/
private static mapFieldTypeToPropertyType(field: ConfigFieldDefinition): PropertyType {
if (field.options && field.options.length > 0) {
return PropertyType.Select;
}
switch (field.type) {
case 'string':
return PropertyType.String;
case 'number':
return PropertyType.Number;
case 'boolean':
return PropertyType.Boolean;
case 'array':
case 'object':
default:
return PropertyType.String;
}
}
/**
* NodeType转字符串
*/
private static nodeTypeToString(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return 'composite';
case NodeType.Decorator:
return 'decorator';
case NodeType.Action:
return 'action';
case NodeType.Condition:
return 'condition';
default:
return 'unknown';
}
}
/**
* 根据NodeType获取默认分类
*/
private static getCategoryByNodeType(nodeType: NodeType): string {
switch (nodeType) {
case NodeType.Composite:
return '组合';
case NodeType.Decorator:
return '装饰器';
case NodeType.Action:
return '动作';
case NodeType.Condition:
return '条件';
default:
return '其他';
}
}
/**
* 根据节点类型获取默认图标和颜色
*/
private static getIconAndColorByType(nodeType: NodeType, _category: string): { icon: string; color: string } {
// 根据节点类型设置默认值
switch (nodeType) {
case NodeType.Composite:
return { icon: 'GitBranch', color: '#1976d2' }; // 蓝色
case NodeType.Decorator:
return { icon: 'Settings', color: '#fb8c00' }; // 橙色
case NodeType.Action:
return { icon: 'Play', color: '#388e3c' }; // 绿色
case NodeType.Condition:
return { icon: 'HelpCircle', color: '#d32f2f' }; // 红色
default:
return { icon: 'Circle', color: '#757575' }; // 灰色
}
}
}

View File

@@ -1,382 +0,0 @@
import { Entity, IService, createLogger } from '@esengine/ecs-framework';
import {
LoadingState,
LoadingTask,
LoadingTaskHandle,
LoadingOptions,
LoadingProgress,
TimeoutError,
CircularDependencyError,
EntityDestroyedError
} from './AssetLoadingTypes';
const logger = createLogger('AssetLoadingManager');
/**
* 资产加载管理器
*
* 统一管理行为树资产的异步加载,提供:
* - 超时检测和自动重试
* - 循环引用检测
* - 实体生命周期安全
* - 加载状态追踪
*
* @example
* ```typescript
* const manager = new AssetLoadingManager();
*
* const handle = manager.startLoading(
* 'patrol',
* parentEntity,
* () => assetLoader.loadBehaviorTree('patrol'),
* { timeoutMs: 5000, maxRetries: 3 }
* );
*
* // 在系统的 process() 中轮询检查
* const state = handle.getState();
* if (state === LoadingState.Loaded) {
* const entity = await handle.promise;
* // 使用加载的实体
* }
* ```
*/
export class AssetLoadingManager implements IService {
/** 正在进行的加载任务 */
private tasks: Map<string, LoadingTask> = new Map();
/** 加载栈(用于循环检测) */
private loadingStack: Set<string> = new Set();
/** 默认配置 */
private defaultOptions: Required<Omit<LoadingOptions, 'parentAssetId'>> = {
timeoutMs: 5000,
maxRetries: 3,
retryDelayBase: 100,
maxRetryDelay: 2000
};
/**
* 开始加载资产
*
* @param assetId 资产ID
* @param parentEntity 父实体(用于生命周期检查)
* @param loader 加载函数
* @param options 加载选项
* @returns 加载任务句柄
*/
startLoading(
assetId: string,
parentEntity: Entity,
loader: () => Promise<Entity>,
options: LoadingOptions = {}
): LoadingTaskHandle {
// 合并选项
const finalOptions = {
...this.defaultOptions,
...options
};
// 循环引用检测
if (options.parentAssetId) {
if (this.detectCircularDependency(assetId, options.parentAssetId)) {
const error = new CircularDependencyError(
`检测到循环引用: ${options.parentAssetId}${assetId}\n` +
`加载栈: ${Array.from(this.loadingStack).join(' → ')}`
);
logger.error(error.message);
throw error;
}
}
// 检查是否已有任务
const existingTask = this.tasks.get(assetId);
if (existingTask) {
logger.debug(`资产 ${assetId} 已在加载中,返回现有任务`);
return this.createHandle(existingTask);
}
// 创建新任务
const task: LoadingTask = {
assetId,
promise: null as any, // 稍后设置
startTime: Date.now(),
lastRetryTime: 0,
retryCount: 0,
maxRetries: finalOptions.maxRetries,
timeoutMs: finalOptions.timeoutMs,
state: LoadingState.Pending,
parentEntityId: parentEntity.id,
parentEntity: parentEntity,
parentAssetId: options.parentAssetId
};
// 添加到加载栈(循环检测)
this.loadingStack.add(assetId);
// 创建带超时和重试的Promise
task.promise = this.loadWithTimeoutAndRetry(task, loader, finalOptions);
task.state = LoadingState.Loading;
this.tasks.set(assetId, task);
logger.info(`开始加载资产: ${assetId}`, {
timeoutMs: finalOptions.timeoutMs,
maxRetries: finalOptions.maxRetries,
parentAssetId: options.parentAssetId
});
return this.createHandle(task);
}
/**
* 带超时和重试的加载
*/
private async loadWithTimeoutAndRetry(
task: LoadingTask,
loader: () => Promise<Entity>,
options: Required<Omit<LoadingOptions, 'parentAssetId'>>
): Promise<Entity> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= task.maxRetries; attempt++) {
// 检查父实体是否还存在
if (task.parentEntity.isDestroyed) {
const error = new EntityDestroyedError(
`父实体已销毁,取消加载: ${task.assetId}`
);
task.state = LoadingState.Cancelled;
this.cleanup(task.assetId);
logger.warn(error.message);
throw error;
}
try {
task.retryCount = attempt;
task.lastRetryTime = Date.now();
logger.debug(`加载尝试 ${attempt + 1}/${task.maxRetries + 1}: ${task.assetId}`);
// 使用超时包装
const result = await this.withTimeout(
loader(),
task.timeoutMs,
`加载资产 ${task.assetId} 超时(${task.timeoutMs}ms`
);
// 加载成功
task.state = LoadingState.Loaded;
task.result = result;
this.cleanup(task.assetId);
logger.info(`资产加载成功: ${task.assetId}`, {
attempts: attempt + 1,
elapsedMs: Date.now() - task.startTime
});
return result;
} catch (error) {
lastError = error as Error;
// 记录错误类型
if (error instanceof TimeoutError) {
task.state = LoadingState.Timeout;
logger.warn(`资产加载超时: ${task.assetId} (尝试 ${attempt + 1})`);
} else if (error instanceof EntityDestroyedError) {
// 实体已销毁,不需要重试
throw error;
} else {
logger.warn(`资产加载失败: ${task.assetId} (尝试 ${attempt + 1})`, error);
}
// 最后一次尝试失败
if (attempt === task.maxRetries) {
task.state = LoadingState.Failed;
task.error = lastError;
this.cleanup(task.assetId);
logger.error(`资产加载最终失败: ${task.assetId}`, {
attempts: attempt + 1,
error: lastError.message
});
throw lastError;
}
// 计算重试延迟(指数退避)
const delayMs = Math.min(
Math.pow(2, attempt) * options.retryDelayBase,
options.maxRetryDelay
);
logger.debug(`等待 ${delayMs}ms 后重试...`);
await this.delay(delayMs);
}
}
throw lastError!;
}
/**
* Promise 超时包装
*/
private withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
message: string
): Promise<T> {
let timeoutId: NodeJS.Timeout | number;
const timeoutPromise = new Promise<T>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new TimeoutError(message));
}, timeoutMs);
});
return Promise.race([
promise.then(result => {
clearTimeout(timeoutId as any);
return result;
}),
timeoutPromise
]).catch(error => {
clearTimeout(timeoutId as any);
throw error;
});
}
/**
* 循环依赖检测
*/
private detectCircularDependency(assetId: string, parentAssetId: string): boolean {
// 如果父资产正在加载中,说明有循环
if (this.loadingStack.has(parentAssetId)) {
return true;
}
// TODO: 更复杂的循环检测(检查完整的依赖链)
// 当前只检测直接循环A→B→A
// 未来可以检测间接循环A→B→C→A
return false;
}
/**
* 获取任务状态
*/
getTaskState(assetId: string): LoadingState {
return this.tasks.get(assetId)?.state ?? LoadingState.Idle;
}
/**
* 获取任务
*/
getTask(assetId: string): LoadingTask | undefined {
return this.tasks.get(assetId);
}
/**
* 取消加载
*/
cancelLoading(assetId: string): void {
const task = this.tasks.get(assetId);
if (task) {
task.state = LoadingState.Cancelled;
this.cleanup(assetId);
logger.info(`取消加载: ${assetId}`);
}
}
/**
* 清理任务
*/
private cleanup(assetId: string): void {
const task = this.tasks.get(assetId);
if (task) {
// 清除实体引用帮助GC
(task as any).parentEntity = null;
}
this.tasks.delete(assetId);
this.loadingStack.delete(assetId);
}
/**
* 延迟
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 创建任务句柄
*/
private createHandle(task: LoadingTask): LoadingTaskHandle {
return {
assetId: task.assetId,
getState: () => task.state,
getError: () => task.error,
getProgress: (): LoadingProgress => {
const now = Date.now();
const elapsed = now - task.startTime;
const remaining = Math.max(0, task.timeoutMs - elapsed);
return {
state: task.state,
elapsedMs: elapsed,
remainingTimeoutMs: remaining,
retryCount: task.retryCount,
maxRetries: task.maxRetries
};
},
cancel: () => this.cancelLoading(task.assetId),
promise: task.promise
};
}
/**
* 获取所有正在加载的资产
*/
getLoadingAssets(): string[] {
return Array.from(this.tasks.keys());
}
/**
* 获取加载统计信息
*/
getStats(): {
totalTasks: number;
loadingTasks: number;
failedTasks: number;
timeoutTasks: number;
} {
const tasks = Array.from(this.tasks.values());
return {
totalTasks: tasks.length,
loadingTasks: tasks.filter(t => t.state === LoadingState.Loading).length,
failedTasks: tasks.filter(t => t.state === LoadingState.Failed).length,
timeoutTasks: tasks.filter(t => t.state === LoadingState.Timeout).length
};
}
/**
* 清空所有任务
*/
clear(): void {
logger.info('清空所有加载任务', this.getStats());
this.tasks.clear();
this.loadingStack.clear();
}
/**
* 释放资源
*/
dispose(): void {
this.clear();
}
}

View File

@@ -1,158 +0,0 @@
import { Entity } from '@esengine/ecs-framework';
/**
* 资产加载状态
*/
export enum LoadingState {
/** 未开始 */
Idle = 'idle',
/** 即将开始 */
Pending = 'pending',
/** 加载中 */
Loading = 'loading',
/** 加载成功 */
Loaded = 'loaded',
/** 加载失败 */
Failed = 'failed',
/** 加载超时 */
Timeout = 'timeout',
/** 已取消 */
Cancelled = 'cancelled'
}
/**
* 加载任务
*/
export interface LoadingTask {
/** 资产ID */
assetId: string;
/** 加载Promise */
promise: Promise<Entity>;
/** 开始时间 */
startTime: number;
/** 上次重试时间 */
lastRetryTime: number;
/** 当前重试次数 */
retryCount: number;
/** 最大重试次数 */
maxRetries: number;
/** 超时时间(毫秒) */
timeoutMs: number;
/** 当前状态 */
state: LoadingState;
/** 错误信息 */
error?: Error;
/** 父实体ID */
parentEntityId: number;
/** 父实体引用需要在使用前检查isDestroyed */
parentEntity: Entity;
/** 父资产ID用于循环检测 */
parentAssetId?: string;
/** 加载结果(缓存) */
result?: Entity;
}
/**
* 加载任务句柄
*/
export interface LoadingTaskHandle {
/** 资产ID */
assetId: string;
/** 获取当前状态 */
getState(): LoadingState;
/** 获取错误信息 */
getError(): Error | undefined;
/** 获取加载进度信息 */
getProgress(): LoadingProgress;
/** 取消加载 */
cancel(): void;
/** 加载Promise */
promise: Promise<Entity>;
}
/**
* 加载进度信息
*/
export interface LoadingProgress {
/** 当前状态 */
state: LoadingState;
/** 已耗时(毫秒) */
elapsedMs: number;
/** 剩余超时时间(毫秒) */
remainingTimeoutMs: number;
/** 当前重试次数 */
retryCount: number;
/** 最大重试次数 */
maxRetries: number;
}
/**
* 加载选项
*/
export interface LoadingOptions {
/** 超时时间毫秒默认5000 */
timeoutMs?: number;
/** 最大重试次数默认3 */
maxRetries?: number;
/** 父资产ID用于循环检测 */
parentAssetId?: string;
/** 重试延迟基数毫秒默认100 */
retryDelayBase?: number;
/** 最大重试延迟毫秒默认2000 */
maxRetryDelay?: number;
}
/**
* 超时错误
*/
export class TimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}
/**
* 循环依赖错误
*/
export class CircularDependencyError extends Error {
constructor(message: string) {
super(message);
this.name = 'CircularDependencyError';
}
}
/**
* 实体已销毁错误
*/
export class EntityDestroyedError extends Error {
constructor(message: string) {
super(message);
this.name = 'EntityDestroyedError';
}
}

View File

@@ -1,227 +0,0 @@
import type { IService } from '@esengine/ecs-framework';
import { IAssetLoader } from './IAssetLoader';
import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset';
import { BehaviorTreeAssetSerializer, DeserializationOptions } from '../Serialization/BehaviorTreeAssetSerializer';
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('FileSystemAssetLoader');
/**
* 文件系统资产加载器配置
*/
export interface FileSystemAssetLoaderConfig {
/** 资产基础路径 */
basePath: string;
/** 资产格式 */
format: 'json' | 'binary';
/** 文件扩展名(可选,默认根据格式自动设置) */
extension?: string;
/** 是否启用缓存 */
enableCache?: boolean;
/** 自定义文件读取函数(可选) */
readFile?: (path: string) => Promise<string | Uint8Array>;
}
/**
* 文件系统资产加载器
*
* 从文件系统加载行为树资产,支持 JSON 和 Binary 格式。
* 提供资产缓存和预加载功能。
*
* @example
* ```typescript
* // 创建加载器
* const loader = new FileSystemAssetLoader({
* basePath: 'assets/behavior-trees',
* format: 'json',
* enableCache: true
* });
*
* // 加载资产
* const asset = await loader.loadBehaviorTree('patrol');
* ```
*/
export class FileSystemAssetLoader implements IAssetLoader, IService {
private config: Required<FileSystemAssetLoaderConfig>;
private cache: Map<string, BehaviorTreeAsset> = new Map();
constructor(config: FileSystemAssetLoaderConfig) {
this.config = {
basePath: config.basePath,
format: config.format,
extension: config.extension || (config.format === 'json' ? '.btree.json' : '.btree.bin'),
enableCache: config.enableCache ?? true,
readFile: config.readFile || this.defaultReadFile.bind(this)
};
// 规范化路径
this.config.basePath = this.config.basePath.replace(/\\/g, '/').replace(/\/$/, '');
}
/**
* 加载行为树资产
*/
async loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset> {
// 检查缓存
if (this.config.enableCache && this.cache.has(assetId)) {
logger.debug(`从缓存加载资产: ${assetId}`);
return this.cache.get(assetId)!;
}
logger.info(`加载行为树资产: ${assetId}`);
try {
// 构建文件路径
const filePath = this.resolveAssetPath(assetId);
// 读取文件
const data = await this.config.readFile(filePath);
// 反序列化(自动根据 data 类型判断格式)
const options: DeserializationOptions = {
validate: true,
strict: true
};
const asset = BehaviorTreeAssetSerializer.deserialize(data, options);
// 缓存资产
if (this.config.enableCache) {
this.cache.set(assetId, asset);
}
logger.info(`成功加载资产: ${assetId}`);
return asset;
} catch (error) {
logger.error(`加载资产失败: ${assetId}`, error);
throw new Error(`Failed to load behavior tree asset '${assetId}': ${error}`);
}
}
/**
* 检查资产是否存在
*/
async exists(assetId: string): Promise<boolean> {
// 如果在缓存中,直接返回 true
if (this.config.enableCache && this.cache.has(assetId)) {
return true;
}
try {
const filePath = this.resolveAssetPath(assetId);
// 尝试读取文件(如果文件不存在会抛出异常)
await this.config.readFile(filePath);
return true;
} catch {
return false;
}
}
/**
* 预加载资产
*/
async preload(assetIds: string[]): Promise<void> {
logger.info(`预加载 ${assetIds.length} 个资产...`);
const promises = assetIds.map(id => this.loadBehaviorTree(id).catch(error => {
logger.warn(`预加载资产失败: ${id}`, error);
}));
await Promise.all(promises);
logger.info(`预加载完成`);
}
/**
* 卸载资产
*/
unload(assetId: string): void {
if (this.cache.has(assetId)) {
this.cache.delete(assetId);
logger.debug(`卸载资产: ${assetId}`);
}
}
/**
* 清空缓存
*/
clearCache(): void {
this.cache.clear();
logger.info('缓存已清空');
}
/**
* 获取缓存的资产数量
*/
getCacheSize(): number {
return this.cache.size;
}
/**
* 释放资源
*/
dispose(): void {
this.clearCache();
}
/**
* 解析资产路径
*/
private resolveAssetPath(assetId: string): string {
// 移除开头的斜杠
const normalizedId = assetId.replace(/^\/+/, '');
// 构建完整路径
return `${this.config.basePath}/${normalizedId}${this.config.extension}`;
}
/**
* 默认文件读取实现
*
* 注意:此实现依赖运行环境
* - 浏览器:需要通过 fetch 或 XMLHttpRequest
* - Node.js需要使用 fs
* - 游戏引擎:需要使用引擎的文件 API
*
* 用户应该提供自己的 readFile 实现
*/
private async defaultReadFile(path: string): Promise<string | Uint8Array> {
// 检测运行环境
if (typeof window !== 'undefined' && typeof fetch !== 'undefined') {
// 浏览器环境
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (this.config.format === 'binary') {
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
} else {
return await response.text();
}
} else if (typeof require !== 'undefined') {
// Node.js 环境
try {
const fs = require('fs').promises;
if (this.config.format === 'binary') {
const buffer = await fs.readFile(path);
return new Uint8Array(buffer);
} else {
return await fs.readFile(path, 'utf-8');
}
} catch (error) {
throw new Error(`Failed to read file '${path}': ${error}`);
}
} else {
throw new Error(
'No default file reading implementation available. ' +
'Please provide a custom readFile function in the config.'
);
}
}
}

View File

@@ -1,6 +1,5 @@
import { IService } from '@esengine/ecs-framework';
import { BlackboardValueType } from '../Types/TaskStatus';
import { BlackboardVariable } from '../Components/BlackboardComponent';
import { BlackboardValueType, BlackboardVariable } from '../Types/TaskStatus';
/**
* 全局黑板配置
@@ -43,13 +42,18 @@ export class GlobalBlackboardService implements IService {
description?: string;
}
): void {
this.variables.set(name, {
const variable: BlackboardVariable = {
name,
type,
value: initialValue,
readonly: options?.readonly ?? false,
description: options?.description
});
readonly: options?.readonly ?? false
};
if (options?.description !== undefined) {
variable.description = options.description;
}
this.variables.set(name, variable);
}
/**

View File

@@ -1,68 +0,0 @@
import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset';
/**
* 资产加载器接口
*
* 提供可扩展的资产加载机制,允许用户自定义资产加载逻辑。
* 支持从文件系统、网络、数据库、自定义打包格式等加载资产。
*
* @example
* ```typescript
* // 使用默认的文件系统加载器
* const loader = new FileSystemAssetLoader({
* basePath: 'assets/behavior-trees',
* format: 'json'
* });
* core.services.registerInstance(FileSystemAssetLoader, loader);
*
* // 或实现自定义加载器
* class NetworkAssetLoader implements IAssetLoader {
* async loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset> {
* const response = await fetch(`/api/assets/${assetId}`);
* return response.json();
* }
*
* async exists(assetId: string): Promise<boolean> {
* const response = await fetch(`/api/assets/${assetId}/exists`);
* return response.json();
* }
* }
* core.services.registerInstance(FileSystemAssetLoader, new NetworkAssetLoader());
* ```
*/
export interface IAssetLoader {
/**
* 加载行为树资产
*
* @param assetId 资产逻辑ID例如 'patrol' 或 'ai/patrol'
* @returns 行为树资产对象
* @throws 如果资产不存在或加载失败
*/
loadBehaviorTree(assetId: string): Promise<BehaviorTreeAsset>;
/**
* 检查资产是否存在
*
* @param assetId 资产逻辑ID
* @returns 资产是否存在
*/
exists(assetId: string): Promise<boolean>;
/**
* 预加载资产(可选)
*
* 用于提前加载资产到缓存,减少运行时延迟
*
* @param assetIds 要预加载的资产ID列表
*/
preload?(assetIds: string[]): Promise<void>;
/**
* 卸载资产(可选)
*
* 释放资产占用的内存
*
* @param assetId 资产ID
*/
unload?(assetId: string): void;
}

View File

@@ -1,355 +0,0 @@
import { IService } from '@esengine/ecs-framework';
/**
* 资产类型
*/
export enum AssetType {
BehaviorTree = 'behavior-tree',
Blackboard = 'blackboard',
Unknown = 'unknown'
}
/**
* 资产注册信息
*/
export interface AssetRegistry {
/** 资产唯一ID */
id: string;
/** 资产名称 */
name: string;
/** 资产相对路径(相对于工作区根目录) */
path: string;
/** 资产类型 */
type: AssetType;
/** 依赖的其他资产ID列表 */
dependencies: string[];
/** 最后修改时间 */
lastModified?: number;
/** 资产元数据 */
metadata?: Record<string, any>;
}
/**
* 工作区配置
*/
export interface WorkspaceConfig {
/** 工作区名称 */
name: string;
/** 工作区版本 */
version: string;
/** 工作区根目录(绝对路径) */
rootPath: string;
/** 资产目录配置 */
assetPaths: {
/** 行为树目录 */
behaviorTrees: string;
/** 黑板目录 */
blackboards: string;
};
/** 资产注册表 */
assets: AssetRegistry[];
}
/**
* 工作区服务
*
* 管理项目的工作区配置和资产注册表,提供:
* - 工作区配置的加载和保存
* - 资产注册和查询
* - 依赖关系追踪
* - 循环依赖检测
*/
export class WorkspaceService implements IService {
private config: WorkspaceConfig | null = null;
private assetMap: Map<string, AssetRegistry> = new Map();
private assetPathMap: Map<string, AssetRegistry> = new Map();
/**
* 初始化工作区
*/
initialize(config: WorkspaceConfig): void {
this.config = config;
this.rebuildAssetMaps();
}
/**
* 重建资产映射表
*/
private rebuildAssetMaps(): void {
this.assetMap.clear();
this.assetPathMap.clear();
if (!this.config) return;
for (const asset of this.config.assets) {
this.assetMap.set(asset.id, asset);
this.assetPathMap.set(asset.path, asset);
}
}
/**
* 获取工作区配置
*/
getConfig(): WorkspaceConfig | null {
return this.config;
}
/**
* 更新工作区配置
*/
updateConfig(config: WorkspaceConfig): void {
this.config = config;
this.rebuildAssetMaps();
}
/**
* 注册资产
*/
registerAsset(asset: AssetRegistry): void {
if (!this.config) {
throw new Error('工作区未初始化');
}
// 检查是否已存在
const existing = this.config.assets.find(a => a.id === asset.id);
if (existing) {
// 更新现有资产
Object.assign(existing, asset);
} else {
// 添加新资产
this.config.assets.push(asset);
}
this.rebuildAssetMaps();
}
/**
* 取消注册资产
*/
unregisterAsset(assetId: string): void {
if (!this.config) return;
const index = this.config.assets.findIndex(a => a.id === assetId);
if (index !== -1) {
this.config.assets.splice(index, 1);
this.rebuildAssetMaps();
}
}
/**
* 通过ID获取资产
*/
getAssetById(assetId: string): AssetRegistry | undefined {
return this.assetMap.get(assetId);
}
/**
* 通过路径获取资产
*/
getAssetByPath(path: string): AssetRegistry | undefined {
return this.assetPathMap.get(path);
}
/**
* 获取所有资产
*/
getAllAssets(): AssetRegistry[] {
return this.config?.assets || [];
}
/**
* 按类型获取资产
*/
getAssetsByType(type: AssetType): AssetRegistry[] {
return this.getAllAssets().filter(a => a.type === type);
}
/**
* 获取行为树资产列表
*/
getBehaviorTreeAssets(): AssetRegistry[] {
return this.getAssetsByType(AssetType.BehaviorTree);
}
/**
* 获取黑板资产列表
*/
getBlackboardAssets(): AssetRegistry[] {
return this.getAssetsByType(AssetType.Blackboard);
}
/**
* 获取资产的所有依赖(递归)
*/
getAssetDependencies(assetId: string, visited = new Set<string>()): AssetRegistry[] {
if (visited.has(assetId)) {
return [];
}
visited.add(assetId);
const asset = this.getAssetById(assetId);
if (!asset) {
return [];
}
const dependencies: AssetRegistry[] = [];
for (const depId of asset.dependencies) {
const depAsset = this.getAssetById(depId);
if (depAsset) {
dependencies.push(depAsset);
// 递归获取依赖的依赖
dependencies.push(...this.getAssetDependencies(depId, visited));
}
}
return dependencies;
}
/**
* 检测循环依赖
*
* @param assetId 要检查的资产ID
* @returns 如果存在循环依赖,返回循环路径;否则返回 null
*/
detectCircularDependency(assetId: string): string[] | null {
const visited = new Set<string>();
const path: string[] = [];
const dfs = (currentId: string): boolean => {
if (path.includes(currentId)) {
// 找到循环
path.push(currentId);
return true;
}
if (visited.has(currentId)) {
return false;
}
visited.add(currentId);
path.push(currentId);
const asset = this.getAssetById(currentId);
if (asset) {
for (const depId of asset.dependencies) {
if (dfs(depId)) {
return true;
}
}
}
path.pop();
return false;
};
return dfs(assetId) ? path : null;
}
/**
* 检查是否可以添加依赖(不会造成循环依赖)
*
* @param assetId 资产ID
* @param dependencyId 要添加的依赖ID
* @returns 是否可以安全添加
*/
canAddDependency(assetId: string, dependencyId: string): boolean {
const asset = this.getAssetById(assetId);
if (!asset) return false;
// 临时添加依赖
const originalDeps = [...asset.dependencies];
asset.dependencies.push(dependencyId);
// 检测循环依赖
const hasCircular = this.detectCircularDependency(assetId) !== null;
// 恢复原始依赖
asset.dependencies = originalDeps;
return !hasCircular;
}
/**
* 添加资产依赖
*/
addAssetDependency(assetId: string, dependencyId: string): boolean {
if (!this.canAddDependency(assetId, dependencyId)) {
return false;
}
const asset = this.getAssetById(assetId);
if (!asset) return false;
if (!asset.dependencies.includes(dependencyId)) {
asset.dependencies.push(dependencyId);
}
return true;
}
/**
* 移除资产依赖
*/
removeAssetDependency(assetId: string, dependencyId: string): void {
const asset = this.getAssetById(assetId);
if (!asset) return;
const index = asset.dependencies.indexOf(dependencyId);
if (index !== -1) {
asset.dependencies.splice(index, 1);
}
}
/**
* 解析资产路径(支持相对路径和绝对路径)
*/
resolveAssetPath(path: string): string {
if (!this.config) return path;
// 如果是绝对路径,直接返回
if (path.startsWith('/') || path.match(/^[A-Za-z]:/)) {
return path;
}
// 相对路径,拼接工作区根目录
return `${this.config.rootPath}/${path}`.replace(/\\/g, '/');
}
/**
* 获取资产的相对路径
*/
getRelativePath(absolutePath: string): string {
if (!this.config) return absolutePath;
const rootPath = this.config.rootPath.replace(/\\/g, '/');
const absPath = absolutePath.replace(/\\/g, '/');
if (absPath.startsWith(rootPath)) {
return absPath.substring(rootPath.length + 1);
}
return absolutePath;
}
/**
* 清理资源
*/
dispose(): void {
this.config = null;
this.assetMap.clear();
this.assetPathMap.clear();
}
}

View File

@@ -1,704 +0,0 @@
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
import { CompositeNodeComponent } from '../Components/CompositeNodeComponent';
import { ActiveNode } from '../Components/ActiveNode';
import { BlackboardComponent } from '../Components/BlackboardComponent';
import { TaskStatus, NodeType, CompositeType, AbortType } from '../Types/TaskStatus';
import { SequenceNode } from '../Components/Composites/SequenceNode';
import { SelectorNode } from '../Components/Composites/SelectorNode';
import { RootNode } from '../Components/Composites/RootNode';
import { SubTreeNode } from '../Components/Composites/SubTreeNode';
import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition';
import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition';
import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition';
import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition';
/**
* 复合节点执行系统
*
* 负责处理所有活跃的复合节点
* 读取子节点状态,根据复合规则决定自己的状态和激活哪些子节点
*
* updateOrder: 300 (在叶子节点和装饰器之后执行)
*/
export class CompositeExecutionSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode).exclude(RootNode, SubTreeNode));
this.updateOrder = 300;
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const node = entity.getComponent(BehaviorTreeNode)!;
// 只处理复合节点
if (node.nodeType !== NodeType.Composite) {
continue;
}
// 使用 getComponentByType 支持继承查找
const composite = entity.getComponentByType(CompositeNodeComponent);
if (!composite) {
this.logger.warn(`复合节点 ${entity.name} 没有找到复合节点组件`);
const components = entity.components.map(c => c.constructor.name).join(', ');
this.logger.warn(` 组件列表: ${components}`);
continue;
}
this.executeComposite(entity, node, composite);
}
}
/**
* 执行复合节点逻辑
*/
private executeComposite(entity: Entity, node: BehaviorTreeNode, composite: CompositeNodeComponent): void {
const children = entity.children;
if (children.length === 0) {
node.status = TaskStatus.Success;
this.completeNode(entity);
return;
}
// 根据复合节点类型处理
switch (composite.compositeType) {
case CompositeType.Sequence:
this.handleSequence(entity, node, children);
break;
case CompositeType.Selector:
this.handleSelector(entity, node, children);
break;
case CompositeType.Parallel:
this.handleParallel(entity, node, children);
break;
case CompositeType.ParallelSelector:
this.handleParallelSelector(entity, node, children);
break;
case CompositeType.RandomSequence:
this.handleRandomSequence(entity, node, composite, children);
break;
case CompositeType.RandomSelector:
this.handleRandomSelector(entity, node, composite, children);
break;
default:
node.status = TaskStatus.Failure;
this.completeNode(entity);
break;
}
}
/**
* 序列节点:所有子节点都成功才成功
*/
private handleSequence(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
// 检查是否需要中止
const sequenceNode = entity.getComponentByType(SequenceNode);
if (sequenceNode && sequenceNode.abortType !== AbortType.None) {
if (this.shouldAbort(entity, node, children, sequenceNode.abortType)) {
this.abortExecution(entity, node, children);
return;
}
}
// 检查当前子节点
if (node.currentChildIndex >= children.length) {
// 所有子节点都成功
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
this.completeNode(entity);
return;
}
const currentChild = children[node.currentChildIndex];
const childNode = currentChild.getComponent(BehaviorTreeNode);
if (!childNode) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 如果子节点还没开始执行,激活它
if (childNode.status === TaskStatus.Invalid) {
if (!currentChild.hasComponent(ActiveNode)) {
currentChild.addComponent(new ActiveNode());
}
node.status = TaskStatus.Running;
return;
}
// 检查子节点状态
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else if (childNode.status === TaskStatus.Failure) {
// 任一失败则失败
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
this.completeNode(entity);
} else if (childNode.status === TaskStatus.Success) {
// 成功则移动到下一个子节点
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
childNode.reset();
node.currentChildIndex++;
// 继续保持活跃,下一帧处理下一个子节点
node.status = TaskStatus.Running;
}
}
/**
* 选择器节点:任一子节点成功就成功
*/
private handleSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
// 检查是否需要中止
const selectorNode = entity.getComponentByType(SelectorNode);
if (selectorNode && selectorNode.abortType !== AbortType.None) {
if (this.shouldAbort(entity, node, children, selectorNode.abortType)) {
this.abortExecution(entity, node, children);
return;
}
}
// 检查当前子节点
if (node.currentChildIndex >= children.length) {
// 所有子节点都失败
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
this.completeNode(entity);
return;
}
const currentChild = children[node.currentChildIndex];
const childNode = currentChild.getComponent(BehaviorTreeNode);
if (!childNode) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 如果子节点还没开始执行,激活它
if (childNode.status === TaskStatus.Invalid) {
if (!currentChild.hasComponent(ActiveNode)) {
currentChild.addComponent(new ActiveNode());
}
node.status = TaskStatus.Running;
return;
}
// 检查子节点状态
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else if (childNode.status === TaskStatus.Success) {
// 任一成功则成功
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
this.completeNode(entity);
} else if (childNode.status === TaskStatus.Failure) {
// 失败则移动到下一个子节点
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
childNode.reset();
node.currentChildIndex++;
// 继续保持活跃,下一帧处理下一个子节点
node.status = TaskStatus.Running;
}
}
/**
* 并行节点:所有子节点都执行,全部成功才成功
*/
private handleParallel(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
let hasRunning = false;
let hasFailed = false;
// 激活所有子节点
for (const child of children) {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
}
const childNode = child.getComponent(BehaviorTreeNode);
if (!childNode) continue;
if (childNode.status === TaskStatus.Running) {
hasRunning = true;
} else if (childNode.status === TaskStatus.Failure) {
hasFailed = true;
}
}
if (hasRunning) {
node.status = TaskStatus.Running;
} else if (hasFailed) {
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
this.completeNode(entity);
} else {
// 所有子节点都成功
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
this.completeNode(entity);
}
}
/**
* 并行选择器:任一成功则成功
*/
private handleParallelSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
let hasRunning = false;
let hasSucceeded = false;
// 激活所有子节点
for (const child of children) {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
}
const childNode = child.getComponent(BehaviorTreeNode);
if (!childNode) continue;
if (childNode.status === TaskStatus.Running) {
hasRunning = true;
} else if (childNode.status === TaskStatus.Success) {
hasSucceeded = true;
}
}
if (hasSucceeded) {
// 任一成功则成功
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
// 停止所有子节点
for (const child of children) {
child.removeComponentByType(ActiveNode);
}
this.completeNode(entity);
} else if (hasRunning) {
node.status = TaskStatus.Running;
} else {
// 所有子节点都失败
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
this.completeNode(entity);
}
}
/**
* 随机序列
*/
private handleRandomSequence(
entity: Entity,
node: BehaviorTreeNode,
composite: CompositeNodeComponent,
children: readonly Entity[]
): void {
// 获取洗牌后的子节点索引
const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length);
if (childIndex >= children.length) {
// 所有子节点都成功
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
composite.resetShuffle();
this.completeNode(entity);
return;
}
const currentChild = children[childIndex];
const childNode = currentChild.getComponent(BehaviorTreeNode);
if (!childNode) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 如果子节点还没开始执行,激活它
if (childNode.status === TaskStatus.Invalid) {
if (!currentChild.hasComponent(ActiveNode)) {
currentChild.addComponent(new ActiveNode());
}
node.status = TaskStatus.Running;
return;
}
// 检查子节点状态
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else if (childNode.status === TaskStatus.Failure) {
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
composite.resetShuffle();
this.completeNode(entity);
} else if (childNode.status === TaskStatus.Success) {
// 成功则移动到下一个子节点
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
childNode.reset();
node.currentChildIndex++;
node.status = TaskStatus.Running;
}
}
/**
* 随机选择器
*/
private handleRandomSelector(
entity: Entity,
node: BehaviorTreeNode,
composite: CompositeNodeComponent,
children: readonly Entity[]
): void {
// 获取洗牌后的子节点索引
const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length);
if (childIndex >= children.length) {
// 所有子节点都失败
node.status = TaskStatus.Failure;
node.currentChildIndex = 0; // 只重置索引保持状态为Failure
composite.resetShuffle();
this.completeNode(entity);
return;
}
const currentChild = children[childIndex];
const childNode = currentChild.getComponent(BehaviorTreeNode);
if (!childNode) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 如果子节点还没开始执行,激活它
if (childNode.status === TaskStatus.Invalid) {
if (!currentChild.hasComponent(ActiveNode)) {
currentChild.addComponent(new ActiveNode());
}
node.status = TaskStatus.Running;
return;
}
// 检查子节点状态
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else if (childNode.status === TaskStatus.Success) {
node.status = TaskStatus.Success;
node.currentChildIndex = 0; // 只重置索引保持状态为Success
composite.resetShuffle();
this.completeNode(entity);
} else if (childNode.status === TaskStatus.Failure) {
// 失败则移动到下一个子节点
// 重置已完成的子节点状态,以便下次行为树重新执行时从头开始
childNode.reset();
node.currentChildIndex++;
node.status = TaskStatus.Running;
}
}
/**
* 检查是否应该中止当前执行
*/
private shouldAbort(
entity: Entity,
node: BehaviorTreeNode,
children: readonly Entity[],
abortType: AbortType
): boolean {
const currentIndex = node.currentChildIndex;
// 如果还没开始执行任何子节点,不需要中止
if (currentIndex === 0) {
return false;
}
// Self: 检查当前执行路径中的条件节点是否失败
if (abortType === AbortType.Self || abortType === AbortType.Both) {
// 检查当前正在执行的分支之前的条件节点
for (let i = 0; i < currentIndex; i++) {
const child = children[i];
const childNode = child.getComponent(BehaviorTreeNode);
if (childNode && childNode.nodeType === NodeType.Condition) {
// 如果条件节点现在失败了,应该中止
if (childNode.status === TaskStatus.Failure) {
return true;
}
}
}
}
// LowerPriority: 检查高优先级分支的条件是否满足
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
// 检查当前索引之前的所有分支(优先级更高)
for (let i = 0; i < currentIndex; i++) {
const child = children[i];
const childNode = child.getComponent(BehaviorTreeNode);
if (!childNode) continue;
// 如果是条件节点且现在成功了
if (childNode.nodeType === NodeType.Condition) {
if (this.evaluateCondition(child, childNode)) {
return true;
}
}
// 如果是复合节点,检查其第一个子节点(通常是条件)
else if (childNode.nodeType === NodeType.Composite && child.children.length > 0) {
const firstGrandChild = child.children[0];
const firstGrandChildNode = firstGrandChild.getComponent(BehaviorTreeNode);
if (firstGrandChildNode && firstGrandChildNode.nodeType === NodeType.Condition) {
if (this.evaluateCondition(firstGrandChild, firstGrandChildNode)) {
return true;
}
}
}
}
}
return false;
}
/**
* 评估条件节点
*/
private evaluateCondition(entity: Entity, node: BehaviorTreeNode): boolean {
if (node.nodeType !== NodeType.Condition) {
return false;
}
let result = false;
if (entity.hasComponent(BlackboardCompareCondition)) {
result = this.evaluateBlackboardCompare(entity);
} else if (entity.hasComponent(BlackboardExistsCondition)) {
result = this.evaluateBlackboardExists(entity);
} else if (entity.hasComponent(RandomProbabilityCondition)) {
result = this.evaluateRandomProbability(entity);
} else if (entity.hasComponent(ExecuteCondition)) {
result = this.evaluateCustomCondition(entity);
}
return result;
}
/**
* 评估黑板比较条件
*/
private evaluateBlackboardCompare(entity: Entity): boolean {
const condition = entity.getComponent(BlackboardCompareCondition)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard || !blackboard.hasVariable(condition.variableName)) {
return false;
}
const value = blackboard.getValue(condition.variableName);
let compareValue = condition.compareValue;
if (typeof compareValue === 'string') {
compareValue = this.resolveVariableReferences(compareValue, blackboard);
}
let result = false;
switch (condition.operator) {
case CompareOperator.Equal:
result = value === compareValue;
break;
case CompareOperator.NotEqual:
result = value !== compareValue;
break;
case CompareOperator.Greater:
result = value > compareValue;
break;
case CompareOperator.GreaterOrEqual:
result = value >= compareValue;
break;
case CompareOperator.Less:
result = value < compareValue;
break;
case CompareOperator.LessOrEqual:
result = value <= compareValue;
break;
case CompareOperator.Contains:
if (typeof value === 'string') {
result = value.includes(compareValue);
} else if (Array.isArray(value)) {
result = value.includes(compareValue);
}
break;
case CompareOperator.Matches:
if (typeof value === 'string' && typeof compareValue === 'string') {
const regex = new RegExp(compareValue);
result = regex.test(value);
}
break;
}
return condition.invertResult ? !result : result;
}
/**
* 评估黑板变量存在性
*/
private evaluateBlackboardExists(entity: Entity): boolean {
const condition = entity.getComponent(BlackboardExistsCondition)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard) {
return false;
}
let result = blackboard.hasVariable(condition.variableName);
if (result && condition.checkNotNull) {
const value = blackboard.getValue(condition.variableName);
result = value !== null && value !== undefined;
}
return condition.invertResult ? !result : result;
}
/**
* 评估随机概率
*/
private evaluateRandomProbability(entity: Entity): boolean {
const condition = entity.getComponent(RandomProbabilityCondition)!;
return condition.evaluate();
}
/**
* 评估自定义条件
*/
private evaluateCustomCondition(entity: Entity): boolean {
const condition = entity.getComponent(ExecuteCondition)!;
const func = condition.getFunction();
if (!func) {
return false;
}
const blackboard = this.findBlackboard(entity);
const result = func(entity, blackboard, 0);
return condition.invertResult ? !result : result;
}
/**
* 解析字符串中的变量引用
*/
private resolveVariableReferences(value: string, blackboard: BlackboardComponent): any {
const pureMatch = value.match(/^{{\s*(\w+)\s*}}$/);
if (pureMatch) {
const varName = pureMatch[1];
if (blackboard.hasVariable(varName)) {
return blackboard.getValue(varName);
}
return value;
}
return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
if (blackboard.hasVariable(varName)) {
const val = blackboard.getValue(varName);
return val !== undefined ? String(val) : match;
}
return match;
});
}
/**
* 查找黑板组件
*/
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
let current: Entity | null = entity;
while (current) {
const blackboard = current.getComponent(BlackboardComponent);
if (blackboard) {
return blackboard;
}
current = current.parent;
}
return undefined;
}
/**
* 中止当前执行
*/
private abortExecution(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void {
// 停止当前正在执行的子节点
const currentIndex = node.currentChildIndex;
if (currentIndex < children.length) {
const currentChild = children[currentIndex];
this.deactivateNode(currentChild);
}
// 重置节点状态,从头开始
node.currentChildIndex = 0;
node.status = TaskStatus.Running;
// 不需要 completeNode因为我们要继续执行从头开始
}
/**
* 递归停用节点及其所有子节点
*/
private deactivateNode(entity: Entity): void {
// 移除活跃标记
entity.removeComponentByType(ActiveNode);
// 重置节点状态
const node = entity.getComponent(BehaviorTreeNode);
if (node) {
node.reset();
}
// 递归停用所有子节点
for (const child of entity.children) {
this.deactivateNode(child);
}
}
/**
* 递归重置所有子节点的状态
*/
private resetAllChildren(entity: Entity): void {
for (const child of entity.children) {
const childNode = child.getComponent(BehaviorTreeNode);
if (childNode) {
childNode.reset();
}
// 递归重置孙子节点
this.resetAllChildren(child);
}
}
/**
* 完成节点执行
*/
private completeNode(entity: Entity): void {
entity.removeComponentByType(ActiveNode);
// 如果是复合节点完成,重置所有子节点状态
const node = entity.getComponent(BehaviorTreeNode);
if (node && node.nodeType === NodeType.Composite) {
this.resetAllChildren(entity);
}
// 通知父节点
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
if (!entity.parent.hasComponent(ActiveNode)) {
entity.parent.addComponent(new ActiveNode());
}
}
}
protected override getLoggerName(): string {
return 'CompositeExecutionSystem';
}
}

View File

@@ -1,515 +0,0 @@
import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
import { DecoratorNodeComponent } from '../Components/DecoratorNodeComponent';
import { BlackboardComponent } from '../Components/BlackboardComponent';
import { ActiveNode } from '../Components/ActiveNode';
import { PropertyBindings } from '../Components/PropertyBindings';
import { LogOutput } from '../Components/LogOutput';
import { TaskStatus, NodeType, DecoratorType } from '../Types/TaskStatus';
import { RepeaterNode } from '../Components/Decorators/RepeaterNode';
import { ConditionalNode } from '../Components/Decorators/ConditionalNode';
import { CooldownNode } from '../Components/Decorators/CooldownNode';
import { TimeoutNode } from '../Components/Decorators/TimeoutNode';
/**
* 装饰器节点执行系统
*
* 负责处理所有活跃的装饰器节点
* 读取子节点状态,根据装饰器规则决定自己的状态
*
* updateOrder: 200 (在叶子节点之后执行)
*/
export class DecoratorExecutionSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode));
this.updateOrder = 200;
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const node = entity.getComponent(BehaviorTreeNode)!;
// 只处理装饰器节点
if (node.nodeType !== NodeType.Decorator) {
continue;
}
// 使用 getComponentByType 支持继承查找
const decorator = entity.getComponentByType(DecoratorNodeComponent);
if (!decorator) {
continue;
}
this.executeDecorator(entity, node, decorator);
}
}
/**
* 执行装饰器逻辑
*/
private executeDecorator(entity: Entity, node: BehaviorTreeNode, decorator: DecoratorNodeComponent): void {
const children = entity.children;
if (children.length === 0) {
this.logger.warn('装饰器节点没有子节点');
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
const child = children[0]; // 装饰器只有一个子节点
const childNode = child.getComponent(BehaviorTreeNode);
if (!childNode) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 根据装饰器类型处理
switch (decorator.decoratorType) {
case DecoratorType.Inverter:
this.handleInverter(entity, node, child, childNode);
break;
case DecoratorType.Repeater:
this.handleRepeater(entity, node, decorator, child, childNode);
break;
case DecoratorType.UntilSuccess:
this.handleUntilSuccess(entity, node, child, childNode);
break;
case DecoratorType.UntilFail:
this.handleUntilFail(entity, node, child, childNode);
break;
case DecoratorType.AlwaysSucceed:
this.handleAlwaysSucceed(entity, node, child, childNode);
break;
case DecoratorType.AlwaysFail:
this.handleAlwaysFail(entity, node, child, childNode);
break;
case DecoratorType.Conditional:
this.handleConditional(entity, node, decorator, child, childNode);
break;
case DecoratorType.Cooldown:
this.handleCooldown(entity, node, decorator, child, childNode);
break;
case DecoratorType.Timeout:
this.handleTimeout(entity, node, decorator, child, childNode);
break;
default:
node.status = TaskStatus.Failure;
this.completeNode(entity);
break;
}
}
/**
* 反转装饰器
*/
private handleInverter(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
if (!child.hasComponent(ActiveNode)) {
// 子节点未激活,激活它
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
} else {
// 子节点正在执行
node.status = TaskStatus.Running;
}
// 如果子节点完成了
if (childNode.status === TaskStatus.Success || childNode.status === TaskStatus.Failure) {
// 反转结果
node.status = childNode.status === TaskStatus.Success ? TaskStatus.Failure : TaskStatus.Success;
this.completeNode(entity);
}
}
/**
* 重复装饰器
*/
private handleRepeater(
entity: Entity,
node: BehaviorTreeNode,
decorator: DecoratorNodeComponent,
child: Entity,
childNode: BehaviorTreeNode
): void {
const repeater = decorator as RepeaterNode;
// 从 PropertyBindings 读取绑定的黑板变量值
const repeatCount = this.resolvePropertyValue(entity, 'repeatCount', repeater.repeatCount);
const endOnFailure = this.resolvePropertyValue(entity, 'endOnFailure', repeater.endOnFailure);
// 如果子节点未激活,激活它
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
// 子节点正在执行
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
return;
}
// 子节点完成
if (childNode.status === TaskStatus.Failure && endOnFailure) {
node.status = TaskStatus.Failure;
repeater.reset();
this.completeNode(entity);
return;
}
// 增加重复计数
repeater.incrementRepeat();
// 检查是否继续重复(使用解析后的值)
const shouldContinue = (repeatCount === -1) || (repeater.currentRepeatCount < repeatCount);
if (shouldContinue) {
// 重置子节点并继续
childNode.invalidate();
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
} else {
// 完成
node.status = TaskStatus.Success;
repeater.reset();
this.completeNode(entity);
}
}
/**
* 直到成功装饰器
*/
private handleUntilSuccess(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Success) {
node.status = TaskStatus.Success;
this.completeNode(entity);
} else {
// 失败则重试
childNode.invalidate();
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
}
}
/**
* 直到失败装饰器
*/
private handleUntilFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Failure) {
node.status = TaskStatus.Success;
this.completeNode(entity);
} else {
// 成功则重试
childNode.invalidate();
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
}
}
/**
* 总是成功装饰器
*/
private handleAlwaysSucceed(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else {
node.status = TaskStatus.Success;
this.completeNode(entity);
}
}
/**
* 总是失败装饰器
*/
private handleAlwaysFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void {
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
if (childNode.status === TaskStatus.Running) {
node.status = TaskStatus.Running;
} else {
node.status = TaskStatus.Failure;
this.completeNode(entity);
}
}
/**
* 条件装饰器
*/
private handleConditional(
entity: Entity,
node: BehaviorTreeNode,
decorator: DecoratorNodeComponent,
child: Entity,
childNode: BehaviorTreeNode
): void {
const conditional = decorator as ConditionalNode;
// 评估条件
const conditionMet = conditional.evaluateCondition(entity, this.findBlackboard(entity));
if (!conditionMet) {
// 条件不满足,直接失败
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 条件满足,执行子节点
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
node.status = childNode.status;
if (childNode.status !== TaskStatus.Running) {
this.completeNode(entity);
}
}
/**
* 冷却装饰器
*/
private handleCooldown(
entity: Entity,
node: BehaviorTreeNode,
decorator: DecoratorNodeComponent,
child: Entity,
childNode: BehaviorTreeNode
): void {
const cooldown = decorator as CooldownNode;
// 从 PropertyBindings 读取绑定的黑板变量值
const cooldownTime = this.resolvePropertyValue(entity, 'cooldownTime', cooldown.cooldownTime);
// 检查冷却(使用解析后的值)
// 如果从未执行过lastExecutionTime === 0允许执行
const timeSinceLastExecution = Time.totalTime - cooldown.lastExecutionTime;
const canExecute = (cooldown.lastExecutionTime === 0) || (timeSinceLastExecution >= cooldownTime);
// 添加调试日志
this.outputLog(
entity,
`[冷却检查] Time.totalTime=${Time.totalTime.toFixed(3)}, lastExecution=${cooldown.lastExecutionTime.toFixed(3)}, ` +
`cooldownTime=${cooldownTime}, timeSince=${timeSinceLastExecution.toFixed(3)}, canExecute=${canExecute}, childStatus=${childNode.status}`,
'info'
);
if (!canExecute) {
node.status = TaskStatus.Failure;
this.completeNode(entity);
return;
}
// 先检查子节点状态,再决定是否激活
if (childNode.status !== TaskStatus.Invalid && childNode.status !== TaskStatus.Running) {
// 子节点已经完成Success 或 Failure
node.status = childNode.status;
cooldown.recordExecution(Time.totalTime);
this.outputLog(
entity,
`[冷却记录] 记录执行时间: ${Time.totalTime.toFixed(3)}, 下次可执行时间: ${(Time.totalTime + cooldownTime).toFixed(3)}`,
'info'
);
this.completeNode(entity);
return;
}
// 子节点还没开始或正在执行
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
node.status = TaskStatus.Running;
}
/**
* 超时装饰器
*/
private handleTimeout(
entity: Entity,
node: BehaviorTreeNode,
decorator: DecoratorNodeComponent,
child: Entity,
childNode: BehaviorTreeNode
): void {
const timeout = decorator as TimeoutNode;
// 从 PropertyBindings 读取绑定的黑板变量值
const timeoutDuration = this.resolvePropertyValue(entity, 'timeoutDuration', timeout.timeoutDuration);
timeout.recordStartTime(Time.totalTime);
// 检查超时(使用解析后的值)
const isTimeout = timeout.startTime > 0 && (Time.totalTime - timeout.startTime >= timeoutDuration);
if (isTimeout) {
node.status = TaskStatus.Failure;
timeout.reset();
// 移除子节点的活跃标记
child.removeComponentByType(ActiveNode);
this.completeNode(entity);
return;
}
if (!child.hasComponent(ActiveNode)) {
child.addComponent(new ActiveNode());
node.status = TaskStatus.Running;
return;
}
node.status = childNode.status;
if (childNode.status !== TaskStatus.Running) {
timeout.reset();
this.completeNode(entity);
}
}
/**
* 完成节点执行
*/
private completeNode(entity: Entity): void {
entity.removeComponentByType(ActiveNode);
// 通知父节点
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
if (!entity.parent.hasComponent(ActiveNode)) {
entity.parent.addComponent(new ActiveNode());
}
}
}
/**
* 查找黑板组件(向上遍历父节点)
*/
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
let current: Entity | null = entity;
while (current) {
const blackboard = current.getComponent(BlackboardComponent);
if (blackboard) {
return blackboard;
}
current = current.parent;
}
return undefined;
}
/**
* 解析属性值
* 如果属性绑定到黑板变量,从黑板读取最新值
*/
private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any {
const bindings = entity.getComponent(PropertyBindings);
if (!bindings || !bindings.hasBinding(propertyName)) {
return defaultValue;
}
const blackboardKey = bindings.getBinding(propertyName)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard || !blackboard.hasVariable(blackboardKey)) {
return defaultValue;
}
return blackboard.getValue(blackboardKey);
}
/**
* 查找根实体
*/
private findRootEntity(entity: Entity): Entity | null {
let current: Entity | null = entity;
while (current) {
if (!current.parent) {
return current;
}
current = current.parent;
}
return null;
}
/**
* 统一的日志输出方法
* 同时输出到控制台和LogOutput组件确保用户在UI中能看到
*/
private outputLog(
entity: Entity,
message: string,
level: 'log' | 'info' | 'warn' | 'error' = 'info'
): void {
switch (level) {
case 'info':
this.logger.info(message);
break;
case 'warn':
this.logger.warn(message);
break;
case 'error':
this.logger.error(message);
break;
default:
this.logger.info(message);
break;
}
const rootEntity = this.findRootEntity(entity);
if (rootEntity) {
const logOutput = rootEntity.getComponent(LogOutput);
if (logOutput) {
logOutput.addMessage(message, level);
}
}
}
protected override getLoggerName(): string {
return 'DecoratorExecutionSystem';
}
}

View File

@@ -1,603 +0,0 @@
import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
import { BlackboardComponent } from '../Components/BlackboardComponent';
import { ActiveNode } from '../Components/ActiveNode';
import { PropertyBindings } from '../Components/PropertyBindings';
import { LogOutput } from '../Components/LogOutput';
import { TaskStatus, NodeType } from '../Types/TaskStatus';
// 导入具体的动作组件
import { WaitAction } from '../Components/Actions/WaitAction';
import { LogAction } from '../Components/Actions/LogAction';
import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction';
import { ModifyBlackboardValueAction, ModifyOperation } from '../Components/Actions/ModifyBlackboardValueAction';
import { ExecuteAction } from '../Components/Actions/ExecuteAction';
// 导入具体的条件组件
import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition';
import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition';
import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition';
import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition';
/**
* 叶子节点执行系统
*
* 负责执行所有活跃的叶子节点Action 和 Condition
* 只处理带有 ActiveNode 标记的节点
*
* updateOrder: 100 (最先执行)
*/
export class LeafExecutionSystem extends EntitySystem {
constructor() {
// 只处理活跃的叶子节点
super(Matcher.empty().all(BehaviorTreeNode, ActiveNode));
this.updateOrder = 100;
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const node = entity.getComponent(BehaviorTreeNode)!;
// 只处理叶子节点
if (node.nodeType === NodeType.Action) {
this.executeAction(entity, node);
} else if (node.nodeType === NodeType.Condition) {
this.executeCondition(entity, node);
}
}
}
/**
* 执行动作节点
*/
private executeAction(entity: Entity, node: BehaviorTreeNode): void {
let status = TaskStatus.Failure;
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
// 检测实体有哪些动作组件并执行
if (entity.hasComponent(WaitAction)) {
status = this.executeWaitAction(entity);
} else if (entity.hasComponent(LogAction)) {
status = this.executeLogAction(entity);
} else if (entity.hasComponent(SetBlackboardValueAction)) {
status = this.executeSetBlackboardValue(entity);
} else if (entity.hasComponent(ModifyBlackboardValueAction)) {
status = this.executeModifyBlackboardValue(entity);
} else if (entity.hasComponent(ExecuteAction)) {
status = this.executeCustomAction(entity);
} else {
status = this.executeGenericAction(entity);
if (status === TaskStatus.Failure) {
this.outputLog(entity, `动作节点没有找到任何已知的动作组件`, 'warn');
}
}
node.status = status;
// 输出节点执行后的状态
const statusText = status === TaskStatus.Success ? 'Success' :
status === TaskStatus.Failure ? 'Failure' :
status === TaskStatus.Running ? 'Running' : 'Unknown';
if (status !== TaskStatus.Running) {
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 执行完成 -> ${statusText}`,
status === TaskStatus.Success ? 'info' : 'warn');
}
// 如果不是 Running 状态,节点执行完成
if (status !== TaskStatus.Running) {
this.deactivateNode(entity);
this.notifyParent(entity);
}
}
/**
* 执行等待动作
*/
private executeWaitAction(entity: Entity): TaskStatus {
const waitAction = entity.getComponent(WaitAction)!;
const node = entity.getComponent(BehaviorTreeNode);
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
// 从 PropertyBindings 读取绑定的黑板变量值
const waitTime = this.resolvePropertyValue(entity, 'waitTime', waitAction.waitTime);
waitAction.elapsedTime += Time.deltaTime;
// 输出调试信息显示在UI中
this.outputLog(
entity,
`[${displayName}#${nodeIdShort}] deltaTime=${Time.deltaTime.toFixed(3)}s, ` +
`elapsed=${waitAction.elapsedTime.toFixed(3)}s/${waitTime.toFixed(3)}s`,
'info'
);
if (waitAction.elapsedTime >= waitTime) {
waitAction.reset();
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 等待完成,返回成功`, 'info');
return TaskStatus.Success;
}
return TaskStatus.Running;
}
/**
* 执行日志动作
*/
private executeLogAction(entity: Entity): TaskStatus {
const logAction = entity.getComponent(LogAction)!;
const node = entity.getComponent(BehaviorTreeNode);
// 从 PropertyBindings 读取绑定的黑板变量值
let message = this.resolvePropertyValue(entity, 'message', logAction.message);
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
// 在消息前添加节点ID信息
if (node) {
message = `[${displayName}#${nodeIdShort}] ${message}`;
}
if (logAction.includeEntityInfo) {
message = `[Entity: ${entity.name}] ${message}`;
}
// 输出到浏览器控制台
switch (logAction.level) {
case 'info':
console.info(message);
break;
case 'warn':
console.warn(message);
break;
case 'error':
console.error(message);
break;
default:
console.log(message);
break;
}
// 同时记录到LogOutput组件以便在UI中显示
const rootEntity = this.findRootEntity(entity);
if (rootEntity) {
const logOutput = rootEntity.getComponent(LogOutput);
if (logOutput) {
logOutput.addMessage(message, logAction.level);
}
}
return TaskStatus.Success;
}
/**
* 查找根实体
*/
private findRootEntity(entity: Entity): Entity | null {
let current: Entity | null = entity;
while (current) {
if (!current.parent) {
return current;
}
current = current.parent;
}
return null;
}
/**
* 执行设置黑板变量值
*/
private executeSetBlackboardValue(entity: Entity): TaskStatus {
const action = entity.getComponent(SetBlackboardValueAction)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard) {
this.outputLog(entity, '未找到黑板组件', 'warn');
return TaskStatus.Failure;
}
let valueToSet: any;
// 如果指定了源变量,从中读取值
if (action.sourceVariable) {
if (!blackboard.hasVariable(action.sourceVariable)) {
this.outputLog(entity, `源变量不存在: ${action.sourceVariable}`, 'warn');
return TaskStatus.Failure;
}
valueToSet = blackboard.getValue(action.sourceVariable);
} else {
// 从 PropertyBindings 读取绑定的值
valueToSet = this.resolvePropertyValue(entity, 'value', action.value);
}
const success = blackboard.setValue(action.variableName, valueToSet, action.force);
return success ? TaskStatus.Success : TaskStatus.Failure;
}
/**
* 执行修改黑板变量值
*/
private executeModifyBlackboardValue(entity: Entity): TaskStatus {
const action = entity.getComponent(ModifyBlackboardValueAction)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard) {
this.outputLog(entity, '未找到黑板组件', 'warn');
return TaskStatus.Failure;
}
if (!blackboard.hasVariable(action.variableName)) {
this.outputLog(entity, `变量不存在: ${action.variableName}`, 'warn');
return TaskStatus.Failure;
}
let currentValue = blackboard.getValue(action.variableName);
// 从 PropertyBindings 读取绑定的值
let operand = this.resolvePropertyValue(entity, 'operand', action.operand);
// 执行操作
let newValue: any;
switch (action.operation) {
case ModifyOperation.Add:
newValue = Number(currentValue) + Number(operand);
break;
case ModifyOperation.Subtract:
newValue = Number(currentValue) - Number(operand);
break;
case ModifyOperation.Multiply:
newValue = Number(currentValue) * Number(operand);
break;
case ModifyOperation.Divide:
if (Number(operand) === 0) {
this.outputLog(entity, '除数不能为0', 'warn');
return TaskStatus.Failure;
}
newValue = Number(currentValue) / Number(operand);
break;
case ModifyOperation.Modulo:
newValue = Number(currentValue) % Number(operand);
break;
case ModifyOperation.Append:
if (Array.isArray(currentValue)) {
newValue = [...currentValue, operand];
} else if (typeof currentValue === 'string') {
newValue = currentValue + operand;
} else {
this.outputLog(entity, `变量 ${action.variableName} 不支持 append 操作`, 'warn');
return TaskStatus.Failure;
}
break;
case ModifyOperation.Remove:
if (Array.isArray(currentValue)) {
newValue = currentValue.filter(item => item !== operand);
} else {
this.outputLog(entity, `变量 ${action.variableName} 不是数组,不支持 remove 操作`, 'warn');
return TaskStatus.Failure;
}
break;
default:
return TaskStatus.Failure;
}
const success = blackboard.setValue(action.variableName, newValue, action.force);
return success ? TaskStatus.Success : TaskStatus.Failure;
}
/**
* 执行自定义动作
*/
private executeCustomAction(entity: Entity): TaskStatus {
const action = entity.getComponent(ExecuteAction)!;
const func = action.getFunction();
if (!func) {
return TaskStatus.Failure;
}
const blackboard = this.findBlackboard(entity);
return func(entity, blackboard, Time.deltaTime);
}
/**
* 执行通用动作组件
* 查找实体上具有 execute 方法的自定义组件并执行
*/
private executeGenericAction(entity: Entity): TaskStatus {
for (const component of entity.components) {
if (component instanceof BehaviorTreeNode ||
component instanceof ActiveNode ||
component instanceof BlackboardComponent ||
component instanceof PropertyBindings ||
component instanceof LogOutput) {
continue;
}
if (typeof (component as any).execute === 'function') {
try {
const blackboard = this.findBlackboard(entity);
const status = (component as any).execute(entity, blackboard);
if (typeof status === 'number' &&
(status === TaskStatus.Success ||
status === TaskStatus.Failure ||
status === TaskStatus.Running)) {
return status;
}
} catch (error) {
this.outputLog(entity, `执行动作组件时发生错误: ${error}`, 'error');
return TaskStatus.Failure;
}
}
}
return TaskStatus.Failure;
}
/**
* 执行条件节点
*/
private executeCondition(entity: Entity, node: BehaviorTreeNode): void {
let result = false;
const { displayName, nodeIdShort } = this.getNodeInfo(entity);
// 检测实体有哪些条件组件并评估
if (entity.hasComponent(BlackboardCompareCondition)) {
result = this.evaluateBlackboardCompare(entity);
} else if (entity.hasComponent(BlackboardExistsCondition)) {
result = this.evaluateBlackboardExists(entity);
} else if (entity.hasComponent(RandomProbabilityCondition)) {
result = this.evaluateRandomProbability(entity);
} else if (entity.hasComponent(ExecuteCondition)) {
result = this.evaluateCustomCondition(entity);
} else {
this.outputLog(entity, '条件节点没有找到任何已知的条件组件', 'warn');
}
node.status = result ? TaskStatus.Success : TaskStatus.Failure;
// 输出条件评估结果
const statusText = result ? 'Success (true)' : 'Failure (false)';
this.outputLog(entity, `[${displayName}#${nodeIdShort}] 条件评估 -> ${statusText}`,
result ? 'info' : 'warn');
// 条件节点总是立即完成
this.deactivateNode(entity);
this.notifyParent(entity);
}
/**
* 评估黑板比较条件
*/
private evaluateBlackboardCompare(entity: Entity): boolean {
const condition = entity.getComponent(BlackboardCompareCondition)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard || !blackboard.hasVariable(condition.variableName)) {
return false;
}
const value = blackboard.getValue(condition.variableName);
// 从 PropertyBindings 读取绑定的值
let compareValue = this.resolvePropertyValue(entity, 'compareValue', condition.compareValue);
let result = false;
switch (condition.operator) {
case CompareOperator.Equal:
result = value === compareValue;
break;
case CompareOperator.NotEqual:
result = value !== compareValue;
break;
case CompareOperator.Greater:
result = value > compareValue;
break;
case CompareOperator.GreaterOrEqual:
result = value >= compareValue;
break;
case CompareOperator.Less:
result = value < compareValue;
break;
case CompareOperator.LessOrEqual:
result = value <= compareValue;
break;
case CompareOperator.Contains:
if (typeof value === 'string') {
result = value.includes(compareValue);
} else if (Array.isArray(value)) {
result = value.includes(compareValue);
}
break;
case CompareOperator.Matches:
if (typeof value === 'string' && typeof compareValue === 'string') {
const regex = new RegExp(compareValue);
result = regex.test(value);
}
break;
}
return condition.invertResult ? !result : result;
}
/**
* 评估黑板变量存在性
*/
private evaluateBlackboardExists(entity: Entity): boolean {
const condition = entity.getComponent(BlackboardExistsCondition)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard) {
return false;
}
let result = blackboard.hasVariable(condition.variableName);
if (result && condition.checkNotNull) {
const value = blackboard.getValue(condition.variableName);
result = value !== null && value !== undefined;
}
return condition.invertResult ? !result : result;
}
/**
* 评估随机概率
*/
private evaluateRandomProbability(entity: Entity): boolean {
const condition = entity.getComponent(RandomProbabilityCondition)!;
// 从 PropertyBindings 读取绑定的黑板变量值
const probability = this.resolvePropertyValue(entity, 'probability', condition.probability);
// 使用解析后的概率值进行评估
if (condition.alwaysRandomize || condition['cachedResult'] === undefined) {
condition['cachedResult'] = Math.random() < probability;
}
return condition['cachedResult'];
}
/**
* 评估自定义条件
*/
private evaluateCustomCondition(entity: Entity): boolean {
const condition = entity.getComponent(ExecuteCondition)!;
const func = condition.getFunction();
if (!func) {
return false;
}
const blackboard = this.findBlackboard(entity);
const result = func(entity, blackboard, Time.deltaTime);
return condition.invertResult ? !result : result;
}
/**
* 解析属性值
* 如果属性绑定到黑板变量,从黑板读取最新值
*/
private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any {
// 检查实体是否有 PropertyBindings 组件
const bindings = entity.getComponent(PropertyBindings);
if (!bindings || !bindings.hasBinding(propertyName)) {
// 没有绑定,返回默认值
return defaultValue;
}
// 有绑定,从黑板读取值
const blackboardKey = bindings.getBinding(propertyName)!;
const blackboard = this.findBlackboard(entity);
if (!blackboard) {
this.outputLog(entity, `[属性绑定] 未找到黑板组件,实体: ${entity.name}`, 'warn');
return defaultValue;
}
if (!blackboard.hasVariable(blackboardKey)) {
this.outputLog(entity, `[属性绑定] 黑板变量不存在: ${blackboardKey}`, 'warn');
return defaultValue;
}
const value = blackboard.getValue(blackboardKey);
return value;
}
/**
* 移除节点的活跃标记
*/
private deactivateNode(entity: Entity): void {
entity.removeComponentByType(ActiveNode);
}
/**
* 通知父节点子节点已完成
*/
private notifyParent(entity: Entity): void {
if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) {
// 为父节点添加活跃标记,让它在下一帧被处理
if (!entity.parent.hasComponent(ActiveNode)) {
entity.parent.addComponent(new ActiveNode());
}
}
}
/**
* 查找黑板组件(向上遍历父节点)
*/
private findBlackboard(entity: Entity): BlackboardComponent | undefined {
let current: Entity | null = entity;
while (current) {
const blackboard = current.getComponent(BlackboardComponent);
if (blackboard) {
return blackboard;
}
current = current.parent;
}
return undefined;
}
/**
* 从Entity提取节点显示名称和ID
*/
private getNodeInfo(entity: Entity): { displayName: string; nodeIdShort: string } {
let displayName = 'Node';
let nodeIdShort = '';
if (entity.name && entity.name.includes('#')) {
const parts = entity.name.split('#');
displayName = parts[0];
nodeIdShort = parts[1];
} else {
nodeIdShort = entity.id.toString().substring(0, 8);
}
return { displayName, nodeIdShort };
}
/**
* 统一的日志输出方法
* 同时输出到控制台和LogOutput组件确保用户在UI中能看到
*/
private outputLog(
entity: Entity,
message: string,
level: 'log' | 'info' | 'warn' | 'error' = 'info'
): void {
// 输出到浏览器控制台(方便开发调试)
switch (level) {
case 'info':
this.logger.info(message);
break;
case 'warn':
this.logger.warn(message);
break;
case 'error':
this.logger.error(message);
break;
default:
this.logger.info(message);
break;
}
// 输出到LogOutput组件显示在UI中
const rootEntity = this.findRootEntity(entity);
if (rootEntity) {
const logOutput = rootEntity.getComponent(LogOutput);
if (logOutput) {
logOutput.addMessage(message, level);
}
}
}
protected override getLoggerName(): string {
return 'LeafExecutionSystem';
}
}

Some files were not shown because too many files have changed in this diff Show More