From d7454e3ca4f9a940007fdc7ca30b9b07653db447 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 4 Dec 2025 22:43:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E6=B7=BB=E5=8A=A0=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E6=A8=A1=E5=BC=8F=E6=A0=87=E5=BF=97=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E7=BC=96=E8=BE=91=E5=99=A8UI=E6=98=BE=E7=A4=BA=20(#27?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(engine): 添加编辑器模式标志控制编辑器UI显示 - 在 Rust 引擎中添加 isEditor 标志,控制网格、gizmos、坐标轴指示器的显示 - 运行时模式下自动隐藏所有编辑器专用 UI - 编辑器预览和浏览器运行时通过 setEditorMode(false) 禁用编辑器 UI - 添加 Scene.isEditorMode 延迟组件生命周期回调,直到 begin() 调用 - 修复用户组件注册到 Core ComponentRegistry 以支持序列化 - 修复 Run in Browser 时用户组件加载问题 * fix: 复制引擎模块的类型定义文件到 dist/engine * fix: 修复用户项目 tsconfig paths 类型定义路径 - 从 module.json 读取实际包名而不是使用目录名 - 修复 .d.ts 文件复制逻辑,支持 .mjs 扩展名 --- packages/core/src/ECS/Entity.ts | 10 +- packages/core/src/ECS/IScene.ts | 21 +++++ packages/core/src/ECS/Scene.ts | 59 +++++++++++- .../src/core/EngineBridge.ts | 22 +++++ .../src/wasm/es_engine.d.ts | 16 ++++ .../src-tauri/src/commands/system.rs | 24 ++++- .../src/components/ContentBrowser.tsx | 18 ++-- .../editor-app/src/components/Viewport.tsx | 93 ++++++++++++++++--- .../editor-app/src/services/EngineService.ts | 20 ++++ packages/editor-app/vite.config.ts | 9 ++ .../src/Services/SceneManagerService.ts | 9 ++ .../src/Services/UserCode/UserCodeService.ts | 24 ++++- packages/engine/src/core/engine.rs | 47 ++++++++-- packages/engine/src/lib.rs | 18 ++++ packages/platform-web/src/BrowserRuntime.ts | 6 +- packages/runtime-core/src/GameRuntime.ts | 37 ++++++++ 16 files changed, 393 insertions(+), 40 deletions(-) diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index a4a944d6..70728ead 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -375,7 +375,15 @@ export class Entity { if (this.scene.referenceTracker) { this.scene.referenceTracker.registerEntityScene(this.id, this.scene); } - component.onAddedToEntity(); + + // 编辑器模式下延迟执行 onAddedToEntity | Defer onAddedToEntity in editor mode + if (this.scene.isEditorMode) { + this.scene.queueDeferredComponentCallback(() => { + component.onAddedToEntity(); + }); + } else { + component.onAddedToEntity(); + } if (this.scene && this.scene.eventSystem) { this.scene.eventSystem.emitSync('component:added', { diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts index df9ad67e..f320aee0 100644 --- a/packages/core/src/ECS/IScene.ts +++ b/packages/core/src/ECS/IScene.ts @@ -78,6 +78,18 @@ export interface IScene { */ readonly services: ServiceContainer; + /** + * 编辑器模式标志 + * + * 当为 true 时,组件的生命周期回调(如 onAddedToEntity)会被延迟, + * 直到调用 begin() 开始运行场景时才会触发。 + * + * Editor mode flag. + * When true, component lifecycle callbacks (like onAddedToEntity) are deferred + * until begin() is called to start running the scene. + */ + isEditorMode: boolean; + /** * 获取系统列表 */ @@ -98,6 +110,15 @@ export interface IScene { */ unload(): void; + /** + * 添加延迟的组件生命周期回调 + * + * Queue a deferred component lifecycle callback. + * + * @param callback 要延迟执行的回调 | The callback to defer + */ + queueDeferredComponentCallback(callback: () => void): void; + /** * 开始场景 */ diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index f135a30c..88556d08 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -117,6 +117,30 @@ export class Scene implements IScene { */ private _didSceneBegin: boolean = false; + /** + * 编辑器模式标志 + * + * 当为 true 时,组件的生命周期回调(如 onAddedToEntity)会被延迟, + * 直到调用 begin() 开始运行场景时才会触发。 + * + * Editor mode flag. + * When true, component lifecycle callbacks (like onAddedToEntity) are deferred + * until begin() is called to start running the scene. + */ + public isEditorMode: boolean = false; + + /** + * 延迟的组件生命周期回调队列 + * + * 在编辑器模式下,组件的 onAddedToEntity 回调会被加入此队列, + * 等到 begin() 调用时统一执行。 + * + * Deferred component lifecycle callback queue. + * In editor mode, component's onAddedToEntity callbacks are queued here, + * and will be executed when begin() is called. + */ + private _deferredComponentCallbacks: Array<() => void> = []; + /** * 系统列表缓存 */ @@ -319,14 +343,47 @@ export class Scene implements IScene { */ public unload(): void {} + /** + * 添加延迟的组件生命周期回调 + * + * 在编辑器模式下,组件的 onAddedToEntity 回调会通过此方法加入队列。 + * + * Queue a deferred component lifecycle callback. + * In editor mode, component's onAddedToEntity callbacks are queued via this method. + * + * @param callback 要延迟执行的回调 | The callback to defer + */ + public queueDeferredComponentCallback(callback: () => void): void { + this._deferredComponentCallbacks.push(callback); + } + /** * 开始场景,启动实体处理器等 * * 这个方法会启动场景。它将启动实体处理器等,并调用onStart方法。 + * 在编辑器模式下,此方法还会执行所有延迟的组件生命周期回调。 + * + * This method starts the scene. It will start entity processors and call onStart. + * In editor mode, this method also executes all deferred component lifecycle callbacks. */ public begin() { - // 标记场景已开始运行并调用onStart方法 + // 标记场景已开始运行 this._didSceneBegin = true; + + // 执行所有延迟的组件生命周期回调 | Execute all deferred component lifecycle callbacks + if (this._deferredComponentCallbacks.length > 0) { + for (const callback of this._deferredComponentCallbacks) { + try { + callback(); + } catch (error) { + this.logger.error('Error executing deferred component callback:', error); + } + } + // 清空队列 | Clear the queue + this._deferredComponentCallbacks = []; + } + + // 调用onStart方法 this.onStart(); } diff --git a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts index afd552e9..2affdeb1 100644 --- a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts +++ b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts @@ -598,6 +598,28 @@ export class EngineBridge implements IEngineBridge { this.getEngine().setShowGizmos(show); } + /** + * Set editor mode. + * 设置编辑器模式。 + * + * When false (runtime mode), editor-only UI like grid, gizmos, + * and axis indicator are automatically hidden. + * 当为 false(运行时模式)时,编辑器专用 UI 会自动隐藏。 + */ + setEditorMode(isEditor: boolean): void { + if (!this.initialized) return; + this.getEngine().setEditorMode(isEditor); + } + + /** + * Get editor mode. + * 获取编辑器模式。 + */ + isEditorMode(): boolean { + if (!this.initialized) return true; + return this.getEngine().isEditorMode(); + } + // ===== Multi-viewport API ===== // ===== 多视口 API ===== diff --git a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts index 3dcb7d9e..2ba5c5f4 100644 --- a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts +++ b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts @@ -117,6 +117,11 @@ export class GameEngine { * The shader ID for referencing this shader | 用于引用此着色器的ID */ compileShader(vertex_source: string, fragment_source: string): number; + /** + * Get editor mode. + * 获取编辑器模式。 + */ + isEditorMode(): boolean; /** * Render sprites as overlay (without clearing screen). * 渲染精灵作为叠加层(不清除屏幕)。 @@ -156,6 +161,15 @@ export class GameEngine { * * `r`, `g`, `b`, `a` - Color components (0.0-1.0) | 颜色分量 (0.0-1.0) */ setClearColor(r: number, g: number, b: number, a: number): void; + /** + * Set editor mode. + * 设置编辑器模式。 + * + * When false (runtime mode), editor-only UI like grid, gizmos, + * and axis indicator are automatically hidden. + * 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。 + */ + setEditorMode(is_editor: boolean): void; /** * Set gizmo visibility. * 设置辅助工具可见性。 @@ -374,6 +388,7 @@ export interface InitOutput { readonly gameengine_hasMaterial: (a: number, b: number) => number; readonly gameengine_hasShader: (a: number, b: number) => number; readonly gameengine_height: (a: number) => number; + readonly gameengine_isEditorMode: (a: number) => number; readonly gameengine_isKeyDown: (a: number, b: number, c: number) => number; readonly gameengine_loadTexture: (a: number, b: number, c: number, d: number) => [number, number]; readonly gameengine_loadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; @@ -389,6 +404,7 @@ export interface InitOutput { readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number; readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void; + readonly gameengine_setEditorMode: (a: number, b: number) => void; readonly gameengine_setMaterialBlendMode: (a: number, b: number, c: number) => number; readonly gameengine_setMaterialColor: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; readonly gameengine_setMaterialFloat: (a: number, b: number, c: number, d: number, e: number) => number; diff --git a/packages/editor-app/src-tauri/src/commands/system.rs b/packages/editor-app/src-tauri/src/commands/system.rs index 8395c893..40f382ac 100644 --- a/packages/editor-app/src-tauri/src/commands/system.rs +++ b/packages/editor-app/src-tauri/src/commands/system.rs @@ -475,12 +475,26 @@ fn update_tsconfig_file( // Check for index.d.ts // 检查是否存在 index.d.ts let dts_path = module_path.join("index.d.ts"); - if dts_path.exists() { - let module_name = format!("@esengine/{}", module_id); - let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id); - paths.insert(module_name, serde_json::json!([dts_path_str])); - module_count += 1; + if !dts_path.exists() { + continue; } + + // Read module.json to get the actual package name + // 读取 module.json 获取实际的包名 + let module_json_path = module_path.join("module.json"); + let module_name = if module_json_path.exists() { + fs::read_to_string(&module_json_path) + .ok() + .and_then(|content| serde_json::from_str::(&content).ok()) + .and_then(|json| json.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())) + .unwrap_or_else(|| format!("@esengine/{}", module_id)) + } else { + format!("@esengine/{}", module_id) + }; + + let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id); + paths.insert(module_name, serde_json::json!([dts_path_str])); + module_count += 1; } } diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 8e499381..4b5259ec 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -248,14 +248,20 @@ export class ${className} extends Component { @Property({ type: 'number', label: 'Example Property' }) public exampleProperty: number = 0; - onInitialize(): void { - // 组件初始化时调用 - // Called when component is initialized + /** + * 组件添加到实体时调用 + * Called when component is added to entity + */ + onAddedToEntity(): void { + console.log('${className} added to entity'); } - onDestroy(): void { - // 组件销毁时调用 - // Called when component is destroyed + /** + * 组件从实体移除时调用 + * Called when component is removed from entity + */ + onRemovedFromEntity(): void { + console.log('${className} removed from entity'); } } `; diff --git a/packages/editor-app/src/components/Viewport.tsx b/packages/editor-app/src/components/Viewport.tsx index df32d238..e5a213ee 100644 --- a/packages/editor-app/src/components/Viewport.tsx +++ b/packages/editor-app/src/components/Viewport.tsx @@ -25,8 +25,12 @@ import type { ModuleManifest } from '../services/RuntimeResolver'; * * This matches the structure of published builds for consistency * 这与发布构建的结构一致 + * + * @param importMap - Import map for module resolution + * @param modules - Module manifests for plugin loading + * @param hasUserRuntime - Whether user-runtime.js exists and should be loaded */ -function generateRuntimeHtml(importMap: Record, modules: ModuleManifest[]): string { +function generateRuntimeHtml(importMap: Record, modules: ModuleManifest[], hasUserRuntime: boolean = false): string { const importMapScript = ``; @@ -45,6 +49,44 @@ function generateRuntimeHtml(importMap: Record, modules: ModuleM }` ).join('\n'); + // Generate user runtime loading code + // 生成用户运行时加载代码 + const userRuntimeCode = hasUserRuntime ? ` + updateLoading('Loading user scripts...'); + try { + // Import ECS framework and set up global for user-runtime.js shim + // 导入 ECS 框架并为 user-runtime.js 设置全局变量 + const ecsFramework = await import('@esengine/ecs-framework'); + window.__ESENGINE__ = window.__ESENGINE__ || {}; + window.__ESENGINE__.ecsFramework = ecsFramework; + + // Load user-runtime.js which contains compiled user components + // 加载 user-runtime.js,其中包含编译的用户组件 + const userRuntimeScript = document.createElement('script'); + userRuntimeScript.src = './user-runtime.js?_=' + Date.now(); + await new Promise((resolve, reject) => { + userRuntimeScript.onload = resolve; + userRuntimeScript.onerror = reject; + document.head.appendChild(userRuntimeScript); + }); + + // Register user components to ComponentRegistry + // 将用户组件注册到 ComponentRegistry + if (window.__USER_RUNTIME_EXPORTS__) { + const { ComponentRegistry, Component } = ecsFramework; + const exports = window.__USER_RUNTIME_EXPORTS__; + for (const [name, exported] of Object.entries(exports)) { + if (typeof exported === 'function' && exported.prototype instanceof Component) { + ComponentRegistry.register(exported); + console.log('[Preview] Registered user component:', name); + } + } + } + } catch (e) { + console.warn('[Preview] Failed to load user scripts:', e.message); + } +` : ''; + return ` @@ -136,7 +178,7 @@ ${importMapScript} ${pluginImportCode} await runtime.initialize(wasmModule); - +${userRuntimeCode} updateLoading('Loading scene...'); await runtime.loadScene('./scene.json?_=' + Date.now()); @@ -681,9 +723,9 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { // Save editor camera state editorCameraRef.current = { x: camera2DOffset.x, y: camera2DOffset.y, zoom: camera2DZoom }; setPlayState('playing'); - // Hide grid and gizmos in play mode - EngineService.getInstance().setShowGrid(false); - EngineService.getInstance().setShowGizmos(false); + // Disable editor mode (hides grid, gizmos, axis indicator) + // 禁用编辑器模式(隐藏网格、gizmos、坐标轴指示器) + EngineService.getInstance().setEditorMode(false); // Switch to player camera syncPlayerCamera(); engine.start(); @@ -708,9 +750,9 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { // Restore editor camera state setCamera2DOffset({ x: editorCameraRef.current.x, y: editorCameraRef.current.y }); setCamera2DZoom(editorCameraRef.current.zoom); - // Restore grid and gizmos - EngineService.getInstance().setShowGrid(showGrid); - EngineService.getInstance().setShowGizmos(showGizmos); + // Restore editor mode (restores grid, gizmos, axis indicator based on settings) + // 恢复编辑器模式(根据设置恢复网格、gizmos、坐标轴指示器) + EngineService.getInstance().setEditorMode(true); // Restore editor default background color EngineService.getInstance().setClearColor(0.1, 0.1, 0.12, 1.0); }; @@ -888,8 +930,21 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { await TauriAPI.writeFileContent(`${runtimeDir}/asset-catalog.json`, JSON.stringify(assetCatalog, null, 2)); console.log(`[Viewport] Asset catalog created with ${Object.keys(catalogEntries).length} entries`); + // Copy user-runtime.js if it exists + // 如果存在用户运行时,复制 user-runtime.js + let hasUserRuntime = false; + if (projectPath) { + const userRuntimePath = `${projectPath}\\.esengine\\compiled\\user-runtime.js`; + const userRuntimeExists = await TauriAPI.pathExists(userRuntimePath); + if (userRuntimeExists) { + await TauriAPI.copyFile(userRuntimePath, `${runtimeDir}\\user-runtime.js`); + console.log('[Viewport] Copied user-runtime.js'); + hasUserRuntime = true; + } + } + // Generate HTML with import maps (matching published build structure) - const runtimeHtml = generateRuntimeHtml(importMap, modules); + const runtimeHtml = generateRuntimeHtml(importMap, modules, hasUserRuntime); await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml); // Start local server and open browser @@ -954,10 +1009,26 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { } } - // Write scene data and HTML with import maps + // Write scene data const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData); await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr); - await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, generateRuntimeHtml(importMap, modules)); + + // Copy user-runtime.js if it exists + // 如果存在用户运行时,复制 user-runtime.js + let hasUserRuntime = false; + const currentProject = projectService?.getCurrentProject(); + if (currentProject?.path) { + const userRuntimePath = `${currentProject.path}\\.esengine\\compiled\\user-runtime.js`; + const userRuntimeExists = await TauriAPI.pathExists(userRuntimePath); + if (userRuntimeExists) { + await TauriAPI.copyFile(userRuntimePath, `${runtimeDir}\\user-runtime.js`); + console.log('[Viewport] Copied user-runtime.js for device preview'); + hasUserRuntime = true; + } + } + + // Write HTML with import maps + await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, generateRuntimeHtml(importMap, modules, hasUserRuntime)); // Copy textures referenced in scene const assetsDir = `${runtimeDir}\\assets`; diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts index ec173cd8..2eb8d692 100644 --- a/packages/editor-app/src/services/EngineService.ts +++ b/packages/editor-app/src/services/EngineService.ts @@ -612,6 +612,26 @@ export class EngineService { return this._runtime?.renderSystem?.getShowGizmos() ?? true; } + /** + * Set editor mode. + * 设置编辑器模式。 + * + * When false (runtime mode), editor-only UI like grid, gizmos, + * and axis indicator are automatically hidden. + * 当为 false(运行时模式)时,编辑器专用 UI 会自动隐藏。 + */ + setEditorMode(isEditor: boolean): void { + this._runtime?.setEditorMode(isEditor); + } + + /** + * Get editor mode. + * 获取编辑器模式。 + */ + isEditorMode(): boolean { + return this._runtime?.isEditorMode() ?? true; + } + /** * Set UI canvas size for boundary display. */ diff --git a/packages/editor-app/vite.config.ts b/packages/editor-app/vite.config.ts index c8a6c266..14105264 100644 --- a/packages/editor-app/vite.config.ts +++ b/packages/editor-app/vite.config.ts @@ -213,6 +213,15 @@ function copyEngineModulesPlugin(): Plugin { if (fs.existsSync(sourceMapPath)) { fs.copyFileSync(sourceMapPath, path.join(moduleOutputDir, 'index.js.map')); } + // Copy type definitions if exists + // 复制类型定义文件(如果存在) + // Handle both .js and .mjs extensions + // 处理 .js 和 .mjs 两种扩展名 + const distDir = path.dirname(module.distPath); + const dtsPath = path.join(distDir, 'index.d.ts'); + if (fs.existsSync(dtsPath)) { + fs.copyFileSync(dtsPath, path.join(moduleOutputDir, 'index.d.ts')); + } hasRuntime = true; // Copy additional included files (e.g., chunks) diff --git a/packages/editor-core/src/Services/SceneManagerService.ts b/packages/editor-core/src/Services/SceneManagerService.ts index 12ebef18..38cdc891 100644 --- a/packages/editor-core/src/Services/SceneManagerService.ts +++ b/packages/editor-core/src/Services/SceneManagerService.ts @@ -62,6 +62,10 @@ export class SceneManagerService implements IService { throw new Error('No active scene'); } + // 确保编辑器模式下设置 isEditorMode,延迟组件生命周期回调 + // Ensure isEditorMode is set in editor to defer component lifecycle callbacks + scene.isEditorMode = true; + // 只移除实体,保留系统(系统由模块管理) // Only remove entities, preserve systems (systems managed by modules) scene.entities.removeAllEntities(); @@ -117,6 +121,11 @@ export class SceneManagerService implements IService { if (!scene) { throw new Error('No active scene'); } + + // 确保编辑器模式下设置 isEditorMode,延迟组件生命周期回调 + // Ensure isEditorMode is set in editor to defer component lifecycle callbacks + scene.isEditorMode = true; + scene.deserialize(jsonData, { strategy: 'replace' }); diff --git a/packages/editor-core/src/Services/UserCode/UserCodeService.ts b/packages/editor-core/src/Services/UserCode/UserCodeService.ts index 5242a540..9eeb9fa3 100644 --- a/packages/editor-core/src/Services/UserCode/UserCodeService.ts +++ b/packages/editor-core/src/Services/UserCode/UserCodeService.ts @@ -7,7 +7,7 @@ */ import type { IService } from '@esengine/ecs-framework'; -import { Injectable, createLogger, PlatformDetector } from '@esengine/ecs-framework'; +import { Injectable, createLogger, PlatformDetector, ComponentRegistry as CoreComponentRegistry } from '@esengine/ecs-framework'; import type { IUserCodeService, UserScriptInfo, @@ -333,9 +333,23 @@ export class UserCodeService implements IService, IUserCodeService { if (this._isComponentClass(exported)) { logger.debug(`Found component: ${name} | 发现组件: ${name}`); - // Register with ComponentRegistry if provided | 如果提供了 ComponentRegistry 则注册 - // ComponentRegistry expects ComponentTypeInfo object, not the class directly - // ComponentRegistry 期望 ComponentTypeInfo 对象,而不是直接传入类 + // Register with Core ComponentRegistry for serialization/deserialization + // 注册到核心 ComponentRegistry 用于序列化/反序列化 + try { + CoreComponentRegistry.register(exported); + // Debug: verify registration + const registeredType = CoreComponentRegistry.getComponentType(name); + if (registeredType) { + logger.info(`Component ${name} registered to core registry successfully`); + } else { + logger.warn(`Component ${name} registered but not found by name lookup`); + } + } catch (err) { + logger.warn(`Failed to register component ${name} to core registry | 注册组件 ${name} 到核心注册表失败:`, err); + } + + // Register with Editor ComponentRegistry for UI display + // 注册到编辑器 ComponentRegistry 用于 UI 显示 if (componentRegistry && typeof componentRegistry.register === 'function') { try { componentRegistry.register({ @@ -345,7 +359,7 @@ export class UserCodeService implements IService, IUserCodeService { description: `User component: ${name}` }); } catch (err) { - logger.warn(`Failed to register component ${name} | 注册组件 ${name} 失败:`, err); + logger.warn(`Failed to register component ${name} to editor registry | 注册组件 ${name} 到编辑器注册表失败:`, err); } } diff --git a/packages/engine/src/core/engine.rs b/packages/engine/src/core/engine.rs index c0153157..4ef90ff2 100644 --- a/packages/engine/src/core/engine.rs +++ b/packages/engine/src/core/engine.rs @@ -77,6 +77,14 @@ pub struct Engine { /// Whether to show gizmos. /// 是否显示辅助工具。 show_gizmos: bool, + + /// Whether the engine is running in editor mode. + /// 引擎是否在编辑器模式下运行。 + /// + /// When false (runtime mode), editor-only UI like grid, gizmos, + /// and axis indicator are automatically hidden. + /// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。 + is_editor: bool, } impl Engine { @@ -116,6 +124,7 @@ impl Engine { show_grid: true, viewport_manager: ViewportManager::new(), show_gizmos: true, + is_editor: true, // 默认为编辑器模式 | Default to editor mode }) } @@ -154,6 +163,7 @@ impl Engine { show_grid: true, viewport_manager: ViewportManager::new(), show_gizmos: true, + is_editor: true, // 默认为编辑器模式 | Default to editor mode }) } @@ -212,8 +222,9 @@ impl Engine { let [r, g, b, a] = self.renderer.get_clear_color(); self.context.clear(r, g, b, a); - // Render grid first (background) - if self.show_grid { + // Render grid first (background) - only in editor mode + // 首先渲染网格(背景)- 仅在编辑器模式下 + if self.is_editor && self.show_grid { self.grid_renderer.render(self.context.gl(), self.renderer.camera()); self.grid_renderer.render_axes(self.context.gl(), self.renderer.camera()); } @@ -221,8 +232,9 @@ impl Engine { // Render sprites self.renderer.render(self.context.gl(), &self.texture_manager)?; - // Render gizmos on top - if self.show_gizmos { + // Render gizmos on top - only in editor mode + // 在顶部渲染 gizmos - 仅在编辑器模式下 + if self.is_editor && self.show_gizmos { self.gizmo_renderer.render(self.context.gl(), self.renderer.camera()); // Render axis indicator in corner // 在角落渲染坐标轴指示器 @@ -411,6 +423,23 @@ impl Engine { self.show_gizmos } + /// Set editor mode. + /// 设置编辑器模式。 + /// + /// When false (runtime mode), editor-only UI like grid, gizmos, + /// and axis indicator are automatically hidden regardless of their individual settings. + /// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器) + /// 会自动隐藏,无论它们的单独设置如何。 + pub fn set_editor_mode(&mut self, is_editor: bool) { + self.is_editor = is_editor; + } + + /// Get editor mode. + /// 获取编辑器模式。 + pub fn is_editor(&self) -> bool { + self.is_editor + } + /// Set clear color for the active viewport. /// 设置活动视口的清除颜色。 pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) { @@ -504,8 +533,9 @@ impl Engine { renderer_camera.rotation = camera.rotation; renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height()); - // Render grid if enabled - if show_grid { + // Render grid if enabled - only in editor mode + // 渲染网格(如果启用)- 仅在编辑器模式下 + if self.is_editor && show_grid { self.grid_renderer.render(viewport.gl(), &camera); self.grid_renderer.render_axes(viewport.gl(), &camera); } @@ -513,8 +543,9 @@ impl Engine { // Render sprites self.renderer.render(viewport.gl(), &self.texture_manager)?; - // Render gizmos if enabled - if show_gizmos { + // Render gizmos if enabled - only in editor mode + // 渲染 gizmos(如果启用)- 仅在编辑器模式下 + if self.is_editor && show_gizmos { self.gizmo_renderer.render(viewport.gl(), &camera); // Render axis indicator in corner // 在角落渲染坐标轴指示器 diff --git a/packages/engine/src/lib.rs b/packages/engine/src/lib.rs index 362a3fa3..ae55ffea 100644 --- a/packages/engine/src/lib.rs +++ b/packages/engine/src/lib.rs @@ -390,6 +390,24 @@ impl GameEngine { self.engine.set_show_gizmos(show); } + /// Set editor mode. + /// 设置编辑器模式。 + /// + /// When false (runtime mode), editor-only UI like grid, gizmos, + /// and axis indicator are automatically hidden. + /// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。 + #[wasm_bindgen(js_name = setEditorMode)] + pub fn set_editor_mode(&mut self, is_editor: bool) { + self.engine.set_editor_mode(is_editor); + } + + /// Get editor mode. + /// 获取编辑器模式。 + #[wasm_bindgen(js_name = isEditorMode)] + pub fn is_editor_mode(&self) -> bool { + self.engine.is_editor() + } + // ===== Multi-viewport API ===== // ===== 多视口 API ===== diff --git a/packages/platform-web/src/BrowserRuntime.ts b/packages/platform-web/src/BrowserRuntime.ts index 9771f421..dec80e5e 100644 --- a/packages/platform-web/src/BrowserRuntime.ts +++ b/packages/platform-web/src/BrowserRuntime.ts @@ -138,9 +138,9 @@ export class BrowserRuntime { this._runtime.assetManager.setReader(this._assetReader); } - // Browser-specific settings (no editor UI) - this._runtime.setShowGrid(false); - this._runtime.setShowGizmos(false); + // Disable editor mode (hides grid, gizmos, axis indicator) + // 禁用编辑器模式(隐藏网格、gizmos、坐标轴指示器) + this._runtime.setEditorMode(false); this._initialized = true; console.log('[Runtime] Initialized'); diff --git a/packages/runtime-core/src/GameRuntime.ts b/packages/runtime-core/src/GameRuntime.ts index ce1b5e9a..7c253371 100644 --- a/packages/runtime-core/src/GameRuntime.ts +++ b/packages/runtime-core/src/GameRuntime.ts @@ -218,6 +218,12 @@ export class GameRuntime { Core.setScene(this._scene); } + // 编辑器模式下设置 isEditorMode,延迟组件生命周期回调 + // Set isEditorMode in editor mode to defer component lifecycle callbacks + if (this._platform.isEditorMode()) { + this._scene.isEditorMode = true; + } + // 6. 添加基础系统 this._scene.addSystem(new HierarchySystem()); this._scene.addSystem(new TransformSystem()); @@ -402,6 +408,12 @@ export class GameRuntime { this._renderSystem.setPreviewMode(true); } + // 调用场景 begin() 触发延迟的组件生命周期回调 + // Call scene begin() to trigger deferred component lifecycle callbacks + if (this._scene) { + this._scene.begin(); + } + // 启用游戏逻辑系统 this._enableGameLogicSystems(); @@ -576,6 +588,31 @@ export class GameRuntime { } } + /** + * 设置编辑器模式 + * Set editor mode + * + * When false (runtime mode), editor-only UI like grid, gizmos, + * and axis indicator are automatically hidden. + * 当为 false(运行时模式)时,编辑器专用 UI 会自动隐藏。 + */ + setEditorMode(isEditor: boolean): void { + if (this._bridge) { + this._bridge.setEditorMode(isEditor); + } + } + + /** + * 获取编辑器模式 + * Get editor mode + */ + isEditorMode(): boolean { + if (this._bridge) { + return this._bridge.isEditorMode(); + } + return true; + } + /** * 设置清除颜色 * Set clear color