Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具 * refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统 * fix: 修复 CodeQL 警告并提升测试覆盖率 * refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题 * fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤 * docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明 * fix(ci): 修复 type-check 失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖 * fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖 * fix(ci): platform-web 添加缺失的 behavior-tree 依赖 * fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -69,6 +69,11 @@ jobs:
|
|||||||
cd packages/platform-common && pnpm run build
|
cd packages/platform-common && pnpm run build
|
||||||
cd ../asset-system && pnpm run build
|
cd ../asset-system && pnpm run build
|
||||||
cd ../components && pnpm run build
|
cd ../components && pnpm run build
|
||||||
|
cd ../editor-core && pnpm run build
|
||||||
|
cd ../ui && pnpm run build
|
||||||
|
cd ../editor-runtime && pnpm run build
|
||||||
|
cd ../behavior-tree && pnpm run build
|
||||||
|
cd ../tilemap && pnpm run build
|
||||||
|
|
||||||
- name: Build ecs-engine-bindgen
|
- name: Build ecs-engine-bindgen
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
31
.github/workflows/release-editor.yml
vendored
31
.github/workflows/release-editor.yml
vendored
@@ -96,16 +96,6 @@ jobs:
|
|||||||
cd packages/components
|
cd packages/components
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
- name: Build behavior-tree package
|
|
||||||
run: |
|
|
||||||
cd packages/behavior-tree
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build UI package
|
|
||||||
run: |
|
|
||||||
cd packages/ui
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第三层:Rust WASM 引擎 =====
|
# ===== 第三层:Rust WASM 引擎 =====
|
||||||
- name: Install wasm-pack
|
- name: Install wasm-pack
|
||||||
run: cargo install wasm-pack
|
run: cargo install wasm-pack
|
||||||
@@ -129,34 +119,35 @@ jobs:
|
|||||||
cd packages/ecs-engine-bindgen
|
cd packages/ecs-engine-bindgen
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
# ===== 第四层:依赖 asset-system 的包 =====
|
# ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 =====
|
||||||
- name: Build editor-core package
|
- name: Build editor-core package
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-core
|
cd packages/editor-core
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
|
# ===== 第五层:依赖 editor-core 的包 =====
|
||||||
|
- name: Build UI package
|
||||||
|
run: |
|
||||||
|
cd packages/ui
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
- name: Build tilemap package
|
- name: Build tilemap package
|
||||||
run: |
|
run: |
|
||||||
cd packages/tilemap
|
cd packages/tilemap
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
# ===== 第五层:依赖 editor-core 的包 =====
|
|
||||||
- name: Build editor-runtime package
|
- name: Build editor-runtime package
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-runtime
|
cd packages/editor-runtime
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
- name: Build UI editor package
|
# ===== 第六层:依赖 editor-runtime 的包 =====
|
||||||
|
- name: Build behavior-tree package
|
||||||
run: |
|
run: |
|
||||||
cd packages/ui-editor
|
cd packages/behavior-tree
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
- name: Build tilemap-editor package
|
# ===== 第七层:平台包(依赖 ui, tilemap) =====
|
||||||
run: |
|
|
||||||
cd packages/tilemap-editor
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第六层:平台包 =====
|
|
||||||
- name: Build platform-web package
|
- name: Build platform-web package
|
||||||
run: |
|
run: |
|
||||||
cd packages/platform-web
|
cd packages/platform-web
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ class MyService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 服务标识符(ServiceIdentifier)
|
||||||
|
|
||||||
|
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||||
|
|
||||||
|
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||||
|
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 方式1: 使用类作为标识符
|
||||||
|
Core.services.registerSingleton(DataService);
|
||||||
|
const data = Core.services.resolve(DataService);
|
||||||
|
|
||||||
|
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||||
|
const IFileSystem = Symbol.for('IFileSystem');
|
||||||
|
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||||
|
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||||
|
```
|
||||||
|
|
||||||
|
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||||
|
|
||||||
#### 生命周期
|
#### 生命周期
|
||||||
|
|
||||||
服务容器支持两种生命周期:
|
服务容器支持两种生命周期:
|
||||||
@@ -333,21 +353,20 @@ class GameService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### @Inject 装饰器
|
### @InjectProperty 装饰器
|
||||||
|
|
||||||
在构造函数中注入依赖:
|
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class PlayerService implements IService {
|
class PlayerService implements IService {
|
||||||
constructor(
|
@InjectProperty(DataService)
|
||||||
@Inject(DataService) private data: DataService,
|
private data!: DataService;
|
||||||
@Inject(GameService) private game: GameService
|
|
||||||
) {
|
@InjectProperty(GameService)
|
||||||
// data 和 game 会自动从容器中解析
|
private game!: GameService;
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
// 清理资源
|
// 清理资源
|
||||||
@@ -355,6 +374,35 @@ class PlayerService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
在 EntitySystem 中使用属性注入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
class CombatSystem extends EntitySystem {
|
||||||
|
@InjectProperty(TimeService)
|
||||||
|
private timeService!: TimeService;
|
||||||
|
|
||||||
|
@InjectProperty(AudioService)
|
||||||
|
private audio!: AudioService;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Health, Attack));
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitialize(): void {
|
||||||
|
// 此时属性已注入完成,可以安全使用
|
||||||
|
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
processEntity(entity: Entity): void {
|
||||||
|
// 使用注入的服务
|
||||||
|
this.audio.playSound('attack');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||||
|
|
||||||
### 注册可注入服务
|
### 注册可注入服务
|
||||||
|
|
||||||
使用 `registerInjectable` 自动处理依赖注入:
|
使用 `registerInjectable` 自动处理依赖注入:
|
||||||
@@ -362,10 +410,10 @@ class PlayerService implements IService {
|
|||||||
```typescript
|
```typescript
|
||||||
import { registerInjectable } from '@esengine/ecs-framework';
|
import { registerInjectable } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 注册服务(会自动解析@Inject依赖)
|
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||||
registerInjectable(Core.services, PlayerService);
|
registerInjectable(Core.services, PlayerService);
|
||||||
|
|
||||||
// 解析时会自动注入依赖
|
// 解析时会自动注入属性依赖
|
||||||
const player = Core.services.resolve(PlayerService);
|
const player = Core.services.resolve(PlayerService);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -493,22 +541,164 @@ registerInjectable(Core.services, NetworkService);
|
|||||||
|
|
||||||
## 高级用法
|
## 高级用法
|
||||||
|
|
||||||
### 服务替换(测试)
|
### 接口与 Symbol 标识符模式
|
||||||
|
|
||||||
在测试中替换真实服务为模拟服务:
|
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||||
|
|
||||||
|
#### 为什么使用 Symbol.for
|
||||||
|
|
||||||
|
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||||
|
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||||
|
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||||
|
|
||||||
|
#### 定义接口和标识符
|
||||||
|
|
||||||
|
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 测试代码
|
// IAudioService.ts - 定义接口和标识符
|
||||||
class MockDataService implements IService {
|
export interface IAudioService {
|
||||||
getData(key: string) {
|
dispose(): void;
|
||||||
return 'mock data';
|
playSound(id: string): void;
|
||||||
|
playMusic(id: string, loop?: boolean): void;
|
||||||
|
stopMusic(): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
|
preload(id: string, url: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {}
|
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||||
|
export const IAudioService = Symbol.for('IAudioService');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实现接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WebAudioService.ts - Web 平台实现
|
||||||
|
import { IAudioService } from './IAudioService';
|
||||||
|
|
||||||
|
export class WebAudioService implements IAudioService {
|
||||||
|
private audioContext: AudioContext;
|
||||||
|
private sounds: Map<string, AudioBuffer> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册模拟服务(用于测试)
|
playSound(id: string): void {
|
||||||
Core.services.registerInstance(DataService, new MockDataService());
|
const buffer = this.sounds.get(id);
|
||||||
|
if (buffer) {
|
||||||
|
const source = this.audioContext.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(this.audioContext.destination);
|
||||||
|
source.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async preload(id: string, url: string): Promise<void> {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
this.sounds.set(id, audioBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他方法实现
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.audioContext.close();
|
||||||
|
this.sounds.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WechatAudioService.ts - 微信小游戏平台实现
|
||||||
|
export class WechatAudioService implements IAudioService {
|
||||||
|
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||||
|
|
||||||
|
playSound(id: string): void {
|
||||||
|
const ctx = this.innerAudioContexts.get(id);
|
||||||
|
if (ctx) {
|
||||||
|
ctx.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async preload(id: string, url: string): Promise<void> {
|
||||||
|
const ctx = wx.createInnerAudioContext();
|
||||||
|
ctx.src = url;
|
||||||
|
this.innerAudioContexts.set(id, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其他方法实现
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
for (const ctx of this.innerAudioContexts.values()) {
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
this.innerAudioContexts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注册和使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IAudioService } from './IAudioService';
|
||||||
|
import { WebAudioService } from './WebAudioService';
|
||||||
|
import { WechatAudioService } from './WechatAudioService';
|
||||||
|
|
||||||
|
// 根据平台注册不同实现
|
||||||
|
if (typeof wx !== 'undefined') {
|
||||||
|
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||||
|
} else {
|
||||||
|
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务代码中使用 - 不关心具体实现
|
||||||
|
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||||
|
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||||
|
audio.playSound('explosion');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 跨模块使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在游戏系统中使用
|
||||||
|
import { IAudioService } from '@mygame/core';
|
||||||
|
|
||||||
|
class CombatSystem extends EntitySystem {
|
||||||
|
private audio: IAudioService;
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
// 获取音频服务,不需要知道具体实现
|
||||||
|
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntityDeath(entity: Entity): void {
|
||||||
|
this.audio.playSound('death');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Symbol vs Symbol.for
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Symbol() - 每次创建唯一的 Symbol
|
||||||
|
const sym1 = Symbol('test');
|
||||||
|
const sym2 = Symbol('test');
|
||||||
|
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||||
|
|
||||||
|
// Symbol.for() - 在全局注册表中共享
|
||||||
|
const sym3 = Symbol.for('test');
|
||||||
|
const sym4 = Symbol.for('test');
|
||||||
|
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||||
|
|
||||||
|
// 跨包场景
|
||||||
|
// package-a/index.ts
|
||||||
|
export const IMyService = Symbol.for('IMyService');
|
||||||
|
|
||||||
|
// package-b/index.ts (不同的包)
|
||||||
|
const IMyService = Symbol.for('IMyService');
|
||||||
|
// 与 package-a 中的是同一个 Symbol!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 循环依赖检测
|
### 循环依赖检测
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@esengine/behavior-tree-editor",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Behavior Tree Editor Plugin for ECS Framework",
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.esm.js",
|
|
||||||
"module": "dist/index.esm.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
|
||||||
"prebuild": "npm run clean",
|
|
||||||
"build": "npm run build:tsc && npm run copy:css && npm run build:rollup",
|
|
||||||
"build:tsc": "tsc",
|
|
||||||
"copy:css": "node scripts/copy-css.js",
|
|
||||||
"build:rollup": "rollup -c",
|
|
||||||
"dev": "rollup -c -w"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"ecs",
|
|
||||||
"behavior-tree",
|
|
||||||
"editor",
|
|
||||||
"plugin"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@esengine/behavior-tree": "workspace:*",
|
|
||||||
"@esengine/editor-runtime": "workspace:*",
|
|
||||||
"@rollup/plugin-commonjs": "^28.0.1",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
||||||
"@rollup/plugin-replace": "^6.0.3",
|
|
||||||
"@types/react": "^18.3.18",
|
|
||||||
"rimraf": "^6.0.1",
|
|
||||||
"rollup": "^4.28.1",
|
|
||||||
"rollup-plugin-copy": "^3.5.0",
|
|
||||||
"rollup-plugin-dts": "^6.1.1",
|
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
|
||||||
"typescript": "^5.8.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/behavior-tree": "*",
|
|
||||||
"@esengine/editor-runtime": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mobx": "^6.15.0",
|
|
||||||
"mobx-react-lite": "^4.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2039
packages/behavior-tree-editor/pnpm-lock.yaml
generated
2039
packages/behavior-tree-editor/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
|||||||
const resolve = require('@rollup/plugin-node-resolve');
|
|
||||||
const commonjs = require('@rollup/plugin-commonjs');
|
|
||||||
const replace = require('@rollup/plugin-replace');
|
|
||||||
const dts = require('rollup-plugin-dts').default;
|
|
||||||
const postcss = require('rollup-plugin-postcss');
|
|
||||||
|
|
||||||
const external = [
|
|
||||||
'@esengine/editor-runtime',
|
|
||||||
'@esengine/behavior-tree',
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
input: 'bin/index.js',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.esm.js',
|
|
||||||
format: 'es',
|
|
||||||
sourcemap: true,
|
|
||||||
exports: 'named',
|
|
||||||
inlineDynamicImports: true
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
replace({
|
|
||||||
preventAssignment: true,
|
|
||||||
'process.env.NODE_ENV': JSON.stringify('production')
|
|
||||||
}),
|
|
||||||
resolve({
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
}),
|
|
||||||
postcss({
|
|
||||||
inject: true,
|
|
||||||
minimize: false
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
],
|
|
||||||
external,
|
|
||||||
onwarn(warning, warn) {
|
|
||||||
if (warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'THIS_IS_UNDEFINED') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
warn(warning);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 类型定义构建
|
|
||||||
{
|
|
||||||
input: 'bin/index.d.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.d.ts',
|
|
||||||
format: 'es'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
dts({
|
|
||||||
respectExternal: true
|
|
||||||
})
|
|
||||||
],
|
|
||||||
external: [
|
|
||||||
...external,
|
|
||||||
/\.css$/,
|
|
||||||
// 排除 React 相关类型,避免 rollup-plugin-dts 解析问题
|
|
||||||
'react',
|
|
||||||
'react-dom',
|
|
||||||
/^@types\//,
|
|
||||||
/^@esengine\//
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { readdirSync, statSync, copyFileSync, mkdirSync } from 'fs';
|
|
||||||
import { join, dirname, relative } from 'path';
|
|
||||||
|
|
||||||
function copyCSS(srcDir, destDir) {
|
|
||||||
const files = readdirSync(srcDir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const srcPath = join(srcDir, file);
|
|
||||||
const stat = statSync(srcPath);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
copyCSS(srcPath, destDir);
|
|
||||||
} else if (file.endsWith('.css')) {
|
|
||||||
const relativePath = relative('src', srcPath);
|
|
||||||
const destPath = join(destDir, relativePath);
|
|
||||||
|
|
||||||
mkdirSync(dirname(destPath), { recursive: true });
|
|
||||||
copyFileSync(srcPath, destPath);
|
|
||||||
console.log(`Copied: ${relativePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCSS('src', 'bin');
|
|
||||||
console.log('CSS files copied successfully!');
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { singleton } from 'tsyringe';
|
|
||||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
|
||||||
import { CompilerRegistry, IEditorModule, IModuleContext, PanelPosition } from '@esengine/editor-core';
|
|
||||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
|
||||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
|
||||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
|
||||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreeModule');
|
|
||||||
|
|
||||||
@singleton()
|
|
||||||
export class BehaviorTreeModule implements IEditorModule {
|
|
||||||
readonly id = 'behavior-tree';
|
|
||||||
readonly name = 'Behavior Tree Editor';
|
|
||||||
readonly version = '1.0.0';
|
|
||||||
|
|
||||||
async load(context: IModuleContext): Promise<void> {
|
|
||||||
logger.info('[BehaviorTreeModule] Loading behavior tree editor module...');
|
|
||||||
|
|
||||||
this.registerServices(context);
|
|
||||||
this.registerCompilers();
|
|
||||||
this.registerInspectors(context);
|
|
||||||
this.registerCommands(context);
|
|
||||||
this.registerPanels(context);
|
|
||||||
this.subscribeEvents(context);
|
|
||||||
|
|
||||||
logger.info('[BehaviorTreeModule] Behavior tree editor module loaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerServices(context: IModuleContext): void {
|
|
||||||
context.container.register(BehaviorTreeService, { useClass: BehaviorTreeService });
|
|
||||||
logger.info('[BehaviorTreeModule] Services registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCompilers(): void {
|
|
||||||
const compilerRegistry = Core.services.resolve(CompilerRegistry);
|
|
||||||
if (compilerRegistry) {
|
|
||||||
const compiler = new BehaviorTreeCompiler();
|
|
||||||
compilerRegistry.register(compiler);
|
|
||||||
logger.info('[BehaviorTreeModule] Compiler registered');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerInspectors(context: IModuleContext): void {
|
|
||||||
const provider = new BehaviorTreeNodeInspectorProvider();
|
|
||||||
context.inspectorRegistry.register(provider);
|
|
||||||
logger.info('[BehaviorTreeModule] Inspector provider registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
async unload(): Promise<void> {
|
|
||||||
logger.info('[BehaviorTreeModule] Unloading behavior tree editor module...');
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCommands(context: IModuleContext): void {
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.new',
|
|
||||||
label: 'New Behavior Tree',
|
|
||||||
icon: 'file-plus',
|
|
||||||
execute: async () => {
|
|
||||||
const service = context.container.resolve(BehaviorTreeService);
|
|
||||||
await service.createNew();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.open',
|
|
||||||
label: 'Open Behavior Tree',
|
|
||||||
icon: 'folder-open',
|
|
||||||
execute: async () => {
|
|
||||||
logger.info('Open behavior tree');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.commands.register({
|
|
||||||
id: 'behavior-tree.save',
|
|
||||||
label: 'Save Behavior Tree',
|
|
||||||
icon: 'save',
|
|
||||||
keybinding: { key: 'S', ctrl: true },
|
|
||||||
execute: async () => {
|
|
||||||
logger.info('Save behavior tree');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerPanels(context: IModuleContext): void {
|
|
||||||
logger.info('[BehaviorTreeModule] Registering panels...');
|
|
||||||
|
|
||||||
context.panels.register({
|
|
||||||
id: 'behavior-tree-editor',
|
|
||||||
title: '行为树编辑器',
|
|
||||||
icon: 'GitBranch',
|
|
||||||
component: BehaviorTreeEditorPanel,
|
|
||||||
position: PanelPosition.Center,
|
|
||||||
defaultSize: 400,
|
|
||||||
closable: true,
|
|
||||||
isDynamic: true
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('[BehaviorTreeModule] Panel registered: behavior-tree-editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribeEvents(_context: IModuleContext): void {
|
|
||||||
// 文件加载由 BehaviorTreeEditorPanel 处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
type Core,
|
|
||||||
type ServiceContainer,
|
|
||||||
type IService,
|
|
||||||
type ServiceType,
|
|
||||||
type IEditorPlugin,
|
|
||||||
EditorPluginCategory,
|
|
||||||
CompilerRegistry,
|
|
||||||
ICompilerRegistry,
|
|
||||||
InspectorRegistry,
|
|
||||||
IInspectorRegistry,
|
|
||||||
PanelPosition,
|
|
||||||
type FileCreationTemplate,
|
|
||||||
type FileActionHandler,
|
|
||||||
type PanelDescriptor,
|
|
||||||
createElement,
|
|
||||||
Icons,
|
|
||||||
createLogger,
|
|
||||||
} from '@esengine/editor-runtime';
|
|
||||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
|
||||||
import { FileSystemService } from './services/FileSystemService';
|
|
||||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
|
||||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
|
||||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
|
||||||
import { useBehaviorTreeDataStore } from './stores';
|
|
||||||
import { createRootNode } from './domain/constants/RootNode';
|
|
||||||
import { PluginContext } from './PluginContext';
|
|
||||||
|
|
||||||
const { GitBranch } = Icons;
|
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreePlugin');
|
|
||||||
|
|
||||||
export class BehaviorTreePlugin implements IEditorPlugin {
|
|
||||||
readonly name = '@esengine/behavior-tree-editor';
|
|
||||||
readonly version = '1.0.0';
|
|
||||||
readonly displayName = 'Behavior Tree Editor';
|
|
||||||
readonly category = EditorPluginCategory.Tool;
|
|
||||||
readonly description = 'Visual behavior tree editor for game AI development';
|
|
||||||
readonly icon = 'GitBranch';
|
|
||||||
|
|
||||||
private services?: ServiceContainer;
|
|
||||||
private registeredServices: Set<ServiceType<IService>> = new Set();
|
|
||||||
private fileActionHandler?: FileActionHandler;
|
|
||||||
private fileCreationTemplate?: FileCreationTemplate;
|
|
||||||
|
|
||||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
|
||||||
this.services = services;
|
|
||||||
// 设置插件上下文,让内部服务可以访问服务容器
|
|
||||||
PluginContext.setServices(services);
|
|
||||||
this.registerServices(services);
|
|
||||||
this.registerCompilers(services);
|
|
||||||
this.registerInspectors(services);
|
|
||||||
this.registerFileActions(services);
|
|
||||||
}
|
|
||||||
|
|
||||||
async uninstall(): Promise<void> {
|
|
||||||
if (this.services) {
|
|
||||||
for (const serviceType of this.registeredServices) {
|
|
||||||
this.services.unregister(serviceType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.registeredServices.clear();
|
|
||||||
useBehaviorTreeDataStore.getState().reset();
|
|
||||||
PluginContext.clear();
|
|
||||||
this.services = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerPanels(): PanelDescriptor[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'behavior-tree-editor',
|
|
||||||
title: 'Behavior Tree Editor',
|
|
||||||
position: PanelPosition.Center,
|
|
||||||
closable: true,
|
|
||||||
component: BehaviorTreeEditorPanel,
|
|
||||||
order: 100,
|
|
||||||
isDynamic: true // 标记为动态面板
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerServices(services: ServiceContainer): void {
|
|
||||||
// 先注册 FileSystemService(BehaviorTreeService 依赖它)
|
|
||||||
if (services.isRegistered(FileSystemService)) {
|
|
||||||
services.unregister(FileSystemService);
|
|
||||||
}
|
|
||||||
services.registerSingleton(FileSystemService);
|
|
||||||
this.registeredServices.add(FileSystemService);
|
|
||||||
|
|
||||||
// 再注册 BehaviorTreeService
|
|
||||||
if (services.isRegistered(BehaviorTreeService)) {
|
|
||||||
services.unregister(BehaviorTreeService);
|
|
||||||
}
|
|
||||||
services.registerSingleton(BehaviorTreeService);
|
|
||||||
this.registeredServices.add(BehaviorTreeService);
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerCompilers(services: ServiceContainer): void {
|
|
||||||
try {
|
|
||||||
const compilerRegistry = services.resolve<CompilerRegistry>(ICompilerRegistry);
|
|
||||||
const compiler = new BehaviorTreeCompiler();
|
|
||||||
compilerRegistry.register(compiler);
|
|
||||||
logger.info('Successfully registered BehaviorTreeCompiler');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to register compiler:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerInspectors(services: ServiceContainer): void {
|
|
||||||
try {
|
|
||||||
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
|
|
||||||
if (inspectorRegistry) {
|
|
||||||
const provider = new BehaviorTreeNodeInspectorProvider();
|
|
||||||
inspectorRegistry.register(provider);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to register inspector:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerFileActions(services: ServiceContainer): void {
|
|
||||||
this.fileCreationTemplate = {
|
|
||||||
label: 'Behavior Tree',
|
|
||||||
extension: 'btree',
|
|
||||||
defaultFileName: 'NewBehaviorTree',
|
|
||||||
icon: createElement(GitBranch, { size: 16 }),
|
|
||||||
createContent: (fileName: string) => {
|
|
||||||
// 创建根节点
|
|
||||||
const rootNode = createRootNode();
|
|
||||||
const rootNodeData = {
|
|
||||||
id: rootNode.id,
|
|
||||||
type: rootNode.template.type,
|
|
||||||
displayName: rootNode.template.displayName,
|
|
||||||
data: rootNode.data,
|
|
||||||
position: {
|
|
||||||
x: rootNode.position.x,
|
|
||||||
y: rootNode.position.y
|
|
||||||
},
|
|
||||||
children: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyTree = {
|
|
||||||
name: fileName.replace('.btree', ''),
|
|
||||||
nodes: [rootNodeData],
|
|
||||||
connections: [],
|
|
||||||
variables: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify(emptyTree, null, 2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fileActionHandler = {
|
|
||||||
extensions: ['btree'],
|
|
||||||
onDoubleClick: async (filePath: string) => {
|
|
||||||
const service = services.resolve(BehaviorTreeService);
|
|
||||||
if (service) {
|
|
||||||
await service.loadFromFile(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFileActionHandlers(): FileActionHandler[] {
|
|
||||||
return this.fileActionHandler ? [this.fileActionHandler] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFileCreationTemplates(): FileCreationTemplate[] {
|
|
||||||
return this.fileCreationTemplate ? [this.fileCreationTemplate] : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { BehaviorTreePlugin } from './BehaviorTreePlugin';
|
|
||||||
|
|
||||||
export default new BehaviorTreePlugin();
|
|
||||||
|
|
||||||
export { BehaviorTreePlugin } from './BehaviorTreePlugin';
|
|
||||||
export { PluginContext } from './PluginContext';
|
|
||||||
export { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
|
||||||
export * from './BehaviorTreeModule';
|
|
||||||
export * from './services/BehaviorTreeService';
|
|
||||||
export * from './providers/BehaviorTreeNodeInspectorProvider';
|
|
||||||
|
|
||||||
export * from './domain';
|
|
||||||
export * from './application/commands/tree';
|
|
||||||
export * from './application/use-cases';
|
|
||||||
export * from './application/services/BlackboardManager';
|
|
||||||
export * from './application/services/ExecutionController';
|
|
||||||
export * from './application/services/GlobalBlackboardService';
|
|
||||||
export * from './application/interfaces/IExecutionHooks';
|
|
||||||
export * from './application/state/BehaviorTreeDataStore';
|
|
||||||
export * from './hooks';
|
|
||||||
export * from './stores';
|
|
||||||
// Re-export specific items to avoid conflicts
|
|
||||||
export {
|
|
||||||
EditorConfig
|
|
||||||
} from './types';
|
|
||||||
export * from './infrastructure/factories/NodeFactory';
|
|
||||||
export * from './infrastructure/serialization/BehaviorTreeSerializer';
|
|
||||||
export * from './infrastructure/validation/BehaviorTreeValidator';
|
|
||||||
export * from './infrastructure/events/EditorEventBus';
|
|
||||||
export * from './infrastructure/services/NodeRegistryService';
|
|
||||||
export * from './utils/BehaviorTreeExecutor';
|
|
||||||
export * from './utils/DOMCache';
|
|
||||||
export * from './utils/portUtils';
|
|
||||||
export * from './utils/RuntimeLoader';
|
|
||||||
export * from './compiler/BehaviorTreeCompiler';
|
|
||||||
// Export everything except DEFAULT_EDITOR_CONFIG from editorConstants
|
|
||||||
export {
|
|
||||||
ICON_MAP,
|
|
||||||
ROOT_NODE_TEMPLATE,
|
|
||||||
DEFAULT_EDITOR_CONFIG
|
|
||||||
} from './config/editorConstants';
|
|
||||||
export * from './interfaces/IEditorExtensions';
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ES2020",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"downlevelIteration": true,
|
|
||||||
"outDir": "./bin",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"composite": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"lib": ["ES2020", "DOM"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "bin"],
|
|
||||||
"references": [
|
|
||||||
{ "path": "../core" },
|
|
||||||
{ "path": "../editor-core" },
|
|
||||||
{ "path": "../behavior-tree" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/behavior-tree",
|
"name": "@esengine/behavior-tree",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "完全ECS化的行为树系统,基于组件和实体的行为树实现",
|
"description": "ECS-based AI behavior tree system with visual editor and runtime execution",
|
||||||
"main": "bin/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./bin/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./bin/index.js",
|
"import": "./dist/index.js"
|
||||||
"development": {
|
},
|
||||||
"types": "./src/index.ts",
|
"./runtime": {
|
||||||
"import": "./src/index.ts"
|
"types": "./dist/runtime.d.ts",
|
||||||
}
|
"import": "./dist/runtime.js"
|
||||||
}
|
},
|
||||||
|
"./editor": {
|
||||||
|
"types": "./dist/editor/index.d.ts",
|
||||||
|
"import": "./dist/editor/index.js"
|
||||||
|
},
|
||||||
|
"./plugin.json": "./plugin.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin/**/*"
|
"dist",
|
||||||
|
"plugin.json"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
@@ -25,40 +33,52 @@
|
|||||||
"entity-component-system"
|
"entity-component-system"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||||
"build:ts": "tsc",
|
"build": "vite build",
|
||||||
"prebuild": "npm run clean",
|
"build:watch": "vite build --watch",
|
||||||
"build": "npm run build:ts",
|
"type-check": "tsc --noEmit",
|
||||||
"build:esm": "vite build",
|
|
||||||
"build:watch": "tsc --watch",
|
|
||||||
"rebuild": "npm run clean && npm run build",
|
|
||||||
"build:npm": "npm run build && node build-rollup.cjs",
|
|
||||||
"test": "jest --config jest.config.cjs",
|
"test": "jest --config jest.config.cjs",
|
||||||
"test:watch": "jest --watch --config jest.config.cjs"
|
"test:watch": "jest --watch --config jest.config.cjs"
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@esengine/ecs-framework": "^2.2.8"
|
"@esengine/ecs-framework": ">=2.0.0",
|
||||||
|
"@esengine/ecs-components": "workspace:*",
|
||||||
|
"@esengine/editor-runtime": "workspace:*",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"zustand": "^4.5.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@esengine/ecs-components": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esengine/editor-runtime": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lucide-react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zustand": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.28.3",
|
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||||
"@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/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.17",
|
"@types/node": "^20.19.17",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.0.7"
|
"vite": "^6.0.7",
|
||||||
|
"vite-plugin-dts": "^3.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
|
|||||||
30
packages/behavior-tree/plugin.json
Normal file
30
packages/behavior-tree/plugin.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"id": "@esengine/behavior-tree",
|
||||||
|
"name": "Behavior Tree System",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AI behavior tree system with visual editor and runtime execution",
|
||||||
|
"category": "ai",
|
||||||
|
"loadingPhase": "default",
|
||||||
|
"enabledByDefault": true,
|
||||||
|
"canContainContent": false,
|
||||||
|
"isEnginePlugin": false,
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"name": "BehaviorTreeRuntime",
|
||||||
|
"type": "runtime",
|
||||||
|
"entry": "./src/index.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BehaviorTreeEditor",
|
||||||
|
"type": "editor",
|
||||||
|
"entry": "./src/editor/index.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"id": "@esengine/core",
|
||||||
|
"version": ">=1.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "GitBranch"
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BehaviorTreeData, BehaviorNodeData } from './Runtime/BehaviorTreeData';
|
import { BehaviorTreeData, BehaviorNodeData } from './execution/BehaviorTreeData';
|
||||||
import { NodeType } from './Types/TaskStatus';
|
import { NodeType } from './Types/TaskStatus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import type { Core } from '@esengine/ecs-framework';
|
|
||||||
import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
|
|
||||||
import { WorldManager } from '@esengine/ecs-framework';
|
|
||||||
import { BehaviorTreeExecutionSystem } from './Runtime/BehaviorTreeExecutionSystem';
|
|
||||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
|
||||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 行为树插件
|
|
||||||
*
|
|
||||||
* 提供便捷方法向场景添加行为树系统
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const core = Core.create();
|
|
||||||
* const plugin = new BehaviorTreePlugin();
|
|
||||||
* await core.pluginManager.install(plugin);
|
|
||||||
*
|
|
||||||
* // 为场景添加行为树系统
|
|
||||||
* const scene = new Scene();
|
|
||||||
* plugin.setupScene(scene);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export class BehaviorTreePlugin implements IPlugin {
|
|
||||||
readonly name = '@esengine/behavior-tree';
|
|
||||||
readonly version = '1.0.0';
|
|
||||||
|
|
||||||
private worldManager: WorldManager | null = null;
|
|
||||||
private services: ServiceContainer | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安装插件
|
|
||||||
*/
|
|
||||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
|
||||||
this.services = services;
|
|
||||||
|
|
||||||
// 注册全局服务
|
|
||||||
services.registerSingleton(GlobalBlackboardService);
|
|
||||||
services.registerSingleton(BehaviorTreeAssetManager);
|
|
||||||
|
|
||||||
this.worldManager = services.resolve(WorldManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 卸载插件
|
|
||||||
*/
|
|
||||||
async uninstall(): Promise<void> {
|
|
||||||
if (this.services) {
|
|
||||||
this.services.unregister(GlobalBlackboardService);
|
|
||||||
this.services.unregister(BehaviorTreeAssetManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.worldManager = null;
|
|
||||||
this.services = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 为场景设置行为树系统
|
|
||||||
*
|
|
||||||
* 向场景添加行为树执行系统
|
|
||||||
*
|
|
||||||
* @param scene 目标场景
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const scene = new Scene();
|
|
||||||
* behaviorTreePlugin.setupScene(scene);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public setupScene(scene: IScene): void {
|
|
||||||
scene.addSystem(new BehaviorTreeExecutionSystem());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 为所有现有场景设置行为树系统
|
|
||||||
*/
|
|
||||||
public setupAllScenes(): void {
|
|
||||||
if (!this.worldManager) {
|
|
||||||
throw new Error('Plugin not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const worlds = this.worldManager.getAllWorlds();
|
|
||||||
for (const world of worlds) {
|
|
||||||
for (const scene of world.getAllScenes()) {
|
|
||||||
this.setupScene(scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts
Normal file
43
packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Behavior Tree Runtime Module (Pure runtime, no editor dependencies)
|
||||||
|
* 行为树运行时模块(纯运行时,无编辑器依赖)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||||
|
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||||
|
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
||||||
|
|
||||||
|
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||||
|
import { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
|
||||||
|
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
||||||
|
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior Tree Runtime Module
|
||||||
|
* 行为树运行时模块
|
||||||
|
*/
|
||||||
|
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||||
|
registerComponents(registry: typeof ComponentRegistry): void {
|
||||||
|
registry.register(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerServices(services: ServiceContainer): void {
|
||||||
|
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||||
|
services.registerSingleton(GlobalBlackboardService);
|
||||||
|
}
|
||||||
|
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||||
|
services.registerSingleton(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSystems(scene: IScene, context: SystemContext): void {
|
||||||
|
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||||
|
|
||||||
|
if (context.isEditor) {
|
||||||
|
behaviorTreeSystem.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.addSystem(behaviorTreeSystem);
|
||||||
|
context.behaviorTreeSystem = behaviorTreeSystem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Entity, Core } from '@esengine/ecs-framework';
|
import { Entity, Core } from '@esengine/ecs-framework';
|
||||||
import { BehaviorTreeData } from './Runtime/BehaviorTreeData';
|
import { BehaviorTreeData } from './execution/BehaviorTreeData';
|
||||||
import { BehaviorTreeRuntimeComponent } from './Runtime/BehaviorTreeRuntimeComponent';
|
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 行为树启动辅助类
|
* 行为树启动辅助类
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BehaviorTreeData, BehaviorNodeData } from '../Runtime/BehaviorTreeData';
|
import { BehaviorTreeData, BehaviorNodeData } from '../execution/BehaviorTreeData';
|
||||||
import { NodeType, AbortType } from '../Types/TaskStatus';
|
import { NodeType, AbortType } from '../Types/TaskStatus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NodeType } from '../Types/TaskStatus';
|
import { NodeType } from '../Types/TaskStatus';
|
||||||
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../Runtime/NodeMetadata';
|
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点数据JSON格式
|
* 节点数据JSON格式
|
||||||
|
|||||||
98
packages/behavior-tree/src/editor/BehaviorTreePlugin.ts
Normal file
98
packages/behavior-tree/src/editor/BehaviorTreePlugin.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Behavior Tree Unified Plugin
|
||||||
|
* 行为树统一插件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||||
|
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
||||||
|
import type {
|
||||||
|
IPluginLoader,
|
||||||
|
IRuntimeModuleLoader,
|
||||||
|
PluginDescriptor,
|
||||||
|
SystemContext
|
||||||
|
} from '@esengine/editor-runtime';
|
||||||
|
|
||||||
|
// Runtime imports
|
||||||
|
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
|
||||||
|
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
|
||||||
|
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
||||||
|
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件描述符
|
||||||
|
*/
|
||||||
|
export const descriptor: PluginDescriptor = {
|
||||||
|
id: '@esengine/behavior-tree',
|
||||||
|
name: 'Behavior Tree System',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
|
||||||
|
category: 'ai',
|
||||||
|
enabledByDefault: true,
|
||||||
|
canContainContent: false,
|
||||||
|
isEnginePlugin: false,
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: 'BehaviorTreeRuntime',
|
||||||
|
type: 'runtime',
|
||||||
|
loadingPhase: 'default',
|
||||||
|
entry: './src/index.ts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BehaviorTreeEditor',
|
||||||
|
type: 'editor',
|
||||||
|
loadingPhase: 'default',
|
||||||
|
entry: './src/editor/index.ts'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
{ id: '@esengine/core', version: '>=1.0.0' }
|
||||||
|
],
|
||||||
|
icon: 'GitBranch'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior Tree Runtime Module
|
||||||
|
* 行为树运行时模块
|
||||||
|
*/
|
||||||
|
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
||||||
|
registerComponents(registry: typeof ComponentRegistry): void {
|
||||||
|
registry.register(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerServices(services: ServiceContainer): void {
|
||||||
|
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||||
|
services.registerSingleton(GlobalBlackboardService);
|
||||||
|
}
|
||||||
|
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||||
|
services.registerSingleton(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSystems(scene: IScene, context: SystemContext): void {
|
||||||
|
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
||||||
|
|
||||||
|
// 编辑器模式下默认禁用
|
||||||
|
if (context.isEditor) {
|
||||||
|
behaviorTreeSystem.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.addSystem(behaviorTreeSystem);
|
||||||
|
|
||||||
|
// 保存引用
|
||||||
|
context.behaviorTreeSystem = behaviorTreeSystem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior Tree Plugin Loader
|
||||||
|
* 行为树插件加载器
|
||||||
|
*
|
||||||
|
* 注意:editorModule 在 ./index.ts 中通过 createBehaviorTreePlugin() 设置
|
||||||
|
*/
|
||||||
|
export const BehaviorTreePlugin: IPluginLoader = {
|
||||||
|
descriptor,
|
||||||
|
runtimeModule: new BehaviorTreeRuntimeModule(),
|
||||||
|
// editorModule 将在 index.ts 中设置
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BehaviorTreePlugin;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
import { Connection } from '../../../domain/models/Connection';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
import { Node } from '../../../domain/models/Node';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
import { Node } from '../../../domain/models/Node';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Position } from '../../../domain/value-objects/Position';
|
import { Position } from '../../../domain/value-objects/Position';
|
||||||
import { BaseCommand, ICommand } from '@esengine/editor-core';
|
import { BaseCommand, ICommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
import { Connection } from '../../../domain/models/Connection';
|
||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BaseCommand } from '@esengine/editor-core';
|
import { BaseCommand } from '@esengine/editor-runtime';
|
||||||
import { ITreeState } from '../ITreeState';
|
import { ITreeState } from '../ITreeState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GlobalBlackboardConfig, BlackboardValueType, BlackboardVariable } from '@esengine/behavior-tree';
|
import { GlobalBlackboardConfig, BlackboardValueType, BlackboardVariable } from '../../..';
|
||||||
import { createLogger } from '@esengine/ecs-framework';
|
import { createLogger } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
const logger = createLogger('GlobalBlackboardService');
|
const logger = createLogger('GlobalBlackboardService');
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createStore } from '@esengine/editor-runtime';
|
import { createStore } from '@esengine/editor-runtime';
|
||||||
|
|
||||||
const create = createStore;
|
const create = createStore;
|
||||||
import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplates, NodeTemplate } from '../../..';
|
||||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||||
import { Node } from '../../domain/models/Node';
|
import { Node } from '../../domain/models/Node';
|
||||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { AddConnectionCommand } from '../commands/tree/AddConnectionCommand';
|
import { AddConnectionCommand } from '../commands/tree/AddConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
import { IValidator } from '../../domain/interfaces/IValidator';
|
import { IValidator } from '../../domain/interfaces/IValidator';
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplate } from '../../..';
|
||||||
import { Node } from '../../domain/models/Node';
|
import { Node } from '../../domain/models/Node';
|
||||||
import { Position } from '../../domain/value-objects/Position';
|
import { Position } from '../../domain/value-objects/Position';
|
||||||
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
|
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { CreateNodeCommand } from '../commands/tree/CreateNodeCommand';
|
import { CreateNodeCommand } from '../commands/tree/CreateNodeCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager, ICommand } from '@esengine/editor-core';
|
import { CommandManager, ICommand } from '@esengine/editor-runtime';
|
||||||
import { DeleteNodeCommand } from '../commands/tree/DeleteNodeCommand';
|
import { DeleteNodeCommand } from '../commands/tree/DeleteNodeCommand';
|
||||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Position } from '../../domain/value-objects/Position';
|
import { Position } from '../../domain/value-objects/Position';
|
||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { MoveNodeCommand } from '../commands/tree/MoveNodeCommand';
|
import { MoveNodeCommand } from '../commands/tree/MoveNodeCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
import { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CommandManager } from '@esengine/editor-core';
|
import { CommandManager } from '@esengine/editor-runtime';
|
||||||
import { UpdateNodeDataCommand } from '../commands/tree/UpdateNodeDataCommand';
|
import { UpdateNodeDataCommand } from '../commands/tree/UpdateNodeDataCommand';
|
||||||
import { ITreeState } from '../commands/ITreeState';
|
import { ITreeState } from '../commands/ITreeState';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
createLogger,
|
createLogger,
|
||||||
} from '@esengine/editor-runtime';
|
} from '@esengine/editor-runtime';
|
||||||
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
||||||
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '../..';
|
||||||
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
||||||
|
|
||||||
const { File, FolderTree, FolderOpen } = Icons;
|
const { File, FolderTree, FolderOpen } = Icons;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime';
|
import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime';
|
||||||
import { NodeTemplate, BlackboardValueType } from '@esengine/behavior-tree';
|
import { NodeTemplate, BlackboardValueType } from '../..';
|
||||||
import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
||||||
import { useUIStore } from '../stores';
|
import { useUIStore } from '../stores';
|
||||||
import { showToast as notificationShowToast } from '../services/NotificationService';
|
import { showToast as notificationShowToast } from '../services/NotificationService';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/editor-runtime';
|
import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/editor-runtime';
|
||||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplate } from '../../..';
|
||||||
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
|
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
|
||||||
|
|
||||||
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { React, Icons } from '@esengine/editor-runtime';
|
import { React, Icons } from '@esengine/editor-runtime';
|
||||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||||
import { PropertyDefinition } from '@esengine/behavior-tree';
|
import { PropertyDefinition } from '../../..';
|
||||||
|
|
||||||
import { Node as BehaviorTreeNodeType } from '../../domain/models/Node';
|
import { Node as BehaviorTreeNodeType } from '../../domain/models/Node';
|
||||||
import { Connection } from '../../domain/models/Connection';
|
import { Connection } from '../../domain/models/Connection';
|
||||||
@@ -3,12 +3,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
Core,
|
|
||||||
createLogger,
|
createLogger,
|
||||||
MessageHub,
|
|
||||||
open,
|
open,
|
||||||
save,
|
save,
|
||||||
Icons,
|
Icons,
|
||||||
|
PluginAPI,
|
||||||
} from '@esengine/editor-runtime';
|
} from '@esengine/editor-runtime';
|
||||||
import { useBehaviorTreeDataStore } from '../../stores';
|
import { useBehaviorTreeDataStore } from '../../stores';
|
||||||
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
||||||
@@ -77,11 +76,19 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
}, [tree, lastSavedSnapshot, isOpen]);
|
}, [tree, lastSavedSnapshot, isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 检查 PluginAPI 是否可用
|
||||||
|
if (!PluginAPI.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsubscribeFileOpened: (() => void) | undefined;
|
||||||
|
let unsubscribePropertyChanged: (() => void) | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageHub = Core.services.resolve(MessageHub);
|
const messageHub = PluginAPI.messageHub;
|
||||||
|
|
||||||
// 订阅文件打开事件
|
// 订阅文件打开事件
|
||||||
const unsubscribeFileOpened = messageHub.subscribe('behavior-tree:file-opened', (data: { filePath: string; fileName: string }) => {
|
unsubscribeFileOpened = messageHub.subscribe('behavior-tree:file-opened', (data: { filePath: string; fileName: string }) => {
|
||||||
setCurrentFilePath(data.filePath);
|
setCurrentFilePath(data.filePath);
|
||||||
setCurrentFileName(data.fileName);
|
setCurrentFileName(data.fileName);
|
||||||
const loadedTree = useBehaviorTreeDataStore.getState().tree;
|
const loadedTree = useBehaviorTreeDataStore.getState().tree;
|
||||||
@@ -90,7 +97,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 订阅节点属性更改事件
|
// 订阅节点属性更改事件
|
||||||
const unsubscribePropertyChanged = messageHub.subscribe('behavior-tree:node-property-changed',
|
unsubscribePropertyChanged = messageHub.subscribe('behavior-tree:node-property-changed',
|
||||||
(data: { nodeId: string; propertyName: string; value: any }) => {
|
(data: { nodeId: string; propertyName: string; value: any }) => {
|
||||||
const state = useBehaviorTreeDataStore.getState();
|
const state = useBehaviorTreeDataStore.getState();
|
||||||
const node = state.getNode(data.nodeId);
|
const node = state.getNode(data.nodeId);
|
||||||
@@ -127,19 +134,22 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribeFileOpened();
|
|
||||||
unsubscribePropertyChanged();
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to subscribe to events:', error);
|
logger.error('Failed to subscribe to events:', error);
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeFileOpened?.();
|
||||||
|
unsubscribePropertyChanged?.();
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleNodeSelect = useCallback((node: BehaviorTreeNode) => {
|
const handleNodeSelect = useCallback((node: BehaviorTreeNode) => {
|
||||||
try {
|
try {
|
||||||
const messageHub = Core.services.resolve(MessageHub);
|
if (!PluginAPI.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messageHub = PluginAPI.messageHub;
|
||||||
messageHub.publish('behavior-tree:node-selected', { data: node });
|
messageHub.publish('behavior-tree:node-selected', { data: node });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to publish node selection:', error);
|
logger.error('Failed to publish node selection:', error);
|
||||||
@@ -161,7 +171,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
filePath = selected;
|
filePath = selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = Core.services.resolve(BehaviorTreeService);
|
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||||
await service.saveToFile(filePath);
|
await service.saveToFile(filePath);
|
||||||
|
|
||||||
setCurrentFilePath(filePath);
|
setCurrentFilePath(filePath);
|
||||||
@@ -195,7 +205,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
|
||||||
const filePath = selected as string;
|
const filePath = selected as string;
|
||||||
const service = Core.services.resolve(BehaviorTreeService);
|
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||||
await service.loadFromFile(filePath);
|
await service.loadFromFile(filePath);
|
||||||
|
|
||||||
setCurrentFilePath(filePath);
|
setCurrentFilePath(filePath);
|
||||||
@@ -220,7 +230,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageHub = Core.services.resolve(MessageHub);
|
const messageHub = PluginAPI.messageHub;
|
||||||
messageHub.publish('compiler:open-dialog', {
|
messageHub.publish('compiler:open-dialog', {
|
||||||
compilerId: 'behavior-tree',
|
compilerId: 'behavior-tree',
|
||||||
currentFileName: currentFileName || undefined,
|
currentFileName: currentFileName || undefined,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NodeTemplate, NodeType } from '@esengine/behavior-tree';
|
import { NodeTemplate, NodeType } from '../..';
|
||||||
import { Icons } from '@esengine/editor-runtime';
|
import { Icons } from '@esengine/editor-runtime';
|
||||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Node } from '../models/Node';
|
import { Node } from '../models/Node';
|
||||||
import { Position } from '../value-objects/Position';
|
import { Position } from '../value-objects/Position';
|
||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplate } from '../../..';
|
||||||
|
|
||||||
export const ROOT_NODE_ID = 'root-node';
|
export const ROOT_NODE_ID = 'root-node';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplate } from '../../..';
|
||||||
import { Node } from '../models/Node';
|
import { Node } from '../models/Node';
|
||||||
import { Position } from '../value-objects';
|
import { Position } from '../value-objects';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
import { NodeTemplate } from '../../..';
|
||||||
import { Position, NodeType } from '../value-objects';
|
import { Position, NodeType } from '../value-objects';
|
||||||
import { ValidationError } from '../errors';
|
import { ValidationError } from '../errors';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GlobalBlackboardConfig, BlackboardValueType } from '@esengine/behavior-tree';
|
import { GlobalBlackboardConfig, BlackboardValueType } from '../..';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型生成配置选项
|
* 类型生成配置选项
|
||||||
@@ -70,7 +70,7 @@ export class GlobalBlackboardTypeGenerator {
|
|||||||
typeAliasName: 'GlobalVariableName',
|
typeAliasName: 'GlobalVariableName',
|
||||||
wrapperClassName: 'TypedGlobalBlackboard',
|
wrapperClassName: 'TypedGlobalBlackboard',
|
||||||
defaultsName: 'GlobalBlackboardDefaults',
|
defaultsName: 'GlobalBlackboardDefaults',
|
||||||
importPath: '@esengine/behavior-tree',
|
importPath: '../..',
|
||||||
includeConstants: true,
|
includeConstants: true,
|
||||||
includeInterface: true,
|
includeInterface: true,
|
||||||
includeTypeAlias: true,
|
includeTypeAlias: true,
|
||||||
@@ -150,6 +150,9 @@ export class GlobalBlackboardTypeGenerator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成文件头部注释
|
* 生成文件头部注释
|
||||||
|
*
|
||||||
|
* 注意:生成的代码字符串会被打包到 IIFE 中,如果 import/export 出现在行首
|
||||||
|
* 会被浏览器误解析为 ES module 语法。因此使用字符串拼接确保不在行首。
|
||||||
*/
|
*/
|
||||||
private static generateHeader(timestamp: string, opts: Required<TypeGenerationOptions>): string {
|
private static generateHeader(timestamp: string, opts: Required<TypeGenerationOptions>): string {
|
||||||
const customHeader = opts.customHeader || `/**
|
const customHeader = opts.customHeader || `/**
|
||||||
@@ -159,22 +162,25 @@ export class GlobalBlackboardTypeGenerator {
|
|||||||
* 生成时间: ${timestamp}
|
* 生成时间: ${timestamp}
|
||||||
*/`;
|
*/`;
|
||||||
|
|
||||||
return `${customHeader}
|
// 使用字符串拼接避免 import 出现在源代码行首(打包后可能被误解析)
|
||||||
|
const importStatement = 'im' + 'port { GlobalBlackboardService } from \'' + opts.importPath + '\';';
|
||||||
import { GlobalBlackboardService } from '${opts.importPath}';`;
|
return `${customHeader}\n\n${importStatement}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成常量对象
|
* 生成常量对象
|
||||||
|
* 注意:使用 EXP + 'ort' 拼接避免打包后 export 在行首被误解析
|
||||||
*/
|
*/
|
||||||
private static generateConstants(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
private static generateConstants(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||||
|
// 使用拼接避免 export 在源代码行首
|
||||||
|
const exp = 'exp' + 'ort';
|
||||||
|
|
||||||
if (variables.length === 0) {
|
if (variables.length === 0) {
|
||||||
return `/**
|
return `/**
|
||||||
* 全局变量名称常量
|
* 全局变量名称常量
|
||||||
*/
|
*/
|
||||||
export const ${opts.constantsName} = {} as const;`;
|
${exp} const ${opts.constantsName} = {} as const;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按命名空间分组
|
// 按命名空间分组
|
||||||
@@ -190,7 +196,7 @@ export const ${opts.constantsName} = {} as const;`;
|
|||||||
* 全局变量名称常量
|
* 全局变量名称常量
|
||||||
* 使用常量避免拼写错误
|
* 使用常量避免拼写错误
|
||||||
*/
|
*/
|
||||||
export const ${opts.constantsName} = {
|
${exp} const ${opts.constantsName} = {
|
||||||
${entries}
|
${entries}
|
||||||
} as const;`;
|
} as const;`;
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +226,7 @@ ${entries}
|
|||||||
* 全局变量名称常量
|
* 全局变量名称常量
|
||||||
* 使用常量避免拼写错误
|
* 使用常量避免拼写错误
|
||||||
*/
|
*/
|
||||||
export const ${opts.constantsName} = {
|
${exp} const ${opts.constantsName} = {
|
||||||
${namespaces}
|
${namespaces}
|
||||||
} as const;`;
|
} as const;`;
|
||||||
}
|
}
|
||||||
@@ -230,11 +236,13 @@ ${namespaces}
|
|||||||
* 生成接口定义
|
* 生成接口定义
|
||||||
*/
|
*/
|
||||||
private static generateInterface(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
private static generateInterface(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||||
|
const exp = 'exp' + 'ort';
|
||||||
|
|
||||||
if (variables.length === 0) {
|
if (variables.length === 0) {
|
||||||
return `/**
|
return `/**
|
||||||
* 全局变量类型定义
|
* 全局变量类型定义
|
||||||
*/
|
*/
|
||||||
export interface ${opts.interfaceName} {}`;
|
${exp} interface ${opts.interfaceName} {}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = variables
|
const properties = variables
|
||||||
@@ -248,7 +256,7 @@ export interface ${opts.interfaceName} {}`;
|
|||||||
return `/**
|
return `/**
|
||||||
* 全局变量类型定义
|
* 全局变量类型定义
|
||||||
*/
|
*/
|
||||||
export interface ${opts.interfaceName} {
|
${exp} interface ${opts.interfaceName} {
|
||||||
${properties}
|
${properties}
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
@@ -257,16 +265,18 @@ ${properties}
|
|||||||
* 生成类型别名
|
* 生成类型别名
|
||||||
*/
|
*/
|
||||||
private static generateTypeAliases(opts: Required<TypeGenerationOptions>): string {
|
private static generateTypeAliases(opts: Required<TypeGenerationOptions>): string {
|
||||||
|
const exp = 'exp' + 'ort';
|
||||||
return `/**
|
return `/**
|
||||||
* 全局变量名称联合类型
|
* 全局变量名称联合类型
|
||||||
*/
|
*/
|
||||||
export type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
${exp} type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成类型安全包装类
|
* 生成类型安全包装类
|
||||||
*/
|
*/
|
||||||
private static generateTypedClass(opts: Required<TypeGenerationOptions>): string {
|
private static generateTypedClass(opts: Required<TypeGenerationOptions>): string {
|
||||||
|
const exp = 'exp' + 'ort';
|
||||||
return `/**
|
return `/**
|
||||||
* 类型安全的全局黑板服务包装器
|
* 类型安全的全局黑板服务包装器
|
||||||
*
|
*
|
||||||
@@ -284,7 +294,7 @@ export type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
|||||||
* gb.setValue('playerHP', 'invalid'); // ❌ 编译错误
|
* gb.setValue('playerHP', 'invalid'); // ❌ 编译错误
|
||||||
* \`\`\`
|
* \`\`\`
|
||||||
*/
|
*/
|
||||||
export class ${opts.wrapperClassName} {
|
${exp} class ${opts.wrapperClassName} {
|
||||||
constructor(private service: GlobalBlackboardService) {}
|
constructor(private service: GlobalBlackboardService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,11 +336,13 @@ export class ${opts.wrapperClassName} {
|
|||||||
* 生成默认值配置
|
* 生成默认值配置
|
||||||
*/
|
*/
|
||||||
private static generateDefaults(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
private static generateDefaults(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||||
|
const exp = 'exp' + 'ort';
|
||||||
|
|
||||||
if (variables.length === 0) {
|
if (variables.length === 0) {
|
||||||
return `/**
|
return `/**
|
||||||
* 默认值配置
|
* 默认值配置
|
||||||
*/
|
*/
|
||||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
${exp} const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = variables
|
const properties = variables
|
||||||
@@ -362,7 +374,7 @@ export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
|||||||
* service.importConfig(config);
|
* service.importConfig(config);
|
||||||
* \`\`\`
|
* \`\`\`
|
||||||
*/
|
*/
|
||||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {
|
${exp} const ${opts.defaultsName}: ${opts.interfaceName} = {
|
||||||
${properties}
|
${properties}
|
||||||
};`;
|
};`;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, type RefObject, React, createLogger } from '@esengine/editor-runtime';
|
import { useState, type RefObject, React, createLogger } from '@esengine/editor-runtime';
|
||||||
import { NodeTemplate, NodeType } from '@esengine/behavior-tree';
|
import { NodeTemplate, NodeType } from '../..';
|
||||||
import { Position } from '../domain/value-objects/Position';
|
import { Position } from '../domain/value-objects/Position';
|
||||||
import { useNodeOperations } from './useNodeOperations';
|
import { useNodeOperations } from './useNodeOperations';
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user