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:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

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

View File

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

View File

@@ -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;
dispose(): void {} setVolume(volume: number): void;
preload(id: string, url: string): Promise<void>;
} }
// 注册模拟服务(用于测试) // 使用 Symbol.for 确保跨包共享同一个 Symbol
Core.services.registerInstance(DataService, new MockDataService()); 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 {
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
``` ```
### 循环依赖检测 ### 循环依赖检测

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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\//
]
}
];

View File

@@ -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!');

View File

@@ -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 处理
}
}

View File

@@ -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 {
// 先注册 FileSystemServiceBehaviorTreeService 依赖它)
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] : [];
}
}

View File

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

View File

@@ -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" }
]
}

View File

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

View 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"
}

View File

@@ -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';
/** /**

View File

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

View 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;
}
}

View File

@@ -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';
/** /**
* 行为树启动辅助类 * 行为树启动辅助类

View File

@@ -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';
/** /**

View File

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

View 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;

View File

@@ -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';
/** /**

View File

@@ -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';
/** /**

View File

@@ -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';
/** /**

View File

@@ -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';
/** /**

View File

@@ -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';
/** /**

View File

@@ -1,4 +1,4 @@
import { BaseCommand } from '@esengine/editor-core'; import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState'; import { ITreeState } from '../ITreeState';
/** /**

View File

@@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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