fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性
This commit is contained in:
yhh
2025-12-16 11:07:48 +08:00
parent 5d5537e4c7
commit 38755c9014

View File

@@ -7,7 +7,7 @@ import {
Scene,
PrefabSerializer,
HierarchySystem,
ComponentRegistry
GlobalComponentRegistry
} from '@esengine/ecs-framework';
import type { ComponentType } from '@esengine/ecs-framework';
import type { SceneResourceManager } from '@esengine/asset-system';
@@ -24,6 +24,10 @@ export interface SceneState {
sceneName: string;
isModified: boolean;
isSaved: boolean;
/** 文件最后已知的修改时间(毫秒)| Last known file modification time (ms) */
lastKnownMtime: number | null;
/** 文件是否被外部修改 | Whether file was modified externally */
externallyModified: boolean;
}
/**
@@ -55,7 +59,9 @@ export class SceneManagerService implements IService {
currentScenePath: null,
sceneName: 'Untitled',
isModified: false,
isSaved: false
isSaved: false,
lastKnownMtime: null,
externallyModified: false
};
/** 预制体编辑模式状态 | Prefab edit mode state */
@@ -118,7 +124,9 @@ export class SceneManagerService implements IService {
currentScenePath: null,
sceneName: 'Untitled',
isModified: false,
isSaved: false
isSaved: false,
lastKnownMtime: null,
externallyModified: false
};
// 同步到 EntityStore
@@ -148,6 +156,18 @@ export class SceneManagerService implements IService {
}
}
// 在加载新场景前,清理旧场景的纹理映射(释放 GPU 资源)
// Before loading new scene, clear old scene's texture mappings (release GPU resources)
// 注意:路径稳定 ID 缓存 (_pathIdCache) 不会被清除
// Note: Path-stable ID cache (_pathIdCache) is NOT cleared
if (this.sceneResourceManager) {
const oldScene = Core.scene as Scene | null;
if (oldScene && this.sceneState.currentScenePath) {
logger.info(`[openScene] Unloading old scene resources from: ${this.sceneState.currentScenePath}`);
await this.sceneResourceManager.unloadSceneResources(oldScene);
}
}
try {
const jsonData = await this.fileAPI.readFileContent(path);
@@ -165,10 +185,42 @@ export class SceneManagerService implements IService {
// Ensure isEditorMode is set in editor to defer component lifecycle callbacks
scene.isEditorMode = true;
// 调试:检查缺失的组件类型 | Debug: check missing component types
const registeredComponents = GlobalComponentRegistry.getAllComponentNames();
try {
const sceneData = JSON.parse(jsonData);
const requiredTypes = new Set<string>();
for (const entity of sceneData.entities || []) {
for (const comp of entity.components || []) {
requiredTypes.add(comp.type);
}
}
// 检查缺失的组件类型 | Check missing component types
const missingTypes = Array.from(requiredTypes).filter(t => !registeredComponents.has(t));
if (missingTypes.length > 0) {
logger.warn(`[SceneManagerService.openScene] Missing component types (scene will load without these):`, missingTypes);
logger.debug(`Registered components (${registeredComponents.size}):`, Array.from(registeredComponents.keys()));
}
} catch (e) {
// JSON parsing should not fail at this point since we validated earlier
}
// 调试:反序列化前场景状态 | Debug: scene state before deserialize
logger.info(`[openScene] Before deserialize: entities.count = ${scene.entities.count}`);
scene.deserialize(jsonData, {
strategy: 'replace'
});
// 调试:反序列化后场景状态 | Debug: scene state after deserialize
logger.info(`[openScene] After deserialize: entities.count = ${scene.entities.count}`);
if (scene.entities.count > 0) {
const entityNames: string[] = [];
scene.entities.forEach(e => entityNames.push(e.name));
logger.info(`[openScene] Entity names: ${entityNames.join(', ')}`);
}
// 加载场景资源 / Load scene resources
if (this.sceneResourceManager) {
await this.sceneResourceManager.loadSceneResources(scene);
@@ -179,11 +231,23 @@ export class SceneManagerService implements IService {
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
const sceneName = fileName.replace('.ecs', '');
// 获取文件修改时间 | Get file modification time
let mtime: number | null = null;
if (this.fileAPI.getFileMtime) {
try {
mtime = await this.fileAPI.getFileMtime(path);
} catch (e) {
logger.warn('Failed to get file mtime:', e);
}
}
this.sceneState = {
currentScenePath: path,
sceneName,
isModified: false,
isSaved: true
isSaved: true,
lastKnownMtime: mtime,
externallyModified: false
};
this.entityStore?.syncFromScene();
@@ -200,12 +264,22 @@ export class SceneManagerService implements IService {
}
}
public async saveScene(): Promise<void> {
public async saveScene(force: boolean = false): Promise<void> {
if (!this.sceneState.currentScenePath) {
await this.saveSceneAs();
return;
}
// 检查文件是否被外部修改 | Check if file was modified externally
if (!force && await this.checkExternalModification()) {
// 发布事件让 UI 显示确认对话框 | Publish event for UI to show confirmation dialog
await this.messageHub.publish('scene:externalModification', {
path: this.sceneState.currentScenePath,
sceneName: this.sceneState.sceneName
});
return; // 等待用户确认 | Wait for user confirmation
}
try {
const scene = Core.scene as Scene | null;
if (!scene) {
@@ -219,8 +293,18 @@ export class SceneManagerService implements IService {
await this.fileAPI.saveProject(this.sceneState.currentScenePath, jsonData);
// 更新 mtime | Update mtime
if (this.fileAPI.getFileMtime) {
try {
this.sceneState.lastKnownMtime = await this.fileAPI.getFileMtime(this.sceneState.currentScenePath);
} catch (e) {
logger.warn('Failed to update file mtime after save:', e);
}
}
this.sceneState.isModified = false;
this.sceneState.isSaved = true;
this.sceneState.externallyModified = false;
await this.messageHub.publish('scene:saved', {
path: this.sceneState.currentScenePath
@@ -232,6 +316,89 @@ export class SceneManagerService implements IService {
}
}
/**
* 检查场景文件是否被外部修改
* Check if scene file was modified externally
*
* @returns true 如果文件被外部修改 | true if file was modified externally
*/
public async checkExternalModification(): Promise<boolean> {
const path = this.sceneState.currentScenePath;
const lastMtime = this.sceneState.lastKnownMtime;
if (!path || lastMtime === null || !this.fileAPI.getFileMtime) {
return false;
}
try {
const currentMtime = await this.fileAPI.getFileMtime(path);
const isModified = currentMtime > lastMtime;
if (isModified) {
this.sceneState.externallyModified = true;
logger.warn(`Scene file externally modified: ${path} (${lastMtime} -> ${currentMtime})`);
}
return isModified;
} catch (e) {
logger.warn('Failed to check file mtime:', e);
return false;
}
}
/**
* 重新加载当前场景(放弃本地更改)
* Reload current scene (discard local changes)
*/
public async reloadScene(): Promise<void> {
const path = this.sceneState.currentScenePath;
if (!path) {
logger.warn('No scene to reload');
return;
}
// 强制打开场景,绕过修改检查 | Force open scene, bypass modification check
const scene = Core.scene as Scene | null;
if (!scene) {
throw new Error('No active scene');
}
try {
const jsonData = await this.fileAPI.readFileContent(path);
const validation = SceneSerializer.validate(jsonData);
if (!validation.valid) {
throw new Error(`场景文件损坏: ${validation.errors?.join(', ')}`);
}
scene.isEditorMode = true;
scene.deserialize(jsonData, { strategy: 'replace' });
if (this.sceneResourceManager) {
await this.sceneResourceManager.loadSceneResources(scene);
}
// 更新 mtime | Update mtime
if (this.fileAPI.getFileMtime) {
try {
this.sceneState.lastKnownMtime = await this.fileAPI.getFileMtime(path);
} catch (e) {
logger.warn('Failed to update file mtime after reload:', e);
}
}
this.sceneState.isModified = false;
this.sceneState.isSaved = true;
this.sceneState.externallyModified = false;
this.entityStore?.syncFromScene();
await this.messageHub.publish('scene:reloaded', { path });
logger.info(`Scene reloaded: ${path}`);
} catch (error) {
logger.error('Failed to reload scene:', error);
throw error;
}
}
public async saveSceneAs(filePath?: string): Promise<void> {
let path: string | null | undefined = filePath;
if (!path) {
@@ -269,11 +436,23 @@ export class SceneManagerService implements IService {
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
const sceneName = fileName.replace('.ecs', '');
// 获取文件修改时间 | Get file modification time
let mtime: number | null = null;
if (this.fileAPI.getFileMtime) {
try {
mtime = await this.fileAPI.getFileMtime(path);
} catch (e) {
logger.warn('Failed to get file mtime after save:', e);
}
}
this.sceneState = {
currentScenePath: path,
sceneName,
isModified: false,
isSaved: true
isSaved: true,
lastKnownMtime: mtime,
externallyModified: false
};
await this.messageHub.publish('scene:saved', { path });
@@ -405,11 +584,11 @@ export class SceneManagerService implements IService {
}
// 6. 获取组件注册表 | Get component registry
// ComponentRegistry.getAllComponentNames() 返回 Map<string, Function>
// GlobalComponentRegistry.getAllComponentNames() 返回 Map<string, Function>
// 需要转换为 Map<string, ComponentType>
const nameToType = ComponentRegistry.getAllComponentNames();
const nameToType = GlobalComponentRegistry.getAllComponentNames();
const componentRegistry = new Map<string, ComponentType>();
nameToType.forEach((type, name) => {
nameToType.forEach((type: Function, name: string) => {
componentRegistry.set(name, type as ComponentType);
});
@@ -471,7 +650,9 @@ export class SceneManagerService implements IService {
currentScenePath: null,
sceneName: `Prefab: ${prefabName}`,
isModified: false,
isSaved: true
isSaved: true,
lastKnownMtime: null,
externallyModified: false
};
// 11. 同步到 EntityStore | Sync to EntityStore
@@ -537,7 +718,9 @@ export class SceneManagerService implements IService {
currentScenePath: originalState.originalScenePath,
sceneName: originalState.originalSceneName,
isModified: originalState.originalSceneModified,
isSaved: !originalState.originalSceneModified
isSaved: !originalState.originalSceneModified,
lastKnownMtime: null,
externallyModified: false
};
// 5. 清除预制体编辑模式状态 | Clear prefab edit mode state