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 ../asset-system && 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
|
||||
run: |
|
||||
|
||||
31
.github/workflows/release-editor.yml
vendored
31
.github/workflows/release-editor.yml
vendored
@@ -96,16 +96,6 @@ jobs:
|
||||
cd packages/components
|
||||
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 引擎 =====
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
@@ -129,34 +119,35 @@ jobs:
|
||||
cd packages/ecs-engine-bindgen
|
||||
pnpm run build
|
||||
|
||||
# ===== 第四层:依赖 asset-system 的包 =====
|
||||
# ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 =====
|
||||
- name: Build editor-core package
|
||||
run: |
|
||||
cd packages/editor-core
|
||||
pnpm run build
|
||||
|
||||
# ===== 第五层:依赖 editor-core 的包 =====
|
||||
- name: Build UI package
|
||||
run: |
|
||||
cd packages/ui
|
||||
pnpm run build
|
||||
|
||||
- name: Build tilemap package
|
||||
run: |
|
||||
cd packages/tilemap
|
||||
pnpm run build
|
||||
|
||||
# ===== 第五层:依赖 editor-core 的包 =====
|
||||
- name: Build editor-runtime package
|
||||
run: |
|
||||
cd packages/editor-runtime
|
||||
pnpm run build
|
||||
|
||||
- name: Build UI editor package
|
||||
# ===== 第六层:依赖 editor-runtime 的包 =====
|
||||
- name: Build behavior-tree package
|
||||
run: |
|
||||
cd packages/ui-editor
|
||||
cd packages/behavior-tree
|
||||
pnpm run build
|
||||
|
||||
- name: Build tilemap-editor package
|
||||
run: |
|
||||
cd packages/tilemap-editor
|
||||
pnpm run build
|
||||
|
||||
# ===== 第六层:平台包 =====
|
||||
# ===== 第七层:平台包(依赖 ui, tilemap) =====
|
||||
- name: Build platform-web package
|
||||
run: |
|
||||
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
|
||||
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
constructor(
|
||||
@Inject(DataService) private data: DataService,
|
||||
@Inject(GameService) private game: GameService
|
||||
) {
|
||||
// data 和 game 会自动从容器中解析
|
||||
}
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
|
||||
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` 自动处理依赖注入:
|
||||
@@ -362,10 +410,10 @@ class PlayerService implements IService {
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析@Inject依赖)
|
||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||
registerInjectable(Core.services, 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
|
||||
// 测试代码
|
||||
class MockDataService implements IService {
|
||||
getData(key: string) {
|
||||
return 'mock data';
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
// IAudioService.ts - 定义接口和标识符
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 注册模拟服务(用于测试)
|
||||
Core.services.registerInstance(DataService, new MockDataService());
|
||||
// 使用 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 {
|
||||
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",
|
||||
"version": "1.0.1",
|
||||
"description": "完全ECS化的行为树系统,基于组件和实体的行为树实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"description": "ECS-based AI behavior tree system with visual editor and runtime execution",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./bin/index.d.ts",
|
||||
"import": "./bin/index.js",
|
||||
"development": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"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": [
|
||||
"bin/**/*"
|
||||
"dist",
|
||||
"plugin.json"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
@@ -25,40 +33,52 @@
|
||||
"entity-component-system"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:esm": "vite build",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"@types/react": "^18.3.12",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7"
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-dts": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 { BehaviorTreeData } from './Runtime/BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './Runtime/BehaviorTreeRuntimeComponent';
|
||||
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||
import { BehaviorTreeData } from './execution/BehaviorTreeData';
|
||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NodeType } from '../Types/TaskStatus';
|
||||
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../Runtime/NodeMetadata';
|
||||
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
|
||||
|
||||
/**
|
||||
* 节点数据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 { BaseCommand } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from '../../../domain/models/Node';
|
||||
import { BaseCommand } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Connection } from '../../../domain/models/Connection';
|
||||
import { BaseCommand } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
import { ITreeState } from '../ITreeState';
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseCommand } from '@esengine/editor-core';
|
||||
import { BaseCommand } from '@esengine/editor-runtime';
|
||||
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';
|
||||
|
||||
const logger = createLogger('GlobalBlackboardService');
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
import { NodeTemplates, NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { NodeTemplates, NodeTemplate } from '../../..';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
import { Connection, ConnectionType } from '../../domain/models/Connection';
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ITreeState } from '../commands/ITreeState';
|
||||
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 { Position } from '../../domain/value-objects/Position';
|
||||
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 { 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 { RemoveConnectionCommand } from '../commands/tree/RemoveConnectionCommand';
|
||||
import { ITreeState } from '../commands/ITreeState';
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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 { 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 { ITreeState } from '../commands/ITreeState';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
createLogger,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator';
|
||||
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree';
|
||||
import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '../..';
|
||||
import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore';
|
||||
|
||||
const { File, FolderTree, FolderOpen } = Icons;
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useUIStore } from '../stores';
|
||||
import { showToast as notificationShowToast } from '../services/NotificationService';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { React, useRef, useEffect, useState, useMemo, Icons } 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';
|
||||
|
||||
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { React, Icons } 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 { Connection } from '../../domain/models/Connection';
|
||||
@@ -3,12 +3,11 @@ import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
Core,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
open,
|
||||
save,
|
||||
Icons,
|
||||
PluginAPI,
|
||||
} from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore } from '../../stores';
|
||||
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
||||
@@ -77,11 +76,19 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
}, [tree, lastSavedSnapshot, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// 检查 PluginAPI 是否可用
|
||||
if (!PluginAPI.isAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unsubscribeFileOpened: (() => void) | undefined;
|
||||
let unsubscribePropertyChanged: (() => void) | undefined;
|
||||
|
||||
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);
|
||||
setCurrentFileName(data.fileName);
|
||||
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 }) => {
|
||||
const state = useBehaviorTreeDataStore.getState();
|
||||
const node = state.getNode(data.nodeId);
|
||||
@@ -127,19 +134,22 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeFileOpened();
|
||||
unsubscribePropertyChanged();
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to subscribe to events:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
unsubscribeFileOpened?.();
|
||||
unsubscribePropertyChanged?.();
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleNodeSelect = useCallback((node: BehaviorTreeNode) => {
|
||||
try {
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
if (!PluginAPI.isAvailable) {
|
||||
return;
|
||||
}
|
||||
const messageHub = PluginAPI.messageHub;
|
||||
messageHub.publish('behavior-tree:node-selected', { data: node });
|
||||
} catch (error) {
|
||||
logger.error('Failed to publish node selection:', error);
|
||||
@@ -161,7 +171,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
filePath = selected;
|
||||
}
|
||||
|
||||
const service = Core.services.resolve(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
await service.saveToFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
@@ -195,7 +205,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
if (!selected) return;
|
||||
|
||||
const filePath = selected as string;
|
||||
const service = Core.services.resolve(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
await service.loadFromFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
@@ -220,7 +230,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
}
|
||||
|
||||
try {
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const messageHub = PluginAPI.messageHub;
|
||||
messageHub.publish('compiler:open-dialog', {
|
||||
compilerId: 'behavior-tree',
|
||||
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 type { LucideIcon } from '@esengine/editor-runtime';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Node } from '../models/Node';
|
||||
import { Position } from '../value-objects/Position';
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { NodeTemplate } from '../../..';
|
||||
|
||||
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 { Position } from '../value-objects';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { NodeTemplate } from '../../..';
|
||||
import { Position, NodeType } from '../value-objects';
|
||||
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',
|
||||
wrapperClassName: 'TypedGlobalBlackboard',
|
||||
defaultsName: 'GlobalBlackboardDefaults',
|
||||
importPath: '@esengine/behavior-tree',
|
||||
importPath: '../..',
|
||||
includeConstants: true,
|
||||
includeInterface: true,
|
||||
includeTypeAlias: true,
|
||||
@@ -150,6 +150,9 @@ export class GlobalBlackboardTypeGenerator {
|
||||
|
||||
/**
|
||||
* 生成文件头部注释
|
||||
*
|
||||
* 注意:生成的代码字符串会被打包到 IIFE 中,如果 import/export 出现在行首
|
||||
* 会被浏览器误解析为 ES module 语法。因此使用字符串拼接确保不在行首。
|
||||
*/
|
||||
private static generateHeader(timestamp: string, opts: Required<TypeGenerationOptions>): string {
|
||||
const customHeader = opts.customHeader || `/**
|
||||
@@ -159,22 +162,25 @@ export class GlobalBlackboardTypeGenerator {
|
||||
* 生成时间: ${timestamp}
|
||||
*/`;
|
||||
|
||||
return `${customHeader}
|
||||
|
||||
import { GlobalBlackboardService } from '${opts.importPath}';`;
|
||||
// 使用字符串拼接避免 import 出现在源代码行首(打包后可能被误解析)
|
||||
const importStatement = 'im' + 'port { GlobalBlackboardService } from \'' + opts.importPath + '\';';
|
||||
return `${customHeader}\n\n${importStatement}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成常量对象
|
||||
* 注意:使用 EXP + 'ort' 拼接避免打包后 export 在行首被误解析
|
||||
*/
|
||||
private static generateConstants(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||
// 使用拼接避免 export 在源代码行首
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
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}
|
||||
} as const;`;
|
||||
} else {
|
||||
@@ -220,7 +226,7 @@ ${entries}
|
||||
* 全局变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*/
|
||||
export const ${opts.constantsName} = {
|
||||
${exp} const ${opts.constantsName} = {
|
||||
${namespaces}
|
||||
} as const;`;
|
||||
}
|
||||
@@ -230,11 +236,13 @@ ${namespaces}
|
||||
* 生成接口定义
|
||||
*/
|
||||
private static generateInterface(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
export interface ${opts.interfaceName} {}`;
|
||||
${exp} interface ${opts.interfaceName} {}`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
@@ -248,7 +256,7 @@ export interface ${opts.interfaceName} {}`;
|
||||
return `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
export interface ${opts.interfaceName} {
|
||||
${exp} interface ${opts.interfaceName} {
|
||||
${properties}
|
||||
}`;
|
||||
}
|
||||
@@ -257,16 +265,18 @@ ${properties}
|
||||
* 生成类型别名
|
||||
*/
|
||||
private static generateTypeAliases(opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
return `/**
|
||||
* 全局变量名称联合类型
|
||||
*/
|
||||
export type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||
${exp} type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成类型安全包装类
|
||||
*/
|
||||
private static generateTypedClass(opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
return `/**
|
||||
* 类型安全的全局黑板服务包装器
|
||||
*
|
||||
@@ -284,7 +294,7 @@ export type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||
* gb.setValue('playerHP', 'invalid'); // ❌ 编译错误
|
||||
* \`\`\`
|
||||
*/
|
||||
export class ${opts.wrapperClassName} {
|
||||
${exp} class ${opts.wrapperClassName} {
|
||||
constructor(private service: GlobalBlackboardService) {}
|
||||
|
||||
/**
|
||||
@@ -326,11 +336,13 @@ export class ${opts.wrapperClassName} {
|
||||
* 生成默认值配置
|
||||
*/
|
||||
private static generateDefaults(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 默认值配置
|
||||
*/
|
||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||
${exp} const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
@@ -362,7 +374,7 @@ export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||
* service.importConfig(config);
|
||||
* \`\`\`
|
||||
*/
|
||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {
|
||||
${exp} const ${opts.defaultsName}: ${opts.interfaceName} = {
|
||||
${properties}
|
||||
};`;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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