fix(editor): 修复 Play/Stop 循环中的场景管理器和动态实体问题 (#307)

问题修复:
1. RuntimeSceneManager 在 Stop 后失效
   - 根因:SceneLoadTriggerSystem 闭包缓存了 sceneManager 引用
   - 修复:每次点击时动态从 Core.services 获取服务

2. Play 期间创建的动态实体(如 ClickFx 粒子)Stop 后残留
   - 根因:EntityList.removeAllEntities() 只清空 _entitiesToAdd 队列但没有销毁实体
   - 修复:先销毁待添加队列中的实体再清空

3. 场景切换后动态实体残留
   - 根因:editorSceneLoader 中 saveSceneSnapshot() 覆盖了初始快照
   - 修复:移除该调用,保持 Play 开始时的快照不被覆盖

架构改进:
- RuntimeSceneManager 新增 reset() 方法,区分会话重置和完全销毁
- Viewport 复用 RuntimeSceneManager 实例而非每次创建
- IRuntimeSceneManager 接口补充 setSceneLoader/setBaseUrl 方法
This commit is contained in:
YHH
2025-12-16 15:07:11 +08:00
committed by GitHub
parent a18eb5aa3c
commit 9e195ae3fd
4 changed files with 172 additions and 151 deletions

View File

@@ -841,54 +841,51 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
// Switch to player camera
syncPlayerCamera();
// Register RuntimeSceneManager for scene switching in play mode
// 注册 RuntimeSceneManager 以支持 Play 模式下的场景切换
// 设置 RuntimeSceneManager 用于 Play 模式场景切换
// Setup RuntimeSceneManager for scene switching in play mode
//
// 生命周期设计:
// - 首次 Play创建新实例
// - 后续 Play复用实例只更新 sceneLoader
// - Stop调用 reset() 清理会话状态,保留实例
//
// Lifecycle design:
// - First Play: Create new instance
// - Subsequent Plays: Reuse instance, only update sceneLoader
// - Stop: Call reset() to clear session state, keep instance
const projectService = Core.services.tryResolve(ProjectService);
const projectPath = projectService?.getCurrentProject()?.path;
if (projectPath) {
// Create scene loader function that reads scene files using Tauri API
// 创建使用 Tauri API 读取场景文件的场景加载器函数
// 创建场景加载函数
// Create scene loader function
const editorSceneLoader = async (scenePath: string): Promise<void> => {
try {
// Normalize path: handle both relative and absolute paths
// 标准化路径:处理相对路径和绝对路径
let fullPath = scenePath;
if (!scenePath.includes(':') && !scenePath.startsWith('/')) {
// Relative path - construct full path
// 相对路径 - 构建完整路径
const normalizedPath = scenePath.replace(/^\.\//, '').replace(/\//g, '\\');
fullPath = `${projectPath}\\${normalizedPath}`;
} else {
// Absolute path - normalize separators for Windows
// 绝对路径 - 为 Windows 规范化分隔符
fullPath = scenePath.replace(/\//g, '\\');
}
// Read scene file content
// 读取场景文件内容
const sceneJson = await TauriAPI.readFileContent(fullPath);
// Validate scene data
// 验证场景数据
const validation = SceneSerializer.validate(sceneJson);
if (!validation.valid) {
throw new Error(`Invalid scene: ${validation.errors?.join(', ')}`);
}
// Save current scene snapshot (so we can go back)
// 保存当前场景快照(以便返回)
EngineService.getInstance().saveSceneSnapshot();
// 注意:不要在这里保存快照!
// 初始快照在 Play 开始时已保存Stop 时应恢复到那个状态
// 如果在这里保存,会覆盖初始快照,导致动态创建的实体残留
//
// Note: Don't save snapshot here!
// Initial snapshot was saved at Play start, Stop should restore to that state
// Saving here would overwrite initial snapshot, causing dynamically created entities to remain
// Load new scene by deserializing into current scene
// 通过反序列化加载新场景到当前场景
const scene = Core.scene;
if (scene) {
scene.deserialize(sceneJson, { strategy: 'replace' });
// Reset particle component textureIds after scene switch
// 场景切换后重置粒子组件的 textureId
// This ensures ParticleUpdateSystem will reload textures
// 这确保 ParticleUpdateSystem 会重新加载纹理
for (const entity of scene.entities.buffer) {
const particleComponent = entity.getComponent(ParticleSystemComponent);
if (particleComponent) {
@@ -896,28 +893,17 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
}
}
// Re-register user code components and systems after scene switch
// 场景切换后重新注册用户代码组件和系统
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
const runtimeModule = userCodeService.getModule(UserCodeTarget.Runtime);
if (runtimeModule) {
// Re-register components (ensures GlobalComponentRegistry has correct references)
// 重新注册组件(确保 GlobalComponentRegistry 有正确的引用)
userCodeService.registerComponents(runtimeModule, GlobalComponentRegistry);
// Re-register systems (recreates systems with correct component references)
// 重新注册系统(使用正确的组件引用重建系统)
userCodeService.registerSystems(runtimeModule, scene);
}
}
// Load scene resources (textures, etc.)
// 加载场景资源(纹理等)
await EngineService.getInstance().loadSceneResources();
// Sync entity store
// 同步实体存储
const entityStore = Core.services.tryResolve(EntityStoreService);
entityStore?.syncFromScene();
}
@@ -929,22 +915,35 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
}
};
// Create and register RuntimeSceneManager
// 创建并注册 RuntimeSceneManager
const sceneManager = new RuntimeSceneManager(
editorSceneLoader,
`${projectPath}\\scenes`
);
runtimeSceneManagerRef.current = sceneManager;
// Register to Core.services with the global key
// 使用全局 key 注册到 Core.services
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, sceneManager);
}
console.log('[Viewport] RuntimeSceneManager registered for play mode');
if (runtimeSceneManagerRef.current) {
// 复用已有实例:重置状态并更新 sceneLoader
// Reuse existing instance: reset state and update sceneLoader
runtimeSceneManagerRef.current.reset();
runtimeSceneManagerRef.current.setSceneLoader(editorSceneLoader);
runtimeSceneManagerRef.current.setBaseUrl(`${projectPath}\\scenes`);
// 确保已注册到服务容器
// Ensure registered to service container
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, runtimeSceneManagerRef.current);
}
console.log('[Viewport] RuntimeSceneManager reused for play mode');
} else {
// 首次创建实例
// First time: create new instance
const sceneManager = new RuntimeSceneManager(
editorSceneLoader,
`${projectPath}\\scenes`
);
runtimeSceneManagerRef.current = sceneManager;
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, sceneManager);
}
console.log('[Viewport] RuntimeSceneManager created for play mode');
}
}
// Register user code components and systems before starting engine
@@ -984,16 +983,19 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
setPlayState('stopped');
engine.stop();
// Unregister RuntimeSceneManager
// 注销 RuntimeSceneManager
// 重置 RuntimeSceneManager 状态(但保留实例以便下次 Play 复用)
// Reset RuntimeSceneManager state (but keep instance for next Play reuse)
if (runtimeSceneManagerRef.current) {
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.unregister(GlobalSceneManagerKey);
}
runtimeSceneManagerRef.current.dispose();
runtimeSceneManagerRef.current = null;
console.log('[Viewport] RuntimeSceneManager unregistered');
// 调用 reset() 而不是 dispose(),保留 sceneLoader 以便复用
// Call reset() instead of dispose(), keep sceneLoader for reuse
runtimeSceneManagerRef.current.reset();
// 注意:不设置 ref 为 null保留实例
// Note: Don't set ref to null, keep the instance
console.log('[Viewport] RuntimeSceneManager reset (instance kept for reuse)');
}
// Restore scene snapshot