diff --git a/docs/guide/behavior-tree/advanced-usage.md b/docs/guide/behavior-tree/advanced-usage.md index 6687dfcd..f190ca5a 100644 --- a/docs/guide/behavior-tree/advanced-usage.md +++ b/docs/guide/behavior-tree/advanced-usage.md @@ -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('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 = 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('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('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)学习可视化编辑 diff --git a/docs/guide/behavior-tree/asset-management.md b/docs/guide/behavior-tree/asset-management.md new file mode 100644 index 00000000..4a4a6ea0 --- /dev/null +++ b/docs/guide/behavior-tree/asset-management.md @@ -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('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)优化你的行为树设计 diff --git a/docs/guide/behavior-tree/best-practices.md b/docs/guide/behavior-tree/best-practices.md index 29740e6d..c116b135 100644 --- a/docs/guide/behavior-tree/best-practices.md +++ b/docs/guide/behavior-tree/best-practices.md @@ -1,22 +1,22 @@ # 最佳实践 -本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。 +本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。 ## 行为树设计原则 ### 1. 保持树的层次清晰 -将复杂行为分解成清晰的层次结构: +将复杂行为分解成清晰的层次结构: ``` Root Selector -├── Emergency (高优先级:紧急情况) +├── Emergency (高优先级:紧急情况) │ ├── FleeFromDanger │ └── CallForHelp -├── Combat (中优先级:战斗) +├── Combat (中优先级:战斗) │ ├── Attack │ └── Defend -└── Idle (低优先级:空闲) +└── Idle (低优先级:空闲) ├── Patrol └── Rest ``` @@ -24,309 +24,272 @@ Root Selector ### 2. 单一职责原则 -每个节点应该只做一件事: +每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。 ```typescript -// 好的设计 +// 好的设计 - 使用内置节点 .sequence('AttackSequence') - .condition(hasTarget, 'CheckTarget') - .action(aim, 'Aim') - .action(fire, 'Fire') + .blackboardExists('target', 'CheckTarget') + .log('瞄准', 'Aim') + .log('开火', 'Fire') .end() - -// 不好的设计 - 一个动作做太多事 -.action('AttackPlayer', () => { - checkTarget(); - aim(); - fire(); - playAnimation(); - playSound(); - // 太多职责了! -}) ``` ### 3. 使用描述性名称 -节点名称应该清楚地表达其功能: +节点名称应该清楚地表达其功能: ```typescript // 好的命名 -.condition(isHealthLow, 'CheckHealthLow') -.action(findNearestHealthPack, 'FindHealthPack') -.action(moveToHealthPack, 'MoveToHealthPack') +.blackboardCompare('health', 20, 'less', 'CheckHealthLow') +.log('寻找最近的医疗包', 'FindHealthPack') +.log('移动到医疗包', 'MoveToHealthPack') // 不好的命名 -.condition(check1, 'C1') -.action(doSomething, 'Action1') -.action(move, 'A2') +.blackboardCompare('health', 20, 'less', 'C1') +.log('Do something', 'Action1') +.log('Move', 'A2') ``` ## 黑板变量管理 ### 1. 变量命名规范 -使用清晰的命名约定: +使用清晰的命名约定: ```typescript -.blackboard() +const tree = BehaviorTreeBuilder.create('AI') // 状态变量 - .defineVariable('currentState', BlackboardValueType.String, 'idle') - .defineVariable('isMoving', BlackboardValueType.Boolean, false) + .defineBlackboardVariable('currentState', 'idle') + .defineBlackboardVariable('isMoving', false) // 目标和引用 - .defineVariable('targetEnemy', BlackboardValueType.Object, null) - .defineVariable('patrolPoints', BlackboardValueType.Array, []) + .defineBlackboardVariable('targetEnemy', null) + .defineBlackboardVariable('patrolPoints', []) // 配置参数 - .defineVariable('attackRange', BlackboardValueType.Number, 5.0) - .defineVariable('moveSpeed', BlackboardValueType.Number, 10.0) + .defineBlackboardVariable('attackRange', 5.0) + .defineBlackboardVariable('moveSpeed', 10.0) // 临时数据 - .defineVariable('lastAttackTime', BlackboardValueType.Number, 0) - .defineVariable('searchAttempts', BlackboardValueType.Number, 0) -.endBlackboard() + .defineBlackboardVariable('lastAttackTime', 0) + .defineBlackboardVariable('searchAttempts', 0) + // ... + .build(); ``` ### 2. 避免过度使用黑板 -只在需要跨节点共享的数据才放入黑板: +只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量: ```typescript -// 不好的做法 - 局部变量放黑板 -.action('Calculate', (e, bb) => { - bb?.setValue('temp1', 10); // 不需要 - bb?.setValue('temp2', 20); // 不需要 - bb?.setValue('result', 30); // 如果其他节点需要,这个可以 - return TaskStatus.Success; -}) - // 好的做法 - 使用局部变量 -.action('Calculate', (e, bb) => { - const temp1 = 10; - const temp2 = 20; - const result = temp1 + temp2; - bb?.setValue('calculationResult', result); // 只保存需要共享的结果 - return TaskStatus.Success; -}) +export class CalculateAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + // 局部计算 + const temp1 = 10; + const temp2 = 20; + const result = temp1 + temp2; + + // 只保存需要共享的结果 + context.runtime.setBlackboardValue('calculationResult', result); + + return TaskStatus.Success; + } +} ``` ### 3. 使用类型安全的访问 ```typescript -// 定义类型接口 -interface EnemyBlackboard { - health: number; - target: Entity | null; - state: 'idle' | 'patrol' | 'chase' | 'attack'; -} +export class TypeSafeAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const { runtime } = context; -// 使用时进行类型检查 -.action('UseBlackboard', (e, bb) => { - const health = bb?.getValue('health'); - const target = bb?.getValue('target'); - const state = bb?.getValue('state'); + // 使用泛型进行类型安全访问 + const health = runtime.getBlackboardValue('health'); + const target = runtime.getBlackboardValue('target'); + const state = runtime.getBlackboardValue('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(context, 'threshold', 30); + const health = context.runtime.getBlackboardValue('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)深入理解原理 diff --git a/docs/guide/behavior-tree/cocos-integration.md b/docs/guide/behavior-tree/cocos-integration.md index d99b3fe9..cc28a199 100644 --- a/docs/guide/behavior-tree/cocos-integration.md +++ b/docs/guide/behavior-tree/cocos-integration.md @@ -112,28 +112,23 @@ export class Main extends Component { import { _decorator, Component, Node } from 'cc'; import { Core, Entity } from '@esengine/ecs-framework'; import { - BehaviorTreeAssetSerializer, - BehaviorTreeAssetLoader, + BehaviorTreeBuilder, BehaviorTreeStarter, - BlackboardComponent + BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree'; -import { resources } from 'cc'; const { ccclass, property } = _decorator; @ccclass('EnemyAIComponent') export class EnemyAIComponent extends Component { - @property - behaviorTreeAsset: string = 'behaviors/enemy-ai.btree'; - private aiEntity: Entity | null = null; async start() { - // 加载行为树资产 - await this.loadBehaviorTree(); + // 创建行为树 + await this.createBehaviorTree(); } - private async loadBehaviorTree() { + private async createBehaviorTree() { try { // 获取Core管理的场景 const scene = Core.scene; @@ -142,41 +137,32 @@ export class EnemyAIComponent extends Component { return; } - // 从 resources 加载JSON资产 - resources.load(this.behaviorTreeAsset, (err, jsonAsset: any) => { - if (err) { - console.error('加载行为树失败:', err); - return; - } + // 使用Builder API创建行为树 + const tree = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('cocosNode', this.node) + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('playerNode', null) + .defineBlackboardVariable('detectionRange', 10) + .defineBlackboardVariable('attackRange', 2) + .selector('MainBehavior') + .sequence('Combat') + .blackboardExists('playerNode') + .blackboardCompare('health', 30, 'greater') + .log('攻击玩家', 'AttackPlayer') + .end() + .sequence('Flee') + .blackboardCompare('health', 30, 'lessOrEqual') + .log('逃跑', 'RunAway') + .end() + .log('巡逻', 'Patrol') + .end() + .build(); - // 获取JSON字符串 - const jsonString = jsonAsset.json ? JSON.stringify(jsonAsset.json) : jsonAsset.text; + // 创建AI实体并启动 + this.aiEntity = scene.createEntity(`AI_${this.node.name}`); + BehaviorTreeStarter.start(this.aiEntity, tree); - // 反序列化 - const btAsset = BehaviorTreeAssetSerializer.deserialize(jsonString); - - // 实例化 - this.aiEntity = BehaviorTreeAssetLoader.instantiate( - btAsset, - scene, - { - namePrefix: this.node.name - } - ); - - // 设置黑板初始值 - const blackboard = this.aiEntity.getComponent(BlackboardComponent); - if (blackboard) { - // 可以在这里设置引用到 Cocos 节点 - blackboard.setValue('cocosNode', this.node); - blackboard.setValue('position', this.node.position.clone()); - } - - // 启动 AI - BehaviorTreeStarter.start(this.aiEntity); - - console.log('敌人 AI 已启动'); - }); + console.log('敌人 AI 已启动'); } catch (error) { console.error('初始化行为树失败:', error); } @@ -194,19 +180,50 @@ export class EnemyAIComponent extends Component { ## 与 Cocos 节点交互 -### 在编辑器ExecuteAction节点中编写代码 +### 创建自定义执行器 -在行为树编辑器中,可以使用 `Execute Action` 节点,并编写代码: +要实现与Cocos节点的交互,需要创建自定义执行器: -```javascript -// 获取 Cocos 节点 -const cocosNode = blackboard.getValue('cocosNode'); +```typescript +import { + INodeExecutor, + NodeExecutionContext, + NodeExecutorMetadata +} from '@esengine/behavior-tree'; +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; +import { Animation } from 'cc'; -// 播放攻击动画 -const animation = cocosNode.getComponent('Animation'); -animation.play('attack'); +@NodeExecutorMetadata({ + implementationType: 'PlayAnimation', + nodeType: NodeType.Action, + displayName: '播放动画', + description: '播放Cocos节点上的动画', + category: 'Cocos', + configSchema: { + animationName: { + type: 'string', + default: 'attack' + } + } +}) +export class PlayAnimationAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const cocosNode = context.runtime.getBlackboardValue('cocosNode'); + const animationName = context.nodeData.config.animationName; -return TaskStatus.Success; + if (!cocosNode) { + return TaskStatus.Failure; + } + + const animation = cocosNode.getComponent(Animation); + if (animation) { + animation.play(animationName); + return TaskStatus.Success; + } + + return TaskStatus.Failure; + } +} ``` @@ -250,7 +267,7 @@ RootSelector ```typescript import { _decorator, Component, Node, Vec3 } from 'cc'; -import { BlackboardComponent } from '@esengine/behavior-tree'; +import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree'; const { ccclass, property } = _decorator; @@ -262,18 +279,18 @@ export class PlayerDetector extends Component { @property detectionRange: number = 10; - private blackboard: BlackboardComponent | null = null; + private runtime: BehaviorTreeRuntimeComponent | null = null; start() { // 假设AI组件在同一节点上 const aiComponent = this.node.getComponent('EnemyAIComponent') as any; if (aiComponent && aiComponent.aiEntity) { - this.blackboard = aiComponent.aiEntity.getComponent(BlackboardComponent); + this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent); } } update(deltaTime: number) { - if (!this.blackboard || !this.player) { + if (!this.runtime || !this.player) { return; } @@ -281,9 +298,9 @@ export class PlayerDetector extends Component { const distance = Vec3.distance(this.node.position, this.player.position); // 更新黑板 - this.blackboard.setValue('playerNode', this.player); - this.blackboard.setValue('playerInRange', distance <= this.detectionRange); - this.blackboard.setValue('distanceToPlayer', distance); + this.runtime.setBlackboardValue('playerNode', this.player); + this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange); + this.runtime.setBlackboardValue('distanceToPlayer', distance); } } ``` @@ -291,52 +308,210 @@ export class PlayerDetector extends Component { ## 资源管理 -### 预加载行为树资产 +### 使用 BehaviorTreeAssetManager -在游戏启动时预加载所有行为树资产: +框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建: ```typescript -import { resources } from 'cc'; +import { Core } from '@esengine/ecs-framework'; +import { + BehaviorTreeAssetManager, + BehaviorTreeBuilder, + BehaviorTreeStarter +} from '@esengine/behavior-tree'; -async function preloadBehaviorTrees() { - const assets = [ - 'behaviors/enemy-ai', - 'behaviors/boss-ai', - 'behaviors/patrol' - ]; +// 获取资产管理器(插件已自动注册) +const assetManager = Core.services.resolve(BehaviorTreeAssetManager); - for (const path of assets) { - await new Promise((resolve, reject) => { - resources.preload(path, (err) => { - if (err) reject(err); - else resolve(null); +// 创建并注册行为树(只创建一次) +const enemyAI = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('health', 100) + .selector('MainBehavior') + .log('攻击') + .end() + .build(); + +assetManager.loadAsset(enemyAI); + +// 为多个敌人实体使用同一份资产 +for (let i = 0; i < 10; i++) { + const enemy = scene.createEntity(`Enemy${i}`); + const tree = assetManager.getAsset('EnemyAI')!; + BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据 +} +``` + +### 从 Cocos Creator 资源加载 + +#### 1. 将行为树 JSON 放入 resources 目录 + +``` +assets/ +└── resources/ + └── behaviors/ + ├── enemy-ai.btree.json + └── boss-ai.btree.json +``` + +#### 2. 创建资源加载器 + +创建 `assets/scripts/BehaviorTreeLoader.ts`: + +```typescript +import { resources, JsonAsset } from 'cc'; +import { Core } from '@esengine/ecs-framework'; +import { + BehaviorTreeAssetManager, + BehaviorTreeAssetSerializer, + BehaviorTreeData +} from '@esengine/behavior-tree'; + +export class BehaviorTreeLoader { + private assetManager: BehaviorTreeAssetManager; + + constructor() { + this.assetManager = Core.services.resolve(BehaviorTreeAssetManager); + } + + /** + * 从 resources 目录加载行为树 + * @param path 相对于 resources 的路径,不带扩展名 + * @example await loader.load('behaviors/enemy-ai') + */ + async load(path: string): Promise { + 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 { + 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)创建自定义行为 diff --git a/docs/guide/behavior-tree/core-concepts.md b/docs/guide/behavior-tree/core-concepts.md index 2e8b8fea..a4bb088b 100644 --- a/docs/guide/behavior-tree/core-concepts.md +++ b/docs/guide/behavior-tree/core-concepts.md @@ -2,19 +2,19 @@ 本文介绍行为树系统的核心概念和工作原理。 -## 什么是行为树? +## 什么是行为树? -行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。 +行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。 ### 与状态机的对比 -传统状态机: +传统状态机: - 基于状态和转换 - 状态之间的转换复杂 - 难以扩展和维护 - 不便于复用 -行为树: +行为树: - 基于任务和层次结构 - 模块化、易于复用 - 可视化编辑 @@ -23,7 +23,7 @@ ## 树结构 -行为树由节点组成,形成树状结构: +行为树由节点组成,形成树状结构: ``` Root (根节点) @@ -37,8 +37,8 @@ Root (根节点) └── Wait (等待) ``` -每个节点都有: -- 父节点(除了根节点) +每个节点都有: +- 父节点(除了根节点) - 零个或多个子节点 - 执行状态 - 返回结果 @@ -46,307 +46,321 @@ Root (根节点) ## 节点类型 -### 复合节点(Composite) +### 复合节点(Composite) -复合节点有多个子节点,按特定规则执行它们。 +复合节点有多个子节点,按特定规则执行它们。 -#### Selector(选择器) +#### Selector(选择器) -按顺序尝试执行子节点,直到某个子节点成功。 +按顺序尝试执行子节点,直到某个子节点成功。 ```typescript -.selector('FindFood') - .action('EatNearbyFood', () => { - if (hasNearbyFood()) { - eat(); - return TaskStatus.Success; - } - return TaskStatus.Failure; - }) - .action('SearchFood', () => { - searchForFood(); - return TaskStatus.Running; - }) - .action('GiveUp', () => { - return TaskStatus.Failure; - }) -.end() +const tree = BehaviorTreeBuilder.create('FindFood') + .selector('FindFoodSelector') + .log('尝试吃附近的食物', 'EatNearby') + .log('搜索食物', 'SearchFood') + .log('放弃', 'GiveUp') + .end() + .build(); ``` -执行逻辑: +执行逻辑: 1. 尝试第一个子节点 -2. 如果返回Success,选择器成功 -3. 如果返回Failure,尝试下一个子节点 -4. 如果返回Running,选择器返回Running -5. 所有子节点都失败时,选择器失败 +2. 如果返回Success,选择器成功 +3. 如果返回Failure,尝试下一个子节点 +4. 如果返回Running,选择器返回Running +5. 所有子节点都失败时,选择器失败 -#### Sequence(序列) +#### Sequence(序列) -按顺序执行所有子节点,直到某个子节点失败。 +按顺序执行所有子节点,直到某个子节点失败。 ```typescript -.sequence('AttackSequence') - .condition((e, bb) => hasTarget()) // 检查是否有目标 - .action('Aim', () => TaskStatus.Success) // 瞄准 - .action('Fire', () => TaskStatus.Success) // 开火 -.end() +const tree = BehaviorTreeBuilder.create('Attack') + .sequence('AttackSequence') + .blackboardExists('target') // 检查是否有目标 + .log('瞄准', 'Aim') + .log('开火', 'Fire') + .end() + .build(); ``` -执行逻辑: +执行逻辑: 1. 依次执行子节点 -2. 如果子节点返回Failure,序列失败 -3. 如果子节点返回Running,序列返回Running -4. 如果子节点返回Success,继续下一个子节点 -5. 所有子节点都成功时,序列成功 +2. 如果子节点返回Failure,序列失败 +3. 如果子节点返回Running,序列返回Running +4. 如果子节点返回Success,继续下一个子节点 +5. 所有子节点都成功时,序列成功 -#### Parallel(并行) +#### Parallel(并行) 同时执行多个子节点。 ```typescript -import { ParallelPolicy } from '@esengine/behavior-tree'; - -.parallel(ParallelPolicy.RequireAll) // 所有任务都要成功 - .action('PlayAnimation', () => TaskStatus.Success) - .action('PlaySound', () => TaskStatus.Success) - .action('SpawnEffect', () => TaskStatus.Success) -.end() -``` - -策略类型: -- `RequireAll`:所有子节点都成功才成功 -- `RequireOne`:任意一个子节点成功就成功 - - -### 装饰器节点(Decorator) - -装饰器节点只有一个子节点,用于修改子节点的行为或结果。 - -#### Inverter(反转) - -反转子节点的结果: - -```typescript -.inverter() - .condition((e, bb) => isEnemyNearby()) // 检查敌人是否附近 -.end() -// 如果有敌人返回false,没有敌人返回true -``` - -#### Repeater(重复) - -重复执行子节点: - -```typescript -.repeat(3) // 重复3次 - .action('Jump', () => TaskStatus.Success) -.end() -``` - -#### Cooldown(冷却) - -限制子节点的执行频率: - -```typescript -.cooldown(5.0) // 5秒冷却 - .action('UseSpecialAbility', () => { - console.log('使用特殊技能'); - return TaskStatus.Success; +const tree = BehaviorTreeBuilder.create('PlayEffects') + .parallel('Effects', { + successPolicy: 'all', // 所有任务都要成功 + failurePolicy: 'one' // 任一失败则失败 }) -.end() + .log('播放动画', 'PlayAnimation') + .log('播放音效', 'PlaySound') + .log('生成粒子', 'SpawnEffect') + .end() + .build(); ``` -#### Timeout(超时) +策略类型: +- `successPolicy: 'all'`: 所有子节点都成功才成功 +- `successPolicy: 'one'`: 任意一个子节点成功就成功 +- `failurePolicy: 'all'`: 所有子节点都失败才失败 +- `failurePolicy: 'one'`: 任意一个子节点失败就失败 -限制子节点的执行时间: + +### 装饰器节点(Decorator) + +装饰器节点只有一个子节点,用于修改子节点的行为或结果。 + +#### Inverter(反转) + +反转子节点的结果: ```typescript -.timeout(10.0) // 10秒超时 - .action('ComplexTask', () => { - // 长时间运行的任务 - return TaskStatus.Running; - }) -.end() +const tree = BehaviorTreeBuilder.create('CheckSafe') + .inverter('NotHasEnemy') + .blackboardExists('enemy') + .end() + .build(); +``` + +#### Repeater(重复) + +重复执行子节点: + +```typescript +const tree = BehaviorTreeBuilder.create('Jump3Times') + .repeater(3, 'RepeatJump') + .log('跳跃', 'Jump') + .end() + .build(); +``` + +#### Cooldown(冷却) + +限制子节点的执行频率: + +```typescript +const tree = BehaviorTreeBuilder.create('UseSkill') + .cooldown(5.0, 'SkillCooldown') + .log('使用特殊技能', 'UseSpecialAbility') + .end() + .build(); +``` + +#### Timeout(超时) + +限制子节点的执行时间: + +```typescript +const tree = BehaviorTreeBuilder.create('TimedTask') + .timeout(10.0, 'TaskTimeout') + .log('长时间运行的任务', 'ComplexTask') + .end() + .build(); ``` -### 叶节点(Leaf) +### 叶节点(Leaf) -叶节点没有子节点,执行具体的任务。 +叶节点没有子节点,执行具体的任务。 -#### Action(动作) +#### Action(动作) -执行具体操作: +执行具体操作。内置动作节点包括: ```typescript -.action('Move', (entity, blackboard, deltaTime) => { - const target = blackboard?.getValue('targetPosition'); - - if (!target) { - return TaskStatus.Failure; - } - - // 移动逻辑 - const moved = moveTowards(target, deltaTime); - - if (moved) { - return TaskStatus.Success; - } - - return TaskStatus.Running; -}) +const tree = BehaviorTreeBuilder.create('Actions') + .sequence() + .wait(2.0) // 等待2秒 + .log('Hello', 'LogAction') // 输出日志 + .setBlackboardValue('score', 100) // 设置黑板值 + .modifyBlackboardValue('score', 'add', 10) // 修改黑板值 + .end() + .build(); ``` -#### Condition(条件) +要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。 -检查条件: +#### Condition(条件) + +检查条件。内置条件节点包括: ```typescript -.condition((entity, blackboard) => { - const health = blackboard?.getValue('health'); - return health > 50; -}, 'CheckHealthHigh') +const tree = BehaviorTreeBuilder.create('Conditions') + .selector() + .blackboardExists('player') // 检查变量是否存在 + .blackboardCompare('health', 50, 'greater') // 比较变量值 + .randomProbability(0.5) // 50%概率 + .end() + .build(); ``` -#### Wait(等待) +#### Wait(等待) -等待指定时间: +等待指定时间: ```typescript -.wait(2.0) // 等待2秒 +const tree = BehaviorTreeBuilder.create('WaitExample') + .wait(2.0, 'Wait2Seconds') + .build(); ``` ## 任务状态 -每个节点执行后返回以下状态之一: +每个节点执行后返回以下状态之一: -### Success(成功) +### Success(成功) 任务成功完成。 ```typescript -.action('CollectCoin', () => { - coin.collect(); - return TaskStatus.Success; -}) +// 内置节点会根据逻辑自动返回Success +.log('任务完成') // 总是返回Success +.blackboardCompare('score', 100, 'greater') // 条件满足时返回Success ``` -### Failure(失败) +### Failure(失败) 任务执行失败。 ```typescript -.condition((e, bb) => { - const hasKey = bb?.getValue('hasKey'); - return hasKey ? TaskStatus.Success : TaskStatus.Failure; -}) +.blackboardCompare('score', 100, 'greater') // 条件不满足返回Failure +.blackboardExists('nonExistent') // 变量不存在返回Failure ``` -### Running(运行中) +### Running(运行中) -任务需要多帧完成,仍在执行中。 +任务需要多帧完成,仍在执行中。 ```typescript -.action('ChargeLaser', (entity, blackboard, deltaTime) => { - let chargeTime = blackboard?.getValue('chargeTime') || 0; - chargeTime += deltaTime; - blackboard?.setValue('chargeTime', chargeTime); - - if (chargeTime >= 3.0) { - // 充能完成 - blackboard?.setValue('chargeTime', 0); - return TaskStatus.Success; - } - - return TaskStatus.Running; // 继续充能 -}) +.wait(3.0) // 等待过程中返回Running,3秒后返回Success ``` -### Invalid(无效) +### Invalid(无效) -节点未初始化或已重置。通常不需要手动返回此状态。 +节点未初始化或已重置。通常不需要手动处理此状态。 ## 黑板系统 -黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。 +黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。 ### 本地黑板 -每个行为树实例都有自己的本地黑板: +每个行为树实例都有自己的本地黑板: ```typescript -const ai = BehaviorTreeBuilder.create(scene, 'EnemyAI') - .blackboard() - .defineVariable('health', BlackboardValueType.Number, 100) - .defineVariable('target', BlackboardValueType.Object, null) - .defineVariable('state', BlackboardValueType.String, 'idle') - .endBlackboard() +const tree = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) + .defineBlackboardVariable('state', 'idle') + // ... .build(); ``` ### 支持的数据类型 -```typescript -import { BlackboardValueType } from '@esengine/behavior-tree'; +黑板支持以下数据类型: +- String:字符串 +- Number:数字 +- Boolean:布尔值 +- Vector2:二维向量 +- Vector3:三维向量 +- Object:对象引用 +- Array:数组 -.blackboard() - .defineVariable('count', BlackboardValueType.Number, 0) - .defineVariable('name', BlackboardValueType.String, 'Enemy') - .defineVariable('isActive', BlackboardValueType.Boolean, true) - .defineVariable('position', BlackboardValueType.Vector2, { x: 0, y: 0 }) - .defineVariable('direction', BlackboardValueType.Vector3, { x: 0, y: 0, z: 0 }) - .defineVariable('data', BlackboardValueType.Object, {}) - .defineVariable('items', BlackboardValueType.Array, []) -.endBlackboard() +示例: + +```typescript +const tree = BehaviorTreeBuilder.create('Variables') + .defineBlackboardVariable('name', 'Enemy') // 字符串 + .defineBlackboardVariable('count', 0) // 数字 + .defineBlackboardVariable('isActive', true) // 布尔值 + .defineBlackboardVariable('position', { x: 0, y: 0 }) // 对象(也可用于Vector2) + .defineBlackboardVariable('velocity', { x: 0, y: 0, z: 0 }) // 对象(也可用于Vector3) + .defineBlackboardVariable('items', []) // 数组 + .build(); ``` ### 读写变量 +通过`BehaviorTreeRuntimeComponent`访问黑板: + ```typescript -.action('UseBlackboard', (entity, blackboard) => { - // 读取变量 - const health = blackboard?.getValue('health'); - const target = blackboard?.getValue('target'); +const runtime = entity.getComponent(BehaviorTreeRuntimeComponent); - // 写入变量 - blackboard?.setValue('health', health - 10); - blackboard?.setValue('lastAttackTime', Date.now()); +// 读取变量 +const health = runtime?.getBlackboardValue('health'); +const target = runtime?.getBlackboardValue('target'); - // 检查变量是否存在 - if (blackboard?.hasVariable('powerup')) { - const powerup = blackboard.getValue('powerup'); - console.log('已获得强化:', powerup); - } +// 写入变量 +runtime?.setBlackboardValue('health', 50); +runtime?.setBlackboardValue('lastAttackTime', Date.now()); - return TaskStatus.Success; -}) +// 获取所有变量 +const allVars = runtime?.getAllBlackboardVariables(); +``` + +也可以使用内置节点操作黑板: + +```typescript +const tree = BehaviorTreeBuilder.create('BlackboardOps') + .sequence() + .setBlackboardValue('score', 100) // 设置值 + .modifyBlackboardValue('score', 'add', 10) // 增加10 + .blackboardCompare('score', 110, 'equals') // 检查是否等于110 + .end() + .build(); ``` ### 全局黑板 -所有行为树实例共享的黑板: +所有行为树实例共享的黑板,通过`GlobalBlackboardService`访问: ```typescript -import { GlobalBlackboard } from '@esengine/behavior-tree'; +import { GlobalBlackboardService } from '@esengine/behavior-tree'; +import { Core } from '@esengine/ecs-framework'; + +const globalBlackboard = Core.services.resolve(GlobalBlackboardService); // 设置全局变量 -GlobalBlackboard.setValue('gameState', 'playing'); -GlobalBlackboard.setValue('difficulty', 5); +globalBlackboard.setValue('gameState', 'playing'); +globalBlackboard.setValue('difficulty', 5); -// 在任何行为树中访问 -.action('CheckGlobalState', () => { - const gameState = GlobalBlackboard.getValue('gameState'); +// 读取全局变量 +const gameState = globalBlackboard.getValue('gameState'); +``` - if (gameState === 'paused') { - return TaskStatus.Failure; +在自定义执行器中访问全局黑板: + +```typescript +import { GlobalBlackboardService } from '@esengine/behavior-tree'; +import { Core } from '@esengine/ecs-framework'; + +export class CheckGameState implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const globalBlackboard = Core.services.resolve(GlobalBlackboardService); + const gameState = globalBlackboard.getValue('gameState'); + + if (gameState === 'paused') { + return TaskStatus.Failure; + } + + return TaskStatus.Success; } - - return TaskStatus.Success; -}) +} ``` @@ -355,18 +369,24 @@ GlobalBlackboard.setValue('difficulty', 5); ### 初始化 ```typescript -// 1. 初始化Core和场景 +// 1. 初始化Core和插件 Core.create(); +const plugin = new BehaviorTreePlugin(); +await Core.installPlugin(plugin); + +// 2. 创建场景 const scene = new Scene(); +plugin.setupScene(scene); Core.setScene(scene); -// 2. 构建行为树 -const ai = BehaviorTreeBuilder.create(scene, 'AI') +// 3. 构建行为树 +const tree = BehaviorTreeBuilder.create('AI') // ... 定义节点 .build(); -// 3. 启动 -BehaviorTreeStarter.start(ai); +// 4. 创建实体并启动 +const entity = scene.createEntity('AIEntity'); +BehaviorTreeStarter.start(entity, tree); ``` ### 更新循环 @@ -383,7 +403,7 @@ gameLoop(() => { ``` 1. 从根节点开始 -2. 根节点执行其逻辑(通常是Selector或Sequence) +2. 根节点执行其逻辑(通常是Selector或Sequence) 3. 根节点的子节点按顺序执行 4. 每个子节点可能有自己的子节点 5. 叶节点执行具体操作并返回状态 @@ -395,90 +415,77 @@ gameLoop(() => { ### 执行示例 ```typescript -const tree = BehaviorTreeBuilder.create(scene, 'Example') +const tree = BehaviorTreeBuilder.create('Example') .selector('Root') // 1. 执行选择器 .sequence('Branch1') // 2. 尝试第一个分支 - .condition(() => false) // 3. 条件失败 - .end() // 4. 序列失败,选择器继续下一个分支 + .blackboardCompare('ready', true, 'equals', 'CheckReady') // 3. 条件失败 + .end() // 4. 序列失败,选择器继续下一个分支 .sequence('Branch2') // 5. 尝试第二个分支 - .condition(() => true) // 6. 条件成功 - .action(() => TaskStatus.Success) // 7. 动作成功 - .end() // 8. 序列成功,选择器成功 + .blackboardCompare('active', true, 'equals', 'CheckActive') // 6. 条件成功 + .log('执行动作', 'DoAction') // 7. 动作成功 + .end() // 8. 序列成功,选择器成功 .end() // 9. 整个树成功 .build(); ``` -执行流程图: +执行流程图: ``` Root(Selector) → Branch1(Sequence) - → Condition: Failure + → CheckReady: Failure → Branch1 fails → Branch2(Sequence) - → Condition: Success - → Action: Success + → CheckActive: Success + → DoAction: Success → Branch2 succeeds → Root succeeds ``` -## ECS集成 +## Runtime架构 -本框架的行为树完全基于ECS架构: +本框架的行为树采用Runtime执行器架构: -### 节点即实体 +### 核心组件 -每个行为树节点都是一个Entity: +- **BehaviorTreeData**: 纯数据结构,描述行为树的结构和配置 +- **BehaviorTreeRuntimeComponent**: 运行时组件,管理执行状态和黑板 +- **BehaviorTreeExecutionSystem**: 执行系统,驱动行为树运行 +- **INodeExecutor**: 节点执行器接口,定义节点的执行逻辑 +- **NodeExecutionContext**: 执行上下文,包含执行所需的所有信息 + +### 架构特点 + +1. **数据与逻辑分离**: BehaviorTreeData是纯数据,执行逻辑在执行器中 +2. **无状态执行器**: 执行器实例可以在多个节点间共享,状态存储在Runtime中 +3. **类型安全**: 通过TypeScript类型系统保证类型安全 +4. **高性能**: 避免不必要的对象创建,优化内存使用 + +### 数据流 -```typescript -// 行为树节点在内部被表示为: -const nodeEntity = scene.createEntity('SelectorNode'); -nodeEntity.addComponent(new SelectorComponent()); -nodeEntity.addComponent(new ParentComponent(parentEntity)); ``` - -### 组件存储数据 - -节点属性存储在组件中: - -```typescript -// Action节点的数据组件 -class ActionComponent extends Component { - actionFunc: ActionFunction; - name: string; -} - -// Blackboard组件 -class BlackboardComponent extends Component { - private variables: Map; -} -``` - -### 系统驱动行为 - -行为树系统负责更新所有节点: - -```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)学习设计模式 diff --git a/docs/guide/behavior-tree/custom-actions.md b/docs/guide/behavior-tree/custom-actions.md index 2c440997..71cc36f4 100644 --- a/docs/guide/behavior-tree/custom-actions.md +++ b/docs/guide/behavior-tree/custom-actions.md @@ -1,400 +1,334 @@ -# 自定义动作组件 +# 自定义节点执行器 -本教程介绍如何为项目创建专用的动作组件,供策划在编辑器中使用。 +本教程介绍如何为项目创建专用的节点执行器,供策划在编辑器中使用。 -## 为什么需要自定义组件? +## 为什么需要自定义执行器? -ExecuteAction节点允许在编辑器中编写JavaScript代码,但这种方式存在以下问题: +虽然框架提供了ExecuteAction等通用节点,但自定义执行器能提供更好的开发体验: -- 策划不懂编程,无法编写代码 -- 没有智能提示,容易出错 -- 缺少类型检查,运行时才发现问题 -- 代码分散在编辑器中,难以维护 +- 类型安全: TypeScript类型检查,编译时发现错误 +- 智能提示: IDE自动补全,提高开发效率 +- 配置化: 策划只需配置参数,无需编程 +- 可复用: 封装通用逻辑,便于维护 +- 黑板绑定: 支持属性绑定到黑板变量 -**推荐做法**:程序员创建专用的动作组件类,策划只需配置参数。 +推荐做法: 程序员创建专用的执行器类,策划在编辑器中配置参数使用。 -## 基础结构 +## 基础架构 -一个自定义动作组件的基本结构: +### Runtime执行器架构 + +行为树采用Runtime执行器架构,将节点定义和执行逻辑分离: + +- 节点执行器: 无状态的执行逻辑类,实现`INodeExecutor`接口 +- 节点元数据: 通过`@NodeExecutorMetadata`装饰器定义 +- 运行时状态: 存储在`NodeRuntimeState`中,不在执行器中 +- 执行上下文: `NodeExecutionContext`包含执行所需的所有信息 + +### 基础结构 + +一个自定义节点执行器的基本结构: ```typescript -import { Component, ECSComponent, Entity } from '@esengine/ecs-framework'; -import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; import { - TaskStatus, - NodeType, - BlackboardComponent, - BehaviorNode, - BehaviorProperty + INodeExecutor, + NodeExecutionContext, + BindingHelper, + NodeExecutorMetadata } from '@esengine/behavior-tree'; -@BehaviorNode({ - displayName: '动作名称', // 在编辑器中显示的名称 - category: '分类', // 节点分类(如"战斗"、"移动") - type: NodeType.Action, // 使用内置类型 - // 或使用自定义类型: - // type: 'custom-behavior', // 自定义节点类型 - icon: 'IconName', // 图标名称(可选) - description: '动作描述', // 描述信息 - color: '#FF5722' // 节点颜色(可选) -}) -@ECSComponent('CustomActionName') // 组件名称 -@Serializable({ version: 1 }) // 可序列化 -export class CustomAction extends Component { - // 属性定义... - - /** - * 执行方法 - * 系统会自动调用此方法 - */ - execute(entity: Entity, blackboard?: BlackboardComponent, deltaTime?: number): TaskStatus { - // 你的逻辑 - return TaskStatus.Success; - } -} -``` - -## 自定义节点类型 - -除了使用内置的节点类型(`Action`、`Condition`、`Composite`、`Decorator`),你也可以定义自己的节点类型: - -```typescript -@BehaviorNode({ - displayName: 'AI决策', - category: '高级', - type: 'ai-decision', // 自定义类型 - description: '使用机器学习进行决策', - color: '#00BCD4' -}) -export class AIDecisionNode extends Component { - execute(...): TaskStatus { - // 自定义逻辑 - return TaskStatus.Success; - } -} -``` - -自定义节点类型的好处: -- 可以创建特殊的执行逻辑 -- 便于编辑器中分类和识别 -- 支持项目特定的工作流 - -## 定义属性 - -使用 `@BehaviorProperty` 装饰器定义可配置的属性: - -### 内置属性类型 - -框架提供了多种常用的属性类型: - -#### 数值类型 - -```typescript -import { PropertyType } from '@esengine/behavior-tree'; - -@BehaviorProperty({ - label: '伤害值', - type: PropertyType.Number, // 或直接使用 'number' - description: '造成的伤害', - min: 0, - max: 999, - step: 1 -}) -@Serialize() -damage: number = 10; -``` - -策划在编辑器中看到的是: -- 标签:"伤害值" -- 滑块:0-999,步长为1 -- 默认值:10 - -#### 选择框类型 - -```typescript -@BehaviorProperty({ - label: '攻击类型', - type: PropertyType.Select, - description: '攻击方式', - options: [ - { label: '近战', value: 'melee' }, - { label: '远程', value: 'ranged' }, - { label: '魔法', value: 'magic' } - ] -}) -@Serialize() -attackType: string = 'melee'; -``` - -策划看到的是下拉选择框,选项为:近战、远程、魔法 - -#### 布尔类型 - -```typescript -@BehaviorProperty({ - label: '是否循环', - type: PropertyType.Boolean, - description: '动画是否循环播放' -}) -@Serialize() -loop: boolean = false; -``` - -策划看到的是复选框 - -#### 字符串类型 - -```typescript -@BehaviorProperty({ - label: '动画名称', - type: PropertyType.String, - description: '要播放的动画名称', - required: true -}) -@Serialize() -animationName: string = ''; -``` - -策划看到的是文本输入框,标记为必填 - -#### 黑板变量引用 - -```typescript -@BehaviorProperty({ - label: '目标位置变量', - type: PropertyType.Blackboard, - description: '黑板中存储目标位置的变量名' -}) -@Serialize() -targetVariableName: string = 'targetPosition'; -``` - -策划看到的是黑板变量选择器 - -#### 代码编辑器 - -```typescript -@BehaviorProperty({ - label: '配置(JSON)', - type: PropertyType.Code, - description: '配置数据,JSON格式' -}) -@Serialize() -configJson: string = '{}'; -``` - -策划看到的是代码编辑器 - -#### 资产引用 - -```typescript -@BehaviorProperty({ - label: '音效文件', - type: PropertyType.Asset, - description: '要播放的音效资产' -}) -@Serialize() -soundAsset: string = ''; -``` - -策划看到的是资产选择器 - -### 自定义属性渲染 - -你可以通过 `renderConfig` 配置自定义属性的渲染方式: - -#### 使用自定义渲染器组件 - -```typescript -@BehaviorProperty({ - label: '颜色', - type: 'color', - description: '选择颜色', - renderConfig: { - component: 'ColorPicker', // 编辑器中的渲染器组件名 - props: { - showAlpha: true, // 是否显示透明度 - presets: [ // 预设颜色 - '#FF0000', - '#00FF00', - '#0000FF' - ] - } - } -}) -@Serialize() -color: string = '#FFFFFF'; -``` - -#### 使用曲线编辑器 - -```typescript -@BehaviorProperty({ - label: '动画曲线', - type: 'curve', - description: '编辑动画曲线', - renderConfig: { - component: 'CurveEditor', - props: { +@NodeExecutorMetadata({ + implementationType: 'AttackAction', // 唯一标识符 + nodeType: NodeType.Action, // 节点类型 + displayName: '攻击目标', // 编辑器显示名称 + description: '对目标造成伤害', // 描述信息 + category: '战斗', // 分类 + configSchema: { // 配置参数定义 + damage: { + type: 'number', + default: 10, + description: '伤害值', min: 0, - max: 1, - defaultCurve: 'linear' - }, - style: { - height: '200px' + max: 999, + supportBinding: true // 支持绑定到黑板变量 } } }) -@Serialize() -curve: string = ''; -``` +export class AttackAction implements INodeExecutor { + /** + * 执行节点逻辑 + */ + execute(context: NodeExecutionContext): TaskStatus { + // 使用BindingHelper获取配置值(支持黑板绑定) + const damage = BindingHelper.getValue(context, 'damage', 10); -#### 使用项目特定的选择器 + // 访问黑板数据 + const target = context.runtime.getBlackboardValue('target'); -```typescript -@BehaviorProperty({ - label: '技能', - type: 'skill', - description: '选择技能', - renderConfig: { - component: 'SkillSelector', // 项目自定义的技能选择器 - props: { - category: 'combat', // 只显示战斗技能 - maxLevel: 10, - showIcon: true - } - } -}) -@Serialize() -skillId: number = 0; -``` - -#### 使用自定义验证 - -```typescript -@BehaviorProperty({ - label: 'IP地址', - type: PropertyType.String, - description: '输入IP地址', - validation: { - pattern: /^(\d{1,3}\.){3}\d{1,3}$/, - message: '请输入有效的IP地址' - } -}) -@Serialize() -ipAddress: string = '127.0.0.1'; -``` - -#### 使用资产浏览器 - -```typescript -@BehaviorProperty({ - label: '音效', - type: 'asset', - description: '选择音效文件', - renderConfig: { - component: 'AssetBrowser', - props: { - filter: ['mp3', 'wav', 'ogg'], // 只显示音频文件 - basePath: 'assets/sounds' // 默认路径 - } - } -}) -@Serialize() -soundPath: string = ''; -``` - -#### 使用滑块和输入框组合 - -```typescript -@BehaviorProperty({ - label: '音量', - type: PropertyType.Number, - min: 0, - max: 100, - renderConfig: { - component: 'SliderWithInput', // 滑块+输入框组合控件 - props: { - showPercentage: true, - marks: { // 刻度标记 - 0: '静音', - 50: '中等', - 100: '最大' - } - } - } -}) -@Serialize() -volume: number = 80; -``` - -### 渲染配置说明 - -`renderConfig` 对象支持以下字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `component` | string | 渲染器组件名称(需在编辑器中注册) | -| `props` | object | 传递给渲染器的属性配置 | -| `className` | string | CSS类名 | -| `style` | object | 内联样式 | - -编辑器会根据 `component` 查找对应的渲染器组件,并将 `props` 传递给它。 - -## 完整示例 - -### 示例1:攻击动作 - -```typescript -import { PropertyType } from '@esengine/behavior-tree'; - -@BehaviorNode({ - displayName: '攻击目标', - category: '战斗', - type: NodeType.Action, - icon: 'Sword', - description: '对目标造成伤害', - color: '#FF5722' -}) -@ECSComponent('AttackAction') -@Serializable({ version: 1 }) -export class AttackAction extends Component { - @BehaviorProperty({ - label: '伤害值', - type: PropertyType.Number, - min: 0, - max: 999 - }) - @Serialize() - damage: number = 10; - - @BehaviorProperty({ - label: '攻击类型', - type: PropertyType.Select, - options: [ - { label: '近战', value: 'melee' }, - { label: '远程', value: 'ranged' }, - { label: '魔法', value: 'magic' } - ] - }) - @Serialize() - attackType: string = 'melee'; - - execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus { - const target = blackboard?.getValue('target'); if (!target) { return TaskStatus.Failure; } // 执行攻击逻辑 - console.log(`使用${this.attackType}攻击,造成${this.damage}点伤害`); + console.log(`造成 ${damage} 点伤害`); + + return TaskStatus.Success; + } + + /** + * 重置节点状态(可选) + * 当节点完成或被中断时调用 + */ + reset(context: NodeExecutionContext): void { + // 清理状态 + } +} +``` + +### 核心概念 + +#### NodeExecutionContext + +执行上下文包含执行所需的所有信息: + +```typescript +interface NodeExecutionContext { + entity: Entity; // 行为树宿主实体 + nodeData: BehaviorNodeData; // 节点配置数据 + state: NodeRuntimeState; // 节点运行时状态 + runtime: BehaviorTreeRuntimeComponent; // 运行时组件(访问黑板等) + treeData: BehaviorTreeData; // 行为树数据 + deltaTime: number; // 当前帧增量时间 + totalTime: number; // 总时间 + executeChild(childId: string): TaskStatus; // 执行子节点 +} +``` + +#### BindingHelper + +BindingHelper用于获取配置值,自动处理黑板绑定: + +```typescript +// 获取配置值(支持黑板绑定) +const damage = BindingHelper.getValue(context, 'damage', 10); + +// 检查是否绑定到黑板 +if (BindingHelper.hasBinding(context, 'damage')) { + const blackboardKey = BindingHelper.getBindingKey(context, 'damage'); + console.log(`damage绑定到黑板变量: ${blackboardKey}`); +} +``` + +#### 访问黑板 + +通过`context.runtime`访问黑板: + +```typescript +// 读取黑板变量 +const target = context.runtime.getBlackboardValue('target'); +const health = context.runtime.getBlackboardValue('health'); + +// 写入黑板变量 +context.runtime.setBlackboardValue('lastAttackTime', context.totalTime); +``` + +#### 状态存储 + +节点状态存储在`context.state`中,不在执行器中: + +```typescript +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; +} +``` + +## 配置参数定义 + +使用`configSchema`定义可配置的参数: + +### 支持的参数类型 + +#### 数值类型 + +```typescript +configSchema: { + damage: { + type: 'number', + default: 10, + description: '伤害值', + min: 0, + max: 999, + supportBinding: true // 支持绑定到黑板变量 + }, + speed: { + type: 'number', + default: 5.0, + min: 0, + max: 100, + supportBinding: true + } +} +``` + +#### 字符串类型 + +```typescript +configSchema: { + animationName: { + type: 'string', + default: '', + description: '动画名称', + supportBinding: true + }, + message: { + type: 'string', + default: 'Hello', + supportBinding: true + } +} +``` + +#### 布尔类型 + +```typescript +configSchema: { + loop: { + type: 'boolean', + default: false, + description: '是否循环', + supportBinding: false + } +} +``` + +#### 对象类型 + +```typescript +configSchema: { + config: { + type: 'object', + default: {}, + description: '配置对象', + supportBinding: true + } +} +``` + +#### 数组类型 + +```typescript +configSchema: { + targets: { + type: 'array', + default: [], + description: '目标列表', + supportBinding: true + } +} +``` + +### 属性连接限制 + +可以控制属性是否允许多个连接: + +```typescript +configSchema: { + target: { + type: 'object', + default: null, + supportBinding: true, + allowMultipleConnections: false // 不允许多个连接(默认) + }, + listeners: { + type: 'array', + default: [], + supportBinding: true, + allowMultipleConnections: true // 允许多个连接 + } +} +``` + +## 完整示例 + +### 示例1: 攻击动作 + +```typescript +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; +import { + INodeExecutor, + NodeExecutionContext, + BindingHelper, + NodeExecutorMetadata +} from '@esengine/behavior-tree'; + +/** + * 攻击动作执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, + displayName: '攻击目标', + description: '对目标造成伤害', + category: '战斗', + configSchema: { + damage: { + type: 'number', + default: 10, + description: '造成的伤害值', + min: 0, + max: 999, + supportBinding: true + }, + attackType: { + type: 'string', + default: 'melee', + description: '攻击类型', + options: ['melee', 'ranged', 'magic'], + supportBinding: true + } + } +}) +export class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const { entity, runtime } = context; + + // 获取配置值(支持黑板绑定) + const damage = BindingHelper.getValue(context, 'damage', 10); + const attackType = BindingHelper.getValue(context, 'attackType', 'melee'); + + // 获取目标 + const target = runtime.getBlackboardValue('target'); + + if (!target) { + return TaskStatus.Failure; + } + + // 执行攻击逻辑 + console.log(`[AttackAction] 使用${attackType}攻击,造成${damage}点伤害`); // 触发事件让游戏逻辑处理 entity.scene?.eventSystem.emit('ai:attack', { attacker: entity, target, - damage: this.damage, - attackType: this.attackType + damage, + attackType }); return TaskStatus.Success; @@ -402,52 +336,62 @@ export class AttackAction extends Component { } ``` -### 示例2:移动动作 +### 示例2: 移动到位置 + +带状态的异步动作示例: ```typescript -@BehaviorNode({ +/** + * 移动到位置执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'MoveToPosition', + nodeType: NodeType.Action, displayName: '移动到位置', + description: '移动到目标位置', category: '移动', - type: NodeType.Action, - icon: 'Navigation', - description: '移动到指定位置', - color: '#2196F3' + configSchema: { + targetPosition: { + type: 'object', + default: { x: 0, y: 0 }, + description: '目标位置', + supportBinding: true + }, + speed: { + type: 'number', + default: 5.0, + description: '移动速度', + min: 0, + max: 100, + supportBinding: true + }, + arrivalDistance: { + type: 'number', + default: 0.5, + description: '到达距离', + min: 0.1, + max: 10, + supportBinding: false + } + } }) -@ECSComponent('MoveToPositionAction') -@Serializable({ version: 1 }) -export class MoveToPositionAction extends Component { - @BehaviorProperty({ - label: '目标位置变量', - type: PropertyType.Blackboard, - description: '黑板中的目标位置变量' - }) - @Serialize() - targetVar: string = 'targetPosition'; +export class MoveToPosition implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const { runtime, deltaTime } = context; - @BehaviorProperty({ - label: '移动速度', - type: PropertyType.Number, - min: 0, - max: 100, - step: 0.1 - }) - @Serialize() - speed: number = 5.0; + // 获取配置值 + const targetPos = BindingHelper.getValue<{x: number, y: number}>( + context, 'targetPosition', { x: 0, y: 0 } + ); + const speed = BindingHelper.getValue(context, 'speed', 5.0); + const arrivalDistance = BindingHelper.getValue( + context, 'arrivalDistance', 0.5 + ); - @BehaviorProperty({ - label: '到达距离', - type: PropertyType.Number, - min: 0.1, - max: 10 - }) - @Serialize() - arrivalDistance: number = 0.5; + // 获取当前位置 + const currentPos = runtime.getBlackboardValue<{x: number, y: number}>('position'); - execute(entity: Entity, blackboard?: BlackboardComponent, deltaTime?: number): TaskStatus { - const targetPos = blackboard?.getValue(this.targetVar); - const currentPos = blackboard?.getValue('position'); - - if (!targetPos || !currentPos) { + if (!currentPos) { return TaskStatus.Failure; } @@ -457,278 +401,625 @@ export class MoveToPositionAction extends Component { const distance = Math.sqrt(dx * dx + dy * dy); // 到达目标 - if (distance <= this.arrivalDistance) { + if (distance <= arrivalDistance) { return TaskStatus.Success; } // 移动 - const moveDistance = this.speed * (deltaTime || 0); + const moveDistance = speed * deltaTime; const ratio = Math.min(moveDistance / distance, 1); - currentPos.x += dx * ratio; - currentPos.y += dy * ratio; - blackboard?.setValue('position', currentPos); + const newPos = { + x: currentPos.x + dx * ratio, + y: currentPos.y + dy * ratio + }; + + runtime.setBlackboardValue('position', newPos); return TaskStatus.Running; } } ``` -### 示例3:播放动画 +### 示例3: 等待并计时 + +使用状态存储的示例: ```typescript -@BehaviorNode({ - displayName: '播放动画', - category: '表现', - type: NodeType.Action, - icon: 'Film', - description: '播放角色动画', - color: '#9C27B0' +/** + * 延迟执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'DelayAction', + nodeType: NodeType.Action, + displayName: '延迟', + description: '等待指定时间', + category: '工具', + configSchema: { + duration: { + type: 'number', + default: 1.0, + description: '等待时长(秒)', + min: 0, + supportBinding: true + } + } }) -@ECSComponent('PlayAnimationAction') -@Serializable({ version: 1 }) -export class PlayAnimationAction extends Component { - @BehaviorProperty({ - label: '动画名称', - type: PropertyType.String, - required: true - }) - @Serialize() - animationName: string = ''; +export class DelayAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const { state, totalTime } = context; + const duration = BindingHelper.getValue(context, 'duration', 1.0); - @BehaviorProperty({ - label: '是否循环', - type: PropertyType.Boolean - }) - @Serialize() - loop: boolean = false; - - execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus { - if (!this.animationName) { - return TaskStatus.Failure; + // 第一次执行,记录开始时间 + if (!state.startTime) { + state.startTime = totalTime; + return TaskStatus.Running; } - // 触发事件让游戏逻辑播放动画 - entity.scene?.eventSystem.emit('ai:playAnimation', { - entity, - animationName: this.animationName, - loop: this.loop - }); + // 检查是否超时 + if (totalTime - state.startTime >= duration) { + return TaskStatus.Success; + } - return TaskStatus.Success; + return TaskStatus.Running; + } + + reset(context: NodeExecutionContext): void { + context.state.startTime = undefined; } } ``` -## 注册组件 - -创建好组件后,需要导入以注册到编辑器: - -在 `src/game/ai/index.ts` 中: +### 示例4: 条件节点 ```typescript -// 导入所有自定义组件以注册到编辑器 -import './AttackAction'; -import './MoveToPositionAction'; -import './PlayAnimationAction'; +/** + * 检查生命值条件执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'CheckHealth', + nodeType: NodeType.Condition, + displayName: '检查生命值', + description: '检查生命值是否满足条件', + category: '条件', + configSchema: { + threshold: { + type: 'number', + default: 50, + description: '阈值', + min: 0, + max: 100, + supportBinding: true + }, + operator: { + type: 'string', + default: 'greater', + description: '比较运算符', + options: ['greater', 'less', 'equal'], + supportBinding: false + } + } +}) +export class CheckHealth implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const threshold = BindingHelper.getValue(context, 'threshold', 50); + const operator = BindingHelper.getValue(context, 'operator', 'greater'); -export function registerCustomActions() { - // 组件会通过装饰器自动注册 + const health = context.runtime.getBlackboardValue('health'); + + if (health === undefined) { + return TaskStatus.Failure; + } + + let result = false; + + switch (operator) { + case 'greater': + result = health > threshold; + break; + case 'less': + result = health < threshold; + break; + case 'equal': + result = health === threshold; + break; + } + + return result ? TaskStatus.Success : TaskStatus.Failure; + } } ``` -在游戏初始化时调用: +### 示例5: 装饰器节点 ```typescript -import { registerCustomActions } from './game/ai'; +/** + * 重试装饰器执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'RetryDecorator', + nodeType: NodeType.Decorator, + displayName: '重试', + description: '子节点失败时重试指定次数', + category: '装饰器', + configSchema: { + maxRetries: { + type: 'number', + default: 3, + description: '最大重试次数', + min: 1, + max: 10, + supportBinding: false + } + } +}) +export class RetryDecorator implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const { nodeData, state } = context; -// 在 Core.create() 之前调用 -registerCustomActions(); -Core.create(); + if (!nodeData.children || nodeData.children.length === 0) { + return TaskStatus.Failure; + } + + const maxRetries = BindingHelper.getValue(context, 'maxRetries', 3); + + // 初始化重试计数 + if (state.retryCount === undefined) { + state.retryCount = 0; + } + + const childId = nodeData.children[0]; + const status = context.executeChild(childId); + + if (status === TaskStatus.Running) { + return TaskStatus.Running; + } + + if (status === TaskStatus.Success) { + state.retryCount = 0; + return TaskStatus.Success; + } + + // 失败时重试 + state.retryCount++; + + if (state.retryCount < maxRetries) { + // 重置子节点状态以便重试 + context.runtime.resetNodeState(childId); + return TaskStatus.Running; + } + + // 达到最大重试次数 + state.retryCount = 0; + return TaskStatus.Failure; + } + + reset(context: NodeExecutionContext): void { + context.state.retryCount = 0; + + if (context.nodeData.children && context.nodeData.children.length > 0) { + context.runtime.resetNodeState(context.nodeData.children[0]); + } + } +} +``` + +## 注册执行器 + +### 自动注册 + +执行器通过`@NodeExecutorMetadata`装饰器自动注册到全局注册表。只需导入执行器文件即可: + +```typescript +// src/game/ai/index.ts +import './executors/AttackAction'; +import './executors/MoveToPosition'; +import './executors/DelayAction'; +import './executors/CheckHealth'; + +// 执行器会自动注册,无需手动调用注册函数 +``` + +### 导入时机 + +在Core初始化之前导入执行器: + +```typescript +// src/main.ts +import { Core } from '@esengine/ecs-framework'; +import { BehaviorTreePlugin } from '@esengine/behavior-tree'; + +// 导入自定义执行器 +import './game/ai'; + +async function main() { + Core.create(); + + const plugin = new BehaviorTreePlugin(); + await Core.installPlugin(plugin); + + // ... +} +``` + +### 插件方式注册 + +如果要创建可复用的行为树插件,参考以下结构: + +```typescript +// my-behavior-plugin/src/plugin.ts +import type { IEditorPlugin } from '@esengine/editor-core'; +import { EditorPluginCategory } from '@esengine/editor-core'; +import type { Core, ServiceContainer } from '@esengine/ecs-framework'; + +// 导入执行器(触发装饰器注册) +import './executors/AttackAction'; +import './executors/MoveToPosition'; + +export class MyBehaviorPlugin implements IEditorPlugin { + readonly name = 'my-behavior-plugin'; + readonly version = '1.0.0'; + readonly category = EditorPluginCategory.Tool; + + async install(core: Core, services: ServiceContainer): Promise { + console.log('[MyBehaviorPlugin] 插件已安装'); + // 执行器已通过装饰器自动注册 + } + + async uninstall(): Promise { + console.log('[MyBehaviorPlugin] 插件已卸载'); + } +} + +export const myBehaviorPlugin = new MyBehaviorPlugin(); ``` ## 与游戏逻辑集成 -### 方式1:通过事件系统(推荐) +### 方式1: 通过事件系统(推荐) -在动作中触发事件: +在执行器中触发事件,保持解耦: ```typescript -execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus { +execute(context: NodeExecutionContext): TaskStatus { + const { entity } = context; + const damage = BindingHelper.getValue(context, 'damage', 10); + const target = context.runtime.getBlackboardValue('target'); + entity.scene?.eventSystem.emit('ai:attack', { attacker: entity, - target: blackboard?.getValue('target'), - damage: this.damage + target, + damage }); + return TaskStatus.Success; } ``` -在游戏代码中监听: +在游戏代码中监听事件: ```typescript Core.scene.eventSystem.on('ai:attack', (data) => { const { attacker, target, damage } = data; - // 执行实际的战斗逻辑 target.takeDamage(damage); }); ``` -### 方式2:通过黑板传递对象 +### 方式2: 通过黑板传递对象 -将游戏对象放入黑板: +将游戏对象放入黑板: ```typescript -const blackboard = aiEntity.getComponent(BlackboardComponent); -blackboard?.setValue('gameController', this.gameController); -blackboard?.setValue('player', this.player); +const runtime = aiEntity.getComponent(BehaviorTreeRuntimeComponent); +runtime.setBlackboardValue('gameController', this.gameController); +runtime.setBlackboardValue('player', this.player); ``` -在动作中使用: +在执行器中使用: ```typescript -execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus { - const gameController = blackboard?.getValue('gameController'); - const player = blackboard?.getValue('player'); +execute(context: NodeExecutionContext): TaskStatus { + const gameController = context.runtime.getBlackboardValue('gameController'); + const player = context.runtime.getBlackboardValue('player'); + + const damage = BindingHelper.getValue(context, 'damage', 10); + gameController?.attack(player, damage); + + return TaskStatus.Success; +} +``` + +### 方式3: 通过Entity组件 + +访问Entity上的其他组件: + +```typescript +execute(context: NodeExecutionContext): TaskStatus { + const { entity } = context; + + // 获取实体上的其他组件 + const transform = entity.getComponent(Transform); + const animator = entity.getComponent(Animator); + + if (animator) { + const animName = BindingHelper.getValue(context, 'animationName', ''); + animator.play(animName); + } - gameController?.attack(player, this.damage); return TaskStatus.Success; } ``` ## 最佳实践 -### 1. 保持动作简单 +### 1. 保持执行器无状态 -每个动作组件应该只做一件事: +执行器实例在所有节点间共享,不要在执行器中存储状态: + +```typescript +// 错误: 状态存储在执行器中 +export class BadAction implements INodeExecutor { + private startTime = 0; // 错误!多个节点会共享这个值 + + execute(context: NodeExecutionContext): TaskStatus { + this.startTime = context.totalTime; // 错误! + 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; + } +} +``` + +### 2. 使用BindingHelper获取配置值 + +始终使用BindingHelper而不是直接访问nodeData.config: + +```typescript +// 错误: 直接访问config,不支持黑板绑定 +execute(context: NodeExecutionContext): TaskStatus { + const damage = context.nodeData.config.damage; // 错误! +} + +// 正确: 使用BindingHelper,自动处理黑板绑定 +execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(context, 'damage', 10); // 正确! +} +``` + +### 3. 为配置参数标记supportBinding + +需要动态值的参数应支持黑板绑定: + +```typescript +configSchema: { + damage: { + type: 'number', + default: 10, + supportBinding: true // 允许绑定到黑板变量 + }, + maxRetries: { + type: 'number', + default: 3, + supportBinding: false // 固定配置,不需要绑定 + } +} +``` + +### 4. 单一职责原则 + +每个执行器只做一件事: ```typescript // 好的做法 -class AttackAction { } // 只负责攻击 -class MoveAction { } // 只负责移动 -class PlayAnimationAction { } // 只负责播放动画 +export class AttackAction { } // 只负责攻击 +export class MoveAction { } // 只负责移动 +export class PlayAnimation { } // 只负责播放动画 // 不好的做法 -class AttackAndMoveAndPlayAnimation { } // 做太多事情 +export class AttackAndMoveAndAnimate { } // 做太多事情 ``` -### 2. 使用事件解耦 - -动作不应该直接调用游戏逻辑,而是通过事件: +### 5. 提供合理的默认值 ```typescript -// 好的做法 -execute(...): TaskStatus { - entity.scene?.eventSystem.emit('ai:attack', data); - return TaskStatus.Success; -} - -// 不好的做法 -execute(...): TaskStatus { - // 直接调用游戏代码,导致耦合 - GameManager.instance.battle.performAttack(...); - return TaskStatus.Success; +configSchema: { + damage: { + type: 'number', + default: 10, // 合理的默认值 + min: 0, + max: 999 + } } ``` -### 3. 参数使用黑板变量 - -需要动态的值应该从黑板读取: +### 6. 添加详细的描述 ```typescript -@BehaviorProperty({ - label: '目标变量', - type: 'blackboard' // 让策划选择黑板变量 -}) -targetVar: string = 'target'; - -execute(...): TaskStatus { - const target = blackboard?.getValue(this.targetVar); - // 使用target... -} -``` - -### 4. 提供合理的默认值 - -```typescript -@BehaviorProperty({ - label: '伤害值', - type: 'number', - min: 0, - max: 100 -}) -@Serialize() -damage: number = 10; // 合理的默认值 -``` - -### 5. 添加详细的描述 - -```typescript -@BehaviorNode({ +@NodeExecutorMetadata({ + implementationType: 'AttackAction', displayName: '攻击目标', - description: '对黑板中的目标造成伤害,如果目标不存在则失败' // 清晰的描述 -}) -@BehaviorProperty({ - label: '伤害值', - description: '每次攻击造成的伤害值' // 参数说明 + description: '对黑板中的目标造成伤害,如果目标不存在则失败', // 清晰的描述 + configSchema: { + damage: { + type: 'number', + default: 10, + description: '每次攻击造成的伤害值' // 参数说明 + } + } }) ``` +### 7. 正确实现reset方法 + +如果节点使用了状态,必须实现reset方法: + +```typescript +export class TimedAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + if (!context.state.startTime) { + context.state.startTime = context.totalTime; + } + + if (context.totalTime - context.state.startTime >= 3.0) { + return TaskStatus.Success; + } + + return TaskStatus.Running; + } + + // 必须重置状态 + reset(context: NodeExecutionContext): void { + context.state.startTime = undefined; + } +} +``` + +### 8. 装饰器节点要重置子节点 + +装饰器节点在reset时要重置子节点状态: + +```typescript +export class MyDecorator implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + if (!context.nodeData.children || context.nodeData.children.length === 0) { + return TaskStatus.Failure; + } + + const childId = context.nodeData.children[0]; + return context.executeChild(childId); + } + + reset(context: NodeExecutionContext): void { + // 重置自己的状态 + context.state.customData = undefined; + + // 重置子节点状态 + if (context.nodeData.children && context.nodeData.children.length > 0) { + context.runtime.resetNodeState(context.nodeData.children[0]); + } + } +} +``` + ## 调试技巧 ### 添加日志 ```typescript -execute(...): TaskStatus { - console.log(`[AttackAction] 攻击目标,伤害=${this.damage}`); +execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(context, 'damage', 10); + console.log(`[AttackAction] 执行攻击, 节点ID=${context.nodeData.id}, 伤害=${damage}`); + // ... } ``` -### 使用黑板监控 +### 监控黑板状态 ```typescript -execute(...): TaskStatus { - console.log('黑板状态:', blackboard?.getAllVariables()); +execute(context: NodeExecutionContext): TaskStatus { + // 输出所有黑板变量 + const allVars = context.runtime.getAllBlackboardVariables(); + console.log('黑板状态:', allVars); + + // ... +} +``` + +### 检查绑定状态 + +```typescript +execute(context: NodeExecutionContext): TaskStatus { + if (BindingHelper.hasBinding(context, 'damage')) { + const key = BindingHelper.getBindingKey(context, 'damage'); + const value = context.runtime.getBlackboardValue(key); + console.log(`damage绑定到 ${key}, 值为 ${value}`); + } else { + console.log('damage使用配置值'); + } + + // ... +} +``` + +### 跟踪执行路径 + +```typescript +execute(context: NodeExecutionContext): TaskStatus { + console.log(`执行节点: ${context.nodeData.name} (${context.nodeData.implementationType})`); + console.log(`当前活动节点:`, Array.from(context.runtime.activeNodeIds)); + // ... } ``` ## 常见问题 -### 编辑器中看不到自定义组件? +### 编辑器中看不到自定义执行器? -确保: -1. 组件文件已被导入 -2. 使用了正确的装饰器(`@BehaviorNode`、`@ECSComponent`) -3. 类型设置为 `NodeType.Action` +确保: +1. 执行器文件已被导入 +2. 使用了`@NodeExecutorMetadata`装饰器 +3. 装饰器参数正确(implementationType唯一,nodeType正确) +4. 在Core.create()之前导入 -### 参数修改后不生效? +### 属性绑定不生效? -检查: -1. 是否使用了 `@Serialize()` 装饰器 -2. 重新加载资产文件 -3. 清除缓存重启编辑器 +检查: +1. configSchema中设置了`supportBinding: true` +2. 使用`BindingHelper.getValue()`获取值 +3. 黑板变量名拼写正确 +4. 黑板变量已定义 -### 如何支持复杂参数? +### 节点状态没有重置? -对于复杂对象,使用JSON字符串: +检查: +1. 是否实现了`reset()`方法 +2. reset方法中是否清理了所有状态 +3. 装饰器节点是否重置了子节点 + +### 多个节点共享状态? + +问题: 在执行器类中定义了成员变量存储状态 + +解决: 状态必须存储在`context.state`中,而不是执行器实例中 + +### 如何支持复杂配置? + +使用object类型: ```typescript -@BehaviorProperty({ - label: '配置(JSON)', - type: 'code' -}) -@Serialize() -configJson: string = '{}'; +configSchema: { + config: { + type: 'object', + default: { + speed: 5, + maxDistance: 100 + }, + description: '复杂配置对象' + } +} -execute(...): TaskStatus { - const config = JSON.parse(this.configJson); - // 使用config... +// 使用 +execute(context: NodeExecutionContext): TaskStatus { + const config = BindingHelper.getValue<{speed: number, maxDistance: number}>( + context, 'config', { speed: 5, maxDistance: 100 } + ); + + console.log(config.speed, config.maxDistance); } ``` ## 下一步 -- 学习[编辑器工作流](./editor-workflow.md) -- 阅读[最佳实践](./best-practices.md) +- 学习[编辑器工作流](./editor-workflow.md)了解如何在编辑器中使用自定义节点 +- 阅读[最佳实践](./best-practices.md)学习行为树设计模式 +- 查看[高级用法](./advanced-usage.md)了解更多功能 diff --git a/docs/guide/behavior-tree/editor-guide.md b/docs/guide/behavior-tree/editor-guide.md index 9230262f..422a8e9b 100644 --- a/docs/guide/behavior-tree/editor-guide.md +++ b/docs/guide/behavior-tree/editor-guide.md @@ -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)学习如何扩展节点 diff --git a/docs/guide/behavior-tree/editor-workflow.md b/docs/guide/behavior-tree/editor-workflow.md index 3da70a86..b5dccc86 100644 --- a/docs/guide/behavior-tree/editor-workflow.md +++ b/docs/guide/behavior-tree/editor-workflow.md @@ -55,17 +55,19 @@ Root: Selector └── Idle (Action) ``` -## 在游戏中加载 +## 在游戏中使用 -### 加载JSON资产 +### 使用Builder API创建 + +推荐使用Builder API在代码中创建行为树: ```typescript import { Core, Scene } from '@esengine/ecs-framework'; import { BehaviorTreePlugin, - BehaviorTreeAssetSerializer, - BehaviorTreeAssetLoader, - BehaviorTreeStarter + BehaviorTreeBuilder, + BehaviorTreeStarter, + BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree'; // 初始化 @@ -77,18 +79,28 @@ const scene = new Scene(); plugin.setupScene(scene); Core.setScene(scene); -// 加载行为树 -const jsonString = await loadJsonFromFile('enemy-ai.btree.json'); -const asset = BehaviorTreeAssetSerializer.deserialize(jsonString); -const aiEntity = BehaviorTreeAssetLoader.instantiate(asset, scene); +// 使用Builder创建行为树 +const tree = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) + .defineBlackboardVariable('moveSpeed', 5.0) + .selector('MainBehavior') + .sequence('AttackBranch') + .blackboardExists('target') + .blackboardCompare('health', 30, 'greater') + .log('攻击目标', 'Attack') + .end() + .log('巡逻', 'Patrol') + .end() + .build(); -// 设置黑板初始值 -const blackboard = aiEntity.getComponent(BlackboardComponent); -blackboard?.setValue('health', 100); -blackboard?.setValue('moveSpeed', 5.0); +// 创建实体并启动行为树 +const entity = scene.createEntity('Enemy'); +BehaviorTreeStarter.start(entity, tree); -// 启动AI -BehaviorTreeStarter.start(aiEntity); +// 访问和修改黑板 +const runtime = entity.getComponent(BehaviorTreeRuntimeComponent); +runtime?.setBlackboardValue('target', someTarget); // 游戏循环 setInterval(() => { @@ -96,103 +108,104 @@ setInterval(() => { }, 16); ``` -## 实现自定义动作 +## 实现自定义执行器 -编辑器中的ExecuteAction节点需要在代码中提供实际逻辑。有两种方式: - -### 方式1:通过事件系统(推荐) - -在Action节点中触发事件,在游戏代码中监听: +要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)): ```typescript -// 在编辑器的ExecuteAction节点中 -entity.scene?.eventSystem.emit('ai:attack', { - attacker: entity, - target: blackboard?.getValue('target') -}); -return TaskStatus.Success; -``` +import { + INodeExecutor, + NodeExecutionContext, + BindingHelper, + NodeExecutorMetadata +} from '@esengine/behavior-tree'; +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; -```typescript -// 在游戏代码中监听 -Core.scene.eventSystem.on('ai:attack', (data) => { - const { attacker, target } = data; - // 执行实际的攻击逻辑 - performAttack(attacker, target); -}); -``` - -### 方式2:创建自定义组件 - -创建专用的Action组件(详见[自定义动作](./custom-actions.md)): - -```typescript -import { Component, ECSComponent, Entity } from '@esengine/ecs-framework'; -import { BehaviorNode, BehaviorProperty, NodeType, TaskStatus } from '@esengine/behavior-tree'; - -@BehaviorNode({ +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, displayName: '攻击目标', + description: '对目标造成伤害', category: '战斗', - type: NodeType.Action, - description: '对目标造成伤害' + configSchema: { + damage: { + type: 'number', + default: 10, + supportBinding: true + } + } }) -@ECSComponent('AttackAction') -export class AttackAction extends Component { - @BehaviorProperty({ - label: '伤害值', - type: 'number' - }) - damage: number = 10; +export class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(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设计 diff --git a/docs/guide/behavior-tree/getting-started.md b/docs/guide/behavior-tree/getting-started.md index 85d8b674..21a516d3 100644 --- a/docs/guide/behavior-tree/getting-started.md +++ b/docs/guide/behavior-tree/getting-started.md @@ -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('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('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('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('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)学习如何创建。 diff --git a/docs/guide/behavior-tree/index.md b/docs/guide/behavior-tree/index.md index 5aa681d4..3ebcace6 100644 --- a/docs/guide/behavior-tree/index.md +++ b/docs/guide/behavior-tree/index.md @@ -1,47 +1,41 @@ # 行为树系统 -行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了完全ECS化的行为树系统,所有节点都是实体和组件,充分利用了ECS的性能优势。 +行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。 -## 什么是行为树? +## 什么是行为树? -行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于: +行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于: -- 游戏AI(敌人、NPC行为) +- 游戏AI(敌人、NPC行为) - 状态机的替代方案 - 复杂的决策逻辑 - 可视化的行为设计 ## 核心特性 -### 完全ECS化 -- 所有节点都是实体(Entity) -- 节点属性存储在组件(Component)中 -- 利用ECS的缓存友好特性 -- 支持大规模AI实例 +### Runtime执行器架构 +- 数据与逻辑分离 +- 无状态执行器设计 +- 高性能执行 +- 类型安全 ### 可视化编辑器 - 图形化节点编辑 - 实时预览和调试 - 拖拽式节点创建 -- 支持子树复用 +- 属性连接和绑定 ### 灵活的黑板系统 -- 本地黑板(单个行为树) -- 全局黑板(所有行为树共享) +- 本地黑板(单个行为树) +- 全局黑板(所有行为树共享) - 类型安全的变量访问 -- 支持多种数据类型 +- 支持属性绑定 -### 强大的序列化 -- JSON格式(可读性好) -- 二进制格式(体积小60-70%) -- 跨平台兼容 -- 支持格式转换 - -### 引擎集成 -- Cocos Creator 支持 -- Laya 引擎支持 -- 纯TypeScript实现 -- 易于扩展到其他引擎 +### 插件系统 +- 自动注册机制 +- 装饰器声明元数据 +- 支持多语言 +- 易于扩展 ## 文档导航 @@ -55,87 +49,149 @@ - **[编辑器使用指南](./editor-guide.md)** - 可视化创建行为树 - **[编辑器工作流](./editor-workflow.md)** - 完整的开发流程 +### 资源管理 + +- **[资产管理](./asset-management.md)** - 加载、管理和复用行为树资产、使用子树 + ### 引擎集成 - **[Cocos Creator 集成](./cocos-integration.md)** - 在 Cocos Creator 中使用行为树 - **[Laya 引擎集成](./laya-integration.md)** - 在 Laya 中使用行为树 +- **[Node.js 服务端使用](./nodejs-usage.md)** - 在服务器、聊天机器人等场景中使用行为树 ### 高级主题 -- **[高级用法](./advanced-usage.md)** - 子树、异步加载、性能优化 -- **[自定义动作](./custom-actions.md)** - 创建自定义行为节点 +- **[高级用法](./advanced-usage.md)** - 性能优化、调试技巧 +- **[自定义节点执行器](./custom-actions.md)** - 创建自定义行为节点 - **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧 ## 快速示例 -### 代码方式创建 +### 使用Builder创建 ```typescript -import { Scene } from '@esengine/ecs-framework'; +import { Core, Scene } from '@esengine/ecs-framework'; import { BehaviorTreeBuilder, BehaviorTreeStarter, - BlackboardValueType, - TaskStatus + BehaviorTreePlugin } from '@esengine/behavior-tree'; -const scene = new Scene(); +// 初始化 +Core.create(); +const plugin = new BehaviorTreePlugin(); +await Core.installPlugin(plugin); -// 创建敌人AI -const enemyAI = BehaviorTreeBuilder.create(scene, 'EnemyAI') - .blackboard() - .defineVariable('health', BlackboardValueType.Number, 100) - .defineVariable('target', BlackboardValueType.Object, null) - .endBlackboard() +const scene = new Scene(); +plugin.setupScene(scene); +Core.setScene(scene); + +// 创建行为树 +const enemyAI = BehaviorTreeBuilder.create('EnemyAI') + .defineBlackboardVariable('health', 100) + .defineBlackboardVariable('target', null) .selector('MainBehavior') - // 如果生命值高,则攻击 + // 如果生命值高,则攻击 .sequence('AttackBranch') - .compareBlackboardValue('health', CompareOperator.Greater, 50) - .action('Attack', () => { - console.log('Attacking player'); - return TaskStatus.Success; - }) + .blackboardCompare('health', 50, 'greater') + .log('攻击玩家', 'Attack') .end() // 否则逃跑 - .action('Flee', () => { - console.log('Fleeing from battle'); - return TaskStatus.Success; - }) + .log('逃离战斗', 'Flee') .end() .build(); // 启动AI -BehaviorTreeStarter.start(enemyAI); +const entity = scene.createEntity('Enemy'); +BehaviorTreeStarter.start(entity, enemyAI); ``` -### 编辑器方式创建 +### 使用编辑器创建 1. 打开行为树编辑器 2. 创建新的行为树资产 3. 拖拽节点到画布 -4. 配置节点属性 -5. 保存并导出 -6. 在代码中加载使用 +4. 配置节点属性和连接 +5. 保存并在代码中使用 + +## 架构说明 + +### Runtime执行器架构 + +本框架采用Runtime执行器架构,将节点定义和执行逻辑分离: + +**核心组件:** +- `BehaviorTreeData`: 纯数据结构,描述行为树 +- `BehaviorTreeRuntimeComponent`: 运行时组件,管理状态和黑板 +- `BehaviorTreeExecutionSystem`: 执行系统,驱动行为树运行 +- `INodeExecutor`: 节点执行器接口 +- `NodeExecutionContext`: 执行上下文 + +**优势:** +- 数据与逻辑分离,易于序列化 +- 执行器无状态,可复用 +- 类型安全,编译时检查 +- 高性能执行 + +### 自定义执行器 + +创建自定义节点非常简单: ```typescript -// 加载编辑器创建的行为树 -const asset = await loadBehaviorTree('enemy-ai.btree.json'); -const ai = BehaviorTreeAssetLoader.instantiate(asset, scene); -BehaviorTreeStarter.start(ai); +import { + INodeExecutor, + NodeExecutionContext, + BindingHelper, + NodeExecutorMetadata +} from '@esengine/behavior-tree'; +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; + +@NodeExecutorMetadata({ + implementationType: 'AttackAction', + nodeType: NodeType.Action, + displayName: '攻击', + description: '攻击目标', + category: '战斗', + configSchema: { + damage: { + type: 'number', + default: 10, + supportBinding: true + } + } +}) +export class AttackAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const damage = BindingHelper.getValue(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) - 加入社区讨论 +- 参考文档中的完整代码示例 diff --git a/docs/guide/behavior-tree/laya-integration.md b/docs/guide/behavior-tree/laya-integration.md index b9026001..0d7e59ab 100644 --- a/docs/guide/behavior-tree/laya-integration.md +++ b/docs/guide/behavior-tree/laya-integration.md @@ -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); } } ``` diff --git a/docs/guide/behavior-tree/nodejs-usage.md b/docs/guide/behavior-tree/nodejs-usage.md new file mode 100644 index 00000000..d83b21e7 --- /dev/null +++ b/docs/guide/behavior-tree/nodejs-usage.md @@ -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('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('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 = new Map(); + + createRoom(roomId: string): GameRoom { + const room = new GameRoom(roomId); + this.rooms.set(roomId, room); + return room; + } + + getRoom(roomId: string): GameRoom | undefined { + return this.rooms.get(roomId); + } + + destroyRoom(roomId: string) { + const room = this.rooms.get(roomId); + if (room) { + room.destroy(); + this.rooms.delete(roomId); + } + } + + update(deltaTime: number) { + this.rooms.forEach(room => room.update(deltaTime)); + } +} + +// 主程序 +async function startGameServer() { + // 初始化 + Core.create(); + const plugin = new BehaviorTreePlugin(); + await Core.installPlugin(plugin); + + // 预加载怪物AI(所有房间共享) + const assetManager = Core.services.resolve(BehaviorTreeAssetManager); + const monsterAI = BehaviorTreeBuilder.create('MonsterAI') + .defineBlackboardVariable('health', 100) + .selector('Behavior') + .log('攻击玩家') + .end() + .build(); + assetManager.loadAsset(monsterAI); + + // 创建房间管理器 + const roomManager = new RoomManager(); + + // 模拟房间创建 + roomManager.createRoom('room_1'); + roomManager.createRoom('room_2'); + + // 服务器主循环(60 TPS) + setInterval(() => { + roomManager.update(1/60); + }, 1000 / 60); + + console.log('游戏服务器已启动'); +} + +startGameServer(); +``` + +## 性能优化 + +### 1. 控制更新频率 + +```typescript +// 不同类型的AI使用不同的更新频率 +class AIManager { + private importantAIs: Entity[] = []; // Boss等重要AI,60 TPS + private normalAIs: Entity[] = []; // 普通敌人,20 TPS + private backgroundAIs: Entity[] = []; // 背景NPC,5 TPS + + update() { + // 重要AI每帧更新 + this.updateAIs(this.importantAIs, 1/60); + + // 普通AI每3帧更新一次 + if (frameCount % 3 === 0) { + this.updateAIs(this.normalAIs, 3/60); + } + + // 背景AI每12帧更新一次 + if (frameCount % 12 === 0) { + this.updateAIs(this.backgroundAIs, 12/60); + } + } +} +``` + +### 2. 资源管理 + +```typescript +// 使用资产管理器避免重复创建 +const assetManager = Core.services.resolve(BehaviorTreeAssetManager); + +// 预加载所有AI +const enemyAI = BehaviorTreeBuilder.create('EnemyAI').build(); +const bossAI = BehaviorTreeBuilder.create('BossAI').build(); + +assetManager.loadAsset(enemyAI); +assetManager.loadAsset(bossAI); + +// 创建1000个敌人,但只使用1份BehaviorTreeData +for (let i = 0; i < 1000; i++) { + const enemy = scene.createEntity(`Enemy${i}`); + const ai = assetManager.getAsset('EnemyAI')!; + BehaviorTreeStarter.start(enemy, ai); +} +``` + +### 3. 使用对象池 + +```typescript +class EntityPool { + private pool: Entity[] = []; + private active: Entity[] = []; + + spawn(scene: Scene, treeId: string): Entity { + let entity = this.pool.pop(); + + if (!entity) { + entity = scene.createEntity(); + const tree = assetManager.getAsset(treeId)!; + BehaviorTreeStarter.start(entity, tree); + } else { + BehaviorTreeStarter.restart(entity); + } + + this.active.push(entity); + return entity; + } + + recycle(entity: Entity) { + BehaviorTreeStarter.pause(entity); + const index = this.active.indexOf(entity); + if (index >= 0) { + this.active.splice(index, 1); + this.pool.push(entity); + } + } +} +``` + +## 最佳实践 + +### 1. 使用环境变量控制调试 + +```typescript +const DEBUG = process.env.NODE_ENV === 'development'; + +const aiTree = BehaviorTreeBuilder.create('AI') + .selector('Main') + .when(DEBUG, builder => + builder.log('调试信息:开始AI逻辑') + ) + // AI 逻辑... + .end() + .build(); +``` + +### 2. 错误处理 + +```typescript +try { + const tree = BehaviorTreeBuilder.create('AI') + // ... 构建逻辑 + .build(); + + assetManager.loadAsset(tree); + BehaviorTreeStarter.start(entity, tree); +} catch (error) { + console.error('启动AI失败:', error); + // 使用默认AI或进行降级处理 +} +``` + +### 3. 监控和日志 + +```typescript +// 定期输出AI状态 +setInterval(() => { + const assetManager = Core.services.resolve(BehaviorTreeAssetManager); + const count = assetManager.getAssetCount(); + const entities = scene.getEntitiesFor(Matcher.empty().all(BehaviorTreeRuntimeComponent)); + + console.log(`[AI监控] 行为树资产: ${count}, 活跃实体: ${entities.length}`); +}, 10000); +``` + +## 常见问题 + +### 如何与 Express/Koa 等框架集成? + +```typescript +import express from 'express'; +import { Core, Scene } from '@esengine/ecs-framework'; + +const app = express(); +const scene = new Scene(); + +// 在单独的循环中更新ECS +setInterval(() => { + Core.update(0.016); +}, 16); + +app.post('/npc/:id/interact', (req, res) => { + const npcId = req.params.id; + const npc = scene.findEntity(npcId); + + if (npc) { + const runtime = npc.getComponent(BehaviorTreeRuntimeComponent); + runtime?.setBlackboardValue('playerRequest', req.body); + + res.json({ success: true }); + } else { + res.status(404).json({ error: 'NPC not found' }); + } +}); + +app.listen(3000); +``` + +### 如何持久化行为树状态? + +```typescript +// 保存状态 +function saveAIState(entity: Entity) { + const runtime = entity.getComponent(BehaviorTreeRuntimeComponent); + if (runtime) { + return { + treeId: runtime.treeId, + blackboard: runtime.getAllBlackboardVariables(), + activeNodes: Array.from(runtime.activeNodeIds) + }; + } +} + +// 恢复状态 +function loadAIState(entity: Entity, savedState: any) { + const runtime = entity.getComponent(BehaviorTreeRuntimeComponent); + if (runtime) { + // 恢复黑板变量 + Object.entries(savedState.blackboard).forEach(([key, value]) => { + runtime.setBlackboardValue(key, value); + }); + } +} +``` + +## 下一步 + +- 查看[资产管理](./asset-management.md)了解资源加载和子树 +- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为 +- 阅读[最佳实践](./best-practices.md)优化你的服务端AI diff --git a/packages/behavior-tree/src/BehaviorTreeBuilder.ts b/packages/behavior-tree/src/BehaviorTreeBuilder.ts index 53d89ea7..4c8e6c61 100644 --- a/packages/behavior-tree/src/BehaviorTreeBuilder.ts +++ b/packages/behavior-tree/src/BehaviorTreeBuilder.ts @@ -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 = {}): 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 = {}): 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 = {}): 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 = {}): 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++}`; } } diff --git a/packages/behavior-tree/src/BehaviorTreePlugin.ts b/packages/behavior-tree/src/BehaviorTreePlugin.ts index 78a9cb4d..a220558e 100644 --- a/packages/behavior-tree/src/BehaviorTreePlugin.ts +++ b/packages/behavior-tree/src/BehaviorTreePlugin.ts @@ -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 { + async install(_core: Core, services: ServiceContainer): Promise { 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 { - // 注销全局黑板服务 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()); } /** diff --git a/packages/behavior-tree/src/BehaviorTreeStarter.ts b/packages/behavior-tree/src/BehaviorTreeStarter.ts index 8fce7363..a060bdd4 100644 --- a/packages/behavior-tree/src/BehaviorTreeStarter.ts +++ b/packages/behavior-tree/src/BehaviorTreeStarter.ts @@ -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; + } } } diff --git a/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts b/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts deleted file mode 100644 index 965d2812..00000000 --- a/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts +++ /dev/null @@ -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 = {}; - - /** 编译后的函数(不序列化) */ - @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; - } -} diff --git a/packages/behavior-tree/src/Components/Actions/LogAction.ts b/packages/behavior-tree/src/Components/Actions/LogAction.ts deleted file mode 100644 index ea6c20ec..00000000 --- a/packages/behavior-tree/src/Components/Actions/LogAction.ts +++ /dev/null @@ -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; -} diff --git a/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts b/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts deleted file mode 100644 index 9b86afc8..00000000 --- a/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts +++ /dev/null @@ -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; -} diff --git a/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts b/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts deleted file mode 100644 index c45ef7e2..00000000 --- a/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts +++ /dev/null @@ -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; -} diff --git a/packages/behavior-tree/src/Components/Actions/WaitAction.ts b/packages/behavior-tree/src/Components/Actions/WaitAction.ts deleted file mode 100644 index a730816d..00000000 --- a/packages/behavior-tree/src/Components/Actions/WaitAction.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/ActiveNode.ts b/packages/behavior-tree/src/Components/ActiveNode.ts deleted file mode 100644 index ff16766d..00000000 --- a/packages/behavior-tree/src/Components/ActiveNode.ts +++ /dev/null @@ -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 { - // 标记组件,无需数据字段 -} diff --git a/packages/behavior-tree/src/Components/AssetMetadata.ts b/packages/behavior-tree/src/Components/AssetMetadata.ts deleted file mode 100644 index 71b6f5b9..00000000 --- a/packages/behavior-tree/src/Components/AssetMetadata.ts +++ /dev/null @@ -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(); - } -} diff --git a/packages/behavior-tree/src/Components/BehaviorTreeNode.ts b/packages/behavior-tree/src/Components/BehaviorTreeNode.ts deleted file mode 100644 index 9c35ca28..00000000 --- a/packages/behavior-tree/src/Components/BehaviorTreeNode.ts +++ /dev/null @@ -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(); - } -} diff --git a/packages/behavior-tree/src/Components/BlackboardComponent.ts b/packages/behavior-tree/src/Components/BlackboardComponent.ts deleted file mode 100644 index 02a8f815..00000000 --- a/packages/behavior-tree/src/Components/BlackboardComponent.ts +++ /dev/null @@ -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 = 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(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(name); - } - - return undefined; - } - - /** - * 获取本地变量值(不查找全局) - */ - getLocalValue(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); - } -} diff --git a/packages/behavior-tree/src/Components/CompositeNodeComponent.ts b/packages/behavior-tree/src/Components/CompositeNodeComponent.ts deleted file mode 100644 index d122203b..00000000 --- a/packages/behavior-tree/src/Components/CompositeNodeComponent.ts +++ /dev/null @@ -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 = []; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/ParallelNode.ts b/packages/behavior-tree/src/Components/Composites/ParallelNode.ts deleted file mode 100644 index 1431ac6d..00000000 --- a/packages/behavior-tree/src/Components/Composites/ParallelNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts b/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts deleted file mode 100644 index 0568b2e5..00000000 --- a/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts b/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts deleted file mode 100644 index c228c03c..00000000 --- a/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts b/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts deleted file mode 100644 index 7fd661a5..00000000 --- a/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/RootNode.ts b/packages/behavior-tree/src/Components/Composites/RootNode.ts deleted file mode 100644 index c1171faf..00000000 --- a/packages/behavior-tree/src/Components/Composites/RootNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/SelectorNode.ts b/packages/behavior-tree/src/Components/Composites/SelectorNode.ts deleted file mode 100644 index 815c5cfe..00000000 --- a/packages/behavior-tree/src/Components/Composites/SelectorNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/SequenceNode.ts b/packages/behavior-tree/src/Components/Composites/SequenceNode.ts deleted file mode 100644 index 781d2880..00000000 --- a/packages/behavior-tree/src/Components/Composites/SequenceNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts b/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts deleted file mode 100644 index ff09e71d..00000000 --- a/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts b/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts deleted file mode 100644 index 92682f82..00000000 --- a/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts +++ /dev/null @@ -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; -} diff --git a/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts b/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts deleted file mode 100644 index cdc0f2d4..00000000 --- a/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts +++ /dev/null @@ -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; -} diff --git a/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts b/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts deleted file mode 100644 index 82fc630e..00000000 --- a/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts +++ /dev/null @@ -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 = {}; - - @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; - } -} diff --git a/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts b/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts deleted file mode 100644 index a7b8aa50..00000000 --- a/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts b/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts deleted file mode 100644 index 14e92898..00000000 --- a/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts +++ /dev/null @@ -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; - -} diff --git a/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts b/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts deleted file mode 100644 index e3dde040..00000000 --- a/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts b/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts deleted file mode 100644 index b276ce0b..00000000 --- a/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts b/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts deleted file mode 100644 index f8434987..00000000 --- a/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts b/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts deleted file mode 100644 index 01287be3..00000000 --- a/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/InverterNode.ts b/packages/behavior-tree/src/Components/Decorators/InverterNode.ts deleted file mode 100644 index d13cea6d..00000000 --- a/packages/behavior-tree/src/Components/Decorators/InverterNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts b/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts deleted file mode 100644 index 7e05b598..00000000 --- a/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts b/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts deleted file mode 100644 index 737d2c07..00000000 --- a/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts b/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts deleted file mode 100644 index 133d88b3..00000000 --- a/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts b/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts deleted file mode 100644 index 6fcbe988..00000000 --- a/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/behavior-tree/src/Components/LogOutput.ts b/packages/behavior-tree/src/Components/LogOutput.ts deleted file mode 100644 index 7232f0dc..00000000 --- a/packages/behavior-tree/src/Components/LogOutput.ts +++ /dev/null @@ -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 = []; - } -} diff --git a/packages/behavior-tree/src/Components/PropertyBindings.ts b/packages/behavior-tree/src/Components/PropertyBindings.ts deleted file mode 100644 index bb34c170..00000000 --- a/packages/behavior-tree/src/Components/PropertyBindings.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -/** - * 属性绑定组件 - * 记录节点属性到黑板变量的绑定关系 - */ -export class PropertyBindings extends Component { - /** - * 属性绑定映射 - * key: 属性名称 (如 'message') - * value: 黑板变量名 (如 'test1') - */ - bindings: Map = 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(); - } -} diff --git a/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts b/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts deleted file mode 100644 index aaa48689..00000000 --- a/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts +++ /dev/null @@ -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(); - - 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 (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) { - 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 }; diff --git a/packages/behavior-tree/src/RegisterAllNodes.ts b/packages/behavior-tree/src/RegisterAllNodes.ts deleted file mode 100644 index dc697d36..00000000 --- a/packages/behavior-tree/src/RegisterAllNodes.ts +++ /dev/null @@ -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 被执行 -} diff --git a/packages/behavior-tree/src/Runtime/BehaviorTreeAssetManager.ts b/packages/behavior-tree/src/Runtime/BehaviorTreeAssetManager.ts new file mode 100644 index 00000000..8f94f4e5 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/BehaviorTreeAssetManager.ts @@ -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 = 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(); + } +} diff --git a/packages/behavior-tree/src/Runtime/BehaviorTreeData.ts b/packages/behavior-tree/src/Runtime/BehaviorTreeData.ts new file mode 100644 index 00000000..f5b5d63b --- /dev/null +++ b/packages/behavior-tree/src/Runtime/BehaviorTreeData.ts @@ -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; + + /** 属性到黑板变量的绑定映射 */ + bindings?: Record; + + /** 中止类型(条件装饰器使用) */ + abortType?: AbortType; +} + +/** + * 行为树定义(可共享的Asset) + */ +export interface BehaviorTreeData { + /** 树ID */ + id: string; + + /** 树名称 */ + name: string; + + /** 根节点ID */ + rootNodeId: string; + + /** 所有节点(扁平化存储) */ + nodes: Map; + + /** 黑板变量定义 */ + blackboardVariables?: Map; +} + +/** + * 节点运行时状态 + * + * 每个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 + }; +} diff --git a/packages/behavior-tree/src/Runtime/BehaviorTreeExecutionSystem.ts b/packages/behavior-tree/src/Runtime/BehaviorTreeExecutionSystem.ts new file mode 100644 index 00000000..794a3a79 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/BehaviorTreeExecutionSystem.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/BehaviorTreeRuntimeComponent.ts b/packages/behavior-tree/src/Runtime/BehaviorTreeRuntimeComponent.ts new file mode 100644 index 00000000..113ce64a --- /dev/null +++ b/packages/behavior-tree/src/Runtime/BehaviorTreeRuntimeComponent.ts @@ -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; + 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 = new Map(); + + /** + * 黑板数据(该Entity独立的数据) + * 不序列化,通过初始化设置 + */ + @IgnoreSerialization() + private blackboard: Map = new Map(); + + /** + * 黑板观察者列表 + */ + @IgnoreSerialization() + private blackboardObservers: Map = new Map(); + + /** + * 当前激活的节点ID列表(用于调试) + */ + @IgnoreSerialization() + activeNodeIds: Set = new Set(); + + /** + * 标记是否需要在下一个tick重置状态 + */ + @IgnoreSerialization() + needsReset: boolean = false; + + /** + * 需要中止的节点ID列表 + */ + @IgnoreSerialization() + nodesToAbort: Set = 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(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): 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(); + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/AlwaysFailExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/AlwaysFailExecutor.ts new file mode 100644 index 00000000..b158d627 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/AlwaysFailExecutor.ts @@ -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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/AlwaysSucceedExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/AlwaysSucceedExecutor.ts new file mode 100644 index 00000000..eb429612 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/AlwaysSucceedExecutor.ts @@ -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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/BlackboardCompare.ts b/packages/behavior-tree/src/Runtime/Executors/BlackboardCompare.ts new file mode 100644 index 00000000..66afeaac --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/BlackboardCompare.ts @@ -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(context, 'key', ''); + const compareValue = BindingHelper.getValue(context, 'compareValue'); + const operator = BindingHelper.getValue(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; + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/BlackboardExists.ts b/packages/behavior-tree/src/Runtime/Executors/BlackboardExists.ts new file mode 100644 index 00000000..01b29640 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/BlackboardExists.ts @@ -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(context, 'key', ''); + const checkNull = BindingHelper.getValue(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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ConditionalExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/ConditionalExecutor.ts new file mode 100644 index 00000000..8dc4db01 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ConditionalExecutor.ts @@ -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(context, 'blackboardKey', ''); + const expectedValue = BindingHelper.getValue(context, 'expectedValue'); + const operator = BindingHelper.getValue(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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/CooldownExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/CooldownExecutor.ts new file mode 100644 index 00000000..9e74327f --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/CooldownExecutor.ts @@ -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(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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ExecuteAction.ts b/packages/behavior-tree/src/Runtime/Executors/ExecuteAction.ts new file mode 100644 index 00000000..64ea3826 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ExecuteAction.ts @@ -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(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; + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ExecuteCondition.ts b/packages/behavior-tree/src/Runtime/Executors/ExecuteCondition.ts new file mode 100644 index 00000000..6b778f69 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ExecuteCondition.ts @@ -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(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; + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/InverterExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/InverterExecutor.ts new file mode 100644 index 00000000..ded98fef --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/InverterExecutor.ts @@ -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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/LogAction.ts b/packages/behavior-tree/src/Runtime/Executors/LogAction.ts new file mode 100644 index 00000000..418fec6f --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/LogAction.ts @@ -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(context, 'message', ''); + const logLevel = BindingHelper.getValue(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; + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ModifyBlackboardValue.ts b/packages/behavior-tree/src/Runtime/Executors/ModifyBlackboardValue.ts new file mode 100644 index 00000000..ec5f5c2c --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ModifyBlackboardValue.ts @@ -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(context, 'key', ''); + const operation = BindingHelper.getValue(context, 'operation', 'add'); + const value = BindingHelper.getValue(context, 'value', 0); + + if (!key) { + return TaskStatus.Failure; + } + + const currentValue = runtime.getBlackboardValue(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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ParallelExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/ParallelExecutor.ts new file mode 100644 index 00000000..40d6d552 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ParallelExecutor.ts @@ -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(context, 'successPolicy', 'all'); + const failurePolicy = BindingHelper.getValue(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); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ParallelSelectorExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/ParallelSelectorExecutor.ts new file mode 100644 index 00000000..0dbefcff --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ParallelSelectorExecutor.ts @@ -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(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); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/RandomProbability.ts b/packages/behavior-tree/src/Runtime/Executors/RandomProbability.ts new file mode 100644 index 00000000..4933d926 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/RandomProbability.ts @@ -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(context, 'probability', 0.5); + + const clampedProbability = Math.max(0, Math.min(1, probability)); + + if (Math.random() < clampedProbability) { + return TaskStatus.Success; + } + + return TaskStatus.Failure; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/RandomSelectorExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/RandomSelectorExecutor.ts new file mode 100644 index 00000000..021be70a --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/RandomSelectorExecutor.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/RandomSequenceExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/RandomSequenceExecutor.ts new file mode 100644 index 00000000..270bde3f --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/RandomSequenceExecutor.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/RepeaterExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/RepeaterExecutor.ts new file mode 100644 index 00000000..e1bb393b --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/RepeaterExecutor.ts @@ -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(context, 'repeatCount', 1); + const endOnFailure = BindingHelper.getValue(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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/SelectorExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/SelectorExecutor.ts new file mode 100644 index 00000000..88e90a87 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/SelectorExecutor.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/SequenceExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/SequenceExecutor.ts new file mode 100644 index 00000000..90f0c815 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/SequenceExecutor.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts b/packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts new file mode 100644 index 00000000..2ad762fe --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts @@ -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 = 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(context, 'serviceName', ''); + const tickInterval = BindingHelper.getValue(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(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 }; diff --git a/packages/behavior-tree/src/Runtime/Executors/SetBlackboardValue.ts b/packages/behavior-tree/src/Runtime/Executors/SetBlackboardValue.ts new file mode 100644 index 00000000..afdc2839 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/SetBlackboardValue.ts @@ -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(context, 'key', ''); + const value = BindingHelper.getValue(context, 'value'); + + if (!key) { + return TaskStatus.Failure; + } + + runtime.setBlackboardValue(key, value); + + return TaskStatus.Success; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts new file mode 100644 index 00000000..ced0f247 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts @@ -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(context, 'treeAssetId', ''); + const shareBlackboard = BindingHelper.getValue(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(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); + } + } + } + } + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/TimeoutExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/TimeoutExecutor.ts new file mode 100644 index 00000000..17d4548a --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/TimeoutExecutor.ts @@ -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(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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/UntilFailExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/UntilFailExecutor.ts new file mode 100644 index 00000000..7b8dd085 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/UntilFailExecutor.ts @@ -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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/UntilSuccessExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/UntilSuccessExecutor.ts new file mode 100644 index 00000000..5d0221e3 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/UntilSuccessExecutor.ts @@ -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]!); + } + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/WaitAction.ts b/packages/behavior-tree/src/Runtime/Executors/WaitAction.ts new file mode 100644 index 00000000..2c816710 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/WaitAction.ts @@ -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(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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/WaitActionExecutor.ts b/packages/behavior-tree/src/Runtime/Executors/WaitActionExecutor.ts new file mode 100644 index 00000000..6bde707f --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/WaitActionExecutor.ts @@ -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; + } +} diff --git a/packages/behavior-tree/src/Runtime/Executors/index.ts b/packages/behavior-tree/src/Runtime/Executors/index.ts new file mode 100644 index 00000000..88b21d47 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/Executors/index.ts @@ -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'; diff --git a/packages/behavior-tree/src/Runtime/NodeExecutor.ts b/packages/behavior-tree/src/Runtime/NodeExecutor.ts new file mode 100644 index 00000000..0983d7ce --- /dev/null +++ b/packages/behavior-tree/src/Runtime/NodeExecutor.ts @@ -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( + 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(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 = 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(); + } +} diff --git a/packages/behavior-tree/src/Runtime/NodeMetadata.ts b/packages/behavior-tree/src/Runtime/NodeMetadata.ts new file mode 100644 index 00000000..33e49968 --- /dev/null +++ b/packages/behavior-tree/src/Runtime/NodeMetadata.ts @@ -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; +} + +/** + * 节点元数据注册表 + */ +export class NodeMetadataRegistry { + private static metadataMap: Map = new Map(); + private static executorClassMap: Map = new Map(); + private static executorConstructors: Map 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 any> { + return new Map(this.executorConstructors); + } +} + +/** + * 节点执行器元数据装饰器 + */ +export function NodeExecutorMetadata(metadata: NodeMetadata) { + return function (target: Function) { + NodeMetadataRegistry.register(target, metadata); + }; +} diff --git a/packages/behavior-tree/src/Runtime/index.ts b/packages/behavior-tree/src/Runtime/index.ts new file mode 100644 index 00000000..812ca73c --- /dev/null +++ b/packages/behavior-tree/src/Runtime/index.ts @@ -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'; diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts index 8b21a1cf..b7b54f20 100644 --- a/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts +++ b/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts @@ -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; + 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; } /** diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts deleted file mode 100644 index 975d17bb..00000000 --- a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts +++ /dev/null @@ -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; - - /** - * 是否作为子树实例化 - * 如果为 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(); - 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(); - - // 递归创建实体树 - 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, - entityMap: Map, - 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): 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): 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): 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): 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 - ): 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 - ): 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} 个绑定`); - } -} diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts b/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts deleted file mode 100644 index 8b0dc4c2..00000000 --- a/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts +++ /dev/null @@ -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 { - 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 { - // 需要在 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'); - } -} diff --git a/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts b/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts index 3f153168..5eaddb90 100644 --- a/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts +++ b/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts @@ -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; + 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[] = []; diff --git a/packages/behavior-tree/src/Serialization/NodeTemplates.ts b/packages/behavior-tree/src/Serialization/NodeTemplates.ts index c44e2727..4301d542 100644 --- a/packages/behavior-tree/src/Serialization/NodeTemplates.ts +++ b/packages/behavior-tree/src/Serialization/NodeTemplates.ts @@ -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 = { + 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 + ): 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' }; // 灰色 + } + } } diff --git a/packages/behavior-tree/src/Services/AssetLoadingManager.ts b/packages/behavior-tree/src/Services/AssetLoadingManager.ts deleted file mode 100644 index 924c6434..00000000 --- a/packages/behavior-tree/src/Services/AssetLoadingManager.ts +++ /dev/null @@ -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 = new Map(); - - /** 加载栈(用于循环检测) */ - private loadingStack: Set = new Set(); - - /** 默认配置 */ - private defaultOptions: Required> = { - 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, - 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, - options: Required> - ): Promise { - 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( - promise: Promise, - timeoutMs: number, - message: string - ): Promise { - let timeoutId: NodeJS.Timeout | number; - - const timeoutPromise = new Promise((_, 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 { - 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(); - } -} diff --git a/packages/behavior-tree/src/Services/AssetLoadingTypes.ts b/packages/behavior-tree/src/Services/AssetLoadingTypes.ts deleted file mode 100644 index 36c87ef2..00000000 --- a/packages/behavior-tree/src/Services/AssetLoadingTypes.ts +++ /dev/null @@ -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; - - /** 开始时间 */ - 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; -} - -/** - * 加载进度信息 - */ -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'; - } -} diff --git a/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts b/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts deleted file mode 100644 index 45c64f9e..00000000 --- a/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts +++ /dev/null @@ -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; -} - -/** - * 文件系统资产加载器 - * - * 从文件系统加载行为树资产,支持 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; - private cache: Map = 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 { - // 检查缓存 - 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 { - // 如果在缓存中,直接返回 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 { - 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 { - // 检测运行环境 - 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.' - ); - } - } -} diff --git a/packages/behavior-tree/src/Services/GlobalBlackboardService.ts b/packages/behavior-tree/src/Services/GlobalBlackboardService.ts index ee724ebe..2c05d404 100644 --- a/packages/behavior-tree/src/Services/GlobalBlackboardService.ts +++ b/packages/behavior-tree/src/Services/GlobalBlackboardService.ts @@ -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); } /** diff --git a/packages/behavior-tree/src/Services/IAssetLoader.ts b/packages/behavior-tree/src/Services/IAssetLoader.ts deleted file mode 100644 index c1f5aa5e..00000000 --- a/packages/behavior-tree/src/Services/IAssetLoader.ts +++ /dev/null @@ -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 { - * const response = await fetch(`/api/assets/${assetId}`); - * return response.json(); - * } - * - * async exists(assetId: string): Promise { - * 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; - - /** - * 检查资产是否存在 - * - * @param assetId 资产逻辑ID - * @returns 资产是否存在 - */ - exists(assetId: string): Promise; - - /** - * 预加载资产(可选) - * - * 用于提前加载资产到缓存,减少运行时延迟 - * - * @param assetIds 要预加载的资产ID列表 - */ - preload?(assetIds: string[]): Promise; - - /** - * 卸载资产(可选) - * - * 释放资产占用的内存 - * - * @param assetId 资产ID - */ - unload?(assetId: string): void; -} diff --git a/packages/behavior-tree/src/Services/WorkspaceService.ts b/packages/behavior-tree/src/Services/WorkspaceService.ts deleted file mode 100644 index 36bee450..00000000 --- a/packages/behavior-tree/src/Services/WorkspaceService.ts +++ /dev/null @@ -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; -} - -/** - * 工作区配置 - */ -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 = new Map(); - private assetPathMap: Map = 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()): 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(); - 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(); - } -} diff --git a/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts b/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts deleted file mode 100644 index 6640cf7f..00000000 --- a/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts +++ /dev/null @@ -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'; - } -} diff --git a/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts b/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts deleted file mode 100644 index c520eff4..00000000 --- a/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts +++ /dev/null @@ -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'; - } -} diff --git a/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts b/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts deleted file mode 100644 index d5e0d86a..00000000 --- a/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts +++ /dev/null @@ -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'; - } -} diff --git a/packages/behavior-tree/src/Systems/RootExecutionSystem.ts b/packages/behavior-tree/src/Systems/RootExecutionSystem.ts deleted file mode 100644 index 1624160e..00000000 --- a/packages/behavior-tree/src/Systems/RootExecutionSystem.ts +++ /dev/null @@ -1,388 +0,0 @@ -import { EntitySystem, Matcher, Entity, Core } from '@esengine/ecs-framework'; -import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; -import { RootNode } from '../Components/Composites/RootNode'; -import { ActiveNode } from '../Components/ActiveNode'; -import { TaskStatus, NodeType } from '../Types/TaskStatus'; -import { SubTreeNode } from '../Components/Composites/SubTreeNode'; -import { LogOutput } from '../Components/LogOutput'; -import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader'; -import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader'; -import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata'; -import { BlackboardComponent } from '../Components/BlackboardComponent'; - -/** - * 预加载状态 - */ -enum PreloadState { - /** 未开始预加载 */ - NotStarted, - /** 正在预加载 */ - Loading, - /** 预加载完成 */ - Completed, - /** 预加载失败 */ - Failed -} - -/** - * 根节点执行系统 - * - * 专门处理根节点的执行逻辑 - * 根节点的职责: - * 1. 扫描并预加载所有标记为 preload=true 的子树 - * 2. 激活第一个子节点,并根据子节点的状态来设置自己的状态 - * - * updateOrder: 350 (在所有其他执行系统之后) - */ -export class RootExecutionSystem extends EntitySystem { - /** 跟踪每个根节点的预加载状态 */ - private preloadStates: Map = new Map(); - - /** 跟踪预加载任务 */ - private preloadTasks: Map> = new Map(); - - /** AssetLoader 实例 */ - private assetLoader?: FileSystemAssetLoader; - - constructor() { - super(Matcher.empty().all(BehaviorTreeNode, ActiveNode)); - this.updateOrder = 350; - } - - protected override process(entities: readonly Entity[]): void { - for (const entity of entities) { - const node = entity.getComponent(BehaviorTreeNode)!; - - // 只处理根节点 - if (node.nodeType !== NodeType.Composite) { - continue; - } - - // 检查是否是根节点 - if (!entity.hasComponent(RootNode)) { - continue; - } - - this.executeRoot(entity, node); - } - } - - /** - * 执行根节点逻辑 - */ - private executeRoot(entity: Entity, node: BehaviorTreeNode): void { - // 检查预加载状态 - const preloadState = this.preloadStates.get(entity.id) || PreloadState.NotStarted; - - if (preloadState === PreloadState.NotStarted) { - // 开始预加载 - this.startPreload(entity, node); - return; - } else if (preloadState === PreloadState.Loading) { - // 正在预加载,等待 - node.status = TaskStatus.Running; - return; - } else if (preloadState === PreloadState.Failed) { - // 预加载失败,标记为失败 - node.status = TaskStatus.Failure; - entity.removeComponentByType(ActiveNode); - return; - } - - // 预加载完成,执行正常逻辑 - const children = entity.children; - - // 如果没有子节点,标记为成功 - if (children.length === 0) { - node.status = TaskStatus.Success; - return; - } - - // 获取第一个子节点 - const firstChild = children[0]; - const childNode = firstChild.getComponent(BehaviorTreeNode); - - if (!childNode) { - node.status = TaskStatus.Failure; - return; - } - - // 激活第一个子节点(如果还没激活) - if (!firstChild.hasComponent(ActiveNode)) { - firstChild.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; - // 移除根节点的 ActiveNode,结束整个行为树 - entity.removeComponentByType(ActiveNode); - } else if (childNode.status === TaskStatus.Failure) { - node.status = TaskStatus.Failure; - // 移除根节点的 ActiveNode,结束整个行为树 - entity.removeComponentByType(ActiveNode); - } - } - - /** - * 开始预加载子树 - */ - private startPreload(rootEntity: Entity, node: BehaviorTreeNode): void { - // 扫描所有需要预加载的子树节点 - const subTreeNodesToPreload = this.scanSubTreeNodes(rootEntity); - - if (subTreeNodesToPreload.length === 0) { - // 没有需要预加载的子树,直接标记为完成 - this.preloadStates.set(rootEntity.id, PreloadState.Completed); - this.outputLog(rootEntity, '没有需要预加载的子树', 'info'); - return; - } - - // 标记为正在加载 - this.preloadStates.set(rootEntity.id, PreloadState.Loading); - node.status = TaskStatus.Running; - - this.outputLog( - rootEntity, - `开始预加载 ${subTreeNodesToPreload.length} 个子树...`, - 'info' - ); - - // 并行加载所有子树 - const loadTask = this.preloadAllSubTrees(rootEntity, subTreeNodesToPreload); - this.preloadTasks.set(rootEntity.id, loadTask); - - // 异步处理加载结果 - loadTask.then(() => { - this.preloadStates.set(rootEntity.id, PreloadState.Completed); - this.outputLog(rootEntity, '所有子树预加载完成', 'info'); - }).catch(error => { - this.preloadStates.set(rootEntity.id, PreloadState.Failed); - this.outputLog(rootEntity, `子树预加载失败: ${error.message}`, 'error'); - }); - } - - /** - * 扫描所有需要预加载的子树节点 - */ - private scanSubTreeNodes(entity: Entity): Array<{ entity: Entity; subTree: SubTreeNode }> { - const result: Array<{ entity: Entity; subTree: SubTreeNode }> = []; - - // 检查当前实体 - const subTree = entity.getComponent(SubTreeNode); - if (subTree && subTree.preload) { - result.push({ entity, subTree }); - } - - // 递归扫描子节点 - for (const child of entity.children) { - result.push(...this.scanSubTreeNodes(child)); - } - - return result; - } - - /** - * 预加载所有子树 - */ - private async preloadAllSubTrees( - rootEntity: Entity, - subTreeNodes: Array<{ entity: Entity; subTree: SubTreeNode }> - ): Promise { - // 确保 AssetLoader 已初始化 - if (!this.assetLoader) { - try { - this.assetLoader = Core.services.resolve(FileSystemAssetLoader); - } catch (error) { - throw new Error('AssetLoader 未配置,无法预加载子树'); - } - } - - // 并行加载所有子树 - await Promise.all( - subTreeNodes.map(({ entity, subTree }) => - this.preloadSingleSubTree(rootEntity, entity, subTree) - ) - ); - } - - /** - * 预加载单个子树 - */ - private async preloadSingleSubTree( - rootEntity: Entity, - subTreeEntity: Entity, - subTree: SubTreeNode - ): Promise { - try { - this.outputLog(rootEntity, `预加载子树: ${subTree.assetId}`, 'info'); - - // 加载资产 - const asset = await this.assetLoader!.loadBehaviorTree(subTree.assetId); - - // 实例化为 Entity 树(作为子树,跳过 RootNode) - const subTreeRoot = BehaviorTreeAssetLoader.instantiate(asset, this.scene!, { - asSubTree: true - }); - - // 设置子树根实体 - subTree.setSubTreeRoot(subTreeRoot); - - // 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点 - subTreeEntity.addChild(subTreeRoot); - - // 添加资产元数据 - const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata()); - metadata.initialize(subTree.assetId, '1.0.0'); - - // 处理黑板继承 - if (subTree.inheritParentBlackboard) { - this.setupBlackboardInheritance(subTreeEntity, subTreeRoot); - } - - // 输出子树内部结构(用于调试) - this.outputLog(rootEntity, `=== 预加载子树 ${subTree.assetId} 的内部结构 ===`, 'info'); - this.logSubTreeStructure(rootEntity, subTreeRoot, 0); - this.outputLog(rootEntity, `=== 预加载子树结构结束 ===`, 'info'); - - this.outputLog(rootEntity, `✓ 子树 ${subTree.assetId} 预加载完成`, 'info'); - } catch (error: any) { - this.outputLog( - rootEntity, - `✗ 子树 ${subTree.assetId} 预加载失败: ${error.message}`, - 'error' - ); - throw error; - } - } - - /** - * 设置黑板继承 - */ - private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void { - const parentBlackboard = this.findBlackboard(parentEntity); - if (!parentBlackboard) { - return; - } - - // 找到子树的黑板 - const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent); - if (subTreeBlackboard) { - subTreeBlackboard.setUseGlobalBlackboard(true); - } - } - - /** - * 查找黑板组件 - */ - 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 findRootEntity(entity: Entity): Entity | null { - let current: Entity | null = entity; - while (current) { - if (!current.parent) { - return current; - } - current = current.parent; - } - return null; - } - - /** - * 统一的日志输出方法 - */ - 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组件 - const rootEntity = this.findRootEntity(entity); - if (rootEntity) { - const logOutput = rootEntity.getComponent(LogOutput); - if (logOutput) { - logOutput.addMessage(message, level); - } - } - } - - /** - * 递归打印子树结构(用于调试) - */ - private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void { - const indent = ' '.repeat(depth); - const btNode = entity.getComponent(BehaviorTreeNode); - - // 获取节点的具体类型组件 - const allComponents = entity.components.map(c => c.constructor.name); - const nodeTypeComponent = allComponents.find(name => - name !== 'BehaviorTreeNode' && name !== 'ActiveNode' && - name !== 'BlackboardComponent' && name !== 'LogOutput' && - name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata' - ) || 'Unknown'; - - // 构建节点显示名称 - let nodeName = entity.name; - if (nodeTypeComponent !== 'Unknown') { - nodeName = `${nodeName} [${nodeTypeComponent}]`; - } - - this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info'); - - // 递归打印子节点 - if (entity.children.length > 0) { - this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info'); - entity.children.forEach((child: Entity) => { - this.logSubTreeStructure(parentEntity, child, depth + 1); - }); - } - } - - /** - * 清理资源 - */ - protected override onDestroy(): void { - this.preloadStates.clear(); - this.preloadTasks.clear(); - super.onDestroy(); - } - - protected override getLoggerName(): string { - return 'RootExecutionSystem'; - } -} diff --git a/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts b/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts deleted file mode 100644 index c7db83dc..00000000 --- a/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts +++ /dev/null @@ -1,667 +0,0 @@ -import { EntitySystem, Matcher, Entity, Core, createLogger } from '@esengine/ecs-framework'; -import { SubTreeNode } from '../Components/Composites/SubTreeNode'; -import { ActiveNode } from '../Components/ActiveNode'; -import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; -import { TaskStatus } from '../Types/TaskStatus'; -import { IAssetLoader } from '../Services/IAssetLoader'; -import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader'; -import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader'; -import { BlackboardComponent } from '../Components/BlackboardComponent'; -import { LogOutput } from '../Components/LogOutput'; -import { AssetLoadingManager } from '../Services/AssetLoadingManager'; -import { - LoadingState, - LoadingTaskHandle, - CircularDependencyError, - EntityDestroyedError -} from '../Services/AssetLoadingTypes'; -import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata'; - -/** - * SubTree 执行系统 - * - * 处理 SubTree 节点的执行,包括: - * - 子树资产加载 - * - 子树实例化 - * - 黑板继承 - * - 子树执行和状态管理 - * - * updateOrder: 300 (与 CompositeExecutionSystem 同级) - */ -export class SubTreeExecutionSystem extends EntitySystem { - private assetLoader?: IAssetLoader; - private assetLoaderInitialized = false; - private hasLoggedMissingAssetLoader = false; - private loadingManager: AssetLoadingManager; - private loadingTasks: Map = new Map(); - - constructor(loadingManager?: AssetLoadingManager) { - super(Matcher.empty().all(SubTreeNode, ActiveNode, BehaviorTreeNode)); - this.updateOrder = 300; - this.loadingManager = loadingManager || new AssetLoadingManager(); - } - - protected override onInitialize(): void { - // 延迟初始化 AssetLoader,不在这里尝试获取 - // 只在第一次真正需要处理 SubTree 节点时才获取 - } - - protected override process(entities: readonly Entity[]): void { - for (const entity of entities) { - const subTree = entity.getComponent(SubTreeNode)!; - const node = entity.getComponent(BehaviorTreeNode)!; - - this.executeSubTree(entity, subTree, node); - } - } - - /** - * 执行子树节点 - */ - private executeSubTree( - entity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode - ): void { - // 验证配置 - const errors = subTree.validate(); - if (errors.length > 0) { - this.logger.error(`SubTree 节点配置错误: ${errors.join(', ')}`); - node.status = TaskStatus.Failure; - this.completeNode(entity); - return; - } - - // 检查是否已有子树(可能是预加载的) - const existingSubTreeRoot = subTree.getSubTreeRoot(); - if (existingSubTreeRoot) { - const subTreeNode = existingSubTreeRoot.getComponent(BehaviorTreeNode); - - if (subTreeNode) { - const statusName = TaskStatus[subTreeNode.status]; - const hasActive = existingSubTreeRoot.hasComponent(ActiveNode); - this.outputLog( - entity, - `检查预加载子树 ${subTree.assetId}: status=${statusName}, hasActive=${hasActive}`, - 'info' - ); - - // 如果子树还没开始执行(状态是 Invalid),需要激活它 - if (subTreeNode.status === TaskStatus.Invalid) { - this.outputLog(entity, `使用预加载的子树: ${subTree.assetId}`, 'info'); - - // 检查子节点 - this.outputLog(entity, `激活前:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info'); - if (existingSubTreeRoot.children.length > 0) { - const firstChild = existingSubTreeRoot.children[0]; - this.outputLog(entity, ` 第一个子节点: ${firstChild.name}`, 'info'); - } - - // 激活根节点 - if (!existingSubTreeRoot.hasComponent(ActiveNode)) { - existingSubTreeRoot.addComponent(new ActiveNode()); - this.outputLog(entity, `为子树根节点添加 ActiveNode: ${existingSubTreeRoot.name}`, 'info'); - } - - const subTreeRootNode = existingSubTreeRoot.getComponent(BehaviorTreeNode); - if (subTreeRootNode) { - this.outputLog(entity, `设置子树根节点状态: ${existingSubTreeRoot.name} -> Running`, 'info'); - subTreeRootNode.status = TaskStatus.Running; - } - - // 再次检查(验证激活后子节点没有丢失) - this.outputLog(entity, `激活后:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info'); - - this.outputLog(entity, `激活预加载的子树: ${subTree.assetId}`, 'info'); - node.status = TaskStatus.Running; - return; - } - } - - // 子树已激活或已完成,更新状态 - this.updateSubTree(entity, subTree, node); - return; - } - - // 子树未预加载,开始运行时加载 - this.outputLog(entity, `子树未预加载,开始运行时加载: ${subTree.assetId}`, 'info'); - this.loadAndInstantiateSubTree(entity, subTree, node); - } - - /** - * 延迟初始化 AssetLoader - */ - private ensureAssetLoaderInitialized(): boolean { - if (!this.assetLoaderInitialized) { - try { - this.assetLoader = Core.services.resolve(FileSystemAssetLoader); - this.assetLoaderInitialized = true; - this.logger.debug('AssetLoader 已初始化'); - } catch (error) { - this.assetLoaderInitialized = true; - this.assetLoader = undefined; - - // 只在第一次失败时记录警告,避免重复日志 - if (!this.hasLoggedMissingAssetLoader) { - this.logger.warn( - 'AssetLoader 未配置。SubTree 节点需要 AssetLoader 来加载子树资产。\n' + - '如果您在编辑器中,请确保已打开项目并配置了项目路径。\n' + - '如果您在运行时环境,请确保已正确注册 FileSystemAssetLoader 服务。' - ); - this.hasLoggedMissingAssetLoader = true; - } - - return false; - } - } - - return this.assetLoader !== undefined; - } - - /** - * 加载并实例化子树(使用加载管理器) - */ - private loadAndInstantiateSubTree( - parentEntity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode - ): void { - // 延迟初始化 AssetLoader - if (!this.ensureAssetLoaderInitialized()) { - this.logger.debug('AssetLoader 不可用,SubTree 节点执行失败'); - node.status = TaskStatus.Failure; - this.completeNode(parentEntity); - return; - } - - const assetId = subTree.assetId; - - // 检查是否有正在进行的加载任务 - let taskHandle = this.loadingTasks.get(parentEntity.id); - - if (taskHandle) { - // 轮询检查状态 - const state = taskHandle.getState(); - - switch (state) { - case LoadingState.Loading: - case LoadingState.Pending: - // 仍在加载中 - node.status = TaskStatus.Running; - - // 输出进度信息 - const progress = taskHandle.getProgress(); - if (progress.elapsedMs > 1000) { - this.logger.debug( - `子树加载中: ${assetId} (已耗时: ${Math.round(progress.elapsedMs / 1000)}s, ` + - `重试: ${progress.retryCount}/${progress.maxRetries})` - ); - } - return; - - case LoadingState.Loaded: - // 加载完成 - this.onLoadingComplete(parentEntity, subTree, node, taskHandle); - return; - - case LoadingState.Failed: - case LoadingState.Timeout: - // 加载失败 - const error = taskHandle.getError(); - this.outputLog( - parentEntity, - `子树加载失败: ${assetId} - ${error?.message || '未知错误'}`, - 'error' - ); - node.status = TaskStatus.Failure; - this.loadingTasks.delete(parentEntity.id); - this.completeNode(parentEntity); - return; - - case LoadingState.Cancelled: - // 已取消(实体被销毁) - this.loadingTasks.delete(parentEntity.id); - return; - } - } - - // 开始新的加载任务 - this.startNewLoading(parentEntity, subTree, node); - } - - /** - * 开始新的加载任务 - */ - private startNewLoading( - parentEntity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode - ): void { - const assetId = subTree.assetId; - - // 获取父树的资产ID(用于循环检测) - const parentAssetId = this.getParentTreeAssetId(parentEntity); - - try { - // 使用加载管理器 - const taskHandle = this.loadingManager.startLoading( - assetId, - parentEntity, - () => this.loadAsset(assetId), - { - timeoutMs: 5000, - maxRetries: 2, - parentAssetId: parentAssetId - } - ); - - this.loadingTasks.set(parentEntity.id, taskHandle); - node.status = TaskStatus.Running; - - this.outputLog( - parentEntity, - `开始加载子树: ${assetId} (父树: ${parentAssetId || 'none'})`, - 'info' - ); - - } catch (error) { - if (error instanceof CircularDependencyError) { - this.outputLog(parentEntity, `检测到循环引用: ${error.message}`, 'error'); - } else { - this.outputLog(parentEntity, `启动加载失败: ${assetId}`, 'error'); - } - - node.status = TaskStatus.Failure; - this.completeNode(parentEntity); - } - } - - /** - * 加载完成时的处理 - */ - private onLoadingComplete( - parentEntity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode, - taskHandle: LoadingTaskHandle - ): void { - // 获取加载结果 - taskHandle.promise.then(subTreeRoot => { - // 再次检查实体是否存在 - if (parentEntity.isDestroyed) { - this.logger.warn(`父实体已销毁,丢弃加载结果: ${taskHandle.assetId}`); - subTreeRoot.destroy(); - return; - } - - // 设置子树 - subTree.setSubTreeRoot(subTreeRoot); - - // 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点 - parentEntity.addChild(subTreeRoot); - - // 添加资产元数据(用于循环检测) - const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata()); - metadata.initialize(taskHandle.assetId, '1.0.0'); - - // 处理黑板继承 - if (subTree.inheritParentBlackboard) { - this.setupBlackboardInheritance(parentEntity, subTreeRoot); - } - - this.outputLog(parentEntity, `子树 ${taskHandle.assetId} 加载成功并激活`, 'info'); - - // 打印子树结构(用于调试) - this.outputLog(parentEntity, `=== 子树 ${taskHandle.assetId} 内部结构 ===`, 'info'); - this.logSubTreeStructure(parentEntity, subTreeRoot, 0); - this.outputLog(parentEntity, `=== 子树结构结束 ===`, 'info'); - - // 激活子树执行 - this.startSubTreeExecution(subTreeRoot, parentEntity); - - // 清理任务 - this.loadingTasks.delete(parentEntity.id); - - }).catch(error => { - // 这里不应该到达,因为错误应该在状态机中处理了 - if (!(error instanceof EntityDestroyedError)) { - this.logger.error('意外错误:', error); - } - }); - } - - /** - * 加载资产 - */ - private async loadAsset(assetId: string): Promise { - if (!this.scene) { - throw new Error('Scene 不存在'); - } - - // 加载资产 - const asset = await this.assetLoader!.loadBehaviorTree(assetId); - - // 实例化为 Entity 树(作为子树,跳过 RootNode) - const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, this.scene, { - asSubTree: true - }); - - return rootEntity; - } - - /** - * 设置黑板继承 - */ - private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void { - const parentBlackboard = this.findBlackboard(parentEntity); - if (!parentBlackboard) { - return; - } - - // 找到子树的黑板 - const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent); - if (subTreeBlackboard) { - // 启用全局黑板查找(这样子树可以访问父树的变量) - subTreeBlackboard.setUseGlobalBlackboard(true); - } - } - - /** - * 查找黑板 - */ - 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 startSubTreeExecution(subTreeRoot: Entity, parentEntity?: Entity): void { - // 调试:检查子树根节点的子节点 - if (parentEntity) { - this.outputLog(parentEntity, `子树根节点 ${subTreeRoot.name} 有 ${subTreeRoot.children.length} 个子节点`, 'info'); - } - - // 激活根节点 - if (!subTreeRoot.hasComponent(ActiveNode)) { - subTreeRoot.addComponent(new ActiveNode()); - if (parentEntity) { - this.outputLog(parentEntity, `为子树根节点添加 ActiveNode: ${subTreeRoot.name}`, 'info'); - } - } - - const node = subTreeRoot.getComponent(BehaviorTreeNode); - if (node) { - if (parentEntity) { - this.outputLog(parentEntity, `设置子树根节点状态: ${subTreeRoot.name} -> Running`, 'info'); - } - node.status = TaskStatus.Running; - } - } - - /** - * 更新子树状态 - */ - private updateSubTree( - parentEntity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode - ): void { - const subTreeRoot = subTree.getSubTreeRoot(); - if (!subTreeRoot) { - return; - } - - // 检查子树是否完成 - const subTreeNode = subTreeRoot.getComponent(BehaviorTreeNode); - if (!subTreeNode) { - return; - } - - // 输出子树当前状态(调试) - const statusName = TaskStatus[subTreeNode.status]; - this.outputLog( - parentEntity, - `子树 ${subTree.assetId} 当前状态: ${statusName}`, - 'info' - ); - - if (subTreeNode.status !== TaskStatus.Running) { - // 子树完成 - this.onSubTreeCompleted(parentEntity, subTree, node, subTreeNode.status); - } else { - // 子树仍在运行 - node.status = TaskStatus.Running; - } - } - - /** - * 子树完成时的处理 - */ - private onSubTreeCompleted( - parentEntity: Entity, - subTree: SubTreeNode, - node: BehaviorTreeNode, - subTreeStatus: TaskStatus - ): void { - this.outputLog(parentEntity, `子树完成,状态: ${TaskStatus[subTreeStatus]}`, 'info'); - - // 检查完成前 SubTreeNode 的子节点 - this.outputLog(parentEntity, `完成前:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info'); - - // 标记子树完成 - subTree.markSubTreeCompleted(subTreeStatus); - - // 决定父节点状态 - if (subTreeStatus === TaskStatus.Success) { - node.status = TaskStatus.Success; - } else if (subTreeStatus === TaskStatus.Failure) { - if (subTree.propagateFailure) { - node.status = TaskStatus.Failure; - } else { - // 忽略失败,返回成功 - node.status = TaskStatus.Success; - } - } else { - node.status = subTreeStatus; - } - - // 清理子树 - this.cleanupSubTree(subTree); - - // 检查清理后 SubTreeNode 的子节点 - this.outputLog(parentEntity, `清理后:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info'); - - // 完成父节点 - this.completeNode(parentEntity); - } - - /** - * 清理子树 - */ - private cleanupSubTree(subTree: SubTreeNode): void { - const subTreeRoot = subTree.getSubTreeRoot(); - if (!subTreeRoot) { - return; - } - - // 如果是预加载的子树,不销毁,只重置状态以便复用 - if (subTree.preload) { - this.logger.debug(`重置预加载子树以便复用: ${subTree.assetId}`); - - // 递归重置整个子树的所有节点 - this.resetSubTreeRecursively(subTreeRoot); - - // 重置 SubTreeNode 的完成状态,但保留 subTreeRoot 引用 - subTree.resetCompletionState(); - } else { - // 运行时加载的子树,销毁并清理 - this.logger.debug(`销毁运行时加载的子树: ${subTree.assetId}`); - subTreeRoot.destroy(); - subTree.setSubTreeRoot(undefined); - subTree.reset(); - } - } - - /** - * 递归重置子树的所有节点 - */ - private resetSubTreeRecursively(entity: Entity): void { - // 移除 ActiveNode - if (entity.hasComponent(ActiveNode)) { - entity.removeComponentByType(ActiveNode); - } - - // 重置节点状态 - const node = entity.getComponent(BehaviorTreeNode); - if (node) { - node.status = TaskStatus.Invalid; - } - - // 递归处理子节点 - for (const child of entity.children) { - this.resetSubTreeRecursively(child); - } - } - - /** - * 完成节点执行 - */ - 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()); - } - } - } - - /** - * 获取父树的资产ID(用于循环检测) - */ - private getParentTreeAssetId(entity: Entity): string | undefined { - let current: Entity | null = entity; - - while (current) { - // 查找带有资产元数据的组件 - const metadata = current.getComponent(BehaviorTreeAssetMetadata); - if (metadata && metadata.assetId) { - return metadata.assetId; - } - current = current.parent; - } - - return undefined; - } - - /** - * 系统销毁时清理 - */ - protected override onDestroy(): void { - // 取消所有正在加载的任务 - for (const taskHandle of this.loadingTasks.values()) { - taskHandle.cancel(); - } - this.loadingTasks.clear(); - - super.onDestroy(); - } - - /** - * 查找根实体 - */ - 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; - } - - // 输出到LogOutput组件(显示在UI中) - const rootEntity = this.findRootEntity(entity); - if (rootEntity) { - const logOutput = rootEntity.getComponent(LogOutput); - if (logOutput) { - logOutput.addMessage(message, level); - } - } - } - - /** - * 递归打印子树结构(用于调试) - */ - private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void { - const indent = ' '.repeat(depth); - const btNode = entity.getComponent(BehaviorTreeNode); - - // 获取节点的具体类型组件 - const allComponents = entity.components.map(c => c.constructor.name); - const nodeTypeComponent = allComponents.find(name => - name !== 'BehaviorTreeNode' && name !== 'ActiveNode' && - name !== 'BlackboardComponent' && name !== 'LogOutput' && - name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata' - ) || 'Unknown'; - - // 构建节点显示名称 - let nodeName = entity.name; - if (nodeTypeComponent !== 'Unknown') { - nodeName = `${nodeName} [${nodeTypeComponent}]`; - } - - this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info'); - - // 递归打印子节点 - if (entity.children.length > 0) { - this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info'); - entity.children.forEach((child: Entity) => { - this.logSubTreeStructure(parentEntity, child, depth + 1); - }); - } - } - - protected override getLoggerName(): string { - return 'SubTreeExecutionSystem'; - } -} diff --git a/packages/behavior-tree/src/Types/TaskStatus.ts b/packages/behavior-tree/src/Types/TaskStatus.ts index d2f3b3a2..ca1bef0a 100644 --- a/packages/behavior-tree/src/Types/TaskStatus.ts +++ b/packages/behavior-tree/src/Types/TaskStatus.ts @@ -112,3 +112,14 @@ export enum BlackboardValueType { Object = 'object', Array = 'array' } + +/** + * 黑板变量定义 + */ +export interface BlackboardVariable { + name: string; + type: BlackboardValueType; + value: any; + readonly?: boolean; + description?: string; +} diff --git a/packages/behavior-tree/src/index.ts b/packages/behavior-tree/src/index.ts index 2908a5b4..f910c169 100644 --- a/packages/behavior-tree/src/index.ts +++ b/packages/behavior-tree/src/index.ts @@ -1,92 +1,29 @@ /** * @esengine/behavior-tree * - * 完全ECS化的行为树系统 + * 行为树系统 * * @packageDocumentation */ -// 注册所有内置节点 -import './RegisterAllNodes'; - // 类型定义 export * from './Types/TaskStatus'; -// 基础组件 -export * from './Components/BehaviorTreeNode'; -export * from './Components/BlackboardComponent'; -export * from './Components/CompositeNodeComponent'; -export * from './Components/DecoratorNodeComponent'; -export * from './Components/ActiveNode'; -export * from './Components/PropertyBindings'; -export * from './Components/LogOutput'; -export * from './Components/AssetMetadata'; - -// 动作组件 -export * from './Components/Actions/WaitAction'; -export * from './Components/Actions/LogAction'; -export * from './Components/Actions/SetBlackboardValueAction'; -export * from './Components/Actions/ModifyBlackboardValueAction'; -export * from './Components/Actions/ExecuteAction'; - -// 条件组件 -export * from './Components/Conditions/BlackboardCompareCondition'; -export * from './Components/Conditions/BlackboardExistsCondition'; -export * from './Components/Conditions/RandomProbabilityCondition'; -export * from './Components/Conditions/ExecuteCondition'; - -// 组合节点 -export * from './Components/Composites/RootNode'; -export * from './Components/Composites/SequenceNode'; -export * from './Components/Composites/SelectorNode'; -export * from './Components/Composites/ParallelNode'; -export * from './Components/Composites/ParallelSelectorNode'; -export * from './Components/Composites/RandomSequenceNode'; -export * from './Components/Composites/RandomSelectorNode'; -export * from './Components/Composites/SubTreeNode'; - -// 装饰器节点 -export * from './Components/Decorators/InverterNode'; -export * from './Components/Decorators/RepeaterNode'; -export * from './Components/Decorators/UntilSuccessNode'; -export * from './Components/Decorators/UntilFailNode'; -export * from './Components/Decorators/AlwaysSucceedNode'; -export * from './Components/Decorators/AlwaysFailNode'; -export * from './Components/Decorators/ConditionalNode'; -export * from './Components/Decorators/CooldownNode'; -export * from './Components/Decorators/TimeoutNode'; - -// 系统 -export * from './Systems/RootExecutionSystem'; -export * from './Systems/LeafExecutionSystem'; -export * from './Systems/DecoratorExecutionSystem'; -export * from './Systems/CompositeExecutionSystem'; -export * from './Systems/SubTreeExecutionSystem'; - -// 服务 -export * from './Services/GlobalBlackboardService'; -export * from './Services/WorkspaceService'; -export * from './Services/IAssetLoader'; -export * from './Services/FileSystemAssetLoader'; -export * from './Services/AssetLoadingManager'; -export * from './Services/AssetLoadingTypes'; - -// 插件 -export * from './BehaviorTreePlugin'; +// Runtime +export * from './Runtime'; // 辅助工具 export * from './BehaviorTreeStarter'; export * from './BehaviorTreeBuilder'; -// 序列化(编辑器支持) -export * from './Serialization/BehaviorTreePersistence'; +// 序列化 export * from './Serialization/NodeTemplates'; - -// 资产系统(运行时) export * from './Serialization/BehaviorTreeAsset'; -export * from './Serialization/BehaviorTreeAssetSerializer'; -export * from './Serialization/BehaviorTreeAssetLoader'; export * from './Serialization/EditorFormatConverter'; +export * from './Serialization/BehaviorTreeAssetSerializer'; -// 装饰器(扩展支持) -export * from './Decorators/BehaviorNodeDecorator'; +// 服务 +export * from './Services/GlobalBlackboardService'; + +// 插件 +export * from './BehaviorTreePlugin'; diff --git a/packages/editor-app/src/components/BehaviorTreeEditor.tsx b/packages/editor-app/src/components/BehaviorTreeEditor.tsx index 5710a0c8..9a383670 100644 --- a/packages/editor-app/src/components/BehaviorTreeEditor.tsx +++ b/packages/editor-app/src/components/BehaviorTreeEditor.tsx @@ -6,7 +6,7 @@ import { Repeat, CheckCircle, XCircle, CheckCheck, HelpCircle, Snowflake, Timer, Clock, FileText, Edit, Calculator, Code, Equal, Dices, Settings, - Database, AlertTriangle, Search, X, + Database, AlertTriangle, AlertCircle, Search, X, LucideIcon } from 'lucide-react'; import { ask } from '@tauri-apps/plugin-dialog'; @@ -199,6 +199,17 @@ export const BehaviorTreeEditor: React.FC = ({ isExecuting } = useBehaviorTreeStore(); + // 右键菜单状态 + const [contextMenu, setContextMenu] = useState<{ + visible: boolean; + position: { x: number; y: number }; + nodeId: string | null; + }>({ + visible: false, + position: { x: 0, y: 0 }, + nodeId: null + }); + // 初始化根节点(仅在首次挂载时检查) useEffect(() => { if (nodes.length === 0) { @@ -212,6 +223,20 @@ export const BehaviorTreeEditor: React.FC = ({ } }, []); + // 初始化executor用于检查执行器是否存在 + useEffect(() => { + if (!executorRef.current) { + executorRef.current = new BehaviorTreeExecutor(); + } + + return () => { + if (executorRef.current) { + executorRef.current.destroy(); + executorRef.current = null; + } + }; + }, []); + // 组件挂载和连线变化时强制更新,确保连线能正确渲染 useEffect(() => { if (nodes.length > 0 || connections.length > 0) { @@ -223,6 +248,20 @@ export const BehaviorTreeEditor: React.FC = ({ } }, [nodes.length, connections.length]); + // 点击其他地方关闭右键菜单 + useEffect(() => { + const handleClick = () => { + if (contextMenu.visible) { + setContextMenu({ ...contextMenu, visible: false }); + } + }; + + if (contextMenu.visible) { + document.addEventListener('click', handleClick); + return () => document.removeEventListener('click', handleClick); + } + }, [contextMenu.visible]); + const [isDragging, setIsDragging] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const canvasRef = useRef(null); @@ -233,11 +272,15 @@ export const BehaviorTreeEditor: React.FC = ({ position: { x: number; y: number }; searchText: string; selectedIndex: number; + mode: 'create' | 'replace'; + replaceNodeId: string | null; }>({ visible: false, position: { x: 0, y: 0 }, searchText: '', - selectedIndex: 0 + selectedIndex: 0, + mode: 'create', + replaceNodeId: null }); const selectedNodeRef = useRef(null); @@ -485,6 +528,83 @@ export const BehaviorTreeEditor: React.FC = ({ onNodeSelect?.(node); }; + const handleNodeContextMenu = (e: React.MouseEvent, node: BehaviorTreeNode) => { + e.preventDefault(); + e.stopPropagation(); + + // 不允许对Root节点右键 + if (node.id === ROOT_NODE_ID) { + return; + } + + setContextMenu({ + visible: true, + position: { x: e.clientX, y: e.clientY }, + nodeId: node.id + }); + }; + + const handleReplaceNode = (newTemplate: NodeTemplate) => { + const nodeToReplace = nodes.find(n => n.id === quickCreateMenu.replaceNodeId); + if (!nodeToReplace) return; + + // 如果行为树正在执行,先停止 + if (executionMode !== 'idle') { + handleStop(); + } + + // 合并数据:新模板的默认配置 + 保留旧节点中同名属性的值 + const newData = { ...newTemplate.defaultConfig }; + + // 获取新模板的属性名列表 + const newPropertyNames = new Set(newTemplate.properties.map(p => p.name)); + + // 遍历旧节点的 data,保留新模板中也存在的属性 + for (const [key, value] of Object.entries(nodeToReplace.data)) { + // 跳过节点类型相关的字段 + if (key === 'nodeType' || key === 'compositeType' || key === 'decoratorType' || + key === 'actionType' || key === 'conditionType') { + continue; + } + + // 如果新模板也有这个属性,保留旧值(包括绑定信息) + if (newPropertyNames.has(key)) { + newData[key] = value; + } + } + + // 创建新节点,保留原节点的位置和连接 + const newNode: BehaviorTreeNode = { + id: nodeToReplace.id, + template: newTemplate, + data: newData, + position: nodeToReplace.position, + children: nodeToReplace.children + }; + + // 替换节点 + setNodes(nodes.map(n => n.id === newNode.id ? newNode : n)); + + // 删除所有指向该节点的属性连接,让用户重新连接 + const updatedConnections = connections.filter(conn => + !(conn.connectionType === 'property' && conn.to === newNode.id) + ); + setConnections(updatedConnections); + + // 关闭快速创建菜单 + setQuickCreateMenu({ + visible: false, + position: { x: 0, y: 0 }, + searchText: '', + selectedIndex: 0, + mode: 'create', + replaceNodeId: null + }); + + // 显示提示 + showToast?.(`已将节点替换为 ${newTemplate.displayName}`, 'success'); + }; + const handleNodeMouseDown = (e: React.MouseEvent, nodeId: string) => { // 只允许左键拖动节点 if (e.button !== 0) return; @@ -703,9 +823,33 @@ export const BehaviorTreeEditor: React.FC = ({ return; } + // 检查目标属性是否允许多个连接 + const toNode = nodes.find((n: BehaviorTreeNode) => n.id === actualTo); + if (toNode && actualToProperty) { + const targetProperty = toNode.template.properties.find( + (p: PropertyDefinition) => p.name === actualToProperty + ); + + // 如果属性不允许多个连接(默认行为) + if (!targetProperty?.allowMultipleConnections) { + // 检查是否已有连接到该属性 + const existingPropertyConnection = connections.find( + (conn: Connection) => + conn.connectionType === 'property' && + conn.to === actualTo && + conn.toProperty === actualToProperty + ); + + if (existingPropertyConnection) { + showToast('该属性已有连接,请先删除现有连接', 'warning'); + clearConnecting(); + return; + } + } + } + // 类型兼容性检查 const fromNode = nodes.find((n: BehaviorTreeNode) => n.id === actualFrom); - const toNode = nodes.find((n: BehaviorTreeNode) => n.id === actualTo); if (fromNode && toNode && actualFromProperty && actualToProperty) { const isFromBlackboard = fromNode.data.nodeType === 'blackboard-variable'; @@ -814,7 +958,9 @@ export const BehaviorTreeEditor: React.FC = ({ y: e.clientY }, searchText: '', - selectedIndex: 0 + selectedIndex: 0, + mode: 'create', + replaceNodeId: null }); // 清除预览连接线,但保留 connectingFrom 用于创建连接 setConnectingToPos(null); @@ -876,6 +1022,13 @@ export const BehaviorTreeEditor: React.FC = ({ }; const handleQuickCreateNode = (template: NodeTemplate) => { + // 如果是替换模式,直接调用替换函数 + if (quickCreateMenu.mode === 'replace') { + handleReplaceNode(template); + return; + } + + // 创建模式:需要连接 if (!connectingFrom) { return; } @@ -941,7 +1094,9 @@ export const BehaviorTreeEditor: React.FC = ({ visible: false, position: { x: 0, y: 0 }, searchText: '', - selectedIndex: 0 + selectedIndex: 0, + mode: 'create', + replaceNodeId: null }); clearConnecting(); @@ -1676,6 +1831,7 @@ export const BehaviorTreeEditor: React.FC = ({ data-node-id={node.id} className={nodeClasses} onClick={(e) => handleNodeClick(e, node)} + onContextMenu={(e) => handleNodeContextMenu(e, node)} onMouseDown={(e) => handleNodeMouseDown(e, node.id)} onMouseUp={(e) => handleNodeMouseUpForConnection(e, node.id)} style={{ @@ -1762,12 +1918,38 @@ export const BehaviorTreeEditor: React.FC = ({ #{node.id} + {/* 缺失执行器警告 */} + {!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className) && ( +
e.stopPropagation()} + > + +
+ 缺失执行器:找不到节点对应的执行器 "{node.template.className}" +
+
+ )} {/* 未生效节点警告 */} {isUncommitted && (
= ({ onMouseDown={(e) => handlePortMouseDown(e, node.id, prop.name)} onMouseUp={(e) => handlePortMouseUp(e, node.id, prop.name)} className={`bt-node-port bt-node-port-property ${hasConnection ? 'connected' : ''}`} - title={`Input: ${prop.label}`} + title={prop.description || prop.name} /> - {prop.label}: + + {prop.name}: + {propValue !== undefined && ( {String(propValue)} @@ -2212,7 +2399,9 @@ export const BehaviorTreeEditor: React.FC = ({ visible: false, position: { x: 0, y: 0 }, searchText: '', - selectedIndex: 0 + selectedIndex: 0, + mode: 'create', + replaceNodeId: null }); clearConnecting(); } else if (e.key === 'ArrowDown') { @@ -2251,7 +2440,9 @@ export const BehaviorTreeEditor: React.FC = ({ visible: false, position: { x: 0, y: 0 }, searchText: '', - selectedIndex: 0 + selectedIndex: 0, + mode: 'create', + replaceNodeId: null }); clearConnecting(); }} @@ -2407,6 +2598,50 @@ export const BehaviorTreeEditor: React.FC = ({ onSpeedChange={handleSpeedChange} />
+ + {/* 右键菜单 */} + {contextMenu.visible && ( +
e.stopPropagation()} + > +
{ + setQuickCreateMenu({ + visible: true, + position: contextMenu.position, + searchText: '', + selectedIndex: 0, + mode: 'replace', + replaceNodeId: contextMenu.nodeId + }); + setContextMenu({ ...contextMenu, visible: false }); + }} + style={{ + padding: '8px 16px', + cursor: 'pointer', + color: '#cccccc', + fontSize: '13px', + transition: 'background-color 0.15s' + }} + onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#094771'} + onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} + > + 替换节点 +
+
+ )} ); }; diff --git a/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx b/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx index 10eca25a..05c844b8 100644 --- a/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx +++ b/packages/editor-app/src/components/BehaviorTreeNodePalette.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree'; import { Core } from '@esengine/ecs-framework'; import { EditorPluginManager, MessageHub } from '@esengine/editor-core'; @@ -8,6 +8,20 @@ interface BehaviorTreeNodePaletteProps { onNodeSelect?: (template: NodeTemplate) => void; } +/** + * 获取节点类型对应的颜色 + */ +const getTypeColor = (type: string): string => { + switch (type) { + case 'composite': return '#1976d2'; + case 'action': return '#388e3c'; + case 'condition': return '#d32f2f'; + case 'decorator': return '#fb8c00'; + case 'blackboard': return '#8e24aa'; + default: return '#7b1fa2'; + } +}; + /** * 行为树节点面板 * @@ -83,14 +97,18 @@ export const BehaviorTreeNodePalette: React.FC = ( }, []); // 按类别分组(排除根节点类别) - const categories = ['all', ...new Set(allTemplates - .filter(t => t.category !== '根节点') - .map(t => t.category))]; + const categories = useMemo(() => + ['all', ...new Set(allTemplates + .filter(t => t.category !== '根节点') + .map(t => t.category))] + , [allTemplates]); - const filteredTemplates = (selectedCategory === 'all' - ? allTemplates - : allTemplates.filter(t => t.category === selectedCategory)) - .filter(t => t.category !== '根节点'); + const filteredTemplates = useMemo(() => + (selectedCategory === 'all' + ? allTemplates + : allTemplates.filter(t => t.category === selectedCategory)) + .filter(t => t.category !== '根节点') + , [allTemplates, selectedCategory]); const handleNodeClick = (template: NodeTemplate) => { onNodeSelect?.(template); @@ -108,17 +126,6 @@ export const BehaviorTreeNodePalette: React.FC = ( } }; - const getTypeColor = (type: string): string => { - switch (type) { - case 'composite': return '#1976d2'; - case 'action': return '#388e3c'; - case 'condition': return '#d32f2f'; - case 'decorator': return '#fb8c00'; - case 'blackboard': return '#8e24aa'; - default: return '#7b1fa2'; - } - }; - return (
(
-
)) )} diff --git a/packages/editor-app/src/components/ConsolePanel.tsx b/packages/editor-app/src/components/ConsolePanel.tsx index ea7e7b44..3e72daa5 100644 --- a/packages/editor-app/src/components/ConsolePanel.tsx +++ b/packages/editor-app/src/components/ConsolePanel.tsx @@ -140,6 +140,8 @@ const LogEntryItem = memo(({ LogEntryItem.displayName = 'LogEntryItem'; +const MAX_LOGS = 1000; + export function ConsolePanel({ logService }: ConsolePanelProps) { const [logs, setLogs] = useState([]); const [filter, setFilter] = useState(''); @@ -157,10 +159,16 @@ export function ConsolePanel({ logService }: ConsolePanelProps) { const logContainerRef = useRef(null); useEffect(() => { - setLogs(logService.getLogs()); + setLogs(logService.getLogs().slice(-MAX_LOGS)); const unsubscribe = logService.subscribe((entry) => { - setLogs(prev => [...prev, entry]); + setLogs(prev => { + const newLogs = [...prev, entry]; + if (newLogs.length > MAX_LOGS) { + return newLogs.slice(-MAX_LOGS); + } + return newLogs; + }); }); return unsubscribe; @@ -348,14 +356,16 @@ export function ConsolePanel({ logService }: ConsolePanelProps) { } }; - const levelCounts = { + const levelCounts = useMemo(() => ({ [LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length, [LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length, [LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length, [LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length - }; + }), [logs]); - const remoteLogCount = logs.filter(l => l.source === 'remote').length; + const remoteLogCount = useMemo(() => + logs.filter(l => l.source === 'remote').length + , [logs]); return (
diff --git a/packages/editor-app/src/plugins/BehaviorTreePlugin.ts b/packages/editor-app/src/plugins/BehaviorTreePlugin.ts index 4cc4d7d5..bb18370e 100644 --- a/packages/editor-app/src/plugins/BehaviorTreePlugin.ts +++ b/packages/editor-app/src/plugins/BehaviorTreePlugin.ts @@ -1,7 +1,7 @@ import type { Core, ServiceContainer } from '@esengine/ecs-framework'; import { IEditorPlugin, EditorPluginCategory, PanelPosition, MessageHub } from '@esengine/editor-core'; import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer } from '@esengine/editor-core'; -import { BehaviorTreePersistence } from '@esengine/behavior-tree'; +import { BehaviorTreeData } from '@esengine/behavior-tree'; /** * 行为树编辑器插件 @@ -112,18 +112,15 @@ export class BehaviorTreePlugin implements IEditorPlugin { getSerializers(): ISerializer[] { return [ { - serialize: (data: any) => { - // 使用行为树持久化工具 - const result = BehaviorTreePersistence.serialize(data.entity, data.pretty ?? true); - if (typeof result === 'string') { - const encoder = new TextEncoder(); - return encoder.encode(result); - } - return result; + serialize: (data: BehaviorTreeData) => { + const json = this.serializeBehaviorTreeData(data); + const encoder = new TextEncoder(); + return encoder.encode(json); }, deserialize: (data: Uint8Array) => { - // 返回原始数据,让上层决定如何反序列化到场景 - return data; + const decoder = new TextDecoder(); + const json = decoder.decode(data); + return this.deserializeBehaviorTreeData(json); }, getSupportedType: () => 'behavior-tree' } @@ -143,10 +140,9 @@ export class BehaviorTreePlugin implements IEditorPlugin { } async onBeforeSave(filePath: string, data: any): Promise { - // 验证行为树数据 if (filePath.endsWith('.behavior-tree.json')) { console.log('[BehaviorTreePlugin] Validating behavior tree before save'); - const isValid = BehaviorTreePersistence.validate(JSON.stringify(data)); + const isValid = this.validateBehaviorTreeData(data); if (!isValid) { throw new Error('Invalid behavior tree data'); } @@ -159,25 +155,83 @@ export class BehaviorTreePlugin implements IEditorPlugin { } } - // 私有方法 - private createNewBehaviorTree(): void { console.log('[BehaviorTreePlugin] Creating new behavior tree'); - // TODO: 实现创建新行为树的逻辑 } private openBehaviorTree(): void { console.log('[BehaviorTreePlugin] Opening behavior tree'); - // TODO: 实现打开行为树的逻辑 } private saveBehaviorTree(): void { console.log('[BehaviorTreePlugin] Saving behavior tree'); - // TODO: 实现保存行为树的逻辑 } private validateBehaviorTree(): void { console.log('[BehaviorTreePlugin] Validating behavior tree'); - // TODO: 实现验证行为树的逻辑 + } + + private serializeBehaviorTreeData(treeData: BehaviorTreeData): string { + const serializable = { + id: treeData.id, + name: treeData.name, + rootNodeId: treeData.rootNodeId, + nodes: Array.from(treeData.nodes.entries()).map(([, node]) => ({ + ...node + })), + blackboardVariables: treeData.blackboardVariables + ? Array.from(treeData.blackboardVariables.entries()).map(([key, value]) => ({ + key, + value + })) + : [] + }; + return JSON.stringify(serializable, null, 2); + } + + private deserializeBehaviorTreeData(json: string): BehaviorTreeData { + const parsed = JSON.parse(json); + const treeData: BehaviorTreeData = { + id: parsed.id, + name: parsed.name, + rootNodeId: parsed.rootNodeId, + nodes: new Map(), + blackboardVariables: new Map() + }; + + if (parsed.nodes) { + for (const node of parsed.nodes) { + treeData.nodes.set(node.id, node); + } + } + + if (parsed.blackboardVariables) { + for (const variable of parsed.blackboardVariables) { + treeData.blackboardVariables!.set(variable.key, variable.value); + } + } + + return treeData; + } + + private validateBehaviorTreeData(data: any): boolean { + if (!data || typeof data !== 'object') { + return false; + } + + if (!data.id || !data.name || !data.rootNodeId) { + return false; + } + + if (!data.nodes || !Array.isArray(data.nodes)) { + return false; + } + + const rootNode = data.nodes.find((n: any) => n.id === data.rootNodeId); + if (!rootNode) { + return false; + } + + return true; } } diff --git a/packages/editor-app/src/services/PluginLoader.ts b/packages/editor-app/src/services/PluginLoader.ts index 4391957a..9ed4f6ea 100644 --- a/packages/editor-app/src/services/PluginLoader.ts +++ b/packages/editor-app/src/services/PluginLoader.ts @@ -1,5 +1,6 @@ -import { EditorPluginManager } from '@esengine/editor-core'; +import { EditorPluginManager, LocaleService, MessageHub } from '@esengine/editor-core'; import type { IEditorPlugin } from '@esengine/editor-core'; +import { Core } from '@esengine/ecs-framework'; import { TauriAPI } from '../api/tauri'; interface PluginPackageJson { @@ -119,6 +120,28 @@ export class PluginLoader { await pluginManager.installEditor(pluginInstance); this.loadedPluginNames.add(packageJson.name); + // 同步插件的语言设置 + try { + const localeService = Core.services.resolve(LocaleService); + const currentLocale = localeService.getCurrentLocale(); + if (pluginInstance.setLocale) { + pluginInstance.setLocale(currentLocale); + console.log(`[PluginLoader] Set locale for plugin ${packageJson.name}: ${currentLocale}`); + } + } catch (error) { + console.warn(`[PluginLoader] Failed to set locale for plugin ${packageJson.name}:`, error); + } + + // 通知节点面板重新加载模板 + try { + const messageHub = Core.services.resolve(MessageHub); + const localeService = Core.services.resolve(LocaleService); + messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() }); + console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`); + } catch (error) { + console.warn(`[PluginLoader] Failed to publish locale:changed event:`, error); + } + console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`); } catch (error) { console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error); diff --git a/packages/editor-app/src/styles/BehaviorTreeNode.css b/packages/editor-app/src/styles/BehaviorTreeNode.css index cce5eab1..5746414c 100644 --- a/packages/editor-app/src/styles/BehaviorTreeNode.css +++ b/packages/editor-app/src/styles/BehaviorTreeNode.css @@ -307,3 +307,35 @@ .bt-node-uncommitted-warning:hover .bt-node-uncommitted-tooltip { display: block; } + +/* 缺失执行器警告tooltip */ +.bt-node-missing-executor-tooltip { + display: none; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + padding: 6px 10px; + background: rgba(244, 67, 54, 0.95); + color: #fff; + font-size: 11px; + white-space: nowrap; + border-radius: 4px; + pointer-events: none; + z-index: 1000; +} + +.bt-node-missing-executor-tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: rgba(244, 67, 54, 0.95); +} + +.bt-node-missing-executor-warning:hover .bt-node-missing-executor-tooltip { + display: block; +} diff --git a/packages/editor-app/src/utils/BehaviorTreeExecutor.ts b/packages/editor-app/src/utils/BehaviorTreeExecutor.ts index f6a79c7c..c796c87b 100644 --- a/packages/editor-app/src/utils/BehaviorTreeExecutor.ts +++ b/packages/editor-app/src/utils/BehaviorTreeExecutor.ts @@ -1,50 +1,14 @@ -import { World, Entity, Scene, createLogger, Time, Core, ComponentRegistry, Component } from '@esengine/ecs-framework'; +import { World, Entity, Scene, createLogger, Time, Core } from '@esengine/ecs-framework'; import { - BehaviorTreeNode as BehaviorTreeNodeComponent, - BlackboardComponent, - ActiveNode, - PropertyBindings, - LogOutput, - RootExecutionSystem, - LeafExecutionSystem, - DecoratorExecutionSystem, - CompositeExecutionSystem, - SubTreeExecutionSystem, - FileSystemAssetLoader, - CompositeNodeComponent, + BehaviorTreeData, + BehaviorNodeData, + BehaviorTreeRuntimeComponent, + BehaviorTreeAssetManager, + BehaviorTreeExecutionSystem, TaskStatus, - NodeType, - WaitAction, - LogAction, - SetBlackboardValueAction, - ModifyBlackboardValueAction, - ExecuteAction, - BlackboardCompareCondition, - BlackboardExistsCondition, - RandomProbabilityCondition, - ExecuteCondition, - RootNode, - SequenceNode, - SelectorNode, - ParallelNode, - ParallelSelectorNode, - RandomSequenceNode, - RandomSelectorNode, - SubTreeNode, - InverterNode, - RepeaterNode, - UntilSuccessNode, - UntilFailNode, - AlwaysSucceedNode, - AlwaysFailNode, - ConditionalNode, - CooldownNode, - TimeoutNode, - AbortType, - CompareOperator + NodeType } from '@esengine/behavior-tree'; import type { BehaviorTreeNode } from '../stores/behaviorTreeStore'; -import { TauriAPI } from '../api/tauri'; const logger = createLogger('BehaviorTreeExecutor'); @@ -68,50 +32,46 @@ export type ExecutionCallback = ( ) => void; /** - * 真实的行为树执行器 + * 行为树执行器 * - * 使用真实的 ECS 系统执行行为树 + * 使用新的Runtime架构执行行为树 */ export class BehaviorTreeExecutor { private world: World; private scene: Scene; - private rootEntity: Entity | null = null; - private entityMap: Map = new Map(); - private blackboardVariables: Record = {}; - private initialBlackboardVariables: Record = {}; + private entity: Entity | null = null; + private runtime: BehaviorTreeRuntimeComponent | null = null; + private treeData: BehaviorTreeData | null = null; private callback: ExecutionCallback | null = null; private isRunning = false; private isPaused = false; private executionLogs: ExecutionLog[] = []; private lastStatuses: Map = new Map(); - private debugMode = false; private tickCount = 0; - // 存储节点ID -> 属性绑定信息的映射 - private propertyBindingsMap: Map> = new Map(); - // 标记是否已添加 SubTreeExecutionSystem - private hasSubTreeSystem = false; + private nodeIdMap: Map = new Map(); + private blackboardKeys: string[] = []; + + private assetManager: BehaviorTreeAssetManager; + private executionSystem: BehaviorTreeExecutionSystem; constructor() { this.world = new World({ name: 'BehaviorTreeWorld' }); this.scene = this.world.createScene('BehaviorTreeScene'); - // 只注册基础执行系统 - this.scene.addSystem(new RootExecutionSystem()); - this.scene.addSystem(new LeafExecutionSystem()); - this.scene.addSystem(new DecoratorExecutionSystem()); - this.scene.addSystem(new CompositeExecutionSystem()); - // SubTreeExecutionSystem 按需添加 + // 尝试获取已存在的 assetManager,如果不存在则创建新的 + try { + this.assetManager = Core.services.resolve(BehaviorTreeAssetManager); + } catch { + this.assetManager = new BehaviorTreeAssetManager(); + Core.services.registerInstance(BehaviorTreeAssetManager, this.assetManager); + } + + this.executionSystem = new BehaviorTreeExecutionSystem(); + this.scene.addSystem(this.executionSystem); } /** - * 检测是否有 SubTree 节点 - */ - private detectSubTreeNodes(nodes: BehaviorTreeNode[]): boolean { - return nodes.some(node => node.template.displayName === '子树'); - } - - /** - * 从编辑器节点构建真实的 Entity 树 + * 从编辑器节点构建行为树数据 */ buildTree( nodes: BehaviorTreeNode[], @@ -122,195 +82,120 @@ export class BehaviorTreeExecutor { projectPath?: string | null ): void { this.cleanup(); - this.blackboardVariables = { ...blackboard }; this.callback = callback; - const nodeMap = new Map(); - nodes.forEach(node => nodeMap.set(node.id, node)); + this.treeData = this.convertToTreeData(nodes, rootNodeId, blackboard, connections); - // 检测是否有 SubTree 节点 - const hasSubTreeNode = this.detectSubTreeNodes(nodes); + this.assetManager.loadAsset(this.treeData); - if (hasSubTreeNode) { - // 按需添加 SubTreeExecutionSystem - if (!this.hasSubTreeSystem) { - this.scene.addSystem(new SubTreeExecutionSystem()); - this.hasSubTreeSystem = true; - logger.debug('检测到 SubTree 节点,已添加 SubTreeExecutionSystem'); - } - - // 配置资产加载器以支持 SubTree 节点 - if (projectPath) { - const assetLoader = new FileSystemAssetLoader({ - basePath: `${projectPath}/.ecs/behaviors`, - format: 'json', - extension: '.btree', - enableCache: true, - readFile: async (path: string) => { - const content = await TauriAPI.readFileContent(path); - return content; - } - }); - - Core.services.registerInstance(FileSystemAssetLoader, assetLoader); - logger.info(`已配置资产加载器: ${projectPath}/.ecs/behaviors`); - } else { - logger.warn( - '检测到 SubTree 节点,但未提供项目路径。\n' + - 'SubTree 节点需要项目路径来加载子树资产。\n' + - '请在编辑器中打开项目,或确保运行时环境已正确配置资产路径。' - ); + this.entity = this.scene.createEntity('BehaviorTreeEntity'); + this.runtime = new BehaviorTreeRuntimeComponent(); + + // 在添加组件之前设置资产ID和autoStart + this.runtime.treeAssetId = this.treeData.id; + this.runtime.autoStart = false; + + this.entity.addComponent(this.runtime); + + if (this.treeData.blackboardVariables) { + this.blackboardKeys = Array.from(this.treeData.blackboardVariables.keys()); + for (const [key, value] of this.treeData.blackboardVariables.entries()) { + this.runtime.setBlackboardValue(key, value); } + } else { + this.blackboardKeys = []; } - const rootNode = nodeMap.get(rootNodeId); + this.addLog('行为树构建完成', 'info'); + } + + /** + * 将编辑器节点转换为BehaviorTreeData + */ + private convertToTreeData( + nodes: BehaviorTreeNode[], + rootNodeId: string, + blackboard: Record, + connections: Array<{ from: string; to: string; fromProperty?: string; toProperty?: string; connectionType: 'node' | 'property' }> + ): BehaviorTreeData { + const rootNode = nodes.find(n => n.id === rootNodeId); if (!rootNode) { - logger.error('未找到根节点'); - return; + throw new Error('未找到根节点'); } - // 先创建黑板组件 - const blackboardComp = new BlackboardComponent(); - Object.entries(this.blackboardVariables).forEach(([key, value]) => { - const type = typeof value === 'number' ? 'number' : - typeof value === 'string' ? 'string' : - typeof value === 'boolean' ? 'boolean' : - 'object'; - blackboardComp.defineVariable(key, type as any, value); - }); + // 如果根节点是编辑器特有的"根节点"且只有一个子节点,使用第一个子节点作为实际根节点 + let actualRootId = rootNodeId; + if (rootNode.template.displayName === '根节点' && rootNode.children.length === 1) { + actualRootId = rootNode.children[0]!; + } - // 在创建实体之前先处理属性连接,这样创建组件时就能读到正确的值 - this.applyPropertyConnections(connections, nodeMap, blackboardComp); + const treeData: BehaviorTreeData = { + id: `tree_${Date.now()}`, + name: 'EditorTree', + rootNodeId: actualRootId, + nodes: new Map(), + blackboardVariables: new Map() + }; - // 创建实体树 - this.rootEntity = this.createEntityFromNode(rootNode, nodeMap, null); + this.nodeIdMap.clear(); - if (this.rootEntity) { - // 将黑板组件添加到根实体 - this.rootEntity.addComponent(blackboardComp); + for (const node of nodes) { + // 跳过编辑器的虚拟根节点 + if (node.id === rootNodeId && node.template.displayName === '根节点' && rootNode.children.length === 1) { + continue; + } - // 添加LogOutput组件,用于收集日志 - this.rootEntity.addComponent(new LogOutput()); + this.nodeIdMap.set(node.id, node.id); - if (this.debugMode) { - this.logDebugTreeStructure(); - // 立即触发一次回调,显示 debug 信息 - if (this.callback) { - this.callback([], this.executionLogs); + const nodeData: BehaviorNodeData = { + id: node.id, + name: node.template.displayName, + nodeType: this.convertNodeType(node.template.type), + implementationType: node.template.className || this.getImplementationType(node.template.displayName, node.template.type), + config: { ...node.data }, + children: node.children + }; + + treeData.nodes.set(node.id, nodeData); + } + + // 处理属性连接,转换为 bindings + for (const conn of connections) { + if (conn.connectionType === 'property' && conn.toProperty) { + const targetNodeData = treeData.nodes.get(conn.to); + const sourceNode = nodes.find(n => n.id === conn.from); + + if (targetNodeData && sourceNode) { + // 检查源节点是否是黑板变量节点 + if (sourceNode.data.nodeType === 'blackboard-variable') { + // 从黑板变量节点获取实际的变量名 + const variableName = sourceNode.data.variableName as string; + + if (variableName) { + // 初始化 bindings 如果不存在 + if (!targetNodeData.bindings) { + targetNodeData.bindings = {}; + } + + // 添加绑定:属性名 -> 黑板变量名 + targetNodeData.bindings[conn.toProperty] = variableName; + } + } } } } - } - /** - * 应用属性连接 - * 记录属性到黑板变量的绑定关系 - */ - private applyPropertyConnections( - connections: Array<{ from: string; to: string; fromProperty?: string; toProperty?: string; connectionType: 'node' | 'property' }>, - nodeMap: Map, - blackboard: BlackboardComponent - ): void { - // 清空之前的绑定信息 - this.propertyBindingsMap.clear(); - - // 过滤出属性类型的连接 - const propertyConnections = connections.filter(conn => conn.connectionType === 'property'); - - logger.info(`[属性绑定] 找到 ${propertyConnections.length} 个属性连接`); - - propertyConnections.forEach(conn => { - const fromNode = nodeMap.get(conn.from); - const toNode = nodeMap.get(conn.to); - - if (!fromNode || !toNode || !conn.toProperty) { - logger.warn(`[属性绑定] 连接数据不完整: from=${conn.from}, to=${conn.to}, toProperty=${conn.toProperty}`); - return; - } - - let variableName: string | undefined; - - // 检查 from 节点是否是黑板变量节点 - if (fromNode.data.nodeType === 'blackboard-variable') { - // 黑板变量节点,变量名在 data.variableName 中 - variableName = fromNode.data.variableName; - } else if (conn.fromProperty) { - // 普通节点的属性连接 - variableName = conn.fromProperty; - } - - if (!variableName) { - logger.warn(`[属性绑定] 无法确定变量名: from节点=${fromNode.template.displayName}`); - return; - } - - if (!blackboard.hasVariable(variableName)) { - logger.warn(`[属性绑定] 黑板变量不存在: ${variableName}`); - return; - } - - // 记录绑定信息到 Map - let nodeBindings = this.propertyBindingsMap.get(toNode.id); - if (!nodeBindings) { - nodeBindings = new Map(); - this.propertyBindingsMap.set(toNode.id, nodeBindings); - } - - nodeBindings.set(conn.toProperty, variableName); - - logger.info(`[属性绑定] 成功绑定: 节点 "${toNode.template.displayName}" 的属性 "${conn.toProperty}" -> 黑板变量 "${variableName}"`); - }); - } - - /** - * 递归创建 Entity - */ - private createEntityFromNode( - node: BehaviorTreeNode, - nodeMap: Map, - parent: Entity | null - ): Entity { - const displayName = node.template.displayName || 'Node'; - const entityName = `${displayName}#${node.id}`; - const entity = this.scene.createEntity(entityName); - this.entityMap.set(node.id, entity); - - if (parent) { - parent.addChild(entity); + for (const [key, value] of Object.entries(blackboard)) { + treeData.blackboardVariables!.set(key, value); } - const btNode = new BehaviorTreeNodeComponent(); - btNode.nodeType = this.getNodeType(node); - entity.addComponent(btNode); - - this.addNodeComponents(entity, node); - - // 检查是否有属性绑定,如果有则添加 PropertyBindings 组件 - const bindings = this.propertyBindingsMap.get(node.id); - if (bindings && bindings.size > 0) { - const propertyBindings = new PropertyBindings(); - bindings.forEach((variableName, propertyName) => { - propertyBindings.addBinding(propertyName, variableName); - }); - entity.addComponent(propertyBindings); - logger.info(`[PropertyBindings] 为节点 "${node.template.displayName}" 添加了 ${bindings.size} 个属性绑定`); - } - - node.children.forEach(childId => { - const childNode = nodeMap.get(childId); - if (childNode) { - this.createEntityFromNode(childNode, nodeMap, entity); - } - }); - - return entity; + return treeData; } /** - * 获取节点类型 + * 转换节点类型 */ - private getNodeType(node: BehaviorTreeNode): NodeType { - const type = node.template.type; + private convertNodeType(type: string): NodeType { if (type === NodeType.Composite) return NodeType.Composite; if (type === NodeType.Decorator) return NodeType.Decorator; if (type === NodeType.Action) return NodeType.Action; @@ -319,202 +204,44 @@ export class BehaviorTreeExecutor { } /** - * 根据节点数据添加对应的组件 + * 根据显示名称获取实现类型 */ - private addNodeComponents(entity: Entity, node: BehaviorTreeNode): void { - const category = node.template.category; - const data = node.data; - const nodeType = node.template.type; + private getImplementationType(displayName: string, nodeType: string): string { + const typeMap: Record = { + '序列': 'Sequence', + '选择': 'Selector', + '并行': 'Parallel', + '并行选择': 'ParallelSelector', + '随机序列': 'RandomSequence', + '随机选择': 'RandomSelector', + '反转': 'Inverter', + '重复': 'Repeater', + '直到成功': 'UntilSuccess', + '直到失败': 'UntilFail', + '总是成功': 'AlwaysSucceed', + '总是失败': 'AlwaysFail', + '条件装饰器': 'Conditional', + '冷却': 'Cooldown', + '超时': 'Timeout', + '等待': 'Wait', + '日志': 'Log', + '设置变量': 'SetBlackboardValue', + '修改变量': 'ModifyBlackboardValue', + '自定义动作': 'ExecuteAction', + '比较变量': 'BlackboardCompare', + '变量存在': 'BlackboardExists', + '随机概率': 'RandomProbability', + '执行条件': 'ExecuteCondition' + }; - if (category === '根节点' || data.nodeType === 'root') { - // 根节点使用专门的 RootNode 组件 - entity.addComponent(new RootNode()); - } else if (nodeType === NodeType.Action) { - // 根据节点类型而不是 category 来判断,这样可以支持自定义 category - this.addActionComponent(entity, node); - } else if (nodeType === NodeType.Condition) { - this.addConditionComponent(entity, node); - } else if (nodeType === NodeType.Composite) { - this.addCompositeComponent(entity, node); - } else if (nodeType === NodeType.Decorator) { - this.addDecoratorComponent(entity, node); - } - } - - /** - * 添加动作组件 - */ - private addActionComponent(entity: Entity, node: BehaviorTreeNode): void { - const displayName = node.template.displayName; - - if (displayName === '等待') { - const action = new WaitAction(); - action.waitTime = node.data.waitTime ?? 1.0; - entity.addComponent(action); - } else if (displayName === '日志') { - const action = new LogAction(); - action.message = node.data.message ?? ''; - action.level = node.data.level ?? 'log'; - entity.addComponent(action); - } else if (displayName === '设置变量') { - const action = new SetBlackboardValueAction(); - action.variableName = node.data.variableName ?? ''; - action.value = node.data.value; - entity.addComponent(action); - } else if (displayName === '修改变量') { - const action = new ModifyBlackboardValueAction(); - action.variableName = node.data.variableName ?? ''; - action.operation = node.data.operation ?? 'add'; - action.operand = node.data.operand ?? 0; - entity.addComponent(action); - } else if (displayName === '自定义动作') { - const action = new ExecuteAction(); - action.actionCode = node.data.actionCode ?? 'return TaskStatus.Success;'; - entity.addComponent(action); - } else { - const ComponentClass = node.template.componentClass || - (node.template.className ? ComponentRegistry.getComponentType(node.template.className) : null); - - if (ComponentClass) { - try { - const component = new (ComponentClass as any)(); - Object.assign(component, node.data); - entity.addComponent(component as Component); - } catch (error) { - logger.error(`创建动作组件失败: ${node.template.className}, error: ${error}`); - } - } else { - logger.warn(`未找到动作组件类: ${node.template.className}`); - } - } - } - - /** - * 添加条件组件 - */ - private addConditionComponent(entity: Entity, node: BehaviorTreeNode): void { - const displayName = node.template.displayName; - - if (displayName === '比较变量') { - const condition = new BlackboardCompareCondition(); - condition.variableName = node.data.variableName ?? ''; - condition.operator = (node.data.operator as CompareOperator) ?? CompareOperator.Equal; - condition.compareValue = node.data.compareValue; - condition.invertResult = node.data.invertResult ?? false; - entity.addComponent(condition); - } else if (displayName === '变量存在') { - const condition = new BlackboardExistsCondition(); - condition.variableName = node.data.variableName ?? ''; - condition.checkNotNull = node.data.checkNotNull ?? false; - condition.invertResult = node.data.invertResult ?? false; - entity.addComponent(condition); - } else if (displayName === '随机概率') { - const condition = new RandomProbabilityCondition(); - condition.probability = node.data.probability ?? 0.5; - entity.addComponent(condition); - } else if (displayName === '执行条件') { - const condition = new ExecuteCondition(); - condition.conditionCode = node.data.conditionCode ?? ''; - condition.invertResult = node.data.invertResult ?? false; - entity.addComponent(condition); - } else { - const ComponentClass = node.template.componentClass || - (node.template.className ? ComponentRegistry.getComponentType(node.template.className) : null); - - if (ComponentClass) { - try { - const component = new (ComponentClass as any)(); - Object.assign(component, node.data); - entity.addComponent(component as Component); - } catch (error) { - logger.error(`创建条件组件失败: ${node.template.className}, error: ${error}`); - } - } else { - logger.warn(`未找到条件组件类: ${node.template.className}`); - } - } - } - - /** - * 添加复合节点组件 - */ - private addCompositeComponent(entity: Entity, node: BehaviorTreeNode): void { - const displayName = node.template.displayName; - - if (displayName === '序列') { - const composite = new SequenceNode(); - composite.abortType = (node.data.abortType as AbortType) ?? AbortType.None; - entity.addComponent(composite); - } else if (displayName === '选择') { - const composite = new SelectorNode(); - composite.abortType = (node.data.abortType as AbortType) ?? AbortType.None; - entity.addComponent(composite); - } else if (displayName === '并行') { - const composite = new ParallelNode(); - composite.successPolicy = node.data.successPolicy ?? 'all'; - composite.failurePolicy = node.data.failurePolicy ?? 'one'; - entity.addComponent(composite); - } else if (displayName === '并行选择') { - const composite = new ParallelSelectorNode(); - composite.failurePolicy = node.data.failurePolicy ?? 'one'; - entity.addComponent(composite); - } else if (displayName === '随机序列') { - const composite = new RandomSequenceNode(); - entity.addComponent(composite); - } else if (displayName === '随机选择') { - const composite = new RandomSelectorNode(); - entity.addComponent(composite); - } else if (displayName === '子树') { - const composite = new SubTreeNode(); - composite.assetId = node.data.assetId ?? ''; - composite.inheritParentBlackboard = node.data.inheritParentBlackboard ?? true; - composite.propagateFailure = node.data.propagateFailure ?? true; - entity.addComponent(composite); - } - } - - /** - * 添加装饰器组件 - */ - private addDecoratorComponent(entity: Entity, node: BehaviorTreeNode): void { - const displayName = node.template.displayName; - - if (displayName === '反转') { - entity.addComponent(new InverterNode()); - } else if (displayName === '重复') { - const decorator = new RepeaterNode(); - decorator.repeatCount = node.data.repeatCount ?? -1; - decorator.endOnFailure = node.data.endOnFailure ?? false; - entity.addComponent(decorator); - } else if (displayName === '直到成功') { - entity.addComponent(new UntilSuccessNode()); - } else if (displayName === '直到失败') { - entity.addComponent(new UntilFailNode()); - } else if (displayName === '总是成功') { - entity.addComponent(new AlwaysSucceedNode()); - } else if (displayName === '总是失败') { - entity.addComponent(new AlwaysFailNode()); - } else if (displayName === '条件装饰器') { - const decorator = new ConditionalNode(); - decorator.conditionCode = node.data.conditionCode; - decorator.shouldReevaluate = node.data.shouldReevaluate ?? true; - entity.addComponent(decorator); - } else if (displayName === '冷却') { - const decorator = new CooldownNode(); - decorator.cooldownTime = node.data.cooldownTime ?? 1.0; - entity.addComponent(decorator); - } else if (displayName === '超时') { - const decorator = new TimeoutNode(); - decorator.timeoutDuration = node.data.timeoutDuration ?? 1.0; - entity.addComponent(decorator); - } + return typeMap[displayName] || displayName; } /** * 开始执行 */ start(): void { - if (!this.rootEntity) { + if (!this.runtime || !this.treeData) { logger.error('未构建行为树'); return; } @@ -525,17 +252,10 @@ export class BehaviorTreeExecutor { this.lastStatuses.clear(); this.tickCount = 0; - // 保存黑板变量的初始值(深拷贝) - this.initialBlackboardVariables = JSON.parse(JSON.stringify(this.blackboardVariables)); + this.runtime.resetAllStates(); + this.runtime.isRunning = true; this.addLog('开始执行行为树', 'info'); - - // 打印树结构用于调试 - this.addLog('=== 行为树结构 ===', 'info'); - this.logEntityStructure(this.rootEntity, 0); - this.addLog('===================', 'info'); - - this.rootEntity.addComponent(new ActiveNode()); } /** @@ -543,6 +263,9 @@ export class BehaviorTreeExecutor { */ pause(): void { this.isPaused = true; + if (this.runtime) { + this.runtime.isRunning = false; + } } /** @@ -550,6 +273,9 @@ export class BehaviorTreeExecutor { */ resume(): void { this.isPaused = false; + if (this.runtime) { + this.runtime.isRunning = true; + } } /** @@ -559,75 +285,26 @@ export class BehaviorTreeExecutor { this.isRunning = false; this.isPaused = false; - if (this.rootEntity) { - this.deactivateAllNodes(this.rootEntity); - - // 恢复黑板变量到初始值 - this.restoreBlackboardVariables(); - } - } - - /** - * 恢复黑板变量到初始值 - */ - private restoreBlackboardVariables(): void { - if (!this.rootEntity) { - return; + if (this.runtime) { + this.runtime.isRunning = false; + this.runtime.resetAllStates(); } - const blackboard = this.rootEntity.getComponent(BlackboardComponent); - if (!blackboard) { - return; - } - - // 恢复所有变量到初始值 - Object.entries(this.initialBlackboardVariables).forEach(([key, value]) => { - blackboard.setValue(key, value, true); - }); - - // 同步到 blackboardVariables - this.blackboardVariables = JSON.parse(JSON.stringify(this.initialBlackboardVariables)); - - this.addLog('已恢复黑板变量到初始值', 'info'); - } - - /** - * 递归停用所有节点 - */ - private deactivateAllNodes(entity: Entity): void { - entity.removeComponentByType(ActiveNode); - - const btNode = entity.getComponent(BehaviorTreeNodeComponent); - if (btNode) { - btNode.reset(); - } - - entity.children.forEach((child: Entity) => this.deactivateAllNodes(child)); + this.addLog('行为树已停止', 'info'); } /** * 执行一帧 */ tick(deltaTime: number): void { - if (!this.isRunning || this.isPaused) { + if (!this.isRunning || this.isPaused || !this.runtime) { return; } - // 更新全局时间信息 Time.update(deltaTime); - this.tickCount++; - if (this.debugMode) { - this.addLog(`=== Tick ${this.tickCount} ===`, 'info'); - } - this.scene.update(); - - if (this.debugMode) { - this.logDebugSystemExecution(); - } - this.collectExecutionStatus(); } @@ -635,32 +312,34 @@ export class BehaviorTreeExecutor { * 收集所有节点的执行状态 */ private collectExecutionStatus(): void { - if (!this.callback) return; + if (!this.callback || !this.runtime || !this.treeData) return; const statuses: ExecutionStatus[] = []; - this.entityMap.forEach((entity, nodeId) => { - const btNode = entity.getComponent(BehaviorTreeNodeComponent); - if (!btNode) return; + for (const [nodeId, nodeData] of this.treeData.nodes.entries()) { + const state = this.runtime.getNodeState(nodeId); let status: 'running' | 'success' | 'failure' | 'idle' = 'idle'; - if (entity.hasComponent(ActiveNode)) { - status = 'running'; + if (state) { + switch (state.status) { + case TaskStatus.Success: + status = 'success'; + break; + case TaskStatus.Failure: + status = 'failure'; + break; + case TaskStatus.Running: + status = 'running'; + break; + default: + status = 'idle'; + } } - if (btNode.status === TaskStatus.Success) { - status = 'success'; - } else if (btNode.status === TaskStatus.Failure) { - status = 'failure'; - } else if (btNode.status === TaskStatus.Running) { - status = 'running'; - } - - // 检测状态变化并记录日志 const lastStatus = this.lastStatuses.get(nodeId); if (lastStatus !== status) { - this.onNodeStatusChanged(nodeId, entity.name, lastStatus || 'idle', status, entity); + this.onNodeStatusChanged(nodeId, nodeData.name, lastStatus || 'idle', status); this.lastStatuses.set(nodeId, status); } @@ -668,28 +347,9 @@ export class BehaviorTreeExecutor { nodeId, status }); - }); - - // 收集LogOutput组件中的日志 - if (this.rootEntity) { - const logOutput = this.rootEntity.getComponent(LogOutput); - if (logOutput && logOutput.messages.length > 0) { - // 将LogOutput中的日志转换为ExecutionLog格式并添加到日志列表 - logOutput.messages.forEach((msg) => { - this.addLog( - msg.message, - msg.level === 'error' ? 'error' : - msg.level === 'warn' ? 'warning' : 'info' - ); - }); - // 清空已处理的日志 - logOutput.clear(); - } } - // 获取当前黑板变量 const currentBlackboardVars = this.getBlackboardVariables(); - this.callback(statuses, this.executionLogs, currentBlackboardVars); } @@ -700,24 +360,14 @@ export class BehaviorTreeExecutor { nodeId: string, nodeName: string, oldStatus: string, - newStatus: string, - entity: Entity + newStatus: string ): void { if (newStatus === 'running') { - this.addLog(`[${nodeName}] 开始执行`, 'info', nodeId); + this.addLog(`[${nodeName}](${nodeId}) 开始执行`, 'info', nodeId); } else if (newStatus === 'success') { - // 检查是否是空的复合节点 - // 排除动态加载子节点的节点(如 SubTreeNode),它们的子节点是运行时动态加载的 - const btNode = entity.getComponent(BehaviorTreeNodeComponent); - const hasDynamicChildren = entity.hasComponent(SubTreeNode); - if (btNode && btNode.nodeType === NodeType.Composite && - entity.children.length === 0 && !hasDynamicChildren) { - this.addLog(`[${nodeName}] 执行成功(空节点,无子节点)`, 'warning', nodeId); - } else { - this.addLog(`[${nodeName}] 执行成功`, 'success', nodeId); - } + this.addLog(`[${nodeName}](${nodeId}) 执行成功`, 'success', nodeId); } else if (newStatus === 'failure') { - this.addLog(`[${nodeName}] 执行失败`, 'error', nodeId); + this.addLog(`[${nodeName}](${nodeId}) 执行失败`, 'error', nodeId); } } @@ -732,7 +382,6 @@ export class BehaviorTreeExecutor { nodeId }); - // 限制日志数量,避免内存泄漏 if (this.executionLogs.length > 1000) { this.executionLogs.shift(); } @@ -749,16 +398,13 @@ export class BehaviorTreeExecutor { * 获取黑板变量 */ getBlackboardVariables(): Record { - if (!this.rootEntity) return {}; - - const blackboard = this.rootEntity.getComponent(BlackboardComponent); - if (!blackboard) return {}; + if (!this.runtime) return {}; const variables: Record = {}; - const names = blackboard.getVariableNames(); - names.forEach((name: string) => { - variables[name] = blackboard.getValue(name); - }); + + for (const name of this.blackboardKeys) { + variables[name] = this.runtime.getBlackboardValue(name); + } return variables; } @@ -767,145 +413,44 @@ export class BehaviorTreeExecutor { * 更新黑板变量 */ updateBlackboardVariable(key: string, value: any): void { - if (!this.rootEntity) { + if (!this.runtime) { logger.warn('无法更新黑板变量:未构建行为树'); return; } - const blackboard = this.rootEntity.getComponent(BlackboardComponent); - if (!blackboard) { - logger.warn('无法更新黑板变量:未找到黑板组件'); - return; - } - - if (!blackboard.hasVariable(key)) { - logger.warn(`无法更新黑板变量:变量 "${key}" 不存在`); - return; - } - - blackboard.setValue(key, value); - this.blackboardVariables[key] = value; + this.runtime.setBlackboardValue(key, value); logger.info(`黑板变量已更新: ${key} = ${JSON.stringify(value)}`); } - /** - * 记录树结构的 debug 信息 - */ - private logDebugTreeStructure(): void { - if (!this.rootEntity) return; - - this.addLog('=== 行为树结构 Debug 信息 ===', 'info'); - this.logEntityStructure(this.rootEntity, 0); - this.addLog('=== Debug 信息结束 ===', 'info'); - } - - /** - * 递归记录实体结构 - */ - private logEntityStructure(entity: Entity, depth: number): void { - const indent = ' '.repeat(depth); - const nodeId = Array.from(this.entityMap.entries()).find(([_, e]) => e === entity)?.[0] || 'unknown'; - const btNode = entity.getComponent(BehaviorTreeNodeComponent); - - // 获取节点的具体类型组件 - const allComponents = entity.components.map(c => c.constructor.name); - const nodeTypeComponent = allComponents.find(name => - name !== 'BehaviorTreeNode' && name !== 'ActiveNode' && - name !== 'BlackboardComponent' && name !== 'LogOutput' && - name !== 'PropertyBindings' - ) || 'Unknown'; - - // 构建节点显示名称 - let nodeName = entity.name; - if (nodeTypeComponent !== 'Unknown') { - nodeName = `${nodeName} [${nodeTypeComponent}]`; - } - - this.addLog( - `${indent}└─ ${nodeName} (id: ${nodeId})`, - 'info' - ); - - // 检查是否是 SubTreeNode,如果是则显示子树内部结构 - const subTreeNode = entity.getComponent(SubTreeNode); - if (subTreeNode) { - const subTreeRoot = subTreeNode.getSubTreeRoot(); - if (subTreeRoot) { - this.addLog(`${indent} [SubTree] 资产ID: ${subTreeNode.assetId}`, 'info'); - this.addLog(`${indent} [SubTree] 内部结构:`, 'info'); - this.logEntityStructure(subTreeRoot, depth + 1); - } else { - this.addLog(`${indent} [SubTree] 资产ID: ${subTreeNode.assetId} (未加载)`, 'info'); - } - } - - if (entity.children.length > 0) { - this.addLog(`${indent} 子节点数: ${entity.children.length}`, 'info'); - entity.children.forEach((child: Entity) => { - this.logEntityStructure(child, depth + 1); - }); - } else if (btNode && (btNode.nodeType === NodeType.Decorator || btNode.nodeType === NodeType.Composite)) { - // SubTreeNode 是特殊情况,不需要静态子节点 - if (!subTreeNode) { - this.addLog(`${indent} ⚠ 警告: 此节点应该有子节点`, 'warning'); - } - } - } - - /** - * 记录系统执行的 debug 信息 - */ - private logDebugSystemExecution(): void { - if (!this.rootEntity) return; - - const activeEntities: Entity[] = []; - this.entityMap.forEach((entity) => { - if (entity.hasComponent(ActiveNode)) { - activeEntities.push(entity); - } - }); - - if (activeEntities.length > 0) { - this.addLog(`活跃节点数: ${activeEntities.length}`, 'info'); - activeEntities.forEach((entity) => { - const nodeId = Array.from(this.entityMap.entries()).find(([_, e]) => e === entity)?.[0]; - const btNode = entity.getComponent(BehaviorTreeNodeComponent); - - // 显示该节点的详细信息 - const components = entity.components.map(c => c.constructor.name).join(', '); - this.addLog( - ` - [${entity.name}] status=${btNode?.status}, nodeType=${btNode?.nodeType}, children=${entity.children.length}`, - 'info', - nodeId - ); - this.addLog(` 组件: ${components}`, 'info', nodeId); - - // 显示子节点状态 - if (entity.children.length > 0) { - entity.children.forEach((child: Entity, index: number) => { - const childBtNode = child.getComponent(BehaviorTreeNodeComponent); - const childActive = child.hasComponent(ActiveNode); - this.addLog( - ` 子节点${index}: [${child.name}] status=${childBtNode?.status}, active=${childActive}`, - 'info', - nodeId - ); - }); - } - }); - } - } - /** * 清理资源 */ cleanup(): void { this.stop(); - this.entityMap.clear(); - this.propertyBindingsMap.clear(); - this.rootEntity = null; + this.nodeIdMap.clear(); + this.lastStatuses.clear(); + this.blackboardKeys = []; - this.scene.destroyAllEntities(); + if (this.entity) { + this.entity.destroy(); + this.entity = null; + } + + // 卸载旧的行为树资产 + if (this.treeData) { + this.assetManager.unloadAsset(this.treeData.id); + } + + this.runtime = null; + this.treeData = null; + } + + /** + * 检查节点的执行器是否存在 + */ + hasExecutor(implementationType: string): boolean { + const registry = this.executionSystem.getExecutorRegistry(); + return registry.has(implementationType); } /** @@ -915,4 +460,3 @@ export class BehaviorTreeExecutor { this.cleanup(); } } - diff --git a/packages/editor-app/vite.config.ts b/packages/editor-app/vite.config.ts index 06a6dc08..d2218b1d 100644 --- a/packages/editor-app/vite.config.ts +++ b/packages/editor-app/vite.config.ts @@ -264,7 +264,7 @@ export default pluginInstance; const pluginTs = `import type { IEditorPlugin } from '@esengine/editor-core'; import { EditorPluginCategory } from '@esengine/editor-core'; import type { Core, ServiceContainer } from '@esengine/ecs-framework'; -import { getRegisteredNodeTemplates } from '@esengine/behavior-tree'; +import { NodeTemplates } from '@esengine/behavior-tree'; import type { NodeTemplate } from '@esengine/behavior-tree'; import { t, setLocale } from './locales'; @@ -294,7 +294,7 @@ export class ${pluginName.split('-').map(s => s.charAt(0).toUpperCase() + s.slic } getNodeTemplates(): NodeTemplate[] { - const templates = getRegisteredNodeTemplates(); + const templates = NodeTemplates.getAllTemplates(); return templates.map(template => ({ ...template, displayName: t(template.displayName), @@ -317,7 +317,9 @@ export const ${pluginName.replace(/-/g, '')}Plugin = new ${pluginName.split('-') fs.mkdirSync(localesDir, { recursive: true }); } - const localesIndexTs = `export const translations = { + const localesIndexTs = `export type LocaleKey = 'zh' | 'en'; + +export const translations: Record> = { zh: { 'plugin.name': '${pluginName}', 'plugin.description': '${pluginName} 行为树插件', @@ -336,10 +338,10 @@ export const ${pluginName.replace(/-/g, '')}Plugin = new ${pluginName.split('-') } }; -let currentLocale = 'zh'; +let currentLocale: LocaleKey = 'zh'; export function setLocale(locale: string) { - currentLocale = locale; + currentLocale = (locale === 'en' ? 'en' : 'zh') as LocaleKey; } export function t(key: string): string { @@ -349,29 +351,31 @@ export function t(key: string): string { fs.writeFileSync(path.join(localesDir, 'index.ts'), localesIndexTs); if (includeExample) { - const exampleActionTs = `import { Component, Entity, ECSComponent, Serialize } from '@esengine/ecs-framework'; -import { BehaviorNode, BehaviorProperty, NodeType, TaskStatus, BlackboardComponent } from '@esengine/behavior-tree'; + const exampleActionTs = `import { TaskStatus, NodeType } from '@esengine/behavior-tree'; +import { INodeExecutor, NodeExecutionContext, BindingHelper, NodeExecutorMetadata } from '@esengine/behavior-tree'; -@ECSComponent('ExampleAction') -@BehaviorNode({ +/** + * 示例动作执行器 + */ +@NodeExecutorMetadata({ + implementationType: 'ExampleAction', + nodeType: NodeType.Action, displayName: 'ExampleAction.name', - category: '自定义', - type: NodeType.Action, - icon: 'Star', description: 'ExampleAction.description', - color: '#FF9800' + category: '自定义', + configSchema: { + message: { + type: 'string', + default: 'Hello from example action!', + description: 'ExampleAction.message.description', + supportBinding: true + } + } }) -export class ExampleAction extends Component { - @Serialize() - @BehaviorProperty({ - label: 'ExampleAction.message.label', - type: 'string', - description: 'ExampleAction.message.description' - }) - message: string = 'Hello from example action!'; - - execute(entity: Entity, blackboard?: BlackboardComponent): TaskStatus { - console.log(this.message); +export class ExampleAction implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const message = BindingHelper.getValue(context, 'message', ''); + console.log('[ExampleAction]', message); return TaskStatus.Success; } } @@ -444,17 +448,30 @@ export const translations = { ### 在代码中使用 \`\`\`typescript -import { t } from '../locales'; +import { TaskStatus, NodeType } from '@esengine/behavior-tree'; +import { INodeExecutor, NodeExecutionContext, BindingHelper, NodeExecutorMetadata } from '@esengine/behavior-tree'; -@BehaviorNode({ - displayName: t('YourNode.name'), - description: t('YourNode.description') +@NodeExecutorMetadata({ + implementationType: 'YourNode', + nodeType: NodeType.Action, + displayName: 'YourNode.name', + description: 'YourNode.description', + category: '自定义', + configSchema: { + propertyName: { + type: 'string', + default: '', + description: 'YourNode.propertyName.description', + supportBinding: true + } + } }) -export class YourNode extends Component { - @BehaviorProperty({ - label: t('YourNode.propertyName.label') - }) - propertyName: string = ''; +export class YourNode implements INodeExecutor { + execute(context: NodeExecutionContext): TaskStatus { + const propertyName = BindingHelper.getValue(context, 'propertyName', ''); + // 执行逻辑 + return TaskStatus.Success; + } } \`\`\`