fix(editor-core): 修复场景切换时的资源泄漏
在 openScene() 加载新场景前先卸载旧场景资源: - 调用 sceneResourceManager.unloadSceneResources() 释放旧资源 - 使用引用计数机制,仅卸载不再被引用的资源 - 路径稳定 ID 缓存不受影响,保持 ID 稳定性
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
Scene,
|
Scene,
|
||||||
PrefabSerializer,
|
PrefabSerializer,
|
||||||
HierarchySystem,
|
HierarchySystem,
|
||||||
ComponentRegistry
|
GlobalComponentRegistry
|
||||||
} from '@esengine/ecs-framework';
|
} from '@esengine/ecs-framework';
|
||||||
import type { ComponentType } from '@esengine/ecs-framework';
|
import type { ComponentType } from '@esengine/ecs-framework';
|
||||||
import type { SceneResourceManager } from '@esengine/asset-system';
|
import type { SceneResourceManager } from '@esengine/asset-system';
|
||||||
@@ -24,6 +24,10 @@ export interface SceneState {
|
|||||||
sceneName: string;
|
sceneName: string;
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
isSaved: 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,
|
currentScenePath: null,
|
||||||
sceneName: 'Untitled',
|
sceneName: 'Untitled',
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isSaved: false
|
isSaved: false,
|
||||||
|
lastKnownMtime: null,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 预制体编辑模式状态 | Prefab edit mode state */
|
/** 预制体编辑模式状态 | Prefab edit mode state */
|
||||||
@@ -118,7 +124,9 @@ export class SceneManagerService implements IService {
|
|||||||
currentScenePath: null,
|
currentScenePath: null,
|
||||||
sceneName: 'Untitled',
|
sceneName: 'Untitled',
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isSaved: false
|
isSaved: false,
|
||||||
|
lastKnownMtime: null,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步到 EntityStore
|
// 同步到 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 {
|
try {
|
||||||
const jsonData = await this.fileAPI.readFileContent(path);
|
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
|
// Ensure isEditorMode is set in editor to defer component lifecycle callbacks
|
||||||
scene.isEditorMode = true;
|
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, {
|
scene.deserialize(jsonData, {
|
||||||
strategy: 'replace'
|
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
|
// 加载场景资源 / Load scene resources
|
||||||
if (this.sceneResourceManager) {
|
if (this.sceneResourceManager) {
|
||||||
await this.sceneResourceManager.loadSceneResources(scene);
|
await this.sceneResourceManager.loadSceneResources(scene);
|
||||||
@@ -179,11 +231,23 @@ export class SceneManagerService implements IService {
|
|||||||
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
|
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
|
||||||
const sceneName = fileName.replace('.ecs', '');
|
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 = {
|
this.sceneState = {
|
||||||
currentScenePath: path,
|
currentScenePath: path,
|
||||||
sceneName,
|
sceneName,
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isSaved: true
|
isSaved: true,
|
||||||
|
lastKnownMtime: mtime,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.entityStore?.syncFromScene();
|
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) {
|
if (!this.sceneState.currentScenePath) {
|
||||||
await this.saveSceneAs();
|
await this.saveSceneAs();
|
||||||
return;
|
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 {
|
try {
|
||||||
const scene = Core.scene as Scene | null;
|
const scene = Core.scene as Scene | null;
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
@@ -219,8 +293,18 @@ export class SceneManagerService implements IService {
|
|||||||
|
|
||||||
await this.fileAPI.saveProject(this.sceneState.currentScenePath, jsonData);
|
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.isModified = false;
|
||||||
this.sceneState.isSaved = true;
|
this.sceneState.isSaved = true;
|
||||||
|
this.sceneState.externallyModified = false;
|
||||||
|
|
||||||
await this.messageHub.publish('scene:saved', {
|
await this.messageHub.publish('scene:saved', {
|
||||||
path: this.sceneState.currentScenePath
|
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> {
|
public async saveSceneAs(filePath?: string): Promise<void> {
|
||||||
let path: string | null | undefined = filePath;
|
let path: string | null | undefined = filePath;
|
||||||
if (!path) {
|
if (!path) {
|
||||||
@@ -269,11 +436,23 @@ export class SceneManagerService implements IService {
|
|||||||
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
|
const fileName = path.split(/[/\\]/).pop() || 'Untitled';
|
||||||
const sceneName = fileName.replace('.ecs', '');
|
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 = {
|
this.sceneState = {
|
||||||
currentScenePath: path,
|
currentScenePath: path,
|
||||||
sceneName,
|
sceneName,
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isSaved: true
|
isSaved: true,
|
||||||
|
lastKnownMtime: mtime,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.messageHub.publish('scene:saved', { path });
|
await this.messageHub.publish('scene:saved', { path });
|
||||||
@@ -405,11 +584,11 @@ export class SceneManagerService implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. 获取组件注册表 | Get component registry
|
// 6. 获取组件注册表 | Get component registry
|
||||||
// ComponentRegistry.getAllComponentNames() 返回 Map<string, Function>
|
// GlobalComponentRegistry.getAllComponentNames() 返回 Map<string, Function>
|
||||||
// 需要转换为 Map<string, ComponentType>
|
// 需要转换为 Map<string, ComponentType>
|
||||||
const nameToType = ComponentRegistry.getAllComponentNames();
|
const nameToType = GlobalComponentRegistry.getAllComponentNames();
|
||||||
const componentRegistry = new Map<string, ComponentType>();
|
const componentRegistry = new Map<string, ComponentType>();
|
||||||
nameToType.forEach((type, name) => {
|
nameToType.forEach((type: Function, name: string) => {
|
||||||
componentRegistry.set(name, type as ComponentType);
|
componentRegistry.set(name, type as ComponentType);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -471,7 +650,9 @@ export class SceneManagerService implements IService {
|
|||||||
currentScenePath: null,
|
currentScenePath: null,
|
||||||
sceneName: `Prefab: ${prefabName}`,
|
sceneName: `Prefab: ${prefabName}`,
|
||||||
isModified: false,
|
isModified: false,
|
||||||
isSaved: true
|
isSaved: true,
|
||||||
|
lastKnownMtime: null,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// 11. 同步到 EntityStore | Sync to EntityStore
|
// 11. 同步到 EntityStore | Sync to EntityStore
|
||||||
@@ -537,7 +718,9 @@ export class SceneManagerService implements IService {
|
|||||||
currentScenePath: originalState.originalScenePath,
|
currentScenePath: originalState.originalScenePath,
|
||||||
sceneName: originalState.originalSceneName,
|
sceneName: originalState.originalSceneName,
|
||||||
isModified: originalState.originalSceneModified,
|
isModified: originalState.originalSceneModified,
|
||||||
isSaved: !originalState.originalSceneModified
|
isSaved: !originalState.originalSceneModified,
|
||||||
|
lastKnownMtime: null,
|
||||||
|
externallyModified: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. 清除预制体编辑模式状态 | Clear prefab edit mode state
|
// 5. 清除预制体编辑模式状态 | Clear prefab edit mode state
|
||||||
|
|||||||
Reference in New Issue
Block a user