Compare commits
23 Commits
editor-v1.
...
style/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c7c3c98af | ||
|
|
be7b3afb4a | ||
|
|
3e037f4ae0 | ||
|
|
6778ccace4 | ||
|
|
1264232533 | ||
|
|
61813e67b6 | ||
|
|
c58e3411fd | ||
|
|
011d795361 | ||
|
|
3f40a04370 | ||
|
|
fc042bb7d9 | ||
|
|
d051e52131 | ||
|
|
fb4316aeb9 | ||
|
|
683203919f | ||
|
|
a0cddbcae6 | ||
|
|
4e81fc7eba | ||
|
|
b410e2de47 | ||
|
|
9868c746e1 | ||
|
|
f0b4453a5f | ||
|
|
6b49471734 | ||
|
|
fe791e83a8 | ||
|
|
edbc9eb27f | ||
|
|
2f63034d9a | ||
|
|
dee0e0284a |
@@ -25,10 +25,38 @@
|
||||
"arrow-parens": ["error", "always"],
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["private"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "require"
|
||||
},
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["public"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": "memberLike",
|
||||
"modifiers": ["protected"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "require"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -37,6 +37,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run type-check
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint
|
||||
|
||||
- name: Build core package first
|
||||
run: npm run build:core
|
||||
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -10,6 +10,7 @@ on:
|
||||
options:
|
||||
- core
|
||||
- behavior-tree
|
||||
- editor-core
|
||||
version_type:
|
||||
description: '版本更新类型'
|
||||
required: true
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build core package (if needed)
|
||||
if: ${{ github.event.inputs.package == 'behavior-tree' }}
|
||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
||||
run: |
|
||||
cd packages/core
|
||||
npm run build
|
||||
@@ -84,7 +85,7 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
cd packages/${{ github.event.inputs.package }}/dist
|
||||
npm publish
|
||||
npm publish --access public
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
|
||||
53
codecov.yml
Normal file
53
codecov.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
# Codecov 配置文件
|
||||
# https://docs.codecov.com/docs/codecov-yaml
|
||||
|
||||
coverage:
|
||||
status:
|
||||
# 项目整体覆盖率要求
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
base: auto
|
||||
|
||||
# 补丁覆盖率要求(针对 PR 中的新代码)
|
||||
patch:
|
||||
default:
|
||||
target: 50% # 降低补丁覆盖率要求到 50%
|
||||
threshold: 5%
|
||||
base: auto
|
||||
|
||||
# 精确度设置
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
|
||||
# 注释设置
|
||||
comment:
|
||||
layout: "reach,diff,flags,tree,files"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
require_base: false
|
||||
require_head: true
|
||||
|
||||
# 忽略的文件/目录
|
||||
ignore:
|
||||
- "tests/**/*"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/test/**/*"
|
||||
- "**/tests/**/*"
|
||||
- "bin/**/*"
|
||||
- "dist/**/*"
|
||||
- "node_modules/**/*"
|
||||
|
||||
# 标志组
|
||||
flags:
|
||||
core:
|
||||
paths:
|
||||
- packages/core/src/
|
||||
carryforward: true
|
||||
|
||||
# GitHub Checks 配置
|
||||
github_checks:
|
||||
annotations: true
|
||||
@@ -82,6 +82,21 @@ export default defineConfig({
|
||||
{ text: 'WorldManager', link: '/guide/world-manager' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '行为树系统 (Behavior Tree)',
|
||||
link: '/guide/behavior-tree/',
|
||||
items: [
|
||||
{ text: '快速开始', link: '/guide/behavior-tree/getting-started' },
|
||||
{ text: '核心概念', link: '/guide/behavior-tree/core-concepts' },
|
||||
{ text: '编辑器指南', link: '/guide/behavior-tree/editor-guide' },
|
||||
{ text: '编辑器工作流', link: '/guide/behavior-tree/editor-workflow' },
|
||||
{ text: '自定义动作组件', link: '/guide/behavior-tree/custom-actions' },
|
||||
{ text: 'Cocos Creator集成', link: '/guide/behavior-tree/cocos-integration' },
|
||||
{ text: 'Laya引擎集成', link: '/guide/behavior-tree/laya-integration' },
|
||||
{ text: '高级用法', link: '/guide/behavior-tree/advanced-usage' },
|
||||
{ text: '最佳实践', link: '/guide/behavior-tree/best-practices' }
|
||||
]
|
||||
},
|
||||
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
|
||||
{ text: '事件系统 (Event)', link: '/guide/event-system' },
|
||||
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
|
||||
|
||||
392
docs/guide/behavior-tree/advanced-usage.md
Normal file
392
docs/guide/behavior-tree/advanced-usage.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# 高级用法
|
||||
|
||||
本文介绍行为树系统的高级功能和使用技巧。
|
||||
|
||||
## 全局黑板
|
||||
|
||||
全局黑板在所有行为树实例之间共享数据。
|
||||
|
||||
### 使用全局黑板
|
||||
|
||||
```typescript
|
||||
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');
|
||||
|
||||
// 读取全局变量
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
const playerCount = globalBlackboard.getValue<number>('playerCount');
|
||||
```
|
||||
|
||||
### 在自定义执行器中访问全局黑板
|
||||
|
||||
```typescript
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '@esengine/behavior-tree';
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 降低更新频率
|
||||
|
||||
对于不需要每帧更新的AI,可以使用冷却装饰器:
|
||||
|
||||
```typescript
|
||||
// 每0.1秒执行一次
|
||||
const ai = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.1, 'ThrottleRoot')
|
||||
.selector('MainLogic')
|
||||
// AI逻辑...
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 条件缓存
|
||||
|
||||
在自定义执行器中缓存昂贵的条件检查结果:
|
||||
|
||||
```typescript
|
||||
export class CachedCheck implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime, totalTime } = context;
|
||||
const cacheTime = state.lastCheckTime || 0;
|
||||
|
||||
// 如果缓存未过期(1秒内),直接使用缓存结果
|
||||
if (totalTime - cacheTime < 1.0) {
|
||||
return state.cachedResult || TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行昂贵的检查
|
||||
const result = performExpensiveCheck();
|
||||
const status = result ? TaskStatus.Success : TaskStatus.Failure;
|
||||
|
||||
// 缓存结果
|
||||
state.cachedResult = status;
|
||||
state.lastCheckTime = totalTime;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.cachedResult = undefined;
|
||||
context.state.lastCheckTime = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 分帧执行
|
||||
|
||||
将大量计算分散到多帧:
|
||||
|
||||
```typescript
|
||||
export class ProcessLargeDataset implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime } = context;
|
||||
|
||||
const data = runtime.getBlackboardValue<any[]>('dataset') || [];
|
||||
let processedIndex = state.processedIndex || 0;
|
||||
|
||||
const batchSize = 100; // 每帧处理100个
|
||||
const endIndex = Math.min(processedIndex + batchSize, data.length);
|
||||
|
||||
for (let i = processedIndex; i < endIndex; i++) {
|
||||
processItem(data[i]);
|
||||
}
|
||||
|
||||
state.processedIndex = endIndex;
|
||||
|
||||
if (endIndex >= data.length) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.processedIndex = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用日志节点
|
||||
|
||||
在关键位置添加日志:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Debug')
|
||||
.log('开始战斗序列', 'StartCombat')
|
||||
.sequence('Combat')
|
||||
.log('检查生命值', 'CheckHealth')
|
||||
.blackboardCompare('health', 0, 'greater')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 监控黑板状态
|
||||
|
||||
```typescript
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 输出所有黑板变量
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
|
||||
// 输出活动节点
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 3. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
export class DebugAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
console.log(`[${nodeData.name}] 开始执行`);
|
||||
console.log('配置:', nodeData.config);
|
||||
console.log('状态:', state);
|
||||
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||
|
||||
// 执行逻辑...
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能分析
|
||||
|
||||
测量节点执行时间:
|
||||
|
||||
```typescript
|
||||
export class ProfiledAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 执行操作
|
||||
doSomething();
|
||||
|
||||
const elapsed = performance.now() - startTime;
|
||||
console.log(`[${context.nodeData.name}] 耗时: ${elapsed.toFixed(2)}ms`);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见模式
|
||||
|
||||
### 1. 状态机模式
|
||||
|
||||
使用行为树实现状态机:
|
||||
|
||||
```typescript
|
||||
const fsm = BehaviorTreeBuilder.create('StateMachine')
|
||||
.defineBlackboardVariable('currentState', 'idle')
|
||||
.selector('StateSwitch')
|
||||
// Idle状态
|
||||
.sequence('IdleState')
|
||||
.blackboardCompare('currentState', 'idle', 'equals')
|
||||
.log('执行Idle行为', 'IdleBehavior')
|
||||
.end()
|
||||
// Move状态
|
||||
.sequence('MoveState')
|
||||
.blackboardCompare('currentState', 'move', 'equals')
|
||||
.log('执行Move行为', 'MoveBehavior')
|
||||
.end()
|
||||
// Attack状态
|
||||
.sequence('AttackState')
|
||||
.blackboardCompare('currentState', 'attack', 'equals')
|
||||
.log('执行Attack行为', 'AttackBehavior')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
状态转换通过修改黑板变量实现:
|
||||
|
||||
```typescript
|
||||
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()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 3. 并行任务模式
|
||||
|
||||
同时执行多个任务:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('ParallelTasks')
|
||||
.parallel('Effects', { successPolicy: 'all' })
|
||||
.log('播放动画', 'PlayAnimation')
|
||||
.log('播放音效', 'PlaySound')
|
||||
.log('生成粒子', 'SpawnParticles')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 4. 重试模式
|
||||
|
||||
失败时重试:
|
||||
|
||||
```typescript
|
||||
// 使用自定义重试装饰器(参见custom-actions.md中的RetryDecorator示例)
|
||||
// 或者使用UntilSuccess装饰器
|
||||
const tree = BehaviorTreeBuilder.create('Retry')
|
||||
.untilSuccess('RetryUntilSuccess')
|
||||
.log('尝试操作', 'TryOperation')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 5. 超时模式
|
||||
|
||||
限制任务执行时间:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Timeout')
|
||||
.timeout(5.0, 'TimeLimit')
|
||||
.log('长时间运行的任务', 'LongTask')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 与游戏引擎集成
|
||||
|
||||
### Cocos Creator集成
|
||||
|
||||
参见[Cocos Creator集成指南](./cocos-integration.md)
|
||||
|
||||
### LayaAir集成
|
||||
|
||||
参见[LayaAir集成指南](./laya-integration.md)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用黑板
|
||||
|
||||
```typescript
|
||||
// 好的做法:使用类型化的黑板访问
|
||||
const health = runtime.getBlackboardValue<number>('health');
|
||||
|
||||
// 好的做法:定义所有黑板变量
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('state', 'idle')
|
||||
// ...
|
||||
```
|
||||
|
||||
### 2. 避免过深的树结构
|
||||
|
||||
```typescript
|
||||
// 不好:嵌套过深
|
||||
.selector()
|
||||
.sequence()
|
||||
.selector()
|
||||
.sequence()
|
||||
.selector()
|
||||
// 太深了!
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 好:使用合理的深度
|
||||
.selector()
|
||||
.sequence()
|
||||
.log('Action1')
|
||||
.log('Action2')
|
||||
.end()
|
||||
.sequence()
|
||||
.log('Action3')
|
||||
.log('Action4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 3. 使用有意义的节点名称
|
||||
|
||||
```typescript
|
||||
// 好的做法
|
||||
.selector('CombatDecision')
|
||||
.sequence('AttackEnemy')
|
||||
.blackboardExists('target', 'HasTarget')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 不好的做法
|
||||
.selector('Node1')
|
||||
.sequence('Node2')
|
||||
.blackboardExists('target', 'Node3')
|
||||
.log('Attack', 'Node4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 4. 模块化设计
|
||||
|
||||
将复杂逻辑分解为多个独立的行为树,在需要时组合使用。
|
||||
|
||||
### 5. 性能考虑
|
||||
|
||||
- 避免在每帧执行昂贵的操作
|
||||
- 使用冷却装饰器控制执行频率
|
||||
- 缓存计算结果
|
||||
- 合理使用并行节点
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices.md)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide.md)学习可视化编辑
|
||||
506
docs/guide/behavior-tree/asset-management.md
Normal file
506
docs/guide/behavior-tree/asset-management.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# 资产管理
|
||||
|
||||
本文介绍如何加载、管理和复用行为树资产。
|
||||
|
||||
## 为什么需要资产管理?
|
||||
|
||||
在实际游戏开发中,你可能会遇到以下场景:
|
||||
|
||||
1. **多个实体共享同一个行为树** - 100个敌人使用同一套AI逻辑
|
||||
2. **动态加载行为树** - 从JSON文件加载行为树配置
|
||||
3. **子树复用** - 将常用的行为片段(如"巡逻"、"追击")做成独立的子树
|
||||
4. **运行时切换行为树** - 敌人在不同阶段使用不同的行为树
|
||||
|
||||
## BehaviorTreeAssetManager
|
||||
|
||||
框架提供了 `BehaviorTreeAssetManager` 服务来统一管理行为树资产。
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **BehaviorTreeData(行为树数据)**:行为树的定义,可以被多个实体共享
|
||||
- **BehaviorTreeRuntimeComponent(运行时组件)**:每个实体独立的运行时状态
|
||||
- **AssetManager(资产管理器)**:统一管理所有 BehaviorTreeData
|
||||
|
||||
### 基本使用
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 1. 获取资产管理器(插件已自动注册)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 2. 创建并注册行为树资产
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('MainBehavior')
|
||||
.log('攻击')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(enemyAI);
|
||||
|
||||
// 3. 为多个实体使用同一份资产
|
||||
const enemy1 = scene.createEntity('Enemy1');
|
||||
const enemy2 = scene.createEntity('Enemy2');
|
||||
const enemy3 = scene.createEntity('Enemy3');
|
||||
|
||||
// 获取共享的资产
|
||||
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||
|
||||
if (sharedTree) {
|
||||
BehaviorTreeStarter.start(enemy1, sharedTree);
|
||||
BehaviorTreeStarter.start(enemy2, sharedTree);
|
||||
BehaviorTreeStarter.start(enemy3, sharedTree);
|
||||
}
|
||||
```
|
||||
|
||||
### 资产管理器 API
|
||||
|
||||
```typescript
|
||||
// 加载资产
|
||||
assetManager.loadAsset(treeData);
|
||||
|
||||
// 获取资产
|
||||
const tree = assetManager.getAsset('TreeID');
|
||||
|
||||
// 检查资产是否存在
|
||||
if (assetManager.hasAsset('TreeID')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 卸载资产
|
||||
assetManager.unloadAsset('TreeID');
|
||||
|
||||
// 获取所有资产ID
|
||||
const allIds = assetManager.getAllAssetIds();
|
||||
|
||||
// 清空所有资产
|
||||
assetManager.clearAll();
|
||||
```
|
||||
|
||||
## 从文件加载行为树
|
||||
|
||||
### JSON 格式
|
||||
|
||||
行为树可以导出为 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"name": "EnemyAI",
|
||||
"description": "敌人AI行为树"
|
||||
},
|
||||
"rootNodeId": "root-1",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "root-1",
|
||||
"name": "RootSelector",
|
||||
"nodeType": "Composite",
|
||||
"data": {
|
||||
"compositeType": "Selector"
|
||||
},
|
||||
"children": ["combat-1", "patrol-1"]
|
||||
},
|
||||
{
|
||||
"id": "combat-1",
|
||||
"name": "Combat",
|
||||
"nodeType": "Action",
|
||||
"data": {
|
||||
"actionType": "LogAction",
|
||||
"message": "攻击敌人"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"blackboard": [
|
||||
{
|
||||
"name": "health",
|
||||
"type": "number",
|
||||
"defaultValue": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 加载 JSON 文件
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeAssetManager
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function loadTreeFromFile(filePath: string) {
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 1. 读取文件内容
|
||||
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||
|
||||
// 2. 反序列化
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||
|
||||
// 3. 加载到资产管理器
|
||||
assetManager.loadAsset(treeData);
|
||||
|
||||
return treeData;
|
||||
}
|
||||
|
||||
// 使用
|
||||
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
## 子树(SubTree)
|
||||
|
||||
子树允许你将常用的行为片段做成独立的树,然后在其他树中引用。
|
||||
|
||||
### 为什么使用子树?
|
||||
|
||||
1. **代码复用** - 避免重复定义相同的行为
|
||||
2. **模块化** - 将复杂的行为树拆分成小的可管理单元
|
||||
3. **团队协作** - 不同成员可以独立开发不同的子树
|
||||
|
||||
### 创建子树
|
||||
|
||||
```typescript
|
||||
// 1. 创建巡逻子树
|
||||
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
|
||||
.sequence('Patrol')
|
||||
.log('选择巡逻点', 'PickWaypoint')
|
||||
.log('移动到巡逻点', 'MoveToWaypoint')
|
||||
.wait(2.0, 'WaitAtWaypoint')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 2. 创建追击子树
|
||||
const chaseTree = BehaviorTreeBuilder.create('ChaseBehavior')
|
||||
.sequence('Chase')
|
||||
.log('锁定目标', 'LockTarget')
|
||||
.log('追击目标', 'ChaseTarget')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 3. 注册子树到资产管理器
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
assetManager.loadAsset(patrolTree);
|
||||
assetManager.loadAsset(chaseTree);
|
||||
```
|
||||
|
||||
### 使用子树
|
||||
|
||||
```typescript
|
||||
// 在主行为树中使用子树
|
||||
const mainTree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('hasTarget', false)
|
||||
|
||||
.selector('MainBehavior')
|
||||
// 条件:发现目标时执行追击子树
|
||||
.sequence('CombatBranch')
|
||||
.blackboardExists('hasTarget')
|
||||
.subTree('ChaseBehavior', { shareBlackboard: true })
|
||||
.end()
|
||||
|
||||
// 默认:执行巡逻子树
|
||||
.subTree('PatrolBehavior', { shareBlackboard: true })
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(mainTree);
|
||||
|
||||
// 启动主行为树
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(enemy, mainTree);
|
||||
```
|
||||
|
||||
### SubTree 配置选项
|
||||
|
||||
```typescript
|
||||
.subTree('SubTreeID', {
|
||||
shareBlackboard: true, // 是否共享黑板(默认true)
|
||||
})
|
||||
```
|
||||
|
||||
- **shareBlackboard: true** - 子树和父树共享黑板变量
|
||||
- **shareBlackboard: false** - 子树使用独立的黑板
|
||||
|
||||
## 资源预加载
|
||||
|
||||
在游戏启动时预加载所有行为树资产:
|
||||
|
||||
```typescript
|
||||
class BehaviorTreePreloader {
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor() {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
async preloadAll() {
|
||||
// 定义所有行为树文件
|
||||
const treeFiles = [
|
||||
'/assets/ai/enemy-ai.btree.json',
|
||||
'/assets/ai/boss-ai.btree.json',
|
||||
'/assets/ai/patrol.btree.json',
|
||||
'/assets/ai/chase.btree.json'
|
||||
];
|
||||
|
||||
// 并行加载所有文件
|
||||
const loadPromises = treeFiles.map(file => this.loadTree(file));
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
console.log(`已加载 ${this.assetManager.getAssetCount()} 个行为树资产`);
|
||||
}
|
||||
|
||||
private async loadTree(filePath: string) {
|
||||
const jsonContent = await fetch(filePath).then(res => res.text());
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonContent);
|
||||
this.assetManager.loadAsset(treeData);
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏启动时调用
|
||||
const preloader = new BehaviorTreePreloader();
|
||||
await preloader.preloadAll();
|
||||
```
|
||||
|
||||
## 运行时切换行为树
|
||||
|
||||
敌人在不同阶段使用不同的行为树:
|
||||
|
||||
```typescript
|
||||
class EnemyAI {
|
||||
private entity: Entity;
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor(entity: Entity) {
|
||||
this.entity = entity;
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
// 切换到巡逻AI
|
||||
switchToPatrol() {
|
||||
const tree = this.assetManager.getAsset('PatrolAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到战斗AI
|
||||
switchToCombat() {
|
||||
const tree = this.assetManager.getAsset('CombatAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到狂暴模式
|
||||
switchToBerserk() {
|
||||
const tree = this.assetManager.getAsset('BerserkAI');
|
||||
if (tree) {
|
||||
BehaviorTreeStarter.stop(this.entity);
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const enemyAI = new EnemyAI(enemyEntity);
|
||||
|
||||
// Boss血量低于30%时进入狂暴
|
||||
const runtime = enemyEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
const health = runtime?.getBlackboardValue<number>('health');
|
||||
|
||||
if (health && health < 30) {
|
||||
enemyAI.switchToBerserk();
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 1. 共享行为树数据
|
||||
|
||||
```typescript
|
||||
// 好的做法:100个敌人共享1份BehaviorTreeData
|
||||
const sharedTree = assetManager.getAsset('EnemyAI');
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, sharedTree!); // 共享数据
|
||||
}
|
||||
|
||||
// 不好的做法:每个敌人创建独立的BehaviorTreeData
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI') // 重复创建
|
||||
// ... 节点定义
|
||||
.build();
|
||||
BehaviorTreeStarter.start(enemy, tree);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 及时卸载不用的资产
|
||||
|
||||
```typescript
|
||||
// 关卡结束时卸载该关卡的AI
|
||||
function onLevelEnd() {
|
||||
assetManager.unloadAsset('Level1BossAI');
|
||||
assetManager.unloadAsset('Level1EnemyAI');
|
||||
}
|
||||
|
||||
// 加载新关卡的AI
|
||||
function onLevelStart() {
|
||||
const boss2AI = await loadTreeFromFile('/assets/level2-boss.btree.json');
|
||||
assetManager.loadAsset(boss2AI);
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:多敌人类型的游戏
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function setupGame() {
|
||||
// 1. 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 2. 创建共享的子树
|
||||
const patrolTree = BehaviorTreeBuilder.create('Patrol')
|
||||
.sequence('PatrolLoop')
|
||||
.log('巡逻')
|
||||
.wait(1.0)
|
||||
.end()
|
||||
.build();
|
||||
|
||||
const combatTree = BehaviorTreeBuilder.create('Combat')
|
||||
.sequence('CombatLoop')
|
||||
.log('战斗')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(patrolTree);
|
||||
assetManager.loadAsset(combatTree);
|
||||
|
||||
// 3. 创建不同类型敌人的AI
|
||||
const meleeEnemyAI = BehaviorTreeBuilder.create('MeleeEnemyAI')
|
||||
.selector('MeleeBehavior')
|
||||
.sequence('Attack')
|
||||
.blackboardExists('target')
|
||||
.log('近战攻击')
|
||||
.end()
|
||||
.subTree('Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
const rangedEnemyAI = BehaviorTreeBuilder.create('RangedEnemyAI')
|
||||
.selector('RangedBehavior')
|
||||
.sequence('Attack')
|
||||
.blackboardExists('target')
|
||||
.log('远程攻击')
|
||||
.end()
|
||||
.subTree('Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(meleeEnemyAI);
|
||||
assetManager.loadAsset(rangedEnemyAI);
|
||||
|
||||
// 4. 创建多个敌人实体
|
||||
// 10个近战敌人共享同一份AI
|
||||
const meleeAI = assetManager.getAsset('MeleeEnemyAI')!;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const enemy = scene.createEntity(`MeleeEnemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, meleeAI);
|
||||
}
|
||||
|
||||
// 5个远程敌人共享同一份AI
|
||||
const rangedAI = assetManager.getAsset('RangedEnemyAI')!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = scene.createEntity(`RangedEnemy${i}`);
|
||||
BehaviorTreeStarter.start(enemy, rangedAI);
|
||||
}
|
||||
|
||||
console.log(`已创建 15 个敌人,使用 ${assetManager.getAssetCount()} 个行为树资产`);
|
||||
|
||||
// 5. 游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.016);
|
||||
}, 16);
|
||||
}
|
||||
|
||||
setupGame();
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何检查资产是否已加载?
|
||||
|
||||
```typescript
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
if (!assetManager.hasAsset('EnemyAI')) {
|
||||
// 加载资产
|
||||
const tree = await loadTreeFromFile('/assets/enemy-ai.btree.json');
|
||||
assetManager.loadAsset(tree);
|
||||
}
|
||||
```
|
||||
|
||||
### 子树找不到怎么办?
|
||||
|
||||
确保子树已经加载到资产管理器中:
|
||||
|
||||
```typescript
|
||||
// 1. 先加载子树
|
||||
const subTree = BehaviorTreeBuilder.create('SubTreeID')
|
||||
// ...
|
||||
.build();
|
||||
assetManager.loadAsset(subTree);
|
||||
|
||||
// 2. 再加载使用子树的主树
|
||||
const mainTree = BehaviorTreeBuilder.create('MainTree')
|
||||
.subTree('SubTreeID')
|
||||
.build();
|
||||
```
|
||||
|
||||
### 如何导出行为树为 JSON?
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||
|
||||
const tree = BehaviorTreeBuilder.create('MyTree')
|
||||
// ... 节点定义
|
||||
.build();
|
||||
|
||||
// 序列化为JSON字符串
|
||||
const json = BehaviorTreeAssetSerializer.serialize(tree);
|
||||
|
||||
// 保存到文件或发送到服务器
|
||||
console.log(json);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[Cocos Creator 集成](./cocos-integration.md)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的行为树设计
|
||||
468
docs/guide/behavior-tree/best-practices.md
Normal file
468
docs/guide/behavior-tree/best-practices.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# 最佳实践
|
||||
|
||||
本文介绍行为树设计和使用的最佳实践,帮助你构建高效、可维护的AI系统。
|
||||
|
||||
## 行为树设计原则
|
||||
|
||||
### 1. 保持树的层次清晰
|
||||
|
||||
将复杂行为分解成清晰的层次结构:
|
||||
|
||||
```
|
||||
Root Selector
|
||||
├── Emergency (高优先级:紧急情况)
|
||||
│ ├── FleeFromDanger
|
||||
│ └── CallForHelp
|
||||
├── Combat (中优先级:战斗)
|
||||
│ ├── Attack
|
||||
│ └── Defend
|
||||
└── Idle (低优先级:空闲)
|
||||
├── Patrol
|
||||
└── Rest
|
||||
```
|
||||
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 使用内置节点
|
||||
.sequence('AttackSequence')
|
||||
.blackboardExists('target', 'CheckTarget')
|
||||
.log('瞄准', 'Aim')
|
||||
.log('开火', 'Fire')
|
||||
.end()
|
||||
```
|
||||
|
||||
### 3. 使用描述性名称
|
||||
|
||||
节点名称应该清楚地表达其功能:
|
||||
|
||||
```typescript
|
||||
// 好的命名
|
||||
.blackboardCompare('health', 20, 'less', 'CheckHealthLow')
|
||||
.log('寻找最近的医疗包', 'FindHealthPack')
|
||||
.log('移动到医疗包', 'MoveToHealthPack')
|
||||
|
||||
// 不好的命名
|
||||
.blackboardCompare('health', 20, 'less', 'C1')
|
||||
.log('Do something', 'Action1')
|
||||
.log('Move', 'A2')
|
||||
```
|
||||
|
||||
## 黑板变量管理
|
||||
|
||||
### 1. 变量命名规范
|
||||
|
||||
使用清晰的命名约定:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
// 状态变量
|
||||
.defineBlackboardVariable('currentState', 'idle')
|
||||
.defineBlackboardVariable('isMoving', false)
|
||||
|
||||
// 目标和引用
|
||||
.defineBlackboardVariable('targetEnemy', null)
|
||||
.defineBlackboardVariable('patrolPoints', [])
|
||||
|
||||
// 配置参数
|
||||
.defineBlackboardVariable('attackRange', 5.0)
|
||||
.defineBlackboardVariable('moveSpeed', 10.0)
|
||||
|
||||
// 临时数据
|
||||
.defineBlackboardVariable('lastAttackTime', 0)
|
||||
.defineBlackboardVariable('searchAttempts', 0)
|
||||
// ...
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 避免过度使用黑板
|
||||
|
||||
只在需要跨节点共享的数据才放入黑板。在自定义执行器中使用局部变量:
|
||||
|
||||
```typescript
|
||||
// 好的做法 - 使用局部变量
|
||||
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
|
||||
export class TypeSafeAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
|
||||
// 使用泛型进行类型安全访问
|
||||
const health = runtime.getBlackboardValue<number>('health');
|
||||
const target = runtime.getBlackboardValue<Entity | null>('target');
|
||||
const state = runtime.getBlackboardValue<string>('currentState');
|
||||
|
||||
if (health !== undefined && health < 30) {
|
||||
runtime.setBlackboardValue('currentState', 'flee');
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 执行器设计
|
||||
|
||||
### 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
|
||||
// 好的做法 - 只读检查
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'IsHealthLow',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '检查生命值低',
|
||||
category: '条件',
|
||||
configSchema: {
|
||||
threshold: {
|
||||
type: 'number',
|
||||
default: 30,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class IsHealthLow implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const threshold = BindingHelper.getValue<number>(context, 'threshold', 30);
|
||||
const health = context.runtime.getBlackboardValue<number>('health') || 0;
|
||||
|
||||
return health < threshold ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
export class SafeAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
try {
|
||||
const resourceId = context.runtime.getBlackboardValue('resourceId');
|
||||
|
||||
if (!resourceId) {
|
||||
console.error('[SafeAction] 资源ID未设置');
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行操作...
|
||||
|
||||
return TaskStatus.Success;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SafeAction] 执行失败:', error);
|
||||
context.runtime.setBlackboardValue('lastError', error.message);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化技巧
|
||||
|
||||
### 1. 使用冷却装饰器
|
||||
|
||||
避免高频执行昂贵操作:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(1.0, 'ThrottleSearch') // 最多每秒执行一次
|
||||
.log('昂贵的搜索操作', 'ExpensiveSearch')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 缓存计算结果
|
||||
|
||||
```typescript
|
||||
export class CachedFindNearest implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, runtime, totalTime } = context;
|
||||
|
||||
// 检查缓存是否有效
|
||||
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;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
context.state.enemyCacheTime = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用早期退出
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('EarlyExit')
|
||||
.selector('FindTarget')
|
||||
// 先检查缓存的目标
|
||||
.blackboardExists('cachedTarget', 'HasCachedTarget')
|
||||
|
||||
// 没有缓存才进行搜索(需要自定义执行器)
|
||||
.log('执行昂贵的搜索', 'SearchNewTarget')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 可维护性
|
||||
|
||||
### 1. 使用有意义的节点名称
|
||||
|
||||
```typescript
|
||||
// 好的做法
|
||||
const tree = BehaviorTreeBuilder.create('CombatAI')
|
||||
.selector('CombatDecision')
|
||||
.sequence('AttackEnemy')
|
||||
.blackboardExists('target', 'HasTarget')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 不好的做法
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
.selector('Node1')
|
||||
.sequence('Node2')
|
||||
.blackboardExists('target', 'Node3')
|
||||
.log('Attack', 'Node4')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 使用编辑器创建复杂树
|
||||
|
||||
对于复杂的AI,使用可视化编辑器:
|
||||
|
||||
- 更直观的结构
|
||||
- 方便非程序员调整
|
||||
- 易于版本控制
|
||||
- 支持实时调试
|
||||
|
||||
|
||||
### 3. 添加注释和文档
|
||||
|
||||
```typescript
|
||||
// 为行为树添加清晰的注释
|
||||
const bossAI = BehaviorTreeBuilder.create('BossAI')
|
||||
.defineBlackboardVariable('phase', 1) // 1=正常, 2=狂暴, 3=濒死
|
||||
|
||||
.selector('MainBehavior')
|
||||
// 阶段3: 生命值<20%,使用终极技能
|
||||
.sequence('Phase3')
|
||||
.blackboardCompare('phase', 3, 'equals')
|
||||
.log('使用终极技能', 'UltimateAbility')
|
||||
.end()
|
||||
|
||||
// 阶段2: 生命值<50%,进入狂暴
|
||||
.sequence('Phase2')
|
||||
.blackboardCompare('phase', 2, 'equals')
|
||||
.log('进入狂暴模式', 'BerserkMode')
|
||||
.end()
|
||||
|
||||
// 阶段1: 正常战斗
|
||||
.sequence('Phase1')
|
||||
.log('普通攻击', 'NormalAttack')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用日志节点
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Debug')
|
||||
.log('开始攻击序列', 'StartAttack')
|
||||
.sequence('Attack')
|
||||
.log('检查目标', 'CheckTarget')
|
||||
.blackboardExists('target')
|
||||
.log('执行攻击', 'DoAttack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
export class DebugAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
console.group(`[${nodeData.name}]`);
|
||||
console.log('配置:', nodeData.config);
|
||||
console.log('状态:', state);
|
||||
console.log('黑板:', runtime.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime.activeNodeIds));
|
||||
console.groupEnd();
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 状态可视化
|
||||
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见反模式
|
||||
|
||||
### 1. 过深的嵌套
|
||||
|
||||
```typescript
|
||||
// 不好 - 太深的嵌套
|
||||
.selector()
|
||||
.sequence()
|
||||
.sequence()
|
||||
.sequence()
|
||||
.log('太深了', 'DeepAction')
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// 好 - 使用合理的深度
|
||||
.selector()
|
||||
.sequence()
|
||||
.log('Action1')
|
||||
.log('Action2')
|
||||
.end()
|
||||
.sequence()
|
||||
.log('Action3')
|
||||
.log('Action4')
|
||||
.end()
|
||||
.end()
|
||||
```
|
||||
|
||||
### 2. 在执行器中存储状态
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 频繁修改黑板
|
||||
|
||||
```typescript
|
||||
// 不好 - 每帧都修改黑板
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 好 - 只在需要时修改
|
||||
export class SmartUpdate implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const oldPos = context.runtime.getBlackboardValue('position');
|
||||
const newPos = getCurrentPosition();
|
||||
|
||||
// 只在位置变化时更新
|
||||
if (!positionsEqual(oldPos, newPos)) {
|
||||
context.runtime.setBlackboardValue('position', newPos);
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[自定义节点执行器](./custom-actions.md)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts.md)深入理解原理
|
||||
683
docs/guide/behavior-tree/cocos-integration.md
Normal file
683
docs/guide/behavior-tree/cocos-integration.md
Normal file
@@ -0,0 +1,683 @@
|
||||
# Cocos Creator 集成
|
||||
|
||||
本教程将引导你在 Cocos Creator 项目中集成和使用行为树系统。
|
||||
|
||||
## 前置要求
|
||||
|
||||
- Cocos Creator 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started.md)教程
|
||||
|
||||
## 安装
|
||||
|
||||
### 步骤1:安装依赖
|
||||
|
||||
在你的 Cocos Creator 项目根目录下:
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||
```
|
||||
|
||||
### 步骤2:配置 tsconfig.json
|
||||
|
||||
确保 `tsconfig.json` 中包含以下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
建议的项目结构:
|
||||
|
||||
```
|
||||
assets/
|
||||
├── scripts/
|
||||
│ ├── ai/
|
||||
│ │ ├── EnemyAIComponent.ts # AI 组件
|
||||
│ │ └── PlayerDetector.ts # 检测器
|
||||
│ ├── systems/
|
||||
│ │ └── BehaviorTreeSystem.ts # 行为树系统
|
||||
│ └── Main.ts # 主入口
|
||||
├── resources/
|
||||
│ └── behaviors/
|
||||
│ ├── enemy-ai.btree.json # 行为树资产
|
||||
│ └── patrol.btree.json # 子树资产
|
||||
└── types/
|
||||
└── enemy-ai.ts # 类型定义
|
||||
```
|
||||
|
||||
|
||||
## 初始化 ECS 和行为树
|
||||
|
||||
### 创建主入口组件
|
||||
|
||||
创建 `assets/scripts/Main.ts`:
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@ccclass('Main')
|
||||
export class Main extends Component {
|
||||
async onLoad() {
|
||||
// 初始化 ECS Core
|
||||
Core.create();
|
||||
|
||||
// 安装行为树插件
|
||||
const behaviorTreePlugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(behaviorTreePlugin);
|
||||
|
||||
// 创建并设置场景
|
||||
const scene = new Scene();
|
||||
behaviorTreePlugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
console.log('ECS 和行为树系统初始化完成');
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
// 更新 ECS(会自动更新场景)
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 清理资源
|
||||
Core.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 添加组件到场景
|
||||
|
||||
1. 在场景中创建一个空节点(命名为 `GameManager`)
|
||||
2. 添加 `Main` 组件到该节点
|
||||
|
||||
|
||||
## 创建 AI 组件
|
||||
|
||||
创建 `assets/scripts/ai/EnemyAIComponent.ts`:
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component, Node } from 'cc';
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('EnemyAIComponent')
|
||||
export class EnemyAIComponent extends Component {
|
||||
private aiEntity: Entity | null = null;
|
||||
|
||||
async start() {
|
||||
// 创建行为树
|
||||
await this.createBehaviorTree();
|
||||
}
|
||||
|
||||
private async createBehaviorTree() {
|
||||
try {
|
||||
// 获取Core管理的场景
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
console.error('场景未初始化');
|
||||
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();
|
||||
|
||||
// 创建AI实体并启动
|
||||
this.aiEntity = scene.createEntity(`AI_${this.node.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
|
||||
console.log('敌人 AI 已启动');
|
||||
} catch (error) {
|
||||
console.error('初始化行为树失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// 停止 AI
|
||||
if (this.aiEntity) {
|
||||
BehaviorTreeStarter.stop(this.aiEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 与 Cocos 节点交互
|
||||
|
||||
### 创建自定义执行器
|
||||
|
||||
要实现与Cocos节点的交互,需要创建自定义执行器:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
import { Animation } from 'cc';
|
||||
|
||||
@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;
|
||||
|
||||
if (!cocosNode) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const animation = cocosNode.getComponent(Animation);
|
||||
if (animation) {
|
||||
animation.play(animationName);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 完整示例:敌人 AI
|
||||
|
||||
### 行为树设计
|
||||
|
||||
使用编辑器创建 `enemy-ai.btree.json`:
|
||||
|
||||
```
|
||||
RootSelector
|
||||
├── CombatSequence
|
||||
│ ├── CheckPlayerInRange (Condition)
|
||||
│ ├── CheckHealthGood (Condition)
|
||||
│ └── AttackPlayer (Action)
|
||||
├── FleeSequence
|
||||
│ ├── CheckHealthLow (Condition)
|
||||
│ └── RunAway (Action)
|
||||
└── PatrolSequence
|
||||
├── PickWaypoint (Action)
|
||||
├── MoveToWaypoint (Action)
|
||||
└── Wait (Action)
|
||||
```
|
||||
|
||||
|
||||
### 黑板变量
|
||||
|
||||
定义以下黑板变量:
|
||||
|
||||
- `cocosNode`:Node - Cocos 节点引用
|
||||
- `health`:Number - 生命值
|
||||
- `playerNode`:Object - 玩家节点引用
|
||||
- `detectionRange`:Number - 检测范围
|
||||
- `attackRange`:Number - 攻击范围
|
||||
- `currentWaypoint`:Number - 当前路点索引
|
||||
|
||||
|
||||
### 实现检测系统
|
||||
|
||||
创建 `assets/scripts/ai/PlayerDetector.ts`:
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component, Node, Vec3 } from 'cc';
|
||||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('PlayerDetector')
|
||||
export class PlayerDetector extends Component {
|
||||
@property(Node)
|
||||
player: Node = null;
|
||||
|
||||
@property
|
||||
detectionRange: number = 10;
|
||||
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
start() {
|
||||
// 假设AI组件在同一节点上
|
||||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||
if (aiComponent && aiComponent.aiEntity) {
|
||||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
if (!this.runtime || !this.player) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算距离
|
||||
const distance = Vec3.distance(this.node.position, this.player.position);
|
||||
|
||||
// 更新黑板
|
||||
this.runtime.setBlackboardValue('playerNode', this.player);
|
||||
this.runtime.setBlackboardValue('playerInRange', distance <= this.detectionRange);
|
||||
this.runtime.setBlackboardValue('distanceToPlayer', distance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 资源管理
|
||||
|
||||
### 使用 BehaviorTreeAssetManager
|
||||
|
||||
框架提供了 `BehaviorTreeAssetManager` 来统一管理行为树资产,避免重复创建:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 获取资产管理器(插件已自动注册)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 创建并注册行为树(只创建一次)
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('MainBehavior')
|
||||
.log('攻击')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
assetManager.loadAsset(enemyAI);
|
||||
|
||||
// 为多个敌人实体使用同一份资产
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const enemy = scene.createEntity(`Enemy${i}`);
|
||||
const tree = assetManager.getAsset('EnemyAI')!;
|
||||
BehaviorTreeStarter.start(enemy, tree); // 10个敌人共享1份数据
|
||||
}
|
||||
```
|
||||
|
||||
### 从 Cocos Creator 资源加载
|
||||
|
||||
#### 1. 将行为树 JSON 放入 resources 目录
|
||||
|
||||
```
|
||||
assets/
|
||||
└── resources/
|
||||
└── behaviors/
|
||||
├── enemy-ai.btree.json
|
||||
└── boss-ai.btree.json
|
||||
```
|
||||
|
||||
#### 2. 创建资源加载器
|
||||
|
||||
创建 `assets/scripts/BehaviorTreeLoader.ts`:
|
||||
|
||||
```typescript
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeAssetManager,
|
||||
BehaviorTreeAssetSerializer,
|
||||
BehaviorTreeData
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
export class BehaviorTreeLoader {
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
|
||||
constructor() {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 resources 目录加载行为树
|
||||
* @param path 相对于 resources 的路径,不带扩展名
|
||||
* @example await loader.load('behaviors/enemy-ai')
|
||||
*/
|
||||
async load(path: string): Promise<BehaviorTreeData | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resources.load(path, JsonAsset, (err, jsonAsset) => {
|
||||
if (err) {
|
||||
console.error(`加载行为树失败: ${path}`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 反序列化 JSON 为 BehaviorTreeData
|
||||
const jsonStr = JSON.stringify(jsonAsset.json);
|
||||
const treeData = BehaviorTreeAssetSerializer.deserialize(jsonStr);
|
||||
|
||||
// 加载到资产管理器
|
||||
this.assetManager.loadAsset(treeData);
|
||||
|
||||
console.log(`行为树已加载: ${treeData.name}`);
|
||||
resolve(treeData);
|
||||
} catch (error) {
|
||||
console.error(`解析行为树失败: ${path}`, error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载所有行为树
|
||||
*/
|
||||
async preloadAll(paths: string[]): Promise<void> {
|
||||
const promises = paths.map(path => this.load(path));
|
||||
await Promise.all(promises);
|
||||
console.log(`已预加载 ${paths.length} 个行为树`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 在游戏启动时预加载
|
||||
|
||||
修改 `Main.ts`:
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
|
||||
|
||||
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('游戏初始化完成');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试
|
||||
|
||||
### 可视化调试信息
|
||||
|
||||
创建调试组件显示 AI 状态:
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component, Label } from 'cc';
|
||||
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('AIDebugger')
|
||||
export class AIDebugger extends Component {
|
||||
@property(Label)
|
||||
debugLabel: Label = null;
|
||||
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
start() {
|
||||
const aiComponent = this.node.getComponent('EnemyAIComponent') as any;
|
||||
if (aiComponent && aiComponent.aiEntity) {
|
||||
this.runtime = aiComponent.aiEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.runtime || !this.debugLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const health = this.runtime.getBlackboardValue('health');
|
||||
const playerNode = this.runtime.getBlackboardValue('playerNode');
|
||||
|
||||
this.debugLabel.string = `Health: ${health}\nHas Target: ${playerNode ? 'Yes' : 'No'}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 限制行为树数量
|
||||
|
||||
合理控制同时运行的行为树数量:
|
||||
|
||||
```typescript
|
||||
class AIManager {
|
||||
private activeAIs: Entity[] = [];
|
||||
private maxAIs: number = 20;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
this.activeAIs.push(entity);
|
||||
}
|
||||
|
||||
removeAI(entity: Entity) {
|
||||
BehaviorTreeStarter.stop(entity);
|
||||
this.activeAIs = this.activeAIs.filter(e => e !== entity);
|
||||
}
|
||||
|
||||
private findFurthestAI(): Entity | null {
|
||||
// 根据距离找到最远的AI
|
||||
// 实现细节略
|
||||
return this.activeAIs[0];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用冷却装饰器
|
||||
|
||||
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||
.selector('MainBehavior')
|
||||
// AI逻辑...
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 3. 缓存计算结果
|
||||
|
||||
在自定义执行器中缓存昂贵的计算:
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 多平台注意事项
|
||||
|
||||
### 性能考虑
|
||||
|
||||
不同平台的性能差异:
|
||||
|
||||
- **Web平台**: 受浏览器性能限制,建议减少同时运行的AI数量
|
||||
- **原生平台**: 性能较好,可以运行更多AI
|
||||
- **小游戏平台**: 内存受限,注意控制行为树数量和复杂度
|
||||
|
||||
### 平台适配
|
||||
|
||||
```typescript
|
||||
import { sys } from 'cc';
|
||||
|
||||
// 根据平台调整AI数量
|
||||
const maxAIs = sys.isNative ? 50 : (sys.isBrowser ? 20 : 30);
|
||||
|
||||
// 根据平台调整更新频率
|
||||
const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 行为树无法加载?
|
||||
|
||||
检查:
|
||||
1. 资源路径是否正确(相对于 `resources` 目录)
|
||||
2. 文件是否已添加到项目中
|
||||
3. 检查控制台错误信息
|
||||
|
||||
### AI 不执行?
|
||||
|
||||
确保:
|
||||
1. `Main` 组件的 `update` 方法被调用
|
||||
2. `Scene.update()` 在每帧被调用
|
||||
3. 行为树已通过 `BehaviorTreeStarter.start()` 启动
|
||||
|
||||
### 黑板变量不更新?
|
||||
|
||||
检查:
|
||||
1. 变量名拼写是否正确
|
||||
2. 是否在正确的时机更新变量
|
||||
3. 使用 `BehaviorTreeRuntimeComponent.getBlackboardValue()` 和 `setBlackboardValue()` 方法
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management.md)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage.md)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices.md)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义行为
|
||||
491
docs/guide/behavior-tree/core-concepts.md
Normal file
491
docs/guide/behavior-tree/core-concepts.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# 核心概念
|
||||
|
||||
本文介绍行为树系统的核心概念和工作原理。
|
||||
|
||||
## 什么是行为树?
|
||||
|
||||
行为树(Behavior Tree)是一种用于控制AI和自动化系统的决策结构。它通过树状层次结构组织任务,从根节点开始逐层执行,直到找到合适的行为。
|
||||
|
||||
### 与状态机的对比
|
||||
|
||||
传统状态机:
|
||||
- 基于状态和转换
|
||||
- 状态之间的转换复杂
|
||||
- 难以扩展和维护
|
||||
- 不便于复用
|
||||
|
||||
行为树:
|
||||
- 基于任务和层次结构
|
||||
- 模块化、易于复用
|
||||
- 可视化编辑
|
||||
- 灵活的决策逻辑
|
||||
|
||||
|
||||
## 树结构
|
||||
|
||||
行为树由节点组成,形成树状结构:
|
||||
|
||||
```
|
||||
Root (根节点)
|
||||
├── Selector (选择器)
|
||||
│ ├── Sequence (序列)
|
||||
│ │ ├── Condition (条件)
|
||||
│ │ └── Action (动作)
|
||||
│ └── Action (动作)
|
||||
└── Sequence (序列)
|
||||
├── Action (动作)
|
||||
└── Wait (等待)
|
||||
```
|
||||
|
||||
每个节点都有:
|
||||
- 父节点(除了根节点)
|
||||
- 零个或多个子节点
|
||||
- 执行状态
|
||||
- 返回结果
|
||||
|
||||
|
||||
## 节点类型
|
||||
|
||||
### 复合节点(Composite)
|
||||
|
||||
复合节点有多个子节点,按特定规则执行它们。
|
||||
|
||||
#### Selector(选择器)
|
||||
|
||||
按顺序尝试执行子节点,直到某个子节点成功。
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('FindFood')
|
||||
.selector('FindFoodSelector')
|
||||
.log('尝试吃附近的食物', 'EatNearby')
|
||||
.log('搜索食物', 'SearchFood')
|
||||
.log('放弃', 'GiveUp')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
执行逻辑:
|
||||
1. 尝试第一个子节点
|
||||
2. 如果返回Success,选择器成功
|
||||
3. 如果返回Failure,尝试下一个子节点
|
||||
4. 如果返回Running,选择器返回Running
|
||||
5. 所有子节点都失败时,选择器失败
|
||||
|
||||
|
||||
#### Sequence(序列)
|
||||
|
||||
按顺序执行所有子节点,直到某个子节点失败。
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Attack')
|
||||
.sequence('AttackSequence')
|
||||
.blackboardExists('target') // 检查是否有目标
|
||||
.log('瞄准', 'Aim')
|
||||
.log('开火', 'Fire')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
执行逻辑:
|
||||
1. 依次执行子节点
|
||||
2. 如果子节点返回Failure,序列失败
|
||||
3. 如果子节点返回Running,序列返回Running
|
||||
4. 如果子节点返回Success,继续下一个子节点
|
||||
5. 所有子节点都成功时,序列成功
|
||||
|
||||
|
||||
#### Parallel(并行)
|
||||
|
||||
同时执行多个子节点。
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('PlayEffects')
|
||||
.parallel('Effects', {
|
||||
successPolicy: 'all', // 所有任务都要成功
|
||||
failurePolicy: 'one' // 任一失败则失败
|
||||
})
|
||||
.log('播放动画', 'PlayAnimation')
|
||||
.log('播放音效', 'PlaySound')
|
||||
.log('生成粒子', 'SpawnEffect')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
策略类型:
|
||||
- `successPolicy: 'all'`: 所有子节点都成功才成功
|
||||
- `successPolicy: 'one'`: 任意一个子节点成功就成功
|
||||
- `failurePolicy: 'all'`: 所有子节点都失败才失败
|
||||
- `failurePolicy: 'one'`: 任意一个子节点失败就失败
|
||||
|
||||
|
||||
### 装饰器节点(Decorator)
|
||||
|
||||
装饰器节点只有一个子节点,用于修改子节点的行为或结果。
|
||||
|
||||
#### Inverter(反转)
|
||||
|
||||
反转子节点的结果:
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
叶节点没有子节点,执行具体的任务。
|
||||
|
||||
#### Action(动作)
|
||||
|
||||
执行具体操作。内置动作节点包括:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Actions')
|
||||
.sequence()
|
||||
.wait(2.0) // 等待2秒
|
||||
.log('Hello', 'LogAction') // 输出日志
|
||||
.setBlackboardValue('score', 100) // 设置黑板值
|
||||
.modifyBlackboardValue('score', 'add', 10) // 修改黑板值
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
#### Condition(条件)
|
||||
|
||||
检查条件。内置条件节点包括:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Conditions')
|
||||
.selector()
|
||||
.blackboardExists('player') // 检查变量是否存在
|
||||
.blackboardCompare('health', 50, 'greater') // 比较变量值
|
||||
.randomProbability(0.5) // 50%概率
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
#### Wait(等待)
|
||||
|
||||
等待指定时间:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('WaitExample')
|
||||
.wait(2.0, 'Wait2Seconds')
|
||||
.build();
|
||||
```
|
||||
|
||||
|
||||
## 任务状态
|
||||
|
||||
每个节点执行后返回以下状态之一:
|
||||
|
||||
### Success(成功)
|
||||
|
||||
任务成功完成。
|
||||
|
||||
```typescript
|
||||
// 内置节点会根据逻辑自动返回Success
|
||||
.log('任务完成') // 总是返回Success
|
||||
.blackboardCompare('score', 100, 'greater') // 条件满足时返回Success
|
||||
```
|
||||
|
||||
### Failure(失败)
|
||||
|
||||
任务执行失败。
|
||||
|
||||
```typescript
|
||||
.blackboardCompare('score', 100, 'greater') // 条件不满足返回Failure
|
||||
.blackboardExists('nonExistent') // 变量不存在返回Failure
|
||||
```
|
||||
|
||||
### Running(运行中)
|
||||
|
||||
任务需要多帧完成,仍在执行中。
|
||||
|
||||
```typescript
|
||||
.wait(3.0) // 等待过程中返回Running,3秒后返回Success
|
||||
```
|
||||
|
||||
### Invalid(无效)
|
||||
|
||||
节点未初始化或已重置。通常不需要手动处理此状态。
|
||||
|
||||
|
||||
## 黑板系统
|
||||
|
||||
黑板(Blackboard)是行为树的数据存储系统,用于在节点之间共享数据。
|
||||
|
||||
### 本地黑板
|
||||
|
||||
每个行为树实例都有自己的本地黑板:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('state', 'idle')
|
||||
// ...
|
||||
.build();
|
||||
```
|
||||
|
||||
### 支持的数据类型
|
||||
|
||||
黑板支持以下数据类型:
|
||||
- String:字符串
|
||||
- Number:数字
|
||||
- Boolean:布尔值
|
||||
- Vector2:二维向量
|
||||
- Vector3:三维向量
|
||||
- Object:对象引用
|
||||
- Array:数组
|
||||
|
||||
示例:
|
||||
|
||||
```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
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 读取变量
|
||||
const health = runtime?.getBlackboardValue('health');
|
||||
const target = runtime?.getBlackboardValue('target');
|
||||
|
||||
// 写入变量
|
||||
runtime?.setBlackboardValue('health', 50);
|
||||
runtime?.setBlackboardValue('lastAttackTime', Date.now());
|
||||
|
||||
// 获取所有变量
|
||||
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 { 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);
|
||||
|
||||
// 读取全局变量
|
||||
const gameState = globalBlackboard.getValue('gameState');
|
||||
```
|
||||
|
||||
在自定义执行器中访问全局黑板:
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 初始化
|
||||
|
||||
```typescript
|
||||
// 1. 初始化Core和插件
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 2. 创建场景
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 3. 构建行为树
|
||||
const tree = BehaviorTreeBuilder.create('AI')
|
||||
// ... 定义节点
|
||||
.build();
|
||||
|
||||
// 4. 创建实体并启动
|
||||
const entity = scene.createEntity('AIEntity');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
### 更新循环
|
||||
|
||||
```typescript
|
||||
// 每帧更新
|
||||
gameLoop(() => {
|
||||
const deltaTime = getDeltaTime();
|
||||
Core.update(deltaTime); // Core会自动更新场景和所有行为树
|
||||
});
|
||||
```
|
||||
|
||||
### 执行顺序
|
||||
|
||||
```
|
||||
1. 从根节点开始
|
||||
2. 根节点执行其逻辑(通常是Selector或Sequence)
|
||||
3. 根节点的子节点按顺序执行
|
||||
4. 每个子节点可能有自己的子节点
|
||||
5. 叶节点执行具体操作并返回状态
|
||||
6. 状态向上传播到父节点
|
||||
7. 父节点根据策略决定如何处理子节点的状态
|
||||
8. 最终根节点返回整体状态
|
||||
```
|
||||
|
||||
### 执行示例
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('Example')
|
||||
.selector('Root') // 1. 执行选择器
|
||||
.sequence('Branch1') // 2. 尝试第一个分支
|
||||
.blackboardCompare('ready', true, 'equals', 'CheckReady') // 3. 条件失败
|
||||
.end() // 4. 序列失败,选择器继续下一个分支
|
||||
.sequence('Branch2') // 5. 尝试第二个分支
|
||||
.blackboardCompare('active', true, 'equals', 'CheckActive') // 6. 条件成功
|
||||
.log('执行动作', 'DoAction') // 7. 动作成功
|
||||
.end() // 8. 序列成功,选择器成功
|
||||
.end() // 9. 整个树成功
|
||||
.build();
|
||||
```
|
||||
|
||||
执行流程图:
|
||||
|
||||
```
|
||||
Root(Selector)
|
||||
→ Branch1(Sequence)
|
||||
→ CheckReady: Failure
|
||||
→ Branch1 fails
|
||||
→ Branch2(Sequence)
|
||||
→ CheckActive: Success
|
||||
→ DoAction: Success
|
||||
→ Branch2 succeeds
|
||||
→ Root succeeds
|
||||
```
|
||||
|
||||
|
||||
## Runtime架构
|
||||
|
||||
本框架的行为树采用Runtime执行器架构:
|
||||
|
||||
### 核心组件
|
||||
|
||||
- **BehaviorTreeData**: 纯数据结构,描述行为树的结构和配置
|
||||
- **BehaviorTreeRuntimeComponent**: 运行时组件,管理执行状态和黑板
|
||||
- **BehaviorTreeExecutionSystem**: 执行系统,驱动行为树运行
|
||||
- **INodeExecutor**: 节点执行器接口,定义节点的执行逻辑
|
||||
- **NodeExecutionContext**: 执行上下文,包含执行所需的所有信息
|
||||
|
||||
### 架构特点
|
||||
|
||||
1. **数据与逻辑分离**: BehaviorTreeData是纯数据,执行逻辑在执行器中
|
||||
2. **无状态执行器**: 执行器实例可以在多个节点间共享,状态存储在Runtime中
|
||||
3. **类型安全**: 通过TypeScript类型系统保证类型安全
|
||||
4. **高性能**: 避免不必要的对象创建,优化内存使用
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
BehaviorTreeBuilder
|
||||
↓ (构建)
|
||||
BehaviorTreeData
|
||||
↓ (加载到)
|
||||
BehaviorTreeAssetManager
|
||||
↓ (读取)
|
||||
BehaviorTreeExecutionSystem
|
||||
↓ (执行)
|
||||
INodeExecutor.execute(context)
|
||||
↓ (返回)
|
||||
TaskStatus
|
||||
↓ (更新)
|
||||
NodeRuntimeState
|
||||
```
|
||||
|
||||
|
||||
## 下一步
|
||||
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
|
||||
- 查看[快速开始](./getting-started.md)创建第一个行为树
|
||||
- 学习[自定义节点执行器](./custom-actions.md)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage.md)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices.md)学习设计模式
|
||||
1025
docs/guide/behavior-tree/custom-actions.md
Normal file
1025
docs/guide/behavior-tree/custom-actions.md
Normal file
File diff suppressed because it is too large
Load Diff
119
docs/guide/behavior-tree/editor-guide.md
Normal file
119
docs/guide/behavior-tree/editor-guide.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 行为树编辑器使用指南
|
||||
|
||||
行为树编辑器提供了可视化的方式来创建和编辑行为树。
|
||||
|
||||
## 启动编辑器
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
## 基本操作
|
||||
|
||||
### 打开行为树编辑器
|
||||
|
||||
通过以下方式打开行为树编辑器窗口:
|
||||
|
||||
1. 在资产浏览器中双击 `.btree` 文件
|
||||
2. 菜单栏:`窗口` → 选择行为树编辑器相关插件
|
||||
|
||||
### 创建新行为树
|
||||
|
||||
在行为树编辑器窗口的工具栏中点击"新建"按钮(加号图标)
|
||||
|
||||
### 保存行为树
|
||||
|
||||
在行为树编辑器窗口的工具栏中点击"保存"按钮(磁盘图标)
|
||||
|
||||
### 添加节点
|
||||
|
||||
从左侧节点面板拖拽节点到画布:
|
||||
- 复合节点:Selector、Sequence、Parallel
|
||||
- 装饰器:Inverter、Repeater、UntilFail等
|
||||
- 动作节点:ExecuteAction、Wait等
|
||||
- 条件节点:Condition
|
||||
|
||||
### 连接节点
|
||||
|
||||
拖拽父节点底部的连接点到子节点顶部建立连接
|
||||
|
||||
### 删除节点
|
||||
|
||||
选中节点后按 `Delete` 或 `Backspace` 键
|
||||
|
||||
### 编辑属性
|
||||
|
||||
点击节点后在右侧属性面板中编辑节点参数
|
||||
|
||||
## 黑板变量
|
||||
|
||||
在黑板面板中管理共享数据:
|
||||
|
||||
1. 点击"添加变量"按钮
|
||||
2. 输入变量名、选择类型并设置默认值
|
||||
3. 在节点中通过变量名引用黑板变量
|
||||
|
||||
支持的变量类型:
|
||||
- String:字符串
|
||||
- Number:数字
|
||||
- Boolean:布尔值
|
||||
- Vector2:二维向量
|
||||
- Vector3:三维向量
|
||||
- Object:对象引用
|
||||
- Array:数组
|
||||
|
||||
## 导出运行时资产
|
||||
|
||||
### 导出步骤
|
||||
|
||||
1. 点击工具栏的"导出"按钮
|
||||
2. 选择导出模式:
|
||||
- 当前文件:仅导出当前打开的行为树
|
||||
- 工作区导出:导出项目中所有行为树
|
||||
3. 选择资产输出路径
|
||||
4. 选择TypeScript类型定义输出路径
|
||||
5. 为每个文件选择导出格式:
|
||||
- 二进制:.btree.bin(默认,文件更小,加载更快)
|
||||
- JSON:.btree.json(可读性好,便于调试)
|
||||
6. 点击"导出"按钮
|
||||
|
||||
### 加载运行时资产
|
||||
|
||||
编辑器导出的文件是编辑器格式,包含UI布局信息。当前版本中,从编辑器导出的资产可以使用Builder API在代码中重新构建,或者等待资产加载系统的完善。
|
||||
|
||||
推荐使用Builder API创建行为树:
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用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();
|
||||
|
||||
// 启动行为树
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
```
|
||||
|
||||
## 支持的操作
|
||||
|
||||
- `Delete` / `Backspace`:删除选中的节点或连线
|
||||
- `Ctrl` + 点击:多选节点
|
||||
- 框选:拖拽空白区域进行框选
|
||||
- 拖拽画布:按住鼠标中键或空格键拖拽
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[编辑器工作流](./editor-workflow.md)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何扩展节点
|
||||
253
docs/guide/behavior-tree/editor-workflow.md
Normal file
253
docs/guide/behavior-tree/editor-workflow.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# 编辑器工作流
|
||||
|
||||
本教程介绍如何使用行为树编辑器创建AI,并在游戏中加载使用。
|
||||
|
||||
## 完整流程
|
||||
|
||||
```
|
||||
1. 启动编辑器
|
||||
2. 创建行为树并定义黑板变量
|
||||
3. 添加和配置节点
|
||||
4. 导出JSON文件
|
||||
5. 在游戏中加载并使用
|
||||
```
|
||||
|
||||
## 使用编辑器创建
|
||||
|
||||
### 启动编辑器
|
||||
|
||||
```bash
|
||||
cd packages/editor-app
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
### 基本操作
|
||||
|
||||
1. **创建行为树**:`文件` → `新建项目` → 创建行为树文件
|
||||
2. **定义黑板变量**:在黑板面板中添加共享变量
|
||||
3. **添加节点**:从节点面板拖拽到画布
|
||||
4. **连接节点**:拖拽连接点建立父子关系
|
||||
5. **配置属性**:选中节点后在属性面板编辑
|
||||
6. **导出**:`文件` → `导出` → `JSON格式`
|
||||
|
||||
### 示例:敌人AI的黑板变量
|
||||
|
||||
在编辑器黑板面板中定义:
|
||||
|
||||
```
|
||||
health: Number = 100
|
||||
target: Object = null
|
||||
moveSpeed: Number = 5.0
|
||||
attackRange: Number = 2.0
|
||||
```
|
||||
|
||||
### 示例:行为树结构
|
||||
|
||||
```
|
||||
Root: Selector
|
||||
├── Combat Sequence
|
||||
│ ├── CheckHasTarget (Condition)
|
||||
│ ├── CheckInAttackRange (Condition)
|
||||
│ └── ExecuteAttack (Action)
|
||||
├── Patrol Sequence
|
||||
│ ├── MoveToNextPatrolPoint (Action)
|
||||
│ └── Wait 2s
|
||||
└── Idle (Action)
|
||||
```
|
||||
|
||||
## 在游戏中使用
|
||||
|
||||
### 使用Builder API创建
|
||||
|
||||
推荐使用Builder API在代码中创建行为树:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(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 entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
// 访问和修改黑板
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('target', someTarget);
|
||||
|
||||
// 游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.016); // 60 FPS
|
||||
}, 16);
|
||||
```
|
||||
|
||||
## 实现自定义执行器
|
||||
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions.md)):
|
||||
|
||||
```typescript
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
BindingHelper,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '攻击目标',
|
||||
description: '对目标造成伤害',
|
||||
category: '战斗',
|
||||
configSchema: {
|
||||
damage: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
const target = context.runtime.getBlackboardValue('target');
|
||||
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// 执行攻击逻辑
|
||||
performAttack(context.entity, target, damage);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
// 清理状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用日志节点
|
||||
|
||||
在行为树中添加Log节点输出调试信息:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('DebugAI')
|
||||
.log('开始战斗序列', 'StartCombat')
|
||||
.sequence('Combat')
|
||||
.blackboardCompare('health', 0, 'greater')
|
||||
.log('执行攻击', 'Attack')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 2. 监控黑板状态
|
||||
|
||||
```typescript
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 3. 在自定义执行器中调试
|
||||
|
||||
```typescript
|
||||
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 } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 使用Builder API构建行为树
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasTarget', false)
|
||||
.selector('Root')
|
||||
.sequence('Combat')
|
||||
.blackboardCompare('hasTarget', true, 'equals')
|
||||
.log('攻击玩家', 'Attack')
|
||||
.end()
|
||||
.log('空闲', 'Idle')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 创建实体并启动
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
// 模拟发现目标
|
||||
setTimeout(() => {
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasTarget', true);
|
||||
}, 2000);
|
||||
|
||||
// 游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.016);
|
||||
}, 16);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions.md)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage.md)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices.md)优化你的AI设计
|
||||
385
docs/guide/behavior-tree/getting-started.md
Normal file
385
docs/guide/behavior-tree/getting-started.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# 快速开始
|
||||
|
||||
本教程将引导你在5分钟内创建第一个行为树。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/behavior-tree
|
||||
```
|
||||
|
||||
## 第一个行为树
|
||||
|
||||
让我们创建一个简单的AI行为树,实现"巡逻-发现敌人-攻击"的逻辑。
|
||||
|
||||
### 步骤1: 导入依赖
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin
|
||||
} from '@esengine/behavior-tree';
|
||||
```
|
||||
|
||||
### 步骤2: 初始化Core并安装插件
|
||||
|
||||
```typescript
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
### 步骤3: 创建场景并设置行为树系统
|
||||
|
||||
```typescript
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
```
|
||||
|
||||
### 步骤4: 构建行为树数据
|
||||
|
||||
```typescript
|
||||
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||
// 定义黑板变量
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
|
||||
// 根选择器
|
||||
.selector('RootSelector')
|
||||
// 分支1: 如果发现敌人且生命值高,则攻击
|
||||
.selector('CombatBranch')
|
||||
.blackboardExists('hasEnemy', 'CheckEnemy')
|
||||
.blackboardCompare('health', 30, 'greater', 'CheckHealth')
|
||||
.log('守卫正在攻击敌人', 'Attack')
|
||||
.end()
|
||||
|
||||
// 分支2: 如果生命值低,则逃跑
|
||||
.selector('FleeBranch')
|
||||
.blackboardCompare('health', 30, 'lessOrEqual', 'CheckLowHealth')
|
||||
.log('守卫生命值过低,正在逃跑', 'Flee')
|
||||
.end()
|
||||
|
||||
// 分支3: 默认巡逻
|
||||
.selector('PatrolBranch')
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1, 'IncrementPatrol')
|
||||
.log('守卫正在巡逻', 'Patrol')
|
||||
.wait(2.0, 'WaitAtPoint')
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 步骤5: 创建实体并启动行为树
|
||||
|
||||
```typescript
|
||||
// 创建守卫实体
|
||||
const guardEntity = scene.createEntity('Guard');
|
||||
|
||||
// 启动行为树
|
||||
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||
```
|
||||
|
||||
### 步骤6: 运行游戏循环
|
||||
|
||||
```typescript
|
||||
// 模拟游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.1); // 传入deltaTime(秒)
|
||||
}, 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
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin,
|
||||
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);
|
||||
|
||||
// 3. 构建行为树数据
|
||||
const guardAITree = BehaviorTreeBuilder.create('GuardAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
.selector('RootSelector')
|
||||
.selector('CombatBranch')
|
||||
.blackboardExists('hasEnemy')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.log('守卫正在攻击敌人')
|
||||
.end()
|
||||
.selector('FleeBranch')
|
||||
.blackboardCompare('health', 30, 'lessOrEqual')
|
||||
.log('守卫生命值过低,正在逃跑')
|
||||
.end()
|
||||
.selector('PatrolBranch')
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1)
|
||||
.log('守卫正在巡逻')
|
||||
.wait(2.0)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 4. 创建守卫实体并启动行为树
|
||||
const guardEntity = scene.createEntity('Guard');
|
||||
BehaviorTreeStarter.start(guardEntity, guardAITree);
|
||||
|
||||
// 5. 运行游戏循环
|
||||
setInterval(() => {
|
||||
Core.update(0.1);
|
||||
}, 100);
|
||||
|
||||
// 6. 模拟游戏事件
|
||||
setTimeout(() => {
|
||||
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();
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你会看到类似的输出:
|
||||
|
||||
```
|
||||
守卫正在巡逻
|
||||
守卫正在巡逻
|
||||
守卫正在巡逻
|
||||
发现敌人!
|
||||
守卫正在攻击敌人
|
||||
守卫正在攻击敌人
|
||||
守卫受伤!
|
||||
守卫生命值过低,正在逃跑
|
||||
```
|
||||
|
||||
## 理解代码
|
||||
|
||||
### 黑板变量
|
||||
|
||||
```typescript
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('hasEnemy', false)
|
||||
.defineBlackboardVariable('patrolPoint', 0)
|
||||
```
|
||||
|
||||
黑板用于在节点之间共享数据。这里定义了三个变量:
|
||||
- `health`: 守卫的生命值
|
||||
- `hasEnemy`: 是否发现敌人
|
||||
- `patrolPoint`: 当前巡逻点编号
|
||||
|
||||
### 选择器节点
|
||||
|
||||
```typescript
|
||||
.selector('RootSelector')
|
||||
// 分支1
|
||||
// 分支2
|
||||
// 分支3
|
||||
.end()
|
||||
```
|
||||
|
||||
选择器按顺序尝试执行子节点,直到某个子节点返回成功。类似于编程中的 `if-else if-else`。
|
||||
|
||||
### 条件节点
|
||||
|
||||
```typescript
|
||||
.blackboardExists('hasEnemy') // 检查变量是否存在
|
||||
.blackboardCompare('health', 30, 'greater') // 比较变量值
|
||||
```
|
||||
|
||||
条件节点用于检查黑板变量的值。
|
||||
|
||||
### 动作节点
|
||||
|
||||
```typescript
|
||||
.log('守卫正在攻击敌人') // 输出日志
|
||||
.wait(2.0) // 等待2秒
|
||||
.modifyBlackboardValue('patrolPoint', 'add', 1) // 修改黑板值
|
||||
```
|
||||
|
||||
动作节点执行具体的操作。
|
||||
|
||||
### Runtime组件
|
||||
|
||||
```typescript
|
||||
const runtime = guardEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('hasEnemy', true);
|
||||
runtime?.getBlackboardValue('health');
|
||||
```
|
||||
|
||||
通过`BehaviorTreeRuntimeComponent`访问和修改黑板变量。
|
||||
|
||||
## 常见任务状态
|
||||
|
||||
行为树的每个节点返回以下状态之一:
|
||||
|
||||
- **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. 学习[资产管理](./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(entity, treeData)`
|
||||
4. 在游戏循环中调用了 `Core.update(deltaTime)`
|
||||
|
||||
### 如何访问黑板变量?
|
||||
|
||||
```typescript
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// 读取
|
||||
const health = runtime?.getBlackboardValue('health');
|
||||
|
||||
// 写入
|
||||
runtime?.setBlackboardValue('health', 50);
|
||||
|
||||
// 获取所有变量
|
||||
const allVars = runtime?.getAllBlackboardVariables();
|
||||
```
|
||||
|
||||
### 如何调试行为树?
|
||||
|
||||
使用日志节点:
|
||||
|
||||
```typescript
|
||||
.log('到达这个节点', 'DebugLog')
|
||||
```
|
||||
|
||||
或者在代码中监控黑板:
|
||||
|
||||
```typescript
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
console.log('黑板变量:', runtime?.getAllBlackboardVariables());
|
||||
console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
```
|
||||
|
||||
### 如何使用自定义逻辑?
|
||||
|
||||
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||
|
||||
参见[自定义节点执行器](./custom-actions.md)学习如何创建。
|
||||
197
docs/guide/behavior-tree/index.md
Normal file
197
docs/guide/behavior-tree/index.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# 行为树系统
|
||||
|
||||
行为树(Behavior Tree)是一种用于游戏AI和自动化控制的强大工具。本框架提供了基于Runtime执行器架构的行为树系统,具有高性能、类型安全、易于扩展的特点。
|
||||
|
||||
## 什么是行为树?
|
||||
|
||||
行为树是一种层次化的任务执行结构,由多个节点组成,每个节点负责特定的任务。行为树特别适合于:
|
||||
|
||||
- 游戏AI(敌人、NPC行为)
|
||||
- 状态机的替代方案
|
||||
- 复杂的决策逻辑
|
||||
- 可视化的行为设计
|
||||
|
||||
## 核心特性
|
||||
|
||||
### Runtime执行器架构
|
||||
- 数据与逻辑分离
|
||||
- 无状态执行器设计
|
||||
- 高性能执行
|
||||
- 类型安全
|
||||
|
||||
### 可视化编辑器
|
||||
- 图形化节点编辑
|
||||
- 实时预览和调试
|
||||
- 拖拽式节点创建
|
||||
- 属性连接和绑定
|
||||
|
||||
### 灵活的黑板系统
|
||||
- 本地黑板(单个行为树)
|
||||
- 全局黑板(所有行为树共享)
|
||||
- 类型安全的变量访问
|
||||
- 支持属性绑定
|
||||
|
||||
### 插件系统
|
||||
- 自动注册机制
|
||||
- 装饰器声明元数据
|
||||
- 支持多语言
|
||||
- 易于扩展
|
||||
|
||||
## 文档导航
|
||||
|
||||
### 入门教程
|
||||
|
||||
- **[快速开始](./getting-started.md)** - 5分钟上手行为树
|
||||
- **[核心概念](./core-concepts.md)** - 理解行为树的基本原理
|
||||
|
||||
### 编辑器使用
|
||||
|
||||
- **[编辑器使用指南](./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)** - 创建自定义行为节点
|
||||
- **[最佳实践](./best-practices.md)** - 行为树设计模式和技巧
|
||||
|
||||
## 快速示例
|
||||
|
||||
### 使用Builder创建
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
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')
|
||||
.blackboardCompare('health', 50, 'greater')
|
||||
.log('攻击玩家', 'Attack')
|
||||
.end()
|
||||
// 否则逃跑
|
||||
.log('逃离战斗', 'Flee')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 启动AI
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, enemyAI);
|
||||
```
|
||||
|
||||
### 使用编辑器创建
|
||||
|
||||
1. 打开行为树编辑器
|
||||
2. 创建新的行为树资产
|
||||
3. 拖拽节点到画布
|
||||
4. 配置节点属性和连接
|
||||
5. 保存并在代码中使用
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Runtime执行器架构
|
||||
|
||||
本框架采用Runtime执行器架构,将节点定义和执行逻辑分离:
|
||||
|
||||
**核心组件:**
|
||||
- `BehaviorTreeData`: 纯数据结构,描述行为树
|
||||
- `BehaviorTreeRuntimeComponent`: 运行时组件,管理状态和黑板
|
||||
- `BehaviorTreeExecutionSystem`: 执行系统,驱动行为树运行
|
||||
- `INodeExecutor`: 节点执行器接口
|
||||
- `NodeExecutionContext`: 执行上下文
|
||||
|
||||
**优势:**
|
||||
- 数据与逻辑分离,易于序列化
|
||||
- 执行器无状态,可复用
|
||||
- 类型安全,编译时检查
|
||||
- 高性能执行
|
||||
|
||||
### 自定义执行器
|
||||
|
||||
创建自定义节点非常简单:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
BindingHelper,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
import { TaskStatus, NodeType } from '@esengine/behavior-tree';
|
||||
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '攻击',
|
||||
description: '攻击目标',
|
||||
category: '战斗',
|
||||
configSchema: {
|
||||
damage: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
const target = context.runtime.getBlackboardValue('target');
|
||||
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
console.log(`造成 ${damage} 点伤害`);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
详细说明请参见[自定义节点执行器](./custom-actions.md)。
|
||||
|
||||
## 下一步
|
||||
|
||||
建议按照以下顺序学习:
|
||||
|
||||
1. 阅读[快速开始](./getting-started.md)了解基础用法
|
||||
2. 学习[核心概念](./core-concepts.md)理解行为树原理
|
||||
3. 学习[资产管理](./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)提升技能
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
|
||||
- 加入社区讨论
|
||||
- 参考文档中的完整代码示例
|
||||
313
docs/guide/behavior-tree/laya-integration.md
Normal file
313
docs/guide/behavior-tree/laya-integration.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# Laya 引擎集成
|
||||
|
||||
本教程将引导你在 Laya 引擎项目中集成和使用行为树系统。
|
||||
|
||||
## 前置要求
|
||||
|
||||
- LayaAir 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started.md)教程
|
||||
|
||||
## 安装
|
||||
|
||||
在你的 Laya 项目根目录下:
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
建议的项目结构:
|
||||
|
||||
```
|
||||
src/
|
||||
├── ai/
|
||||
│ ├── EnemyAI.ts
|
||||
│ └── BossAI.ts
|
||||
├── systems/
|
||||
│ └── AISystem.ts
|
||||
└── Main.ts
|
||||
resources/
|
||||
└── behaviors/
|
||||
├── enemy.btree.json
|
||||
└── boss.btree.json
|
||||
```
|
||||
|
||||
|
||||
## 初始化
|
||||
|
||||
### 在Main.ts中初始化
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
|
||||
export class Main {
|
||||
constructor() {
|
||||
Laya.init(1280, 720).then(() => {
|
||||
this.initECS();
|
||||
this.startGame();
|
||||
});
|
||||
}
|
||||
|
||||
private async initECS() {
|
||||
// 初始化 ECS
|
||||
Core.create();
|
||||
|
||||
// 安装行为树插件
|
||||
const btPlugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(btPlugin);
|
||||
|
||||
// 创建并设置场景
|
||||
const scene = new Scene();
|
||||
btPlugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 启动更新循环
|
||||
Laya.timer.frameLoop(1, this, this.update);
|
||||
}
|
||||
|
||||
private update() {
|
||||
// Core.update会自动更新场景
|
||||
Core.update(Laya.timer.delta / 1000);
|
||||
}
|
||||
|
||||
private startGame() {
|
||||
// 加载场景
|
||||
}
|
||||
}
|
||||
|
||||
new Main();
|
||||
```
|
||||
|
||||
|
||||
## 创建AI组件
|
||||
|
||||
```typescript
|
||||
import { Core, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
export class EnemyAI extends Laya.Script {
|
||||
private aiEntity: Entity;
|
||||
|
||||
onEnable() {
|
||||
this.createBehaviorTree();
|
||||
}
|
||||
|
||||
private createBehaviorTree() {
|
||||
// 获取Core管理的场景
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
console.error('场景未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
const sprite = this.owner as Laya.Sprite;
|
||||
|
||||
// 使用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();
|
||||
|
||||
// 创建AI实体并启动
|
||||
this.aiEntity = scene.createEntity(`AI_${sprite.name}`);
|
||||
BehaviorTreeStarter.start(this.aiEntity, tree);
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
// 停止AI
|
||||
if (this.aiEntity) {
|
||||
BehaviorTreeStarter.stop(this.aiEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 与Laya节点交互
|
||||
|
||||
要实现与Laya节点的交互,需要创建自定义执行器。下面展示一个完整示例。
|
||||
|
||||
## 完整示例
|
||||
|
||||
创建一个使用自定义执行器的敌人AI系统:
|
||||
|
||||
```typescript
|
||||
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;
|
||||
|
||||
private aiEntity: Entity;
|
||||
|
||||
onEnable() {
|
||||
this.buildAI();
|
||||
}
|
||||
|
||||
private buildAI() {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
console.error('场景未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
const sprite = this.owner as Laya.Sprite;
|
||||
|
||||
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();
|
||||
|
||||
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() {
|
||||
if (this.aiEntity) {
|
||||
BehaviorTreeStarter.stop(this.aiEntity);
|
||||
}
|
||||
Laya.timer.clearAll(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 使用冷却装饰器
|
||||
|
||||
对于不需要每帧更新的AI,使用冷却装饰器:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('ThrottledAI')
|
||||
.cooldown(0.2, 'ThrottleRoot') // 每0.2秒执行一次
|
||||
.selector('MainBehavior')
|
||||
// AI逻辑...
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
### 限制同时运行的AI数量
|
||||
|
||||
```typescript
|
||||
class AIManager {
|
||||
private activeAIs: Entity[] = [];
|
||||
private maxAIs: number = 20;
|
||||
|
||||
addAI(entity: Entity, tree: BehaviorTreeData) {
|
||||
if (this.activeAIs.length >= this.maxAIs) {
|
||||
const furthest = this.activeAIs.shift();
|
||||
if (furthest) {
|
||||
BehaviorTreeStarter.stop(furthest);
|
||||
}
|
||||
}
|
||||
|
||||
BehaviorTreeStarter.start(entity, tree);
|
||||
this.activeAIs.push(entity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 资源加载失败?
|
||||
|
||||
确保:
|
||||
1. 资源路径正确
|
||||
2. 资源已添加到项目中
|
||||
3. 使用 `Laya.loader.load()` 加载
|
||||
|
||||
### AI不执行?
|
||||
|
||||
检查:
|
||||
1. `onUpdate()` 是否被调用
|
||||
2. `Scene.update()` 是否执行
|
||||
3. 行为树是否已启动
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[高级用法](./advanced-usage.md)
|
||||
- 学习[最佳实践](./best-practices.md)
|
||||
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
580
docs/guide/behavior-tree/nodejs-usage.md
Normal file
@@ -0,0 +1,580 @@
|
||||
# Node.js 服务端使用
|
||||
|
||||
本文介绍如何在 Node.js 服务端环境(如游戏服务器、机器人、自动化工具)中使用行为树系统。
|
||||
|
||||
## 使用场景
|
||||
|
||||
行为树不仅适用于游戏客户端AI,在服务端也有广泛应用:
|
||||
|
||||
1. **游戏服务器** - NPC AI逻辑、副本关卡脚本
|
||||
2. **聊天机器人** - 对话流程控制、智能回复
|
||||
3. **自动化测试** - 测试用例执行流程
|
||||
4. **工作流引擎** - 业务流程自动化
|
||||
5. **爬虫系统** - 数据采集流程控制
|
||||
|
||||
## 基础设置
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework @esengine/behavior-tree
|
||||
```
|
||||
|
||||
### TypeScript 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 简单的游戏服务器 NPC
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
async function startServer() {
|
||||
// 1. 初始化 ECS Core
|
||||
Core.create();
|
||||
|
||||
// 2. 安装行为树插件
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 3. 创建场景
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 4. 创建 NPC 行为树
|
||||
const npcAI = BehaviorTreeBuilder.create('MerchantNPC')
|
||||
.defineBlackboardVariable('mood', 'friendly')
|
||||
.defineBlackboardVariable('goldAmount', 1000)
|
||||
|
||||
.selector('NPCBehavior')
|
||||
// 如果玩家触发对话
|
||||
.sequence('Dialogue')
|
||||
.blackboardExists('playerRequest')
|
||||
.log('NPC: 欢迎光临!')
|
||||
.end()
|
||||
|
||||
// 默认行为:闲置
|
||||
.sequence('Idle')
|
||||
.log('NPC: 正在整理商品...')
|
||||
.wait(5.0)
|
||||
.end()
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 5. 创建 NPC 实体
|
||||
const npc = scene.createEntity('Merchant');
|
||||
BehaviorTreeStarter.start(npc, npcAI);
|
||||
|
||||
// 6. 启动游戏循环(20 TPS)
|
||||
setInterval(() => {
|
||||
Core.update(0.05); // 50ms = 1/20秒
|
||||
}, 50);
|
||||
|
||||
// 7. 模拟玩家交互
|
||||
setTimeout(() => {
|
||||
const runtime = npc.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime?.setBlackboardValue('playerRequest', 'buy_sword');
|
||||
console.log('玩家发起交易请求');
|
||||
}, 3000);
|
||||
|
||||
console.log('游戏服务器已启动');
|
||||
}
|
||||
|
||||
startServer();
|
||||
```
|
||||
|
||||
## 实战示例:聊天机器人
|
||||
|
||||
创建一个基于行为树的智能聊天机器人:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeRuntimeComponent,
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
TaskStatus,
|
||||
NodeType,
|
||||
NodeExecutorMetadata
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 1. 创建自定义节点:回复消息
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SendMessage',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '发送消息',
|
||||
configSchema: {
|
||||
message: { type: 'string', default: '' }
|
||||
}
|
||||
})
|
||||
class SendMessageAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const message = context.nodeData.config['message'] as string;
|
||||
const userMessage = context.runtime.getBlackboardValue<string>('userMessage');
|
||||
|
||||
console.log(`[机器人回复]: ${message}`);
|
||||
console.log(` 回复给: ${userMessage}`);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建自定义节点:匹配关键词
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'MatchKeyword',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '匹配关键词',
|
||||
configSchema: {
|
||||
keyword: { type: 'string', default: '' }
|
||||
}
|
||||
})
|
||||
class MatchKeywordCondition implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const keyword = context.nodeData.config['keyword'] as string;
|
||||
const userMessage = context.runtime.getBlackboardValue<string>('userMessage') || '';
|
||||
|
||||
return userMessage.includes(keyword) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建聊天机器人类
|
||||
class ChatBot {
|
||||
private botEntity: Entity;
|
||||
private runtime: BehaviorTreeRuntimeComponent | null = null;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
// 创建机器人行为树
|
||||
const botBehavior = BehaviorTreeBuilder.create('ChatBotAI')
|
||||
.defineBlackboardVariable('userMessage', '')
|
||||
.defineBlackboardVariable('userName', 'Guest')
|
||||
|
||||
.selector('ResponseSelector')
|
||||
// 问候语
|
||||
.sequence('Greeting')
|
||||
.executeCondition('MatchKeyword', { keyword: '你好' })
|
||||
.executeAction('SendMessage', { message: '你好!我是智能助手,有什么可以帮你的吗?' })
|
||||
.end()
|
||||
|
||||
// 帮助请求
|
||||
.sequence('Help')
|
||||
.executeCondition('MatchKeyword', { keyword: '帮助' })
|
||||
.executeAction('SendMessage', { message: '我可以帮你回答问题、查询信息。试试问我一些问题吧!' })
|
||||
.end()
|
||||
|
||||
// 查询天气
|
||||
.sequence('Weather')
|
||||
.executeCondition('MatchKeyword', { keyword: '天气' })
|
||||
.executeAction('SendMessage', { message: '今天天气不错,晴天,温度适宜。' })
|
||||
.end()
|
||||
|
||||
// 查询时间
|
||||
.sequence('Time')
|
||||
.executeCondition('MatchKeyword', { keyword: '时间' })
|
||||
.executeAction('SendMessage', { message: `现在时间是 ${new Date().toLocaleString()}` })
|
||||
.end()
|
||||
|
||||
// 默认回复
|
||||
.executeAction('SendMessage', { message: '抱歉,我还不太理解你的意思。可以换个方式问我吗?' })
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// 创建实体并启动
|
||||
this.botEntity = scene.createEntity('ChatBot');
|
||||
BehaviorTreeStarter.start(this.botEntity, botBehavior);
|
||||
this.runtime = this.botEntity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
}
|
||||
|
||||
// 处理用户消息
|
||||
async handleMessage(userName: string, message: string) {
|
||||
if (this.runtime) {
|
||||
this.runtime.setBlackboardValue('userName', userName);
|
||||
this.runtime.setBlackboardValue('userMessage', message);
|
||||
}
|
||||
|
||||
// 等待一帧让行为树执行
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 主程序
|
||||
async function main() {
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// 注册自定义节点
|
||||
const system = scene.getSystem(BehaviorTreeExecutionSystem);
|
||||
if (system) {
|
||||
const registry = system.getExecutorRegistry();
|
||||
registry.register('SendMessage', new SendMessageAction());
|
||||
registry.register('MatchKeyword', new MatchKeywordCondition());
|
||||
}
|
||||
|
||||
// 创建聊天机器人
|
||||
const bot = new ChatBot(scene);
|
||||
|
||||
// 启动更新循环
|
||||
setInterval(() => {
|
||||
Core.update(0.1);
|
||||
}, 100);
|
||||
|
||||
// 模拟用户对话
|
||||
console.log('\n=== 聊天机器人测试 ===\n');
|
||||
|
||||
await bot.handleMessage('Alice', '你好');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Bob', '现在几点了?');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Charlie', '今天天气怎么样');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('David', '你能帮我做什么');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
await bot.handleMessage('Eve', '你好吗?');
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
## 实战示例:多人游戏服务器
|
||||
|
||||
### 房间管理系统
|
||||
|
||||
```typescript
|
||||
import { Core, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreePlugin,
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreeAssetManager
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// 游戏房间
|
||||
class GameRoom {
|
||||
private scene: Scene;
|
||||
private assetManager: BehaviorTreeAssetManager;
|
||||
private monsters: Entity[] = [];
|
||||
|
||||
constructor(roomId: string) {
|
||||
// 创建房间场景
|
||||
this.scene = new Scene();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
plugin.setupScene(this.scene);
|
||||
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
|
||||
// 初始化房间
|
||||
this.spawnMonsters();
|
||||
console.log(`房间 ${roomId} 已创建,怪物数量: ${this.monsters.length}`);
|
||||
}
|
||||
|
||||
private spawnMonsters() {
|
||||
// 从资产管理器获取怪物AI(所有房间共享)
|
||||
const monsterAI = this.assetManager.getAsset('MonsterAI');
|
||||
if (!monsterAI) return;
|
||||
|
||||
// 生成10个怪物
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const monster = this.scene.createEntity(`Monster_${i}`);
|
||||
BehaviorTreeStarter.start(monster, monsterAI);
|
||||
this.monsters.push(monster);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
this.scene.update(deltaTime);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.monsters.forEach(m => m.destroy());
|
||||
this.monsters = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private rooms: Map<string, GameRoom> = new Map();
|
||||
|
||||
createRoom(roomId: string): GameRoom {
|
||||
const room = new GameRoom(roomId);
|
||||
this.rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
getRoom(roomId: string): GameRoom | undefined {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
destroyRoom(roomId: string) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (room) {
|
||||
room.destroy();
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime: number) {
|
||||
this.rooms.forEach(room => room.update(deltaTime));
|
||||
}
|
||||
}
|
||||
|
||||
// 主程序
|
||||
async function startGameServer() {
|
||||
// 初始化
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// 预加载怪物AI(所有房间共享)
|
||||
const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
const monsterAI = BehaviorTreeBuilder.create('MonsterAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.selector('Behavior')
|
||||
.log('攻击玩家')
|
||||
.end()
|
||||
.build();
|
||||
assetManager.loadAsset(monsterAI);
|
||||
|
||||
// 创建房间管理器
|
||||
const roomManager = new RoomManager();
|
||||
|
||||
// 模拟房间创建
|
||||
roomManager.createRoom('room_1');
|
||||
roomManager.createRoom('room_2');
|
||||
|
||||
// 服务器主循环(60 TPS)
|
||||
setInterval(() => {
|
||||
roomManager.update(1/60);
|
||||
}, 1000 / 60);
|
||||
|
||||
console.log('游戏服务器已启动');
|
||||
}
|
||||
|
||||
startGameServer();
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 控制更新频率
|
||||
|
||||
```typescript
|
||||
// 不同类型的AI使用不同的更新频率
|
||||
class AIManager {
|
||||
private importantAIs: Entity[] = []; // Boss等重要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
|
||||
0
docs/guide/editor-plugin-system.md
Normal file
0
docs/guide/editor-plugin-system.md
Normal file
66
eslint.config.mjs
Normal file
66
eslint.config.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['packages/**/src/**/*.{ts,tsx,js,jsx}'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': 'warn',
|
||||
'quotes': 'warn',
|
||||
'indent': 'off',
|
||||
'no-trailing-spaces': 'warn',
|
||||
'eol-last': 'warn',
|
||||
'comma-dangle': 'warn',
|
||||
'object-curly-spacing': 'warn',
|
||||
'array-bracket-spacing': 'warn',
|
||||
'arrow-parens': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'no-multiple-empty-lines': 'warn',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/no-this-alias': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-useless-catch': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'**/node_modules/**',
|
||||
'dist/**',
|
||||
'**/dist/**',
|
||||
'bin/**',
|
||||
'**/bin/**',
|
||||
'build/**',
|
||||
'**/build/**',
|
||||
'coverage/**',
|
||||
'**/coverage/**',
|
||||
'thirdparty/**',
|
||||
'examples/lawn-mower-demo/**',
|
||||
'extensions/**',
|
||||
'**/*.min.js',
|
||||
'**/*.d.ts'
|
||||
]
|
||||
}
|
||||
];
|
||||
456
package-lock.json
generated
456
package-lock.json
generated
@@ -4929,15 +4929,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.2.tgz",
|
||||
"integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz",
|
||||
@@ -7964,6 +7955,242 @@
|
||||
"size-limit": "11.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
|
||||
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.13.5",
|
||||
"@swc/core-darwin-x64": "1.13.5",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.13.5",
|
||||
"@swc/core-linux-arm64-gnu": "1.13.5",
|
||||
"@swc/core-linux-arm64-musl": "1.13.5",
|
||||
"@swc/core-linux-x64-gnu": "1.13.5",
|
||||
"@swc/core-linux-x64-musl": "1.13.5",
|
||||
"@swc/core-win32-arm64-msvc": "1.13.5",
|
||||
"@swc/core-win32-ia32-msvc": "1.13.5",
|
||||
"@swc/core-win32-x64-msvc": "1.13.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz",
|
||||
"integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz",
|
||||
"integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz",
|
||||
"integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz",
|
||||
"integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz",
|
||||
"integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz",
|
||||
"integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz",
|
||||
"integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz",
|
||||
"integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz",
|
||||
"integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz",
|
||||
@@ -8659,17 +8886,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
|
||||
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/type-utils": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/type-utils": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -8683,7 +8910,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -8699,16 +8926,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
|
||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8724,14 +8951,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz",
|
||||
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
||||
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.1",
|
||||
"@typescript-eslint/types": "^8.46.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.2",
|
||||
"@typescript-eslint/types": "^8.46.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8746,14 +8973,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz",
|
||||
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
|
||||
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1"
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -8764,9 +8991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -8781,15 +9008,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -8806,9 +9033,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz",
|
||||
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
|
||||
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -8820,16 +9047,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz",
|
||||
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
|
||||
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.46.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"@typescript-eslint/project-service": "8.46.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -8849,16 +9076,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz",
|
||||
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
|
||||
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1"
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -8873,13 +9100,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz",
|
||||
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==",
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
|
||||
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8931,6 +9158,30 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-swc": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.0.tgz",
|
||||
"integrity": "sha512-/tesahXD1qpkGC6FzMoFOJj0RyZdw9xLELOL+6jbElwmWfwOnIVy+IfpY+o9JfD9PKaR/Eyb6DNrvbXpuvA+8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.43",
|
||||
"@swc/core": "^1.13.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4 || ^5 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-swc/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.43",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz",
|
||||
"integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
@@ -12533,6 +12784,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
@@ -24929,6 +25186,30 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
|
||||
"integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.46.2",
|
||||
"@typescript-eslint/parser": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
@@ -25445,6 +25726,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-swc-transform": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-swc-transform/-/vite-plugin-swc-transform-1.1.1.tgz",
|
||||
"integrity": "sha512-uef69pFsfSQTPM95ubXzhqKevWQWCAUh/VQ/FW7IsEgjDkK/OJOXrdmuCSQT0VHZPH6wyhO3I1PWkV4lHK2IZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.4",
|
||||
"@swc/core": "^1.10.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^7 || ^6 || ^5"
|
||||
}
|
||||
},
|
||||
"node_modules/vitepress": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
|
||||
@@ -26001,7 +26299,7 @@
|
||||
},
|
||||
"packages/behavior-tree": {
|
||||
"name": "@esengine/behavior-tree",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
@@ -26030,10 +26328,10 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.2.9",
|
||||
"version": "2.2.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0",
|
||||
"fflate": "^0.8.2",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26041,24 +26339,27 @@
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"eslint": "^9.37.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.46.1"
|
||||
}
|
||||
},
|
||||
"packages/editor-app": {
|
||||
"name": "@esengine/editor-app",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@esengine/behavior-tree": "file:../behavior-tree",
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
@@ -26077,14 +26378,17 @@
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.13.5",
|
||||
"@tauri-apps/cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-react-swc": "^4.2.0",
|
||||
"sharp": "^0.34.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7"
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-swc-transform": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"packages/editor-app/node_modules/@esbuild/aix-ppc64": {
|
||||
@@ -26617,16 +26921,28 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "^2.2.8"
|
||||
}
|
||||
},
|
||||
"packages/math": {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"copy:worker-demo": "node scripts/update-worker-demo.js",
|
||||
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"type-check": "lerna run type-check",
|
||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/behavior-tree",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "完全ECS化的行为树系统,基于组件和实体的行为树实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
@@ -33,8 +33,7 @@
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||
"test:watch": "jest --watch --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,547 +1,357 @@
|
||||
import { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from './Components/BehaviorTreeNode';
|
||||
import { CompositeNodeComponent } from './Components/CompositeNodeComponent';
|
||||
import { DecoratorNodeComponent } from './Components/DecoratorNodeComponent';
|
||||
import { BlackboardComponent } from './Components/BlackboardComponent';
|
||||
import { NodeType, CompositeType, DecoratorType, BlackboardValueType } from './Types/TaskStatus';
|
||||
|
||||
// 导入动作组件
|
||||
import { WaitAction } from './Components/Actions/WaitAction';
|
||||
import { LogAction } from './Components/Actions/LogAction';
|
||||
import { SetBlackboardValueAction } from './Components/Actions/SetBlackboardValueAction';
|
||||
import { ModifyBlackboardValueAction, ModifyOperation } from './Components/Actions/ModifyBlackboardValueAction';
|
||||
import { ExecuteAction, CustomActionFunction } from './Components/Actions/ExecuteAction';
|
||||
|
||||
// 导入条件组件
|
||||
import { BlackboardCompareCondition, CompareOperator } from './Components/Conditions/BlackboardCompareCondition';
|
||||
import { BlackboardExistsCondition } from './Components/Conditions/BlackboardExistsCondition';
|
||||
import { RandomProbabilityCondition } from './Components/Conditions/RandomProbabilityCondition';
|
||||
import { ExecuteCondition, CustomConditionFunction } from './Components/Conditions/ExecuteCondition';
|
||||
|
||||
// 导入装饰器组件
|
||||
import { RepeaterNode } from './Components/Decorators/RepeaterNode';
|
||||
import { InverterNode } from './Components/Decorators/InverterNode';
|
||||
import { UntilSuccessNode } from './Components/Decorators/UntilSuccessNode';
|
||||
import { UntilFailNode } from './Components/Decorators/UntilFailNode';
|
||||
import { AlwaysSucceedNode } from './Components/Decorators/AlwaysSucceedNode';
|
||||
import { AlwaysFailNode } from './Components/Decorators/AlwaysFailNode';
|
||||
import { ConditionalNode } from './Components/Decorators/ConditionalNode';
|
||||
import { CooldownNode } from './Components/Decorators/CooldownNode';
|
||||
import { TimeoutNode } from './Components/Decorators/TimeoutNode';
|
||||
import { BehaviorTreeData, BehaviorNodeData } from './Runtime/BehaviorTreeData';
|
||||
import { NodeType } from './Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树构建器
|
||||
*
|
||||
* 提供流式 API 来构建行为树结构
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const aiRoot = BehaviorTreeBuilder.create(scene, 'AI')
|
||||
* .blackboard()
|
||||
* .defineVariable('health', BlackboardValueType.Number, 100)
|
||||
* .defineVariable('target', BlackboardValueType.Object, null)
|
||||
* .endBlackboard()
|
||||
* .selector('MainSelector')
|
||||
* .sequence('AttackSequence')
|
||||
* .condition((entity, blackboard) => {
|
||||
* return blackboard?.getValue('health') > 50;
|
||||
* })
|
||||
* .action('Attack', (entity) => TaskStatus.Success)
|
||||
* .end()
|
||||
* .action('Flee', (entity) => TaskStatus.Success)
|
||||
* .end()
|
||||
* .build();
|
||||
* ```
|
||||
* 提供流式API构建行为树数据结构
|
||||
*/
|
||||
export class BehaviorTreeBuilder {
|
||||
private scene: IScene;
|
||||
private currentEntity: Entity;
|
||||
private entityStack: Entity[] = [];
|
||||
private blackboardEntity?: Entity;
|
||||
private treeData: BehaviorTreeData;
|
||||
private nodeStack: string[] = [];
|
||||
private nodeIdCounter: number = 0;
|
||||
|
||||
private constructor(scene: IScene, rootName: string) {
|
||||
this.scene = scene;
|
||||
this.currentEntity = scene.createEntity(rootName);
|
||||
private constructor(treeName: string) {
|
||||
this.treeData = {
|
||||
id: `tree_${Date.now()}`,
|
||||
name: treeName,
|
||||
rootNodeId: '',
|
||||
nodes: new Map(),
|
||||
blackboardVariables: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建行为树构建器
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param rootName 根节点名称
|
||||
* @returns 构建器实例
|
||||
* 创建构建器
|
||||
*/
|
||||
static create(scene: IScene, rootName: string = 'BehaviorTreeRoot'): BehaviorTreeBuilder {
|
||||
return new BehaviorTreeBuilder(scene, rootName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加黑板组件到根节点
|
||||
*/
|
||||
blackboard(): BehaviorTreeBuilder {
|
||||
this.blackboardEntity = this.currentEntity;
|
||||
this.currentEntity.addComponent(new BlackboardComponent());
|
||||
return this;
|
||||
static create(treeName: string = 'BehaviorTree'): BehaviorTreeBuilder {
|
||||
return new BehaviorTreeBuilder(treeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义黑板变量
|
||||
*/
|
||||
defineVariable(
|
||||
name: string,
|
||||
type: BlackboardValueType,
|
||||
initialValue: any,
|
||||
options?: { readonly?: boolean; description?: string }
|
||||
): BehaviorTreeBuilder {
|
||||
if (!this.blackboardEntity) {
|
||||
throw new Error('Must call blackboard() first');
|
||||
defineBlackboardVariable(key: string, initialValue: any): BehaviorTreeBuilder {
|
||||
if (!this.treeData.blackboardVariables) {
|
||||
this.treeData.blackboardVariables = new Map();
|
||||
}
|
||||
|
||||
const blackboard = this.blackboardEntity.getComponent(BlackboardComponent);
|
||||
if (blackboard) {
|
||||
blackboard.defineVariable(name, type, initialValue, options);
|
||||
}
|
||||
|
||||
this.treeData.blackboardVariables.set(key, initialValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束黑板定义
|
||||
* 添加序列节点
|
||||
*/
|
||||
endBlackboard(): BehaviorTreeBuilder {
|
||||
this.blackboardEntity = undefined;
|
||||
return this;
|
||||
sequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Sequence', name || 'Sequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建序列节点
|
||||
* 添加选择器节点
|
||||
*/
|
||||
sequence(name: string = 'Sequence'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Sequence);
|
||||
selector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Selector', name || 'Selector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建选择器节点
|
||||
* 添加并行节点
|
||||
*/
|
||||
selector(name: string = 'Selector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Selector);
|
||||
parallel(name?: string, config?: { successPolicy?: string; failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('Parallel', name || 'Parallel', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并行节点
|
||||
* 添加并行选择器节点
|
||||
*/
|
||||
parallel(name: string = 'Parallel'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.Parallel);
|
||||
parallelSelector(name?: string, config?: { failurePolicy?: string }): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('ParallelSelector', name || 'ParallelSelector', config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并行选择器节点
|
||||
* 添加随机序列节点
|
||||
*/
|
||||
parallelSelector(name: string = 'ParallelSelector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.ParallelSelector);
|
||||
randomSequence(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSequence', name || 'RandomSequence');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机序列节点
|
||||
* 添加随机选择器节点
|
||||
*/
|
||||
randomSequence(name: string = 'RandomSequence'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.RandomSequence);
|
||||
randomSelector(name?: string): BehaviorTreeBuilder {
|
||||
return this.addCompositeNode('RandomSelector', name || 'RandomSelector');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机选择器节点
|
||||
* 添加反转装饰器
|
||||
*/
|
||||
randomSelector(name: string = 'RandomSelector'): BehaviorTreeBuilder {
|
||||
return this.composite(name, CompositeType.RandomSelector);
|
||||
inverter(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Inverter', name || 'Inverter');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建复合节点
|
||||
* 添加重复装饰器
|
||||
*/
|
||||
private composite(name: string, type: CompositeType): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Composite;
|
||||
node.nodeName = name;
|
||||
|
||||
const composite = entity.addComponent(new CompositeNodeComponent());
|
||||
composite.compositeType = type;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
repeater(repeatCount: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Repeater', name || 'Repeater', { repeatCount });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建反转装饰器
|
||||
* 添加总是成功装饰器
|
||||
*/
|
||||
inverter(name: string = 'Inverter'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new InverterNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
alwaysSucceed(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysSucceed', name || 'AlwaysSucceed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建重复装饰器
|
||||
* 添加总是失败装饰器
|
||||
*/
|
||||
repeater(name: string = 'Repeater', count: number = -1, endOnFailure: boolean = false): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new RepeaterNode());
|
||||
decorator.repeatCount = count;
|
||||
decorator.endOnFailure = endOnFailure;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
alwaysFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('AlwaysFail', name || 'AlwaysFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建直到成功装饰器
|
||||
* 添加直到成功装饰器
|
||||
*/
|
||||
untilSuccess(name: string = 'UntilSuccess'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new UntilSuccessNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
untilSuccess(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilSuccess', name || 'UntilSuccess');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建直到失败装饰器
|
||||
* 添加直到失败装饰器
|
||||
*/
|
||||
untilFail(name: string = 'UntilFail'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new UntilFailNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
untilFail(name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('UntilFail', name || 'UntilFail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建总是成功装饰器
|
||||
* 添加条件装饰器
|
||||
*/
|
||||
alwaysSucceed(name: string = 'AlwaysSucceed'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new AlwaysSucceedNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
conditional(blackboardKey: string, expectedValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Conditional', name || 'Conditional', {
|
||||
blackboardKey,
|
||||
expectedValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建总是失败装饰器
|
||||
* 添加冷却装饰器
|
||||
*/
|
||||
alwaysFail(name: string = 'AlwaysFail'): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
entity.addComponent(new AlwaysFailNode());
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
cooldown(cooldownTime: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Cooldown', name || 'Cooldown', { cooldownTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建条件装饰器
|
||||
* 添加超时装饰器
|
||||
*/
|
||||
conditional(name: string, conditionCode: string): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new ConditionalNode());
|
||||
decorator.conditionCode = conditionCode;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
timeout(timeout: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addDecoratorNode('Timeout', name || 'Timeout', { timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建冷却装饰器
|
||||
* 添加等待动作
|
||||
*/
|
||||
cooldown(name: string = 'Cooldown', cooldownTime: number = 1.0): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new CooldownNode());
|
||||
decorator.cooldownTime = cooldownTime;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
wait(duration: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Wait', name || 'Wait', { duration });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建超时装饰器
|
||||
* 添加日志动作
|
||||
*/
|
||||
timeout(name: string = 'Timeout', timeoutDuration: number = 5.0): BehaviorTreeBuilder {
|
||||
this.entityStack.push(this.currentEntity);
|
||||
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Decorator;
|
||||
node.nodeName = name;
|
||||
|
||||
const decorator = entity.addComponent(new TimeoutNode());
|
||||
decorator.timeoutDuration = timeoutDuration;
|
||||
|
||||
this.currentEntity = entity;
|
||||
return this;
|
||||
log(message: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('Log', name || 'Log', { message });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建等待动作
|
||||
* 添加设置黑板值动作
|
||||
*/
|
||||
wait(waitTime: number, name: string = 'Wait'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new WaitAction());
|
||||
action.waitTime = waitTime;
|
||||
|
||||
return this;
|
||||
setBlackboardValue(key: string, value: any, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('SetBlackboardValue', name || 'SetBlackboardValue', { key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日志动作
|
||||
* 添加修改黑板值动作
|
||||
*/
|
||||
log(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log', name: string = 'Log'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new LogAction());
|
||||
action.message = message;
|
||||
action.level = level;
|
||||
|
||||
return this;
|
||||
modifyBlackboardValue(key: string, operation: string, value: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ModifyBlackboardValue', name || 'ModifyBlackboardValue', {
|
||||
key,
|
||||
operation,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设置黑板值动作
|
||||
* 添加执行动作
|
||||
*/
|
||||
setBlackboardValue(variableName: string, value: any, name: string = 'SetValue'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new SetBlackboardValueAction());
|
||||
action.variableName = variableName;
|
||||
action.value = value;
|
||||
|
||||
return this;
|
||||
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建修改黑板值动作
|
||||
* 添加黑板比较条件
|
||||
*/
|
||||
modifyBlackboardValue(
|
||||
variableName: string,
|
||||
operation: ModifyOperation,
|
||||
operand: any,
|
||||
name: string = 'ModifyValue'
|
||||
): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new ModifyBlackboardValueAction());
|
||||
action.variableName = variableName;
|
||||
action.operation = operation;
|
||||
action.operand = operand;
|
||||
|
||||
return this;
|
||||
blackboardCompare(key: string, compareValue: any, operator?: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardCompare', name || 'BlackboardCompare', {
|
||||
key,
|
||||
compareValue,
|
||||
operator: operator || 'equals'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义动作
|
||||
* 添加黑板存在检查条件
|
||||
*/
|
||||
action(name: string, func: CustomActionFunction): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Action;
|
||||
node.nodeName = name;
|
||||
|
||||
const action = entity.addComponent(new ExecuteAction());
|
||||
action.setFunction(func);
|
||||
|
||||
return this;
|
||||
blackboardExists(key: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('BlackboardExists', name || 'BlackboardExists', { key });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建黑板比较条件
|
||||
* 添加随机概率条件
|
||||
*/
|
||||
compareBlackboardValue(
|
||||
variableName: string,
|
||||
operator: CompareOperator,
|
||||
compareValue: any,
|
||||
name: string = 'Compare'
|
||||
): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new BlackboardCompareCondition());
|
||||
condition.variableName = variableName;
|
||||
condition.operator = operator;
|
||||
condition.compareValue = compareValue;
|
||||
|
||||
return this;
|
||||
randomProbability(probability: number, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('RandomProbability', name || 'RandomProbability', { probability });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建黑板变量存在条件
|
||||
* 添加执行条件
|
||||
*/
|
||||
checkBlackboardExists(variableName: string, checkNotNull: boolean = false, name: string = 'Exists'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new BlackboardExistsCondition());
|
||||
condition.variableName = variableName;
|
||||
condition.checkNotNull = checkNotNull;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建随机概率条件
|
||||
*/
|
||||
randomProbability(probability: number, name: string = 'Random'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new RandomProbabilityCondition());
|
||||
condition.probability = probability;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义条件
|
||||
*/
|
||||
condition(func: CustomConditionFunction, name: string = 'Condition'): BehaviorTreeBuilder {
|
||||
const entity = this.scene.createEntity(name);
|
||||
this.currentEntity.addChild(entity);
|
||||
|
||||
const node = entity.addComponent(new BehaviorTreeNode());
|
||||
node.nodeType = NodeType.Condition;
|
||||
node.nodeName = name;
|
||||
|
||||
const condition = entity.addComponent(new ExecuteCondition());
|
||||
condition.setFunction(func);
|
||||
|
||||
return this;
|
||||
executeCondition(conditionName: string, name?: string): BehaviorTreeBuilder {
|
||||
return this.addConditionNode('ExecuteCondition', name || 'ExecuteCondition', { conditionName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前节点,返回父节点
|
||||
*/
|
||||
end(): BehaviorTreeBuilder {
|
||||
if (this.entityStack.length === 0) {
|
||||
throw new Error('No parent node to return to');
|
||||
if (this.nodeStack.length > 0) {
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
|
||||
this.currentEntity = this.entityStack.pop()!;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建并返回根节点实体
|
||||
* 构建行为树数据
|
||||
*/
|
||||
build(): Entity {
|
||||
// 确保返回到根节点
|
||||
while (this.entityStack.length > 0) {
|
||||
this.currentEntity = this.entityStack.pop()!;
|
||||
build(): BehaviorTreeData {
|
||||
if (!this.treeData.rootNodeId) {
|
||||
throw new Error('No root node defined. Add at least one node to the tree.');
|
||||
}
|
||||
return this.treeData;
|
||||
}
|
||||
|
||||
private addCompositeNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Composite,
|
||||
implementationType,
|
||||
children: [],
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
return this.currentEntity;
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeStack.push(nodeId);
|
||||
return this;
|
||||
}
|
||||
|
||||
private addDecoratorNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Decorator,
|
||||
implementationType,
|
||||
children: [],
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
this.nodeStack.push(nodeId);
|
||||
return this;
|
||||
}
|
||||
|
||||
private addActionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Action,
|
||||
implementationType,
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private addConditionNode(implementationType: string, name: string, config: Record<string, any> = {}): BehaviorTreeBuilder {
|
||||
const nodeId = this.generateNodeId();
|
||||
const node: BehaviorNodeData = {
|
||||
id: nodeId,
|
||||
name,
|
||||
nodeType: NodeType.Condition,
|
||||
implementationType,
|
||||
config
|
||||
};
|
||||
|
||||
this.treeData.nodes.set(nodeId, node);
|
||||
|
||||
if (!this.treeData.rootNodeId) {
|
||||
this.treeData.rootNodeId = nodeId;
|
||||
}
|
||||
|
||||
if (this.nodeStack.length > 0) {
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1]!;
|
||||
const parentNode = this.treeData.nodes.get(parentId);
|
||||
if (parentNode && parentNode.children) {
|
||||
parentNode.children.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private generateNodeId(): string {
|
||||
return `node_${this.nodeIdCounter++}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
import { LeafExecutionSystem } from './Systems/LeafExecutionSystem';
|
||||
import { DecoratorExecutionSystem } from './Systems/DecoratorExecutionSystem';
|
||||
import { CompositeExecutionSystem } from './Systems/CompositeExecutionSystem';
|
||||
import { SubTreeExecutionSystem } from './Systems/SubTreeExecutionSystem';
|
||||
import { BehaviorTreeExecutionSystem } from './Runtime/BehaviorTreeExecutionSystem';
|
||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||
|
||||
/**
|
||||
* 行为树插件
|
||||
@@ -33,11 +31,12 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||
this.services = services;
|
||||
|
||||
// 注册全局黑板服务
|
||||
// 注册全局服务
|
||||
services.registerSingleton(GlobalBlackboardService);
|
||||
services.registerSingleton(BehaviorTreeAssetManager);
|
||||
|
||||
this.worldManager = services.resolve(WorldManager);
|
||||
}
|
||||
@@ -46,9 +45,9 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
* 卸载插件
|
||||
*/
|
||||
async uninstall(): Promise<void> {
|
||||
// 注销全局黑板服务
|
||||
if (this.services) {
|
||||
this.services.unregister(GlobalBlackboardService);
|
||||
this.services.unregister(BehaviorTreeAssetManager);
|
||||
}
|
||||
|
||||
this.worldManager = null;
|
||||
@@ -58,11 +57,7 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
/**
|
||||
* 为场景设置行为树系统
|
||||
*
|
||||
* 向场景添加所有必需的行为树系统:
|
||||
* - LeafExecutionSystem (updateOrder: 100)
|
||||
* - DecoratorExecutionSystem (updateOrder: 200)
|
||||
* - CompositeExecutionSystem (updateOrder: 300)
|
||||
* - SubTreeExecutionSystem (updateOrder: 300)
|
||||
* 向场景添加行为树执行系统
|
||||
*
|
||||
* @param scene 目标场景
|
||||
*
|
||||
@@ -73,10 +68,7 @@ export class BehaviorTreePlugin implements IPlugin {
|
||||
* ```
|
||||
*/
|
||||
public setupScene(scene: IScene): void {
|
||||
scene.addSystem(new LeafExecutionSystem());
|
||||
scene.addSystem(new DecoratorExecutionSystem());
|
||||
scene.addSystem(new CompositeExecutionSystem());
|
||||
scene.addSystem(new SubTreeExecutionSystem());
|
||||
scene.addSystem(new BehaviorTreeExecutionSystem());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { BlackboardComponent } from '../BlackboardComponent';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
|
||||
/**
|
||||
* 自定义动作函数类型
|
||||
*/
|
||||
export type CustomActionFunction = (
|
||||
entity: Entity,
|
||||
blackboard?: BlackboardComponent,
|
||||
deltaTime?: number
|
||||
) => TaskStatus;
|
||||
|
||||
/**
|
||||
* 执行自定义函数动作组件
|
||||
*
|
||||
* 允许用户提供自定义的动作执行函数
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '自定义动作',
|
||||
category: '动作',
|
||||
type: NodeType.Action,
|
||||
icon: 'Code',
|
||||
description: '执行自定义代码',
|
||||
color: '#FFC107'
|
||||
})
|
||||
@ECSComponent('ExecuteAction')
|
||||
@Serializable({ version: 1 })
|
||||
export class ExecuteAction extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '动作代码',
|
||||
type: 'code',
|
||||
description: 'JavaScript 代码,返回 TaskStatus',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
actionCode?: string = 'return TaskStatus.Success;';
|
||||
|
||||
@Serialize()
|
||||
parameters: Record<string, any> = {};
|
||||
|
||||
/** 编译后的函数(不序列化) */
|
||||
@IgnoreSerialization()
|
||||
private compiledFunction?: CustomActionFunction;
|
||||
|
||||
/**
|
||||
* 获取或编译执行函数
|
||||
*/
|
||||
getFunction(): CustomActionFunction | undefined {
|
||||
if (!this.compiledFunction && this.actionCode) {
|
||||
try {
|
||||
const func = new Function(
|
||||
'entity',
|
||||
'blackboard',
|
||||
'deltaTime',
|
||||
'parameters',
|
||||
'TaskStatus',
|
||||
`
|
||||
const { Success, Failure, Running, Invalid } = TaskStatus;
|
||||
try {
|
||||
${this.actionCode}
|
||||
} catch (error) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
this.compiledFunction = (entity, blackboard, deltaTime) => {
|
||||
return func(entity, blackboard, deltaTime, this.parameters, TaskStatus) || TaskStatus.Success;
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.compiledFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义函数(运行时使用)
|
||||
*/
|
||||
setFunction(func: CustomActionFunction): void {
|
||||
this.compiledFunction = func;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
// 标记组件,无需数据字段
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Component, ECSComponent, Core } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { BlackboardValueType } from '../Types/TaskStatus';
|
||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||
|
||||
/**
|
||||
* 黑板变量定义
|
||||
*/
|
||||
export interface BlackboardVariable {
|
||||
name: string;
|
||||
type: BlackboardValueType;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑板组件 - 用于节点间共享数据
|
||||
*
|
||||
* 支持分层查找:
|
||||
* 1. 先查找本地变量
|
||||
* 2. 如果找不到,自动查找全局 Blackboard
|
||||
*
|
||||
* 通常附加到行为树的根节点上
|
||||
*/
|
||||
@ECSComponent('Blackboard')
|
||||
@Serializable({ version: 1 })
|
||||
export class BlackboardComponent extends Component {
|
||||
/** 存储的本地变量 */
|
||||
@Serialize()
|
||||
private variables: Map<string, BlackboardVariable> = new Map();
|
||||
|
||||
/** 是否启用全局 Blackboard 查找 */
|
||||
private useGlobalBlackboard: boolean = true;
|
||||
|
||||
/**
|
||||
* 定义一个新变量
|
||||
*/
|
||||
defineVariable(
|
||||
name: string,
|
||||
type: BlackboardValueType,
|
||||
initialValue: any,
|
||||
options?: {
|
||||
readonly?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): void {
|
||||
this.variables.set(name, {
|
||||
name,
|
||||
type,
|
||||
value: initialValue,
|
||||
readonly: options?.readonly ?? false,
|
||||
description: options?.description
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变量值
|
||||
* 先查找本地变量,找不到则查找全局变量
|
||||
*/
|
||||
getValue<T = any>(name: string): T | undefined {
|
||||
const variable = this.variables.get(name);
|
||||
if (variable !== undefined) {
|
||||
return variable.value as T;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).getValue<T>(name);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地变量值(不查找全局)
|
||||
*/
|
||||
getLocalValue<T = any>(name: string): T | undefined {
|
||||
const variable = this.variables.get(name);
|
||||
return variable?.value as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变量值
|
||||
* 优先设置本地变量,如果本地不存在且全局存在,则设置全局变量
|
||||
*/
|
||||
setValue(name: string, value: any, force: boolean = false): boolean {
|
||||
const variable = this.variables.get(name);
|
||||
|
||||
if (variable) {
|
||||
if (variable.readonly && !force) {
|
||||
return false;
|
||||
}
|
||||
variable.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).setValue(name, value, force);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本地变量值(不影响全局)
|
||||
*/
|
||||
setLocalValue(name: string, value: any, force: boolean = false): boolean {
|
||||
const variable = this.variables.get(name);
|
||||
|
||||
if (!variable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (variable.readonly && !force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
variable.value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查变量是否存在(包括本地和全局)
|
||||
*/
|
||||
hasVariable(name: string): boolean {
|
||||
if (this.variables.has(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
return Core.services.resolve(GlobalBlackboardService).hasVariable(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查本地变量是否存在
|
||||
*/
|
||||
hasLocalVariable(name: string): boolean {
|
||||
return this.variables.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除变量
|
||||
*/
|
||||
removeVariable(name: string): boolean {
|
||||
return this.variables.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量名
|
||||
*/
|
||||
getVariableNames(): string[] {
|
||||
return Array.from(this.variables.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有本地变量
|
||||
*/
|
||||
clear(): void {
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用全局 Blackboard 查找
|
||||
*/
|
||||
setUseGlobalBlackboard(enabled: boolean): void {
|
||||
this.useGlobalBlackboard = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用全局 Blackboard 查找
|
||||
*/
|
||||
isUsingGlobalBlackboard(): boolean {
|
||||
return this.useGlobalBlackboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量(包括本地和全局)
|
||||
*/
|
||||
getAllVariables(): BlackboardVariable[] {
|
||||
const locals = Array.from(this.variables.values());
|
||||
|
||||
if (this.useGlobalBlackboard) {
|
||||
const globals = Core.services.resolve(GlobalBlackboardService).getAllVariables();
|
||||
const localNames = new Set(this.variables.keys());
|
||||
const filteredGlobals = globals.filter(v => !localNames.has(v.name));
|
||||
return [...locals, ...filteredGlobals];
|
||||
}
|
||||
|
||||
return locals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 Blackboard 服务的引用
|
||||
*/
|
||||
static getGlobalBlackboard(): GlobalBlackboardService {
|
||||
return Core.services.resolve(GlobalBlackboardService);
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Component, ECSComponent, Entity } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeType } from '../../Types/TaskStatus';
|
||||
import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator';
|
||||
import { BlackboardComponent } from '../BlackboardComponent';
|
||||
|
||||
/**
|
||||
* 自定义条件函数类型
|
||||
*/
|
||||
export type CustomConditionFunction = (
|
||||
entity: Entity,
|
||||
blackboard?: BlackboardComponent,
|
||||
deltaTime?: number
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* 执行自定义条件组件
|
||||
*
|
||||
* 允许用户提供自定义的条件检查函数
|
||||
*/
|
||||
@BehaviorNode({
|
||||
displayName: '自定义条件',
|
||||
category: '条件',
|
||||
type: NodeType.Condition,
|
||||
icon: 'Code',
|
||||
description: '执行自定义条件代码',
|
||||
color: '#9C27B0'
|
||||
})
|
||||
@ECSComponent('ExecuteCondition')
|
||||
@Serializable({ version: 1 })
|
||||
export class ExecuteCondition extends Component {
|
||||
@BehaviorProperty({
|
||||
label: '条件代码',
|
||||
type: 'code',
|
||||
description: 'JavaScript 代码,返回 boolean',
|
||||
required: true
|
||||
})
|
||||
@Serialize()
|
||||
conditionCode?: string;
|
||||
|
||||
@Serialize()
|
||||
parameters: Record<string, any> = {};
|
||||
|
||||
@BehaviorProperty({
|
||||
label: '反转结果',
|
||||
type: 'boolean'
|
||||
})
|
||||
@Serialize()
|
||||
invertResult: boolean = false;
|
||||
|
||||
/** 编译后的函数(不序列化) */
|
||||
@IgnoreSerialization()
|
||||
private compiledFunction?: CustomConditionFunction;
|
||||
|
||||
/**
|
||||
* 获取或编译条件函数
|
||||
*/
|
||||
getFunction(): CustomConditionFunction | undefined {
|
||||
if (!this.compiledFunction && this.conditionCode) {
|
||||
try {
|
||||
const func = new Function(
|
||||
'entity',
|
||||
'blackboard',
|
||||
'deltaTime',
|
||||
'parameters',
|
||||
`
|
||||
try {
|
||||
${this.conditionCode}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
this.compiledFunction = (entity, blackboard, deltaTime) => {
|
||||
return Boolean(func(entity, blackboard, deltaTime, this.parameters));
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.compiledFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义函数(运行时使用)
|
||||
*/
|
||||
setFunction(func: CustomConditionFunction): void {
|
||||
this.compiledFunction = func;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 属性绑定组件
|
||||
* 记录节点属性到黑板变量的绑定关系
|
||||
*/
|
||||
export class PropertyBindings extends Component {
|
||||
/**
|
||||
* 属性绑定映射
|
||||
* key: 属性名称 (如 'message')
|
||||
* value: 黑板变量名 (如 'test1')
|
||||
*/
|
||||
bindings: Map<string, string> = new Map();
|
||||
|
||||
/**
|
||||
* 添加属性绑定
|
||||
*/
|
||||
addBinding(propertyName: string, blackboardKey: string): void {
|
||||
this.bindings.set(propertyName, blackboardKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性绑定的黑板变量名
|
||||
*/
|
||||
getBinding(propertyName: string): string | undefined {
|
||||
return this.bindings.get(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否绑定到黑板变量
|
||||
*/
|
||||
hasBinding(propertyName: string): boolean {
|
||||
return this.bindings.has(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有绑定
|
||||
*/
|
||||
clearBindings(): void {
|
||||
this.bindings.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import { NodeTemplate, PropertyDefinition } from '../Serialization/NodeTemplates';
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树节点元数据
|
||||
*/
|
||||
export interface BehaviorNodeMetadata {
|
||||
displayName: string;
|
||||
category: string;
|
||||
type: NodeType;
|
||||
icon?: string;
|
||||
description: string;
|
||||
color?: string;
|
||||
className?: string;
|
||||
/**
|
||||
* 是否需要子节点
|
||||
* - true: 节点需要子节点(如 SequenceNode、DecoratorNode)
|
||||
* - false: 节点不需要子节点(如 ActionNode、SubTreeNode)
|
||||
* - undefined: 根据节点类型自动判断
|
||||
*/
|
||||
requiresChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类注册表
|
||||
*/
|
||||
class NodeClassRegistry {
|
||||
private static nodeClasses = new Map<string, {
|
||||
metadata: BehaviorNodeMetadata;
|
||||
constructor: any;
|
||||
}>();
|
||||
|
||||
static registerNodeClass(constructor: any, metadata: BehaviorNodeMetadata): void {
|
||||
const key = `${metadata.category}:${metadata.displayName}`;
|
||||
this.nodeClasses.set(key, { metadata, constructor });
|
||||
}
|
||||
|
||||
static getAllNodeClasses(): Array<{ metadata: BehaviorNodeMetadata; constructor: any }> {
|
||||
return Array.from(this.nodeClasses.values());
|
||||
}
|
||||
|
||||
static getNodeClass(category: string, displayName: string): any {
|
||||
const key = `${category}:${displayName}`;
|
||||
return this.nodeClasses.get(key)?.constructor;
|
||||
}
|
||||
|
||||
static clear(): void {
|
||||
this.nodeClasses.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点装饰器
|
||||
*
|
||||
* 用于标注一个类是可在编辑器中使用的行为树节点
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BehaviorNode({
|
||||
* displayName: '等待',
|
||||
* category: '动作',
|
||||
* type: NodeType.Action,
|
||||
* icon: 'Clock',
|
||||
* description: '等待指定时间',
|
||||
* color: '#9E9E9E'
|
||||
* })
|
||||
* class WaitNode extends Component {
|
||||
* @BehaviorProperty({
|
||||
* label: '持续时间',
|
||||
* type: 'number',
|
||||
* min: 0,
|
||||
* step: 0.1,
|
||||
* description: '等待时间(秒)'
|
||||
* })
|
||||
* duration: number = 1.0;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function BehaviorNode(metadata: BehaviorNodeMetadata) {
|
||||
return function <T extends { new (...args: any[]): any }>(constructor: T) {
|
||||
const metadataWithClassName = {
|
||||
...metadata,
|
||||
className: constructor.name
|
||||
};
|
||||
NodeClassRegistry.registerNodeClass(constructor, metadataWithClassName);
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树属性装饰器
|
||||
*
|
||||
* 用于标注节点的可配置属性,这些属性会在编辑器中显示
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BehaviorNode({ ... })
|
||||
* class MyNode {
|
||||
* @BehaviorProperty({
|
||||
* label: '速度',
|
||||
* type: 'number',
|
||||
* min: 0,
|
||||
* max: 100,
|
||||
* description: '移动速度'
|
||||
* })
|
||||
* speed: number = 10;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function BehaviorProperty(config: Omit<PropertyDefinition, 'name' | 'defaultValue'>) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
if (!target.constructor.__nodeProperties) {
|
||||
target.constructor.__nodeProperties = [];
|
||||
}
|
||||
target.constructor.__nodeProperties.push({
|
||||
name: propertyKey,
|
||||
...config
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 BehaviorProperty 代替
|
||||
*/
|
||||
export const NodeProperty = BehaviorProperty;
|
||||
|
||||
/**
|
||||
* 获取所有注册的节点模板
|
||||
*/
|
||||
export function getRegisteredNodeTemplates(): NodeTemplate[] {
|
||||
return NodeClassRegistry.getAllNodeClasses().map(({ metadata, constructor }) => {
|
||||
// 从类的 __nodeProperties 收集属性定义
|
||||
const propertyDefs = constructor.__nodeProperties || [];
|
||||
|
||||
const defaultConfig: any = {
|
||||
nodeType: metadata.type.toLowerCase()
|
||||
};
|
||||
|
||||
// 从类的默认值中提取配置,并补充 defaultValue
|
||||
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,
|
||||
requiresChildren: metadata.requiresChildren,
|
||||
defaultConfig,
|
||||
properties
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有注册的节点类
|
||||
*/
|
||||
export function clearRegisteredNodes(): void {
|
||||
NodeClassRegistry.clear();
|
||||
}
|
||||
|
||||
export { NodeClassRegistry };
|
||||
@@ -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 被执行
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetManager');
|
||||
|
||||
/**
|
||||
* 行为树资产管理器(服务)
|
||||
*
|
||||
* 管理所有共享的BehaviorTreeData
|
||||
* 多个实例可以引用同一份数据
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* // 注册服务
|
||||
* Core.services.registerSingleton(BehaviorTreeAssetManager);
|
||||
*
|
||||
* // 使用服务
|
||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
* ```
|
||||
*/
|
||||
export class BehaviorTreeAssetManager implements IService {
|
||||
/**
|
||||
* 已加载的行为树资产
|
||||
*/
|
||||
private assets: Map<string, BehaviorTreeData> = new Map();
|
||||
|
||||
/**
|
||||
* 加载行为树资产
|
||||
*/
|
||||
loadAsset(asset: BehaviorTreeData): void {
|
||||
if (this.assets.has(asset.id)) {
|
||||
logger.warn(`行为树资产已存在,将被覆盖: ${asset.id}`);
|
||||
}
|
||||
this.assets.set(asset.id, asset);
|
||||
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为树资产
|
||||
*/
|
||||
getAsset(assetId: string): BehaviorTreeData | undefined {
|
||||
return this.assets.get(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资产是否存在
|
||||
*/
|
||||
hasAsset(assetId: string): boolean {
|
||||
return this.assets.has(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载行为树资产
|
||||
*/
|
||||
unloadAsset(assetId: string): boolean {
|
||||
const result = this.assets.delete(assetId);
|
||||
if (result) {
|
||||
logger.info(`行为树资产已卸载: ${assetId}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有资产
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.assets.clear();
|
||||
logger.info('所有行为树资产已清空');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载资产数量
|
||||
*/
|
||||
getAssetCount(): number {
|
||||
return this.assets.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资产ID
|
||||
*/
|
||||
getAllAssetIds(): string[] {
|
||||
return Array.from(this.assets.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(实现IService接口)
|
||||
*/
|
||||
dispose(): void {
|
||||
this.clearAll();
|
||||
}
|
||||
}
|
||||
99
packages/behavior-tree/src/Runtime/BehaviorTreeData.ts
Normal file
99
packages/behavior-tree/src/Runtime/BehaviorTreeData.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 行为树节点定义(纯数据结构)
|
||||
*
|
||||
* 不依赖Entity,可以被多个实例共享
|
||||
*/
|
||||
export interface BehaviorNodeData {
|
||||
/** 节点唯一ID */
|
||||
id: string;
|
||||
|
||||
/** 节点名称(用于调试) */
|
||||
name: string;
|
||||
|
||||
/** 节点类型 */
|
||||
nodeType: NodeType;
|
||||
|
||||
/** 节点实现类型(对应Component类名) */
|
||||
implementationType: string;
|
||||
|
||||
/** 子节点ID列表 */
|
||||
children?: string[];
|
||||
|
||||
/** 节点特定配置数据 */
|
||||
config: Record<string, any>;
|
||||
|
||||
/** 属性到黑板变量的绑定映射 */
|
||||
bindings?: Record<string, string>;
|
||||
|
||||
/** 中止类型(条件装饰器使用) */
|
||||
abortType?: AbortType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树定义(可共享的Asset)
|
||||
*/
|
||||
export interface BehaviorTreeData {
|
||||
/** 树ID */
|
||||
id: string;
|
||||
|
||||
/** 树名称 */
|
||||
name: string;
|
||||
|
||||
/** 根节点ID */
|
||||
rootNodeId: string;
|
||||
|
||||
/** 所有节点(扁平化存储) */
|
||||
nodes: Map<string, BehaviorNodeData>;
|
||||
|
||||
/** 黑板变量定义 */
|
||||
blackboardVariables?: Map<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点运行时状态
|
||||
*
|
||||
* 每个BehaviorTreeRuntimeComponent实例独立维护
|
||||
*/
|
||||
export interface NodeRuntimeState {
|
||||
/** 当前执行状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 当前执行的子节点索引(复合节点使用) */
|
||||
currentChildIndex: number;
|
||||
|
||||
/** 开始执行时间(某些节点需要) */
|
||||
startTime?: number;
|
||||
|
||||
/** 上次执行时间(冷却节点使用) */
|
||||
lastExecutionTime?: number;
|
||||
|
||||
/** 当前重复次数(重复节点使用) */
|
||||
repeatCount?: number;
|
||||
|
||||
/** 缓存的结果(某些条件节点使用) */
|
||||
cachedResult?: any;
|
||||
|
||||
/** 洗牌后的索引(随机节点使用) */
|
||||
shuffledIndices?: number[];
|
||||
|
||||
/** 是否被中止 */
|
||||
isAborted?: boolean;
|
||||
|
||||
/** 上次条件评估结果(条件装饰器使用) */
|
||||
lastConditionResult?: boolean;
|
||||
|
||||
/** 正在观察的黑板键(条件装饰器使用) */
|
||||
observedKeys?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的运行时状态
|
||||
*/
|
||||
export function createDefaultRuntimeState(): NodeRuntimeState {
|
||||
return {
|
||||
status: TaskStatus.Invalid,
|
||||
currentChildIndex: 0
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 黑板变化监听器
|
||||
*/
|
||||
export type BlackboardChangeListener = (key: string, newValue: any, oldValue: any) => void;
|
||||
|
||||
/**
|
||||
* 黑板观察者信息
|
||||
*/
|
||||
interface BlackboardObserver {
|
||||
nodeId: string;
|
||||
keys: Set<string>;
|
||||
callback: BlackboardChangeListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树运行时组件
|
||||
*
|
||||
* 挂载到游戏Entity上,引用共享的BehaviorTreeData
|
||||
* 维护该Entity独立的运行时状态
|
||||
*/
|
||||
@ECSComponent('BehaviorTreeRuntime')
|
||||
@Serializable({ version: 1 })
|
||||
export class BehaviorTreeRuntimeComponent extends Component {
|
||||
/**
|
||||
* 引用的行为树资产ID(可序列化)
|
||||
*/
|
||||
@Serialize()
|
||||
treeAssetId: string = '';
|
||||
|
||||
/**
|
||||
* 是否自动启动
|
||||
*/
|
||||
@Serialize()
|
||||
autoStart: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否正在运行
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
isRunning: boolean = false;
|
||||
|
||||
/**
|
||||
* 节点运行时状态(每个节点独立)
|
||||
* 不序列化,每次加载时重新初始化
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private nodeStates: Map<string, NodeRuntimeState> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板数据(该Entity独立的数据)
|
||||
* 不序列化,通过初始化设置
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboard: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
* 黑板观察者列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
private blackboardObservers: Map<string, BlackboardObserver[]> = new Map();
|
||||
|
||||
/**
|
||||
* 当前激活的节点ID列表(用于调试)
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
activeNodeIds: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 标记是否需要在下一个tick重置状态
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
needsReset: boolean = false;
|
||||
|
||||
/**
|
||||
* 需要中止的节点ID列表
|
||||
*/
|
||||
@IgnoreSerialization()
|
||||
nodesToAbort: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 获取节点运行时状态
|
||||
*/
|
||||
getNodeState(nodeId: string): NodeRuntimeState {
|
||||
if (!this.nodeStates.has(nodeId)) {
|
||||
this.nodeStates.set(nodeId, createDefaultRuntimeState());
|
||||
}
|
||||
return this.nodeStates.get(nodeId)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置节点状态
|
||||
*/
|
||||
resetNodeState(nodeId: string): void {
|
||||
const state = this.getNodeState(nodeId);
|
||||
state.status = TaskStatus.Invalid;
|
||||
state.currentChildIndex = 0;
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
delete state.repeatCount;
|
||||
delete state.cachedResult;
|
||||
delete state.shuffledIndices;
|
||||
delete state.isAborted;
|
||||
delete state.lastConditionResult;
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有节点状态
|
||||
*/
|
||||
resetAllStates(): void {
|
||||
this.nodeStates.clear();
|
||||
this.activeNodeIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑板值
|
||||
*/
|
||||
getBlackboardValue<T = any>(key: string): T | undefined {
|
||||
return this.blackboard.get(key) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板值
|
||||
*/
|
||||
setBlackboardValue(key: string, value: any): void {
|
||||
const oldValue = this.blackboard.get(key);
|
||||
this.blackboard.set(key, value);
|
||||
|
||||
if (oldValue !== value) {
|
||||
this.notifyBlackboardChange(key, value, oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查黑板是否有某个键
|
||||
*/
|
||||
hasBlackboardKey(key: string): boolean {
|
||||
return this.blackboard.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑板(从树定义的默认值)
|
||||
*/
|
||||
initializeBlackboard(variables?: Map<string, any>): void {
|
||||
if (variables) {
|
||||
variables.forEach((value, key) => {
|
||||
if (!this.blackboard.has(key)) {
|
||||
this.blackboard.set(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空黑板
|
||||
*/
|
||||
clearBlackboard(): void {
|
||||
this.blackboard.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动行为树
|
||||
*/
|
||||
start(): void {
|
||||
this.isRunning = true;
|
||||
this.resetAllStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止行为树
|
||||
*/
|
||||
stop(): void {
|
||||
this.isRunning = false;
|
||||
this.activeNodeIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停行为树
|
||||
*/
|
||||
pause(): void {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复行为树
|
||||
*/
|
||||
resume(): void {
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册黑板观察者
|
||||
*/
|
||||
observeBlackboard(nodeId: string, keys: string[], callback: BlackboardChangeListener): void {
|
||||
const observer: BlackboardObserver = {
|
||||
nodeId,
|
||||
keys: new Set(keys),
|
||||
callback
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
if (!this.blackboardObservers.has(key)) {
|
||||
this.blackboardObservers.set(key, []);
|
||||
}
|
||||
this.blackboardObservers.get(key)!.push(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册黑板观察者
|
||||
*/
|
||||
unobserveBlackboard(nodeId: string): void {
|
||||
for (const observers of this.blackboardObservers.values()) {
|
||||
const index = observers.findIndex(o => o.nodeId === nodeId);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知黑板变化
|
||||
*/
|
||||
private notifyBlackboardChange(key: string, newValue: any, oldValue: any): void {
|
||||
const observers = this.blackboardObservers.get(key);
|
||||
if (!observers) return;
|
||||
|
||||
for (const observer of observers) {
|
||||
try {
|
||||
observer.callback(key, newValue, oldValue);
|
||||
} catch (error) {
|
||||
console.error(`黑板观察者回调错误 (节点: ${observer.nodeId}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止节点
|
||||
*/
|
||||
requestAbort(nodeId: string): void {
|
||||
this.nodesToAbort.add(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查节点是否需要中止
|
||||
*/
|
||||
shouldAbort(nodeId: string): boolean {
|
||||
return this.nodesToAbort.has(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除中止请求
|
||||
*/
|
||||
clearAbortRequest(nodeId: string): void {
|
||||
this.nodesToAbort.delete(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有中止请求
|
||||
*/
|
||||
clearAllAbortRequests(): void {
|
||||
this.nodesToAbort.clear();
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板比较条件执行器
|
||||
*
|
||||
* 比较黑板中的值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardCompare',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板比较',
|
||||
description: '比较黑板中的值',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'object',
|
||||
description: '比较值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardCompare implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const compareValue = BindingHelper.getValue(context, 'compareValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(key);
|
||||
|
||||
if (this.compare(actualValue, compareValue, operator)) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private compare(actualValue: any, compareValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === compareValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== compareValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > compareValue;
|
||||
case 'lessThan':
|
||||
return actualValue < compareValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= compareValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= compareValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 黑板存在检查条件执行器
|
||||
*
|
||||
* 检查黑板中是否存在指定的键
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'BlackboardExists',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '黑板存在',
|
||||
description: '检查黑板中是否存在指定的键',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
checkNull: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '检查是否为null'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class BlackboardExists implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const checkNull = BindingHelper.getValue<boolean>(context, 'checkNull', false);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const value = runtime.getBlackboardValue(key);
|
||||
|
||||
if (value === undefined) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (checkNull && value === null) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { TaskStatus, NodeType, AbortType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 条件装饰器执行器
|
||||
*
|
||||
* 根据条件决定是否执行子节点
|
||||
* 支持动态优先级和中止机制
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Conditional',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '条件',
|
||||
description: '根据条件决定是否执行子节点',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
blackboardKey: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
expectedValue: {
|
||||
type: 'object',
|
||||
description: '期望值',
|
||||
supportBinding: true
|
||||
},
|
||||
operator: {
|
||||
type: 'string',
|
||||
default: 'equals',
|
||||
description: '比较运算符',
|
||||
options: ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual']
|
||||
},
|
||||
abortType: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
description: '中止类型',
|
||||
options: ['none', 'self', 'lower-priority', 'both']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ConditionalExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const blackboardKey = BindingHelper.getValue<string>(context, 'blackboardKey', '');
|
||||
const expectedValue = BindingHelper.getValue(context, 'expectedValue');
|
||||
const operator = BindingHelper.getValue<string>(context, 'operator', 'equals');
|
||||
const abortType = (nodeData.abortType || AbortType.None) as AbortType;
|
||||
|
||||
if (!blackboardKey) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actualValue = runtime.getBlackboardValue(blackboardKey);
|
||||
const conditionMet = this.evaluateCondition(actualValue, expectedValue, operator);
|
||||
|
||||
const wasRunning = state.status === TaskStatus.Running;
|
||||
|
||||
if (abortType !== AbortType.None) {
|
||||
if (!state.observedKeys || state.observedKeys.length === 0) {
|
||||
state.observedKeys = [blackboardKey];
|
||||
this.setupObserver(context, blackboardKey, expectedValue, operator, abortType);
|
||||
}
|
||||
|
||||
if (state.lastConditionResult !== undefined && state.lastConditionResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else if (wasRunning) {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.lastConditionResult = conditionMet;
|
||||
|
||||
if (!conditionMet) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private evaluateCondition(actualValue: any, expectedValue: any, operator: string): boolean {
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return actualValue === expectedValue;
|
||||
case 'notEquals':
|
||||
return actualValue !== expectedValue;
|
||||
case 'greaterThan':
|
||||
return actualValue > expectedValue;
|
||||
case 'lessThan':
|
||||
return actualValue < expectedValue;
|
||||
case 'greaterOrEqual':
|
||||
return actualValue >= expectedValue;
|
||||
case 'lessOrEqual':
|
||||
return actualValue <= expectedValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板观察者
|
||||
*/
|
||||
private setupObserver(
|
||||
context: NodeExecutionContext,
|
||||
blackboardKey: string,
|
||||
expectedValue: any,
|
||||
operator: string,
|
||||
abortType: AbortType
|
||||
): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
runtime.observeBlackboard(nodeData.id, [blackboardKey], (_key, newValue) => {
|
||||
const conditionMet = this.evaluateCondition(newValue, expectedValue, operator);
|
||||
const lastResult = context.state.lastConditionResult;
|
||||
|
||||
if (lastResult !== undefined && lastResult !== conditionMet) {
|
||||
if (conditionMet) {
|
||||
this.handleConditionBecameTrue(context, abortType);
|
||||
} else {
|
||||
this.handleConditionBecameFalse(context, abortType);
|
||||
}
|
||||
}
|
||||
|
||||
context.state.lastConditionResult = conditionMet;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为true
|
||||
*/
|
||||
private handleConditionBecameTrue(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) {
|
||||
this.requestAbortLowerPriority(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理条件变为false
|
||||
*/
|
||||
private handleConditionBecameFalse(context: NodeExecutionContext, abortType: AbortType): void {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (abortType === AbortType.Self || abortType === AbortType.Both) {
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.requestAbort(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中止低优先级节点
|
||||
*/
|
||||
private requestAbortLowerPriority(context: NodeExecutionContext): void {
|
||||
const { runtime } = context;
|
||||
runtime.requestAbort('__lower_priority__');
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
if (state.observedKeys && state.observedKeys.length > 0) {
|
||||
runtime.unobserveBlackboard(nodeData.id);
|
||||
delete state.observedKeys;
|
||||
}
|
||||
|
||||
delete state.lastConditionResult;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 冷却装饰器执行器
|
||||
*
|
||||
* 子节点执行成功后进入冷却时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Cooldown',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '冷却',
|
||||
description: '子节点执行成功后进入冷却时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
cooldownTime: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '冷却时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class CooldownExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const cooldownTime = BindingHelper.getValue<number>(context, 'cooldownTime', 1.0);
|
||||
|
||||
if (state.lastExecutionTime !== undefined) {
|
||||
const timeSinceLastExecution = totalTime - state.lastExecutionTime;
|
||||
if (timeSinceLastExecution < cooldownTime) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Success) {
|
||||
state.lastExecutionTime = totalTime;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.lastExecutionTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行动作执行器
|
||||
*
|
||||
* 执行自定义动作逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '执行动作',
|
||||
description: '执行自定义动作逻辑',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
actionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '动作名称(黑板中action_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const actionName = BindingHelper.getValue<string>(context, 'actionName', '');
|
||||
|
||||
if (!actionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const actionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => TaskStatus>(`action_${actionName}`);
|
||||
|
||||
if (!actionFunction || typeof actionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return actionFunction(entity);
|
||||
} catch (error) {
|
||||
console.error(`ExecuteAction failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 执行条件执行器
|
||||
*
|
||||
* 执行自定义条件逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ExecuteCondition',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '执行条件',
|
||||
description: '执行自定义条件逻辑',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
conditionName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '条件名称(黑板中condition_前缀的函数)'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ExecuteCondition implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, entity } = context;
|
||||
const conditionName = BindingHelper.getValue<string>(context, 'conditionName', '');
|
||||
|
||||
if (!conditionName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const conditionFunction = runtime.getBlackboardValue<(entity: NodeExecutionContext['entity']) => boolean>(`condition_${conditionName}`);
|
||||
|
||||
if (!conditionFunction || typeof conditionFunction !== 'function') {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
try {
|
||||
return conditionFunction(entity) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
} catch (error) {
|
||||
console.error(`ExecuteCondition failed: ${error}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/behavior-tree/src/Runtime/Executors/LogAction.ts
Normal file
71
packages/behavior-tree/src/Runtime/Executors/LogAction.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 日志动作执行器
|
||||
*
|
||||
* 输出日志信息
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Log',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '日志',
|
||||
description: '输出日志信息',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
message: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '日志消息,支持{key}占位符引用黑板变量',
|
||||
supportBinding: true
|
||||
},
|
||||
logLevel: {
|
||||
type: 'string',
|
||||
default: 'info',
|
||||
description: '日志级别',
|
||||
options: ['info', 'warn', 'error']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class LogAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const message = BindingHelper.getValue<string>(context, 'message', '');
|
||||
const logLevel = BindingHelper.getValue<string>(context, 'logLevel', 'info');
|
||||
|
||||
const finalMessage = this.replaceBlackboardVariables(message, runtime);
|
||||
|
||||
this.log(finalMessage, logLevel);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
private replaceBlackboardVariables(message: string, runtime: NodeExecutionContext['runtime']): string {
|
||||
if (!message.includes('{') || !message.includes('}')) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// 使用限制长度的正则表达式避免 ReDoS 攻击
|
||||
// 限制占位符名称最多100个字符,只允许字母、数字、下划线和点号
|
||||
return message.replace(/\{([\w.]{1,100})\}/g, (_, key) => {
|
||||
const value = runtime.getBlackboardValue(key.trim());
|
||||
return value !== undefined ? String(value) : `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
private log(message: string, level: string): void {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error(message);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(message);
|
||||
break;
|
||||
case 'info':
|
||||
default:
|
||||
console.log(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 修改黑板值动作执行器
|
||||
*
|
||||
* 对黑板中的数值进行运算
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ModifyBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '修改黑板值',
|
||||
description: '对黑板中的数值进行运算',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
operation: {
|
||||
type: 'string',
|
||||
default: 'add',
|
||||
description: '运算类型',
|
||||
options: ['add', 'subtract', 'multiply', 'divide', 'set']
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: '操作数',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ModifyBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const operation = BindingHelper.getValue<string>(context, 'operation', 'add');
|
||||
const value = BindingHelper.getValue<number>(context, 'value', 0);
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const currentValue = runtime.getBlackboardValue<number>(key) || 0;
|
||||
let newValue: number;
|
||||
|
||||
switch (operation) {
|
||||
case 'add':
|
||||
newValue = currentValue + value;
|
||||
break;
|
||||
case 'subtract':
|
||||
newValue = currentValue - value;
|
||||
break;
|
||||
case 'multiply':
|
||||
newValue = currentValue * value;
|
||||
break;
|
||||
case 'divide':
|
||||
newValue = value !== 0 ? currentValue / value : currentValue;
|
||||
break;
|
||||
case 'set':
|
||||
newValue = value;
|
||||
break;
|
||||
default:
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, newValue);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行节点执行器
|
||||
*
|
||||
* 同时执行所有子节点
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Parallel',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行',
|
||||
description: '同时执行所有子节点',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
successPolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '成功策略',
|
||||
options: ['all', 'one']
|
||||
},
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'one',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ParallelExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const successPolicy = BindingHelper.getValue<string>(context, 'successPolicy', 'all');
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'one');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successPolicy === 'one' && successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (successPolicy === 'all' && successCount === nodeData.children.length) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Success;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 并行选择器执行器
|
||||
*
|
||||
* 并行执行子节点,任一成功则成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'ParallelSelector',
|
||||
nodeType: NodeType.Composite,
|
||||
displayName: '并行选择器',
|
||||
description: '并行执行子节点,任一成功则成功',
|
||||
category: 'Composite',
|
||||
configSchema: {
|
||||
failurePolicy: {
|
||||
type: 'string',
|
||||
default: 'all',
|
||||
description: '失败策略',
|
||||
options: ['all', 'one']
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ParallelSelectorExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData } = context;
|
||||
const failurePolicy = BindingHelper.getValue<string>(context, 'failurePolicy', 'all');
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
let hasRunning = false;
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
hasRunning = true;
|
||||
} else if (status === TaskStatus.Success) {
|
||||
successCount++;
|
||||
} else if (status === TaskStatus.Failure) {
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'one' && failureCount > 0) {
|
||||
this.stopAllChildren(context);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (failurePolicy === 'all' && failureCount === nodeData.children.length) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return hasRunning ? TaskStatus.Running : TaskStatus.Failure;
|
||||
}
|
||||
|
||||
private stopAllChildren(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.activeNodeIds.delete(childId);
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime } = context;
|
||||
if (!nodeData.children) return;
|
||||
|
||||
for (const childId of nodeData.children) {
|
||||
runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 随机概率条件执行器
|
||||
*
|
||||
* 根据概率返回成功或失败
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'RandomProbability',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: '随机概率',
|
||||
description: '根据概率返回成功或失败',
|
||||
category: 'Condition',
|
||||
configSchema: {
|
||||
probability: {
|
||||
type: 'number',
|
||||
default: 0.5,
|
||||
description: '成功概率(0-1)',
|
||||
min: 0,
|
||||
max: 1,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class RandomProbability implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const probability = BindingHelper.getValue<number>(context, 'probability', 0.5);
|
||||
|
||||
const clampedProbability = Math.max(0, Math.min(1, probability));
|
||||
|
||||
if (Math.random() < clampedProbability) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 重复装饰器执行器
|
||||
*
|
||||
* 重复执行子节点指定次数
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Repeater',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '重复',
|
||||
description: '重复执行子节点指定次数',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
repeatCount: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '重复次数(-1表示无限循环)',
|
||||
supportBinding: true
|
||||
},
|
||||
endOnFailure: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '子节点失败时是否结束'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class RepeaterExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, runtime } = context;
|
||||
const repeatCount = BindingHelper.getValue<number>(context, 'repeatCount', 1);
|
||||
const endOnFailure = BindingHelper.getValue<boolean>(context, 'endOnFailure', false);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
|
||||
if (!state.repeatCount) {
|
||||
state.repeatCount = 0;
|
||||
}
|
||||
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (status === TaskStatus.Failure && endOnFailure) {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.repeatCount++;
|
||||
runtime.resetNodeState(childId);
|
||||
|
||||
const shouldContinue = (repeatCount === -1) || (state.repeatCount < repeatCount);
|
||||
|
||||
if (shouldContinue) {
|
||||
return TaskStatus.Running;
|
||||
} else {
|
||||
state.repeatCount = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.repeatCount;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
144
packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts
Normal file
144
packages/behavior-tree/src/Runtime/Executors/ServiceDecorator.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* Service执行接口
|
||||
*/
|
||||
export interface IServiceExecutor {
|
||||
/**
|
||||
* Service开始执行
|
||||
*/
|
||||
onServiceStart?(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service每帧更新
|
||||
*/
|
||||
onServiceTick(context: NodeExecutionContext): void;
|
||||
|
||||
/**
|
||||
* Service结束执行
|
||||
*/
|
||||
onServiceEnd?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service注册表
|
||||
*/
|
||||
class ServiceRegistry {
|
||||
private static services: Map<string, IServiceExecutor> = new Map();
|
||||
|
||||
static register(name: string, service: IServiceExecutor): void {
|
||||
this.services.set(name, service);
|
||||
}
|
||||
|
||||
static get(name: string): IServiceExecutor | undefined {
|
||||
return this.services.get(name);
|
||||
}
|
||||
|
||||
static has(name: string): boolean {
|
||||
return this.services.has(name);
|
||||
}
|
||||
|
||||
static unregister(name: string): boolean {
|
||||
return this.services.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service装饰器执行器
|
||||
*
|
||||
* 在子节点执行期间持续运行后台逻辑
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Service',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: 'Service',
|
||||
description: '在子节点执行期间持续运行后台逻辑',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
serviceName: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Service名称'
|
||||
},
|
||||
tickInterval: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'Service更新间隔(秒,0表示每帧更新)',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class ServiceDecorator implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
const tickInterval = BindingHelper.getValue<number>(context, 'tickInterval', 0);
|
||||
|
||||
if (!serviceName) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (!service) {
|
||||
console.warn(`未找到Service: ${serviceName}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (state.status !== TaskStatus.Running) {
|
||||
state.startTime = totalTime;
|
||||
state.lastExecutionTime = totalTime;
|
||||
|
||||
if (service.onServiceStart) {
|
||||
service.onServiceStart(context);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldTick = tickInterval === 0 ||
|
||||
(state.lastExecutionTime !== undefined &&
|
||||
(totalTime - state.lastExecutionTime) >= tickInterval);
|
||||
|
||||
if (shouldTick) {
|
||||
service.onServiceTick(context);
|
||||
state.lastExecutionTime = totalTime;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus !== TaskStatus.Running) {
|
||||
if (service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
return childStatus;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const { nodeData, runtime, state } = context;
|
||||
|
||||
const serviceName = BindingHelper.getValue<string>(context, 'serviceName', '');
|
||||
if (serviceName) {
|
||||
const service = ServiceRegistry.get(serviceName);
|
||||
if (service && service.onServiceEnd) {
|
||||
service.onServiceEnd(context);
|
||||
}
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
delete state.lastExecutionTime;
|
||||
|
||||
if (nodeData.children && nodeData.children.length > 0) {
|
||||
runtime.resetNodeState(nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ServiceRegistry };
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 设置黑板值动作执行器
|
||||
*
|
||||
* 设置黑板中的变量值
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SetBlackboardValue',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '设置黑板值',
|
||||
description: '设置黑板中的变量值',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
key: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '黑板变量名'
|
||||
},
|
||||
value: {
|
||||
type: 'object',
|
||||
description: '要设置的值',
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SetBlackboardValue implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime } = context;
|
||||
const key = BindingHelper.getValue<string>(context, 'key', '');
|
||||
const value = BindingHelper.getValue(context, 'value');
|
||||
|
||||
if (!key) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
runtime.setBlackboardValue(key, value);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
161
packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts
Normal file
161
packages/behavior-tree/src/Runtime/Executors/SubTreeExecutor.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
import { BehaviorTreeAssetManager } from '../BehaviorTreeAssetManager';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* SubTree执行器
|
||||
*
|
||||
* 引用并执行其他行为树,实现模块化和复用
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'SubTree',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '子树',
|
||||
description: '引用并执行其他行为树',
|
||||
category: 'Special',
|
||||
configSchema: {
|
||||
treeAssetId: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: '要执行的行为树资产ID',
|
||||
supportBinding: true
|
||||
},
|
||||
shareBlackboard: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: '是否共享黑板数据'
|
||||
}
|
||||
}
|
||||
})
|
||||
export class SubTreeExecutor implements INodeExecutor {
|
||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
||||
|
||||
private getAssetManager(): BehaviorTreeAssetManager {
|
||||
if (!this.assetManager) {
|
||||
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||
}
|
||||
return this.assetManager;
|
||||
}
|
||||
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { runtime, state, entity } = context;
|
||||
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
const shareBlackboard = BindingHelper.getValue<boolean>(context, 'shareBlackboard', true);
|
||||
|
||||
if (!treeAssetId) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (!subTreeData) {
|
||||
console.warn(`未找到子树资产: ${treeAssetId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (!rootNode) {
|
||||
console.warn(`子树根节点未找到: ${subTreeData.rootNodeId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
if (!shareBlackboard && state.status !== TaskStatus.Running) {
|
||||
if (subTreeData.blackboardVariables) {
|
||||
for (const [key, value] of subTreeData.blackboardVariables.entries()) {
|
||||
if (!runtime.hasBlackboardKey(key)) {
|
||||
runtime.setBlackboardValue(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subTreeContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: rootNode,
|
||||
state: runtime.getNodeState(rootNode.id),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: (childId: string) => {
|
||||
const childData = subTreeData.nodes.get(childId);
|
||||
if (!childData) {
|
||||
console.warn(`子树节点未找到: ${childId}`);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childContext: NodeExecutionContext = {
|
||||
entity,
|
||||
nodeData: childData,
|
||||
state: runtime.getNodeState(childId),
|
||||
runtime,
|
||||
treeData: subTreeData,
|
||||
deltaTime: context.deltaTime,
|
||||
totalTime: context.totalTime,
|
||||
executeChild: subTreeContext.executeChild
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(childContext);
|
||||
}
|
||||
};
|
||||
|
||||
return this.executeSubTreeNode(subTreeContext);
|
||||
}
|
||||
|
||||
private executeSubTreeNode(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
const state = runtime.getNodeState(nodeData.id);
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[state.currentChildIndex]!;
|
||||
const childStatus = context.executeChild(childId);
|
||||
|
||||
if (childStatus === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (childStatus === TaskStatus.Failure) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
state.currentChildIndex++;
|
||||
|
||||
if (state.currentChildIndex >= nodeData.children.length) {
|
||||
state.currentChildIndex = 0;
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
const treeAssetId = BindingHelper.getValue<string>(context, 'treeAssetId', '');
|
||||
|
||||
if (treeAssetId) {
|
||||
const assetManager = this.getAssetManager();
|
||||
const subTreeData = assetManager.getAsset(treeAssetId);
|
||||
|
||||
if (subTreeData) {
|
||||
const rootNode = subTreeData.nodes.get(subTreeData.rootNodeId);
|
||||
if (rootNode) {
|
||||
context.runtime.resetNodeState(rootNode.id);
|
||||
|
||||
if (rootNode.children) {
|
||||
for (const childId of rootNode.children) {
|
||||
context.runtime.resetNodeState(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 超时装饰器执行器
|
||||
*
|
||||
* 限制子节点的执行时间
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Timeout',
|
||||
nodeType: NodeType.Decorator,
|
||||
displayName: '超时',
|
||||
description: '限制子节点的执行时间',
|
||||
category: 'Decorator',
|
||||
configSchema: {
|
||||
timeout: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '超时时间(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class TimeoutExecutor implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { nodeData, state, totalTime } = context;
|
||||
|
||||
if (!nodeData.children || nodeData.children.length === 0) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const timeout = BindingHelper.getValue<number>(context, 'timeout', 1.0);
|
||||
|
||||
if (state.startTime === undefined) {
|
||||
state.startTime = totalTime;
|
||||
}
|
||||
|
||||
const elapsedTime = totalTime - state.startTime;
|
||||
if (elapsedTime >= timeout) {
|
||||
delete state.startTime;
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
const childId = nodeData.children[0]!;
|
||||
const status = context.executeChild(childId);
|
||||
|
||||
if (status === TaskStatus.Running) {
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
delete state.startTime;
|
||||
return status;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
if (context.nodeData.children && context.nodeData.children.length > 0) {
|
||||
context.runtime.resetNodeState(context.nodeData.children[0]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
packages/behavior-tree/src/Runtime/Executors/WaitAction.ts
Normal file
46
packages/behavior-tree/src/Runtime/Executors/WaitAction.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { TaskStatus, NodeType } from '../../Types/TaskStatus';
|
||||
import { INodeExecutor, NodeExecutionContext, BindingHelper } from '../NodeExecutor';
|
||||
import { NodeExecutorMetadata } from '../NodeMetadata';
|
||||
|
||||
/**
|
||||
* 等待动作执行器
|
||||
*
|
||||
* 等待指定时间后返回成功
|
||||
*/
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'Wait',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: '等待',
|
||||
description: '等待指定时间后返回成功',
|
||||
category: 'Action',
|
||||
configSchema: {
|
||||
duration: {
|
||||
type: 'number',
|
||||
default: 1.0,
|
||||
description: '等待时长(秒)',
|
||||
min: 0,
|
||||
supportBinding: true
|
||||
}
|
||||
}
|
||||
})
|
||||
export class WaitAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const { state, totalTime } = context;
|
||||
const duration = BindingHelper.getValue<number>(context, 'duration', 1.0);
|
||||
|
||||
if (!state.startTime) {
|
||||
state.startTime = totalTime;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
if (totalTime - state.startTime >= duration) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
reset(context: NodeExecutionContext): void {
|
||||
delete context.state.startTime;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
30
packages/behavior-tree/src/Runtime/Executors/index.ts
Normal file
30
packages/behavior-tree/src/Runtime/Executors/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export { SequenceExecutor } from './SequenceExecutor';
|
||||
export { SelectorExecutor } from './SelectorExecutor';
|
||||
export { ParallelExecutor } from './ParallelExecutor';
|
||||
export { ParallelSelectorExecutor } from './ParallelSelectorExecutor';
|
||||
export { RandomSequenceExecutor } from './RandomSequenceExecutor';
|
||||
export { RandomSelectorExecutor } from './RandomSelectorExecutor';
|
||||
|
||||
export { InverterExecutor } from './InverterExecutor';
|
||||
export { RepeaterExecutor } from './RepeaterExecutor';
|
||||
export { AlwaysSucceedExecutor } from './AlwaysSucceedExecutor';
|
||||
export { AlwaysFailExecutor } from './AlwaysFailExecutor';
|
||||
export { UntilSuccessExecutor } from './UntilSuccessExecutor';
|
||||
export { UntilFailExecutor } from './UntilFailExecutor';
|
||||
export { ConditionalExecutor } from './ConditionalExecutor';
|
||||
export { CooldownExecutor } from './CooldownExecutor';
|
||||
export { TimeoutExecutor } from './TimeoutExecutor';
|
||||
export { ServiceDecorator, ServiceRegistry } from './ServiceDecorator';
|
||||
export type { IServiceExecutor } from './ServiceDecorator';
|
||||
|
||||
export { WaitAction } from './WaitAction';
|
||||
export { LogAction } from './LogAction';
|
||||
export { SetBlackboardValue } from './SetBlackboardValue';
|
||||
export { ModifyBlackboardValue } from './ModifyBlackboardValue';
|
||||
export { ExecuteAction } from './ExecuteAction';
|
||||
export { SubTreeExecutor } from './SubTreeExecutor';
|
||||
|
||||
export { BlackboardCompare } from './BlackboardCompare';
|
||||
export { BlackboardExists } from './BlackboardExists';
|
||||
export { RandomProbability } from './RandomProbability';
|
||||
export { ExecuteCondition } from './ExecuteCondition';
|
||||
181
packages/behavior-tree/src/Runtime/NodeExecutor.ts
Normal file
181
packages/behavior-tree/src/Runtime/NodeExecutor.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { TaskStatus } from '../Types/TaskStatus';
|
||||
import { BehaviorNodeData, BehaviorTreeData, NodeRuntimeState } from './BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
|
||||
/**
|
||||
* 节点执行上下文
|
||||
*
|
||||
* 包含执行节点所需的所有信息
|
||||
*/
|
||||
export interface NodeExecutionContext {
|
||||
/** 游戏Entity(行为树宿主) */
|
||||
readonly entity: Entity;
|
||||
|
||||
/** 节点数据 */
|
||||
readonly nodeData: BehaviorNodeData;
|
||||
|
||||
/** 节点运行时状态 */
|
||||
readonly state: NodeRuntimeState;
|
||||
|
||||
/** 运行时组件(访问黑板等) */
|
||||
readonly runtime: BehaviorTreeRuntimeComponent;
|
||||
|
||||
/** 行为树数据(访问子节点等) */
|
||||
readonly treeData: BehaviorTreeData;
|
||||
|
||||
/** 当前帧增量时间 */
|
||||
readonly deltaTime: number;
|
||||
|
||||
/** 总时间 */
|
||||
readonly totalTime: number;
|
||||
|
||||
/** 执行子节点 */
|
||||
executeChild(childId: string): TaskStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器接口
|
||||
*
|
||||
* 所有节点类型都需要实现对应的执行器
|
||||
* 执行器是无状态的,状态存储在NodeRuntimeState中
|
||||
*/
|
||||
export interface INodeExecutor {
|
||||
/**
|
||||
* 执行节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 执行结果状态
|
||||
*/
|
||||
execute(context: NodeExecutionContext): TaskStatus;
|
||||
|
||||
/**
|
||||
* 重置节点状态(可选)
|
||||
*
|
||||
* 当节点完成或被中断时调用
|
||||
*/
|
||||
reset?(context: NodeExecutionContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行结果
|
||||
*/
|
||||
export interface CompositeExecutionResult {
|
||||
/** 节点状态 */
|
||||
status: TaskStatus;
|
||||
|
||||
/** 要激活的子节点索引列表(undefined表示激活所有) */
|
||||
activateChildren?: number[];
|
||||
|
||||
/** 是否停止所有子节点 */
|
||||
stopAllChildren?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复合节点执行器接口
|
||||
*/
|
||||
export interface ICompositeExecutor extends INodeExecutor {
|
||||
/**
|
||||
* 执行复合节点逻辑
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 复合节点执行结果
|
||||
*/
|
||||
executeComposite(context: NodeExecutionContext): CompositeExecutionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定辅助工具
|
||||
*
|
||||
* 处理配置属性的黑板绑定
|
||||
*/
|
||||
export class BindingHelper {
|
||||
/**
|
||||
* 获取配置值(考虑黑板绑定)
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @param configKey 配置键名
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析后的值
|
||||
*/
|
||||
static getValue<T = any>(
|
||||
context: NodeExecutionContext,
|
||||
configKey: string,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
const { nodeData, runtime } = context;
|
||||
|
||||
if (nodeData.bindings && nodeData.bindings[configKey]) {
|
||||
const blackboardKey = nodeData.bindings[configKey];
|
||||
const boundValue = runtime.getBlackboardValue<T>(blackboardKey);
|
||||
return boundValue !== undefined ? boundValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
const configValue = nodeData.config[configKey];
|
||||
return configValue !== undefined ? configValue : (defaultValue as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否绑定到黑板变量
|
||||
*/
|
||||
static hasBinding(context: NodeExecutionContext, configKey: string): boolean {
|
||||
return !!(context.nodeData.bindings && context.nodeData.bindings[configKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定的黑板变量名
|
||||
*/
|
||||
static getBindingKey(context: NodeExecutionContext, configKey: string): string | undefined {
|
||||
return context.nodeData.bindings?.[configKey];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器注册表
|
||||
*
|
||||
* 管理所有节点类型的执行器
|
||||
*/
|
||||
export class NodeExecutorRegistry {
|
||||
private executors: Map<string, INodeExecutor> = new Map();
|
||||
|
||||
/**
|
||||
* 注册执行器
|
||||
*
|
||||
* @param implementationType 节点实现类型(对应BehaviorNodeData.implementationType)
|
||||
* @param executor 执行器实例
|
||||
*/
|
||||
register(implementationType: string, executor: INodeExecutor): void {
|
||||
if (this.executors.has(implementationType)) {
|
||||
console.warn(`执行器已存在,将被覆盖: ${implementationType}`);
|
||||
}
|
||||
this.executors.set(implementationType, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行器
|
||||
*/
|
||||
get(implementationType: string): INodeExecutor | undefined {
|
||||
return this.executors.get(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有执行器
|
||||
*/
|
||||
has(implementationType: string): boolean {
|
||||
return this.executors.has(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销执行器
|
||||
*/
|
||||
unregister(implementationType: string): boolean {
|
||||
return this.executors.delete(implementationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有执行器
|
||||
*/
|
||||
clear(): void {
|
||||
this.executors.clear();
|
||||
}
|
||||
}
|
||||
79
packages/behavior-tree/src/Runtime/NodeMetadata.ts
Normal file
79
packages/behavior-tree/src/Runtime/NodeMetadata.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
|
||||
/**
|
||||
* 配置参数定义
|
||||
*/
|
||||
export interface ConfigFieldDefinition {
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
||||
default?: any;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
options?: string[];
|
||||
supportBinding?: boolean;
|
||||
allowMultipleConnections?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据
|
||||
*/
|
||||
export interface NodeMetadata {
|
||||
implementationType: string;
|
||||
nodeType: NodeType;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
configSchema?: Record<string, ConfigFieldDefinition>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点元数据注册表
|
||||
*/
|
||||
export class NodeMetadataRegistry {
|
||||
private static metadataMap: Map<string, NodeMetadata> = new Map();
|
||||
private static executorClassMap: Map<Function, string> = new Map();
|
||||
private static executorConstructors: Map<string, new () => any> = new Map();
|
||||
|
||||
static register(target: Function, metadata: NodeMetadata): void {
|
||||
this.metadataMap.set(metadata.implementationType, metadata);
|
||||
this.executorClassMap.set(target, metadata.implementationType);
|
||||
this.executorConstructors.set(metadata.implementationType, target as new () => any);
|
||||
}
|
||||
|
||||
static getMetadata(implementationType: string): NodeMetadata | undefined {
|
||||
return this.metadataMap.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllMetadata(): NodeMetadata[] {
|
||||
return Array.from(this.metadataMap.values());
|
||||
}
|
||||
|
||||
static getByCategory(category: string): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.category === category);
|
||||
}
|
||||
|
||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.nodeType === nodeType);
|
||||
}
|
||||
|
||||
static getImplementationType(executorClass: Function): string | undefined {
|
||||
return this.executorClassMap.get(executorClass);
|
||||
}
|
||||
|
||||
static getExecutorConstructor(implementationType: string): (new () => any) | undefined {
|
||||
return this.executorConstructors.get(implementationType);
|
||||
}
|
||||
|
||||
static getAllExecutorConstructors(): Map<string, new () => any> {
|
||||
return new Map(this.executorConstructors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点执行器元数据装饰器
|
||||
*/
|
||||
export function NodeExecutorMetadata(metadata: NodeMetadata) {
|
||||
return function (target: Function) {
|
||||
NodeMetadataRegistry.register(target, metadata);
|
||||
};
|
||||
}
|
||||
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||
export { INodeExecutor, NodeExecutionContext, NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||
export { NodeMetadata, ConfigFieldDefinition, NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
|
||||
|
||||
export * from './Executors';
|
||||
@@ -22,6 +22,14 @@ export interface BlackboardVariableDefinition {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点配置数据
|
||||
*/
|
||||
export interface BehaviorNodeConfigData {
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树节点数据(运行时格式)
|
||||
*/
|
||||
@@ -31,7 +39,7 @@ export interface BehaviorTreeNodeData {
|
||||
nodeType: NodeType;
|
||||
|
||||
// 节点类型特定数据
|
||||
data: Record<string, any>;
|
||||
data: BehaviorNodeConfigData;
|
||||
|
||||
// 子节点ID列表
|
||||
children: string[];
|
||||
@@ -216,11 +224,19 @@ export class BehaviorTreeAssetValidator {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
warnings: warnings.length > 0 ? warnings : undefined
|
||||
const result: AssetValidationResult = {
|
||||
valid: errors.length === 0
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
result.errors = errors;
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
result.warnings = warnings;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
import { Entity, IScene, createLogger } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTreeAsset, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
import { BlackboardComponent } from '../Components/BlackboardComponent';
|
||||
import { PropertyBindings } from '../Components/PropertyBindings';
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
|
||||
// 导入所有节点组件
|
||||
import { RootNode } from '../Components/Composites/RootNode';
|
||||
import { SequenceNode } from '../Components/Composites/SequenceNode';
|
||||
import { SelectorNode } from '../Components/Composites/SelectorNode';
|
||||
import { ParallelNode } from '../Components/Composites/ParallelNode';
|
||||
import { ParallelSelectorNode } from '../Components/Composites/ParallelSelectorNode';
|
||||
import { RandomSequenceNode } from '../Components/Composites/RandomSequenceNode';
|
||||
import { RandomSelectorNode } from '../Components/Composites/RandomSelectorNode';
|
||||
|
||||
import { InverterNode } from '../Components/Decorators/InverterNode';
|
||||
import { RepeaterNode } from '../Components/Decorators/RepeaterNode';
|
||||
import { UntilSuccessNode } from '../Components/Decorators/UntilSuccessNode';
|
||||
import { UntilFailNode } from '../Components/Decorators/UntilFailNode';
|
||||
import { AlwaysSucceedNode } from '../Components/Decorators/AlwaysSucceedNode';
|
||||
import { AlwaysFailNode } from '../Components/Decorators/AlwaysFailNode';
|
||||
import { ConditionalNode } from '../Components/Decorators/ConditionalNode';
|
||||
import { CooldownNode } from '../Components/Decorators/CooldownNode';
|
||||
import { TimeoutNode } from '../Components/Decorators/TimeoutNode';
|
||||
|
||||
import { WaitAction } from '../Components/Actions/WaitAction';
|
||||
import { LogAction } from '../Components/Actions/LogAction';
|
||||
import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction';
|
||||
import { ModifyBlackboardValueAction } from '../Components/Actions/ModifyBlackboardValueAction';
|
||||
import { ExecuteAction } from '../Components/Actions/ExecuteAction';
|
||||
|
||||
import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition';
|
||||
import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition';
|
||||
import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition';
|
||||
import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition';
|
||||
import { AbortType } from '../Types/TaskStatus';
|
||||
|
||||
const logger = createLogger('BehaviorTreeAssetLoader');
|
||||
|
||||
/**
|
||||
* 实例化选项
|
||||
*/
|
||||
export interface InstantiateOptions {
|
||||
/**
|
||||
* 实体名称前缀
|
||||
*/
|
||||
namePrefix?: string;
|
||||
|
||||
/**
|
||||
* 是否共享黑板(如果为true,将使用全局黑板服务)
|
||||
*/
|
||||
sharedBlackboard?: boolean;
|
||||
|
||||
/**
|
||||
* 黑板变量覆盖(用于运行时动态设置初始值)
|
||||
*/
|
||||
blackboardOverrides?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 是否作为子树实例化
|
||||
* 如果为 true,根节点不会添加 RootNode 组件,避免触发预加载逻辑
|
||||
*/
|
||||
asSubTree?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树资产加载器
|
||||
*
|
||||
* 将BehaviorTreeAsset实例化为可运行的Entity树
|
||||
*/
|
||||
export class BehaviorTreeAssetLoader {
|
||||
/**
|
||||
* 从资产实例化行为树
|
||||
*
|
||||
* @param asset 行为树资产
|
||||
* @param scene 目标场景
|
||||
* @param options 实例化选项
|
||||
* @returns 根实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const asset = await loadAssetFromFile('enemy-ai.btree.bin');
|
||||
* const aiRoot = BehaviorTreeAssetLoader.instantiate(asset, scene);
|
||||
* BehaviorTreeStarter.start(aiRoot);
|
||||
* ```
|
||||
*/
|
||||
static instantiate(
|
||||
asset: BehaviorTreeAsset,
|
||||
scene: IScene,
|
||||
options: InstantiateOptions = {}
|
||||
): Entity {
|
||||
logger.info(`开始实例化行为树: ${asset.metadata.name}`);
|
||||
|
||||
// 创建节点映射
|
||||
const nodeMap = new Map<string, BehaviorTreeNodeData>();
|
||||
for (const node of asset.nodes) {
|
||||
nodeMap.set(node.id, node);
|
||||
}
|
||||
|
||||
// 查找根节点
|
||||
const rootNodeData = nodeMap.get(asset.rootNodeId);
|
||||
if (!rootNodeData) {
|
||||
throw new Error(`未找到根节点: ${asset.rootNodeId}`);
|
||||
}
|
||||
|
||||
// 创建实体映射
|
||||
const entityMap = new Map<string, Entity>();
|
||||
|
||||
// 递归创建实体树
|
||||
const rootEntity = this.createEntityTree(
|
||||
rootNodeData,
|
||||
nodeMap,
|
||||
entityMap,
|
||||
scene,
|
||||
options.namePrefix,
|
||||
options.asSubTree
|
||||
);
|
||||
|
||||
// 添加黑板
|
||||
this.setupBlackboard(rootEntity, asset.blackboard, options.blackboardOverrides);
|
||||
|
||||
// 设置属性绑定
|
||||
if (asset.propertyBindings && asset.propertyBindings.length > 0) {
|
||||
this.setupPropertyBindings(asset.propertyBindings, entityMap);
|
||||
}
|
||||
|
||||
logger.info(`行为树实例化完成: ${asset.nodes.length} 个节点`);
|
||||
|
||||
return rootEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归创建实体树
|
||||
*/
|
||||
private static createEntityTree(
|
||||
nodeData: BehaviorTreeNodeData,
|
||||
nodeMap: Map<string, BehaviorTreeNodeData>,
|
||||
entityMap: Map<string, Entity>,
|
||||
scene: IScene,
|
||||
namePrefix?: string,
|
||||
asSubTree?: boolean,
|
||||
isRootOfSubTree: boolean = true
|
||||
): Entity {
|
||||
const entityName = namePrefix ? `${namePrefix}_${nodeData.name}` : nodeData.name;
|
||||
const entity = scene.createEntity(entityName);
|
||||
|
||||
// 记录实体
|
||||
entityMap.set(nodeData.id, entity);
|
||||
|
||||
// 添加BehaviorTreeNode组件
|
||||
const btNode = entity.addComponent(new BehaviorTreeNode());
|
||||
btNode.nodeType = nodeData.nodeType;
|
||||
btNode.nodeName = nodeData.name;
|
||||
|
||||
// 添加节点特定组件(如果是子树的根节点,跳过 RootNode)
|
||||
this.addNodeComponents(entity, nodeData, asSubTree && isRootOfSubTree);
|
||||
|
||||
// 递归创建子节点
|
||||
for (const childId of nodeData.children) {
|
||||
const childData = nodeMap.get(childId);
|
||||
if (!childData) {
|
||||
logger.warn(`子节点未找到: ${childId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const childEntity = this.createEntityTree(
|
||||
childData,
|
||||
nodeMap,
|
||||
entityMap,
|
||||
scene,
|
||||
namePrefix,
|
||||
asSubTree,
|
||||
false // 子节点不是根节点
|
||||
);
|
||||
entity.addChild(childEntity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加节点特定组件
|
||||
* @param skipRootNode 是否跳过添加 RootNode 组件(用于子树)
|
||||
*/
|
||||
private static addNodeComponents(entity: Entity, nodeData: BehaviorTreeNodeData, skipRootNode: boolean = false): void {
|
||||
const { nodeType, data, name } = nodeData;
|
||||
|
||||
logger.debug(`addNodeComponents: name=${name}, data.nodeType=${data.nodeType}, skipRootNode=${skipRootNode}`);
|
||||
|
||||
// 根据节点类型和名称添加对应组件
|
||||
if (data.nodeType === 'root' || name === '根节点' || name === 'Root') {
|
||||
if (!skipRootNode) {
|
||||
logger.debug(`添加 RootNode 组件: ${name}`);
|
||||
entity.addComponent(new RootNode());
|
||||
} else {
|
||||
// 子树的根节点,使用第一个子节点的类型(通常是 SequenceNode)
|
||||
logger.debug(`跳过为子树根节点添加 RootNode: ${name}`);
|
||||
// 添加一个默认的 SequenceNode 作为子树的根
|
||||
this.addCompositeComponent(entity, '序列', data);
|
||||
}
|
||||
}
|
||||
// 组合节点
|
||||
else if (nodeType === NodeType.Composite) {
|
||||
this.addCompositeComponent(entity, name, data);
|
||||
}
|
||||
// 装饰器节点
|
||||
else if (nodeType === NodeType.Decorator) {
|
||||
this.addDecoratorComponent(entity, name, data);
|
||||
}
|
||||
// 动作节点
|
||||
else if (nodeType === NodeType.Action) {
|
||||
this.addActionComponent(entity, name, data);
|
||||
}
|
||||
// 条件节点
|
||||
else if (nodeType === NodeType.Condition) {
|
||||
this.addConditionComponent(entity, name, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加组合节点组件
|
||||
*/
|
||||
private static addCompositeComponent(entity: Entity, name: string, data: Record<string, any>): void {
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
if (nameLower.includes('sequence') || nameLower.includes('序列')) {
|
||||
const node = entity.addComponent(new SequenceNode());
|
||||
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
|
||||
} else if (nameLower.includes('selector') || nameLower.includes('选择')) {
|
||||
const node = entity.addComponent(new SelectorNode());
|
||||
node.abortType = (data.abortType as AbortType) ?? AbortType.None;
|
||||
} else if (nameLower.includes('parallelselector') || nameLower.includes('并行选择')) {
|
||||
const node = entity.addComponent(new ParallelSelectorNode());
|
||||
node.failurePolicy = data.failurePolicy ?? 'one';
|
||||
} else if (nameLower.includes('parallel') || nameLower.includes('并行')) {
|
||||
const node = entity.addComponent(new ParallelNode());
|
||||
node.successPolicy = data.successPolicy ?? 'all';
|
||||
node.failurePolicy = data.failurePolicy ?? 'one';
|
||||
} else if (nameLower.includes('randomsequence') || nameLower.includes('随机序列')) {
|
||||
entity.addComponent(new RandomSequenceNode());
|
||||
} else if (nameLower.includes('randomselector') || nameLower.includes('随机选择')) {
|
||||
entity.addComponent(new RandomSelectorNode());
|
||||
} else {
|
||||
logger.warn(`未知的组合节点类型: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加装饰器组件
|
||||
*/
|
||||
private static addDecoratorComponent(entity: Entity, name: string, data: Record<string, any>): void {
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
if (nameLower.includes('inverter') || nameLower.includes('反转')) {
|
||||
entity.addComponent(new InverterNode());
|
||||
} else if (nameLower.includes('repeater') || nameLower.includes('重复')) {
|
||||
const node = entity.addComponent(new RepeaterNode());
|
||||
node.repeatCount = data.repeatCount ?? -1;
|
||||
node.endOnFailure = data.endOnFailure ?? false;
|
||||
} else if (nameLower.includes('untilsuccess') || nameLower.includes('直到成功')) {
|
||||
entity.addComponent(new UntilSuccessNode());
|
||||
} else if (nameLower.includes('untilfail') || nameLower.includes('直到失败')) {
|
||||
entity.addComponent(new UntilFailNode());
|
||||
} else if (nameLower.includes('alwayssucceed') || nameLower.includes('总是成功')) {
|
||||
entity.addComponent(new AlwaysSucceedNode());
|
||||
} else if (nameLower.includes('alwaysfail') || nameLower.includes('总是失败')) {
|
||||
entity.addComponent(new AlwaysFailNode());
|
||||
} else if (nameLower.includes('conditional') || nameLower.includes('条件装饰')) {
|
||||
const node = entity.addComponent(new ConditionalNode());
|
||||
node.conditionCode = data.conditionCode ?? '';
|
||||
node.shouldReevaluate = data.shouldReevaluate ?? true;
|
||||
} else if (nameLower.includes('cooldown') || nameLower.includes('冷却')) {
|
||||
const node = entity.addComponent(new CooldownNode());
|
||||
node.cooldownTime = data.cooldownTime ?? 1.0;
|
||||
} else if (nameLower.includes('timeout') || nameLower.includes('超时')) {
|
||||
const node = entity.addComponent(new TimeoutNode());
|
||||
node.timeoutDuration = data.timeoutDuration ?? 1.0;
|
||||
} else {
|
||||
logger.warn(`未知的装饰器类型: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加动作组件
|
||||
*/
|
||||
private static addActionComponent(entity: Entity, name: string, data: Record<string, any>): void {
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
if (nameLower.includes('wait') || nameLower.includes('等待')) {
|
||||
const action = entity.addComponent(new WaitAction());
|
||||
action.waitTime = data.waitTime ?? 1.0;
|
||||
} else if (nameLower.includes('log') || nameLower.includes('日志')) {
|
||||
const action = entity.addComponent(new LogAction());
|
||||
action.message = data.message ?? '';
|
||||
action.level = data.level ?? 'log';
|
||||
} else if (nameLower.includes('setblackboard') || nameLower.includes('setvalue') || nameLower.includes('设置变量')) {
|
||||
const action = entity.addComponent(new SetBlackboardValueAction());
|
||||
action.variableName = data.variableName ?? '';
|
||||
action.value = data.value;
|
||||
} else if (nameLower.includes('modifyblackboard') || nameLower.includes('modifyvalue') || nameLower.includes('修改变量')) {
|
||||
const action = entity.addComponent(new ModifyBlackboardValueAction());
|
||||
action.variableName = data.variableName ?? '';
|
||||
action.operation = data.operation ?? 'add';
|
||||
action.operand = data.operand ?? 0;
|
||||
} else if (nameLower.includes('execute') || nameLower.includes('自定义')) {
|
||||
const action = entity.addComponent(new ExecuteAction());
|
||||
action.actionCode = data.actionCode ?? 'return TaskStatus.Success;';
|
||||
} else {
|
||||
logger.warn(`未知的动作类型: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加条件组件
|
||||
*/
|
||||
private static addConditionComponent(entity: Entity, name: string, data: Record<string, any>): void {
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
if (nameLower.includes('compare') || nameLower.includes('比较变量')) {
|
||||
const condition = entity.addComponent(new BlackboardCompareCondition());
|
||||
condition.variableName = data.variableName ?? '';
|
||||
condition.operator = (data.operator as CompareOperator) ?? CompareOperator.Equal;
|
||||
condition.compareValue = data.compareValue;
|
||||
condition.invertResult = data.invertResult ?? false;
|
||||
} else if (nameLower.includes('exists') || nameLower.includes('变量存在')) {
|
||||
const condition = entity.addComponent(new BlackboardExistsCondition());
|
||||
condition.variableName = data.variableName ?? '';
|
||||
condition.checkNotNull = data.checkNotNull ?? false;
|
||||
condition.invertResult = data.invertResult ?? false;
|
||||
} else if (nameLower.includes('random') || nameLower.includes('概率')) {
|
||||
const condition = entity.addComponent(new RandomProbabilityCondition());
|
||||
condition.probability = data.probability ?? 0.5;
|
||||
} else if (nameLower.includes('execute') || nameLower.includes('执行条件')) {
|
||||
const condition = entity.addComponent(new ExecuteCondition());
|
||||
condition.conditionCode = data.conditionCode ?? '';
|
||||
condition.invertResult = data.invertResult ?? false;
|
||||
} else {
|
||||
logger.warn(`未知的条件类型: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置黑板
|
||||
*/
|
||||
private static setupBlackboard(
|
||||
rootEntity: Entity,
|
||||
blackboardDef: BlackboardVariableDefinition[],
|
||||
overrides?: Record<string, any>
|
||||
): void {
|
||||
const blackboard = rootEntity.addComponent(new BlackboardComponent());
|
||||
|
||||
for (const variable of blackboardDef) {
|
||||
const value = overrides && overrides[variable.name] !== undefined
|
||||
? overrides[variable.name]
|
||||
: variable.defaultValue;
|
||||
|
||||
blackboard.defineVariable(
|
||||
variable.name,
|
||||
variable.type,
|
||||
value,
|
||||
{
|
||||
readonly: variable.readonly,
|
||||
description: variable.description
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`已设置黑板: ${blackboardDef.length} 个变量`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性绑定
|
||||
*/
|
||||
private static setupPropertyBindings(
|
||||
bindings: PropertyBinding[],
|
||||
entityMap: Map<string, Entity>
|
||||
): void {
|
||||
for (const binding of bindings) {
|
||||
const entity = entityMap.get(binding.nodeId);
|
||||
if (!entity) {
|
||||
logger.warn(`属性绑定引用的节点不存在: ${binding.nodeId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let propertyBindings = entity.getComponent(PropertyBindings);
|
||||
if (!propertyBindings) {
|
||||
propertyBindings = entity.addComponent(new PropertyBindings());
|
||||
}
|
||||
|
||||
propertyBindings.addBinding(binding.propertyName, binding.variableName);
|
||||
}
|
||||
|
||||
logger.info(`已设置属性绑定: ${bindings.length} 个绑定`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { createLogger, BinarySerializer } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTreeAsset } from './BehaviorTreeAsset';
|
||||
import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset';
|
||||
import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter';
|
||||
@@ -49,7 +48,7 @@ export interface DeserializationOptions {
|
||||
/**
|
||||
* 行为树资产序列化器
|
||||
*
|
||||
* 支持JSON和二进制(MessagePack)两种格式
|
||||
* 支持JSON和二进制两种格式
|
||||
*/
|
||||
export class BehaviorTreeAssetSerializer {
|
||||
/**
|
||||
@@ -110,11 +109,11 @@ export class BehaviorTreeAssetSerializer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为二进制格式(MessagePack)
|
||||
* 序列化为二进制格式
|
||||
*/
|
||||
private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array {
|
||||
try {
|
||||
const binary = encode(asset);
|
||||
const binary = BinarySerializer.encode(asset);
|
||||
logger.info(`已序列化为二进制: ${binary.length} 字节`);
|
||||
return binary;
|
||||
} catch (error) {
|
||||
@@ -208,7 +207,7 @@ export class BehaviorTreeAssetSerializer {
|
||||
*/
|
||||
private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset {
|
||||
try {
|
||||
const asset = decode(binary) as BehaviorTreeAsset;
|
||||
const asset = BinarySerializer.decode(binary) as BehaviorTreeAsset;
|
||||
logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`);
|
||||
return asset;
|
||||
} catch (error) {
|
||||
@@ -251,7 +250,7 @@ export class BehaviorTreeAssetSerializer {
|
||||
if (format === 'json') {
|
||||
asset = JSON.parse(data as string);
|
||||
} else {
|
||||
asset = decode(data as Uint8Array) as BehaviorTreeAsset;
|
||||
asset = BinarySerializer.decode(data as Uint8Array) as BehaviorTreeAsset;
|
||||
}
|
||||
|
||||
const size = typeof data === 'string' ? data.length : data.length;
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import { Entity, IScene, SceneSerializer, SerializedScene, SerializedEntity } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeNode } from '../Components/BehaviorTreeNode';
|
||||
|
||||
/**
|
||||
* 行为树持久化工具
|
||||
*
|
||||
* 使用框架的序列化系统进行二进制/JSON序列化
|
||||
*/
|
||||
export class BehaviorTreePersistence {
|
||||
/**
|
||||
* 序列化行为树(JSON格式)
|
||||
*
|
||||
* @param rootEntity 行为树根实体
|
||||
* @param pretty 是否格式化
|
||||
* @returns 序列化数据(JSON字符串或二进制)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const data = BehaviorTreePersistence.serialize(aiRoot);
|
||||
* ```
|
||||
*/
|
||||
static serialize(rootEntity: Entity, pretty: boolean = true): string | Uint8Array {
|
||||
if (!rootEntity.hasComponent(BehaviorTreeNode)) {
|
||||
throw new Error('Entity must have BehaviorTreeNode component');
|
||||
}
|
||||
|
||||
if (!rootEntity.scene) {
|
||||
throw new Error('Entity must be attached to a scene');
|
||||
}
|
||||
|
||||
// 使用 SceneSerializer,但只序列化这棵行为树
|
||||
// 创建一个临时场景包含只这个实体树
|
||||
return SceneSerializer.serialize(rootEntity.scene, {
|
||||
format: 'json',
|
||||
pretty: pretty,
|
||||
includeMetadata: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从序列化数据加载行为树
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param data 序列化数据(JSON字符串或二进制)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 从文件读取
|
||||
* const json = await readFile('behavior-tree.json');
|
||||
*
|
||||
* // 恢复行为树到场景
|
||||
* BehaviorTreePersistence.deserialize(scene, json);
|
||||
* ```
|
||||
*/
|
||||
static deserialize(scene: IScene, data: string | Uint8Array): void {
|
||||
SceneSerializer.deserialize(scene, data, {
|
||||
strategy: 'merge'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 JSON 字符串
|
||||
*
|
||||
* @param rootEntity 行为树根实体
|
||||
* @param pretty 是否格式化
|
||||
* @returns JSON 字符串
|
||||
*/
|
||||
static toJSON(rootEntity: Entity, pretty: boolean = true): string {
|
||||
const data = this.serialize(rootEntity, pretty);
|
||||
return JSON.stringify(data, null, pretty ? 2 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串加载
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param json JSON 字符串
|
||||
*/
|
||||
static fromJSON(scene: IScene, json: string): void {
|
||||
this.deserialize(scene, json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存到文件(需要 Tauri 环境)
|
||||
*
|
||||
* @param rootEntity 行为树根实体
|
||||
* @param filePath 文件路径
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await BehaviorTreePersistence.saveToFile(aiRoot, 'ai-behavior.json');
|
||||
* ```
|
||||
*/
|
||||
static async saveToFile(rootEntity: Entity, filePath: string): Promise<void> {
|
||||
const json = this.toJSON(rootEntity, true);
|
||||
|
||||
// 需要在 Tauri 环境中使用
|
||||
// const { writeTextFile } = await import('@tauri-apps/api/fs');
|
||||
// await writeTextFile(filePath, json);
|
||||
|
||||
throw new Error('saveToFile requires Tauri environment. Use toJSON() for manual saving.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载(需要 Tauri 环境)
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param filePath 文件路径
|
||||
* @returns 恢复的根实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const aiRoot = await BehaviorTreePersistence.loadFromFile(scene, 'ai-behavior.json');
|
||||
* ```
|
||||
*/
|
||||
static async loadFromFile(scene: IScene, filePath: string): Promise<Entity> {
|
||||
// 需要在 Tauri 环境中使用
|
||||
// const { readTextFile } = await import('@tauri-apps/api/fs');
|
||||
// const json = await readTextFile(filePath);
|
||||
// return this.fromJSON(scene, json);
|
||||
|
||||
throw new Error('loadFromFile requires Tauri environment. Use fromJSON() for manual loading.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否为有效的行为树数据
|
||||
*
|
||||
* @param data 序列化数据(字符串格式)
|
||||
* @returns 是否有效
|
||||
*/
|
||||
static validate(data: string): boolean {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as SerializedScene;
|
||||
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查必要字段
|
||||
if (!parsed.name ||
|
||||
typeof parsed.version !== 'number' ||
|
||||
!Array.isArray(parsed.entities) ||
|
||||
!Array.isArray(parsed.componentTypeRegistry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否至少有一个实体包含 BehaviorTreeNode 组件
|
||||
const hasBehaviorTreeNode = parsed.entities.some((entity: SerializedEntity) => {
|
||||
return entity.components.some(
|
||||
(comp: any) => comp.type === 'BehaviorTreeNode'
|
||||
);
|
||||
});
|
||||
|
||||
return hasBehaviorTreeNode;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆行为树
|
||||
*
|
||||
* @param scene 场景实例
|
||||
* @param rootEntity 要克隆的行为树根实体
|
||||
* @returns 克隆的新实体
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const clonedAI = BehaviorTreePersistence.clone(scene, originalAI);
|
||||
* ```
|
||||
*/
|
||||
static clone(scene: IScene, rootEntity: Entity): Entity {
|
||||
const data = this.serialize(rootEntity);
|
||||
const entityCountBefore = scene.entities.count;
|
||||
|
||||
this.deserialize(scene, data);
|
||||
|
||||
// 找到新添加的根实体(最后添加的实体)
|
||||
const entities = Array.from(scene.entities.buffer);
|
||||
for (let i = entities.length - 1; i >= entityCountBefore; i--) {
|
||||
const entity = entities[i];
|
||||
if (entity.hasComponent(BehaviorTreeNode) && !entity.parent) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to find cloned root entity');
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user