From 3d5fcc1a55f054cb8eb3a219818a79aee0ff1831 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Fri, 5 Dec 2025 18:28:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor-core):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=B3=BB=E7=BB=9F=E8=87=AA=E5=8A=A8=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=8A=9F=E8=83=BD=20(#283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(editor-core): 添加用户系统自动注册功能 - IUserCodeService 新增 registerSystems/unregisterSystems/getRegisteredSystems 方法 - UserCodeService 实现系统检测、实例化和场景注册逻辑 - ServiceRegistry 在预览开始时注册用户系统,停止时移除 - 热更新时自动重新加载用户系统 - 更新 System 脚本模板添加 @ECSSystem 装饰器 * feat(editor-core): 添加编辑器脚本支持(Inspector/Gizmo) - registerEditorExtensions 实际注册用户 Inspector 和 Gizmo - 添加 unregisterEditorExtensions 方法 - ServiceRegistry 在项目加载时编译并加载编辑器脚本 - 项目关闭时自动清理编辑器扩展 - 添加 Inspector 和 Gizmo 脚本创建模板 --- .../src/app/managers/ServiceRegistry.ts | 72 ++++-- .../src/components/ContentBrowser.tsx | 103 ++++++-- .../src/Services/UserCode/IUserCodeService.ts | 45 +++- .../src/Services/UserCode/UserCodeService.ts | 227 ++++++++++++++++-- 4 files changed, 389 insertions(+), 58 deletions(-) diff --git a/packages/editor-app/src/app/managers/ServiceRegistry.ts b/packages/editor-app/src/app/managers/ServiceRegistry.ts index a5b9ae80..8968698f 100644 --- a/packages/editor-app/src/app/managers/ServiceRegistry.ts +++ b/packages/editor-app/src/app/managers/ServiceRegistry.ts @@ -295,26 +295,39 @@ export class ServiceRegistry { PluginSDKRegistry.initialize(); try { - // Compile runtime scripts | 编译运行时脚本 - const compileResult = await userCodeService.compile({ + // 1. 编译运行时脚本 | Compile runtime scripts + const runtimeResult = await userCodeService.compile({ projectPath: projectPath, target: UserCodeTarget.Runtime }); - if (compileResult.success && compileResult.outputPath) { - // Load compiled module | 加载编译后的模块 - const module = await userCodeService.load(compileResult.outputPath, UserCodeTarget.Runtime); - - // Register user components to editor | 注册用户组件到编辑器 + if (runtimeResult.success && runtimeResult.outputPath) { + const module = await userCodeService.load(runtimeResult.outputPath, UserCodeTarget.Runtime); userCodeService.registerComponents(module, componentRegistry); - - // Notify that user code has been reloaded | 通知用户代码已重新加载 messageHub.publish('usercode:reloaded', { projectPath, exports: Object.keys(module.exports) }); - } else if (compileResult.errors.length > 0) { - console.warn('[UserCodeService] Compilation errors:', compileResult.errors); + } else if (runtimeResult.errors.length > 0) { + console.warn('[UserCodeService] Runtime compilation errors:', runtimeResult.errors); + } + + // 2. 编译编辑器脚本 | Compile editor scripts + const editorResult = await userCodeService.compile({ + projectPath: projectPath, + target: UserCodeTarget.Editor + }); + + if (editorResult.success && editorResult.outputPath) { + const editorModule = await userCodeService.load(editorResult.outputPath, UserCodeTarget.Editor); + userCodeService.registerEditorExtensions(editorModule, componentInspectorRegistry); + messageHub.publish('usercode:editor-reloaded', { + projectPath, + exports: Object.keys(editorModule.exports) + }); + } else if (editorResult.errors.length > 0) { + // 编辑器脚本编译错误只记录,不影响运行时 + console.warn('[UserCodeService] Editor compilation errors:', editorResult.errors); } } catch (error) { console.error('[UserCodeService] Failed to compile/load:', error); @@ -333,17 +346,20 @@ export class ServiceRegistry { console.log('[UserCodeService] Hot reload event:', event.changedFiles); if (event.newModule) { - // 1. Register new/updated components to registries // 1. 注册新的/更新的组件到注册表 userCodeService.registerComponents(event.newModule, componentRegistry); - // 2. Hot reload: update prototype chain of existing instances // 2. 热更新:更新现有实例的原型链 const updatedCount = userCodeService.hotReloadInstances(event.newModule); console.log(`[UserCodeService] Hot reloaded ${updatedCount} component instances`); - // 3. Notify that user code has been reloaded - // 3. 通知用户代码已重新加载 + // 3. 如果正在预览,热更新用户系统 + const scene = Core.scene; + if (scene && !scene.isEditorMode) { + userCodeService.hotReloadSystems(event.newModule, scene); + } + + // 4. 通知用户代码已重新加载 messageHub.publish('usercode:reloaded', { projectPath: data.path, exports: Object.keys(event.newModule.exports), @@ -355,11 +371,12 @@ export class ServiceRegistry { }); }); - // Subscribe to project:closed to stop watching - // 订阅 project:closed 以停止监视 + // Subscribe to project:closed to stop watching and cleanup + // 订阅 project:closed 以停止监视和清理 messageHub.subscribe('project:closed', async () => { currentProjectPath = null; await userCodeService.stopWatch(); + userCodeService.unregisterEditorExtensions(componentInspectorRegistry); }); // Subscribe to script file changes (create/delete) from editor operations @@ -378,6 +395,27 @@ export class ServiceRegistry { } }); + // 预览开始时注册用户系统 + // Register user systems when preview starts + messageHub.subscribe('preview:start', () => { + const runtimeModule = userCodeService.getModule(UserCodeTarget.Runtime); + if (runtimeModule) { + const scene = Core.scene; + if (scene) { + userCodeService.registerSystems(runtimeModule, scene); + } + } + }); + + // 预览停止时移除用户系统 + // Unregister user systems when preview stops + messageHub.subscribe('preview:stop', () => { + const scene = Core.scene; + if (scene) { + userCodeService.unregisterSystems(scene); + } + }); + // 注册默认场景模板 - 创建默认相机 // Register default scene template - creates default camera this.registerDefaultSceneTemplate(); diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 4b5259ec..efd346e8 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -275,31 +275,22 @@ export class ${className} extends Component { category: 'Script', getContent: (fileName: string) => { const className = fileName.replace(/\.ts$/, ''); - return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'; + return `import { EntitySystem, Matcher, ECSSystem, type Entity } from '@esengine/ecs-framework'; /** * ${className} */ +@ECSSystem('${className}') export class ${className} extends EntitySystem { - // 定义系统处理的组件类型 - // Define component types this system processes - protected getMatcher(): Matcher { - // 返回匹配器,指定需要哪些组件 - // Return matcher specifying required components - // return Matcher.all(SomeComponent); - return Matcher.empty(); + constructor() { + // 定义系统处理的组件类型 | Define component types this system processes + // super(Matcher.all(SomeComponent)); + super(Matcher.empty()); } protected updateEntity(entity: Entity, deltaTime: number): void { - // 处理每个实体 - // Process each entity + // 处理每个实体 | Process each entity } - - // 可选:系统初始化 - // Optional: System initialization - // onInitialize(): void { - // super.onInitialize(); - // } } `; } @@ -320,6 +311,86 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void { // 在这里编写代码 // Write your code here } +`; + } + }, + { + id: 'ts-inspector', + label: 'Inspector', + extension: '.ts', + icon: 'FileCode', + category: 'Editor', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import React from 'react'; +import type { Component } from '@esengine/ecs-framework'; +import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; + +/** + * ${className} + * + * 自定义组件检查器 | Custom component inspector + * 放置在 scripts/editor/ 目录下 | Place in scripts/editor/ directory + */ +export class ${className} implements IComponentInspector { + readonly id = '${className.toLowerCase()}'; + readonly name = '${className}'; + readonly priority = 10; + // 目标组件类型名称 | Target component type names + readonly targetComponents = ['YourComponent']; + + canHandle(component: Component): boolean { + return this.targetComponents.includes(component.constructor.name); + } + + render(context: ComponentInspectorContext): React.ReactElement { + const { component } = context; + + return React.createElement('div', { className: 'custom-inspector' }, + React.createElement('h4', null, '${className}'), + React.createElement('pre', null, JSON.stringify(component, null, 2)) + ); + } +} +`; + } + }, + { + id: 'ts-gizmo', + label: 'Gizmo', + extension: '.ts', + icon: 'FileCode', + category: 'Editor', + getContent: (fileName: string) => { + const className = fileName.replace(/\.ts$/, ''); + return `import type { Component, Entity } from '@esengine/ecs-framework'; +import type { IGizmoRenderData } from '@esengine/editor-core'; + +/** + * ${className} + * + * 自定义 Gizmo 提供者 | Custom Gizmo provider + * 放置在 scripts/editor/ 目录下 | Place in scripts/editor/ directory + */ +export class ${className} { + // 目标组件类型 | Target component type + // 需要替换为实际的组件类 | Replace with actual component class + readonly targetComponent = null; // YourComponent + + draw(component: Component, entity: Entity, isSelected: boolean): IGizmoRenderData[] { + // 返回要绘制的 Gizmo 数据 | Return gizmo data to draw + return [ + { + type: 'circle', + x: 0, + y: 0, + radius: 10, + strokeColor: isSelected ? '#00ff00' : '#ffffff', + strokeWidth: 2 + } + ]; + } +} `; } } diff --git a/packages/editor-core/src/Services/UserCode/IUserCodeService.ts b/packages/editor-core/src/Services/UserCode/IUserCodeService.ts index dee914b3..86333bbf 100644 --- a/packages/editor-core/src/Services/UserCode/IUserCodeService.ts +++ b/packages/editor-core/src/Services/UserCode/IUserCodeService.ts @@ -219,6 +219,39 @@ export interface IUserCodeService { */ registerComponents(module: UserCodeModule, componentRegistry?: any): void; + /** + * Register user systems to scene. + * 注册用户系统到场景。 + * + * Automatically detects and instantiates System subclasses from user module, + * then adds them to the scene. + * 自动检测用户模块中的 System 子类并实例化,然后添加到场景。 + * + * @param module - User code module | 用户代码模块 + * @param scene - Scene to add systems | 要添加系统的场景 + * @returns Array of registered system instances | 注册的系统实例数组 + */ + registerSystems(module: UserCodeModule, scene: any): any[]; + + /** + * Unregister user systems from scene. + * 从场景注销用户系统。 + * + * Removes previously registered user systems from the scene. + * 从场景移除之前注册的用户系统。 + * + * @param scene - Scene to remove systems | 要移除系统的场景 + */ + unregisterSystems(scene: any): void; + + /** + * Get registered user systems. + * 获取已注册的用户系统。 + * + * @returns Array of registered system instances | 注册的系统实例数组 + */ + getRegisteredSystems(): any[]; + /** * Register editor extensions from user module. * 从用户模块注册编辑器扩展。 @@ -227,11 +260,19 @@ export interface IUserCodeService { * 自动检测并注册: * - Component inspectors * - Gizmo providers - * - Editor panels * * @param module - User code module | 用户代码模块 + * @param inspectorRegistry - Component inspector registry | 组件检查器注册表 */ - registerEditorExtensions(module: UserCodeModule): void; + registerEditorExtensions(module: UserCodeModule, inspectorRegistry?: any): void; + + /** + * Unregister editor extensions. + * 注销编辑器扩展。 + * + * @param inspectorRegistry - Component inspector registry | 组件检查器注册表 + */ + unregisterEditorExtensions(inspectorRegistry?: any): void; /** * Start watching for file changes (hot reload). diff --git a/packages/editor-core/src/Services/UserCode/UserCodeService.ts b/packages/editor-core/src/Services/UserCode/UserCodeService.ts index be765c2d..586efff3 100644 --- a/packages/editor-core/src/Services/UserCode/UserCodeService.ts +++ b/packages/editor-core/src/Services/UserCode/UserCodeService.ts @@ -24,6 +24,8 @@ import { USER_CODE_OUTPUT_DIR } from './IUserCodeService'; import type { IFileSystem, FileEntry } from '../IFileSystem'; +import type { ComponentInspectorRegistry, IComponentInspector } from '../ComponentInspectorRegistry'; +import { GizmoRegistry } from '../../Gizmos/GizmoRegistry'; const logger = createLogger('UserCodeService'); @@ -44,6 +46,24 @@ export class UserCodeService implements IService, IUserCodeService { private _currentProjectPath: string | undefined; private _eventUnlisten: (() => void) | undefined; + /** + * 已注册的用户系统实例 + * Registered user system instances + */ + private _registeredSystems: any[] = []; + + /** + * 已注册的用户 Inspector ID 列表 + * Registered user inspector IDs + */ + private _registeredInspectorIds: string[] = []; + + /** + * 已注册的用户 Gizmo 组件类型 + * Registered user gizmo component types + */ + private _registeredGizmoTypes: any[] = []; + constructor(fileSystem: IFileSystem) { this._fileSystem = fileSystem; } @@ -459,52 +479,213 @@ export class UserCodeService implements IService, IUserCodeService { } /** - * Register editor extensions from user module. - * 从用户模块注册编辑器扩展。 + * Register user systems to scene. + * 注册用户系统到场景。 * * @param module - User code module | 用户代码模块 + * @param scene - Scene to add systems | 要添加系统的场景 + * @returns Array of registered system instances | 注册的系统实例数组 */ - registerEditorExtensions(module: UserCodeModule): void { - if (module.target !== UserCodeTarget.Editor) { - logger.warn('Cannot register editor extensions from runtime module | 无法从运行时模块注册编辑器扩展'); - return; + registerSystems(module: UserCodeModule, scene: any): any[] { + if (module.target !== UserCodeTarget.Runtime) { + logger.warn('Cannot register systems from editor module | 无法从编辑器模块注册系统'); + return []; } - let inspectorCount = 0; - let gizmoCount = 0; - let panelCount = 0; + if (!scene) { + logger.warn('No scene provided for system registration | 未提供场景用于系统注册'); + return []; + } + + // 先移除之前注册的用户系统 | Remove previously registered user systems first + this.unregisterSystems(scene); + + const registeredSystems: any[] = []; for (const [name, exported] of Object.entries(module.exports)) { if (typeof exported !== 'function') { continue; } - // Check for inspector | 检查检查器 + // 检查是否是 System 子类 | Check if it's a System subclass + if (this._isSystemClass(exported)) { + try { + // 获取系统元数据 | Get system metadata + const metadata = (exported as any).__systemMetadata__; + const updateOrder = metadata?.updateOrder ?? 0; + const enabled = metadata?.enabled !== false; + + // 实例化系统 | Instantiate system + const systemInstance = new (exported as any)(); + + // 设置系统属性 | Set system properties + if (typeof systemInstance.updateOrder !== 'undefined') { + systemInstance.updateOrder = updateOrder; + } + if (typeof systemInstance.enabled !== 'undefined') { + systemInstance.enabled = enabled; + } + + // 标记为用户系统,便于后续识别和移除 | Mark as user system for later identification and removal + systemInstance.__isUserSystem__ = true; + systemInstance.__userSystemName__ = name; + + // 添加到场景 | Add to scene + scene.addSystem(systemInstance); + registeredSystems.push(systemInstance); + + logger.info(`Registered user system: ${name} | 注册用户系统: ${name}`, { + updateOrder, + enabled + }); + } catch (err) { + logger.error(`Failed to register system ${name} | 注册系统 ${name} 失败:`, err); + } + } + } + + this._registeredSystems = registeredSystems; + + logger.info(`Registered ${registeredSystems.length} user systems | 注册了 ${registeredSystems.length} 个用户系统`); + + return registeredSystems; + } + + /** + * Unregister user systems from scene. + * 从场景注销用户系统。 + * + * @param scene - Scene to remove systems | 要移除系统的场景 + */ + unregisterSystems(scene: any): void { + if (!scene) { + return; + } + + for (const system of this._registeredSystems) { + try { + scene.removeSystem(system); + logger.debug(`Removed user system: ${system.__userSystemName__} | 移除用户系统: ${system.__userSystemName__}`); + } catch (err) { + logger.warn(`Failed to remove system ${system.__userSystemName__}:`, err); + } + } + + this._registeredSystems = []; + } + + /** + * Get registered user systems. + * 获取已注册的用户系统。 + * + * @returns Array of registered system instances | 注册的系统实例数组 + */ + getRegisteredSystems(): any[] { + return [...this._registeredSystems]; + } + + /** + * Hot reload user systems. + * 热更新用户系统。 + * + * Removes old systems and registers new ones from the updated module. + * 移除旧系统并从更新的模块注册新系统。 + * + * @param module - New user code module | 新的用户代码模块 + * @param scene - Scene to update systems | 要更新系统的场景 + * @returns Array of newly registered system instances | 新注册的系统实例数组 + */ + hotReloadSystems(module: UserCodeModule, scene: any): any[] { + logger.info('Hot reloading user systems | 热更新用户系统'); + return this.registerSystems(module, scene); + } + + /** + * Register editor extensions from user module. + * 从用户模块注册编辑器扩展。 + * + * @param module - User code module | 用户代码模块 + * @param inspectorRegistry - Component inspector registry | 组件检查器注册表 + */ + registerEditorExtensions(module: UserCodeModule, inspectorRegistry?: ComponentInspectorRegistry): void { + if (module.target !== UserCodeTarget.Editor) { + logger.warn('Cannot register editor extensions from runtime module | 无法从运行时模块注册编辑器扩展'); + return; + } + + // 先移除之前注册的扩展 + this.unregisterEditorExtensions(inspectorRegistry); + + let inspectorCount = 0; + let gizmoCount = 0; + + for (const [name, exported] of Object.entries(module.exports)) { + if (typeof exported !== 'function') { + continue; + } + + // 注册 Inspector if (this._isInspectorClass(exported)) { - logger.debug(`Found inspector: ${name} | 发现检查器: ${name}`); - inspectorCount++; + try { + const inspector = new (exported as any)() as IComponentInspector; + if (inspectorRegistry) { + inspectorRegistry.register(inspector); + this._registeredInspectorIds.push(inspector.id); + logger.info(`Registered user inspector: ${name} (${inspector.id})`); + inspectorCount++; + } + } catch (err) { + logger.error(`Failed to register inspector ${name}:`, err); + } } - // Check for gizmo | 检查 Gizmo + // 注册 Gizmo if (this._isGizmoClass(exported)) { - logger.debug(`Found gizmo: ${name} | 发现 Gizmo: ${name}`); - gizmoCount++; - } - - // Check for panel | 检查面板 - if (this._isPanelComponent(exported)) { - logger.debug(`Found panel: ${name} | 发现面板: ${name}`); - panelCount++; + try { + const gizmoProvider = new (exported as any)(); + const targetComponent = gizmoProvider.targetComponent; + if (targetComponent) { + GizmoRegistry.register(targetComponent, (component, entity, isSelected) => { + return gizmoProvider.draw(component, entity, isSelected); + }); + this._registeredGizmoTypes.push(targetComponent); + logger.info(`Registered user gizmo for: ${targetComponent.name || name}`); + gizmoCount++; + } + } catch (err) { + logger.error(`Failed to register gizmo ${name}:`, err); + } } } logger.info(`Registered editor extensions | 注册编辑器扩展`, { inspectors: inspectorCount, - gizmos: gizmoCount, - panels: panelCount + gizmos: gizmoCount }); } + /** + * Unregister editor extensions. + * 注销编辑器扩展。 + * + * @param inspectorRegistry - Component inspector registry | 组件检查器注册表 + */ + unregisterEditorExtensions(inspectorRegistry?: ComponentInspectorRegistry): void { + // 注销 Inspector + if (inspectorRegistry) { + for (const id of this._registeredInspectorIds) { + inspectorRegistry.unregister(id); + } + } + this._registeredInspectorIds = []; + + // 注销 Gizmo + for (const componentType of this._registeredGizmoTypes) { + GizmoRegistry.unregister(componentType); + } + this._registeredGizmoTypes = []; + } + /** * Start watching for file changes (hot reload). * 开始监视文件变更(热更新)。