feat(asset-system): 完善资源加载和场景资源管理 (#289)
- 添加 AudioLoader 支持音频资源加载 (mp3/wav/ogg/m4a/flac/aac) - EngineIntegration 添加音频资源加载/卸载支持 - EngineIntegration 添加数据(JSON)资源加载/卸载支持 - SceneResourceManager 实现完整的引用计数机制 - SceneResourceManager 实现场景资源卸载,仅卸载无引用的资源 - 添加资源统计和引用计数查询接口
This commit is contained in:
@@ -40,6 +40,7 @@ export { TextureLoader } from './loaders/TextureLoader';
|
|||||||
export { JsonLoader } from './loaders/JsonLoader';
|
export { JsonLoader } from './loaders/JsonLoader';
|
||||||
export { TextLoader } from './loaders/TextLoader';
|
export { TextLoader } from './loaders/TextLoader';
|
||||||
export { BinaryLoader } from './loaders/BinaryLoader';
|
export { BinaryLoader } from './loaders/BinaryLoader';
|
||||||
|
export { AudioLoader } from './loaders/AudioLoader';
|
||||||
|
|
||||||
// Integration
|
// Integration
|
||||||
export { EngineIntegration } from './integration/EngineIntegration';
|
export { EngineIntegration } from './integration/EngineIntegration';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { AssetManager } from '../core/AssetManager';
|
import { AssetManager } from '../core/AssetManager';
|
||||||
import { AssetGUID } from '../types/AssetTypes';
|
import { AssetGUID } from '../types/AssetTypes';
|
||||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
import { ITextureAsset, IAudioAsset, IJsonAsset } from '../interfaces/IAssetLoader';
|
||||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +38,26 @@ export interface IEngineBridge {
|
|||||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
getTextureInfo(id: number): { width: number; height: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio asset with runtime ID
|
||||||
|
* 带运行时 ID 的音频资产
|
||||||
|
*/
|
||||||
|
interface AudioAssetEntry {
|
||||||
|
id: number;
|
||||||
|
asset: IAudioAsset;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data asset with runtime ID
|
||||||
|
* 带运行时 ID 的数据资产
|
||||||
|
*/
|
||||||
|
interface DataAssetEntry {
|
||||||
|
id: number;
|
||||||
|
data: unknown;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset system engine integration
|
* Asset system engine integration
|
||||||
* 资产系统引擎集成
|
* 资产系统引擎集成
|
||||||
@@ -48,6 +68,18 @@ export class EngineIntegration {
|
|||||||
private _textureIdMap = new Map<AssetGUID, number>();
|
private _textureIdMap = new Map<AssetGUID, number>();
|
||||||
private _pathToTextureId = new Map<string, number>();
|
private _pathToTextureId = new Map<string, number>();
|
||||||
|
|
||||||
|
// Audio resource mappings | 音频资源映射
|
||||||
|
private _audioIdMap = new Map<AssetGUID, number>();
|
||||||
|
private _pathToAudioId = new Map<string, number>();
|
||||||
|
private _audioAssets = new Map<number, AudioAssetEntry>();
|
||||||
|
private static _nextAudioId = 1;
|
||||||
|
|
||||||
|
// Data resource mappings | 数据资源映射
|
||||||
|
private _dataIdMap = new Map<AssetGUID, number>();
|
||||||
|
private _pathToDataId = new Map<string, number>();
|
||||||
|
private _dataAssets = new Map<number, DataAssetEntry>();
|
||||||
|
private static _nextDataId = 1;
|
||||||
|
|
||||||
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
||||||
this._assetManager = assetManager;
|
this._assetManager = assetManager;
|
||||||
this._engineBridge = engineBridge;
|
this._engineBridge = engineBridge;
|
||||||
@@ -170,14 +202,241 @@ export class EngineIntegration {
|
|||||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||||
*/
|
*/
|
||||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
||||||
// 目前只支持纹理 / Currently only supports textures
|
switch (type) {
|
||||||
if (type === 'texture') {
|
case 'texture':
|
||||||
return this.loadTexturesBatch(paths);
|
return this.loadTexturesBatch(paths);
|
||||||
|
case 'audio':
|
||||||
|
return this.loadAudioBatch(paths);
|
||||||
|
case 'data':
|
||||||
|
return this.loadDataBatch(paths);
|
||||||
|
case 'font':
|
||||||
|
// 字体资源暂未实现 / Font resources not yet implemented
|
||||||
|
console.warn('[EngineIntegration] Font resource loading not yet implemented');
|
||||||
|
return new Map();
|
||||||
|
default:
|
||||||
|
console.warn(`[EngineIntegration] Unknown resource type '${type}'`);
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Audio Resource Methods =============
|
||||||
|
// ============= 音频资源方法 =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load audio for component
|
||||||
|
* 为组件加载音频
|
||||||
|
*
|
||||||
|
* @param audioPath 音频文件路径 / Audio file path
|
||||||
|
* @returns 运行时音频 ID / Runtime audio ID
|
||||||
|
*/
|
||||||
|
async loadAudioForComponent(audioPath: string): Promise<number> {
|
||||||
|
// 检查缓存 / Check cache
|
||||||
|
const existingId = this._pathToAudioId.get(audioPath);
|
||||||
|
if (existingId) {
|
||||||
|
return existingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
// 通过资产系统加载 / Load through asset system
|
||||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
const result = await this._assetManager.loadAssetByPath<IAudioAsset>(audioPath);
|
||||||
return new Map();
|
const audioAsset = result.asset;
|
||||||
|
|
||||||
|
// 分配运行时 ID / Assign runtime ID
|
||||||
|
const audioId = EngineIntegration._nextAudioId++;
|
||||||
|
|
||||||
|
// 缓存映射 / Cache mapping
|
||||||
|
this._pathToAudioId.set(audioPath, audioId);
|
||||||
|
this._audioAssets.set(audioId, {
|
||||||
|
id: audioId,
|
||||||
|
asset: audioAsset,
|
||||||
|
path: audioPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return audioId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch load audio files
|
||||||
|
* 批量加载音频文件
|
||||||
|
*/
|
||||||
|
async loadAudioBatch(paths: string[]): Promise<Map<string, number>> {
|
||||||
|
const results = new Map<string, number>();
|
||||||
|
|
||||||
|
// 收集需要加载的音频 / Collect audio to load
|
||||||
|
const toLoad: string[] = [];
|
||||||
|
for (const path of paths) {
|
||||||
|
const existingId = this._pathToAudioId.get(path);
|
||||||
|
if (existingId) {
|
||||||
|
results.set(path, existingId);
|
||||||
|
} else {
|
||||||
|
toLoad.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toLoad.length === 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行加载所有音频 / Load all audio in parallel
|
||||||
|
const loadPromises = toLoad.map(async (path) => {
|
||||||
|
try {
|
||||||
|
const id = await this.loadAudioForComponent(path);
|
||||||
|
results.set(path, id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load audio: ${path}`, error);
|
||||||
|
results.set(path, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audio asset by ID
|
||||||
|
* 通过 ID 获取音频资产
|
||||||
|
*/
|
||||||
|
getAudioAsset(audioId: number): IAudioAsset | null {
|
||||||
|
const entry = this._audioAssets.get(audioId);
|
||||||
|
return entry?.asset || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audio ID for path
|
||||||
|
* 获取路径的音频 ID
|
||||||
|
*/
|
||||||
|
getAudioId(path: string): number | null {
|
||||||
|
return this._pathToAudioId.get(path) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload audio
|
||||||
|
* 卸载音频
|
||||||
|
*/
|
||||||
|
unloadAudio(audioId: number): void {
|
||||||
|
const entry = this._audioAssets.get(audioId);
|
||||||
|
if (entry) {
|
||||||
|
this._pathToAudioId.delete(entry.path);
|
||||||
|
this._audioAssets.delete(audioId);
|
||||||
|
|
||||||
|
// 从 GUID 映射中清理 / Clean up GUID mapping
|
||||||
|
for (const [guid, id] of this._audioIdMap.entries()) {
|
||||||
|
if (id === audioId) {
|
||||||
|
this._audioIdMap.delete(guid);
|
||||||
|
this._assetManager.unloadAsset(guid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= Data Resource Methods =============
|
||||||
|
// ============= 数据资源方法 =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data (JSON) for component
|
||||||
|
* 为组件加载数据(JSON)
|
||||||
|
*
|
||||||
|
* @param dataPath 数据文件路径 / Data file path
|
||||||
|
* @returns 运行时数据 ID / Runtime data ID
|
||||||
|
*/
|
||||||
|
async loadDataForComponent(dataPath: string): Promise<number> {
|
||||||
|
// 检查缓存 / Check cache
|
||||||
|
const existingId = this._pathToDataId.get(dataPath);
|
||||||
|
if (existingId) {
|
||||||
|
return existingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过资产系统加载 / Load through asset system
|
||||||
|
const result = await this._assetManager.loadAssetByPath<IJsonAsset>(dataPath);
|
||||||
|
const jsonAsset = result.asset;
|
||||||
|
|
||||||
|
// 分配运行时 ID / Assign runtime ID
|
||||||
|
const dataId = EngineIntegration._nextDataId++;
|
||||||
|
|
||||||
|
// 缓存映射 / Cache mapping
|
||||||
|
this._pathToDataId.set(dataPath, dataId);
|
||||||
|
this._dataAssets.set(dataId, {
|
||||||
|
id: dataId,
|
||||||
|
data: jsonAsset.data,
|
||||||
|
path: dataPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch load data files
|
||||||
|
* 批量加载数据文件
|
||||||
|
*/
|
||||||
|
async loadDataBatch(paths: string[]): Promise<Map<string, number>> {
|
||||||
|
const results = new Map<string, number>();
|
||||||
|
|
||||||
|
// 收集需要加载的数据 / Collect data to load
|
||||||
|
const toLoad: string[] = [];
|
||||||
|
for (const path of paths) {
|
||||||
|
const existingId = this._pathToDataId.get(path);
|
||||||
|
if (existingId) {
|
||||||
|
results.set(path, existingId);
|
||||||
|
} else {
|
||||||
|
toLoad.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toLoad.length === 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行加载所有数据 / Load all data in parallel
|
||||||
|
const loadPromises = toLoad.map(async (path) => {
|
||||||
|
try {
|
||||||
|
const id = await this.loadDataForComponent(path);
|
||||||
|
results.set(path, id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load data: ${path}`, error);
|
||||||
|
results.set(path, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data by ID
|
||||||
|
* 通过 ID 获取数据
|
||||||
|
*/
|
||||||
|
getData<T = unknown>(dataId: number): T | null {
|
||||||
|
const entry = this._dataAssets.get(dataId);
|
||||||
|
return (entry?.data as T) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data ID for path
|
||||||
|
* 获取路径的数据 ID
|
||||||
|
*/
|
||||||
|
getDataId(path: string): number | null {
|
||||||
|
return this._pathToDataId.get(path) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload data
|
||||||
|
* 卸载数据
|
||||||
|
*/
|
||||||
|
unloadData(dataId: number): void {
|
||||||
|
const entry = this._dataAssets.get(dataId);
|
||||||
|
if (entry) {
|
||||||
|
this._pathToDataId.delete(entry.path);
|
||||||
|
this._dataAssets.delete(dataId);
|
||||||
|
|
||||||
|
// 从 GUID 映射中清理 / Clean up GUID mapping
|
||||||
|
for (const [guid, id] of this._dataIdMap.entries()) {
|
||||||
|
if (id === dataId) {
|
||||||
|
this._dataIdMap.delete(guid);
|
||||||
|
this._assetManager.unloadAsset(guid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,15 +492,49 @@ export class EngineIntegration {
|
|||||||
this._pathToTextureId.clear();
|
this._pathToTextureId.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all audio mappings
|
||||||
|
* 清空所有音频映射
|
||||||
|
*/
|
||||||
|
clearAudioMappings(): void {
|
||||||
|
this._audioIdMap.clear();
|
||||||
|
this._pathToAudioId.clear();
|
||||||
|
this._audioAssets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all data mappings
|
||||||
|
* 清空所有数据映射
|
||||||
|
*/
|
||||||
|
clearDataMappings(): void {
|
||||||
|
this._dataIdMap.clear();
|
||||||
|
this._pathToDataId.clear();
|
||||||
|
this._dataAssets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all resource mappings
|
||||||
|
* 清空所有资源映射
|
||||||
|
*/
|
||||||
|
clearAllMappings(): void {
|
||||||
|
this.clearTextureMappings();
|
||||||
|
this.clearAudioMappings();
|
||||||
|
this.clearDataMappings();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get statistics
|
* Get statistics
|
||||||
* 获取统计信息
|
* 获取统计信息
|
||||||
*/
|
*/
|
||||||
getStatistics(): {
|
getStatistics(): {
|
||||||
loadedTextures: number;
|
loadedTextures: number;
|
||||||
|
loadedAudio: number;
|
||||||
|
loadedData: number;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
loadedTextures: this._pathToTextureId.size
|
loadedTextures: this._pathToTextureId.size,
|
||||||
|
loadedAudio: this._audioAssets.size,
|
||||||
|
loadedData: this._dataAssets.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TextureLoader } from './TextureLoader';
|
|||||||
import { JsonLoader } from './JsonLoader';
|
import { JsonLoader } from './JsonLoader';
|
||||||
import { TextLoader } from './TextLoader';
|
import { TextLoader } from './TextLoader';
|
||||||
import { BinaryLoader } from './BinaryLoader';
|
import { BinaryLoader } from './BinaryLoader';
|
||||||
|
import { AudioLoader } from './AudioLoader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asset loader factory
|
* Asset loader factory
|
||||||
@@ -38,6 +39,9 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|||||||
|
|
||||||
// 二进制加载器 / Binary loader
|
// 二进制加载器 / Binary loader
|
||||||
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
||||||
|
|
||||||
|
// 音频加载器 / Audio loader
|
||||||
|
this._loaders.set(AssetType.Audio, new AudioLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
102
packages/asset-system/src/loaders/AudioLoader.ts
Normal file
102
packages/asset-system/src/loaders/AudioLoader.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Audio asset loader
|
||||||
|
* 音频资产加载器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AssetType } from '../types/AssetTypes';
|
||||||
|
import { IAssetLoader, IAudioAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
|
||||||
|
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio loader implementation
|
||||||
|
* 音频加载器实现
|
||||||
|
*
|
||||||
|
* Uses Web Audio API to decode audio data into AudioBuffer.
|
||||||
|
* 使用 Web Audio API 将音频数据解码为 AudioBuffer。
|
||||||
|
*/
|
||||||
|
export class AudioLoader implements IAssetLoader<IAudioAsset> {
|
||||||
|
readonly supportedType = AssetType.Audio;
|
||||||
|
readonly supportedExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'];
|
||||||
|
readonly contentType: AssetContentType = 'audio';
|
||||||
|
|
||||||
|
private static _audioContext: AudioContext | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create shared AudioContext
|
||||||
|
* 获取或创建共享的 AudioContext
|
||||||
|
*/
|
||||||
|
private static getAudioContext(): AudioContext {
|
||||||
|
if (!AudioLoader._audioContext) {
|
||||||
|
AudioLoader._audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
|
}
|
||||||
|
return AudioLoader._audioContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse audio from content.
|
||||||
|
* 从内容解析音频。
|
||||||
|
*/
|
||||||
|
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IAudioAsset> {
|
||||||
|
if (!content.audioBuffer) {
|
||||||
|
throw new Error('Audio content is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBuffer = content.audioBuffer;
|
||||||
|
|
||||||
|
const audioAsset: IAudioAsset = {
|
||||||
|
buffer: audioBuffer,
|
||||||
|
duration: audioBuffer.duration,
|
||||||
|
sampleRate: audioBuffer.sampleRate,
|
||||||
|
channels: audioBuffer.numberOfChannels
|
||||||
|
};
|
||||||
|
|
||||||
|
return audioAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose loaded asset
|
||||||
|
* 释放已加载的资产
|
||||||
|
*/
|
||||||
|
dispose(_asset: IAudioAsset): void {
|
||||||
|
// AudioBuffer doesn't need explicit cleanup in most browsers
|
||||||
|
// AudioBuffer 在大多数浏览器中不需要显式清理
|
||||||
|
// The garbage collector will handle it when no references remain
|
||||||
|
// 当没有引用时,垃圾回收器会处理它
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the shared AudioContext
|
||||||
|
* 关闭共享的 AudioContext
|
||||||
|
*
|
||||||
|
* Call this when completely shutting down audio system.
|
||||||
|
* 在完全关闭音频系统时调用。
|
||||||
|
*/
|
||||||
|
static closeAudioContext(): void {
|
||||||
|
if (AudioLoader._audioContext) {
|
||||||
|
AudioLoader._audioContext.close();
|
||||||
|
AudioLoader._audioContext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume AudioContext after user interaction
|
||||||
|
* 用户交互后恢复 AudioContext
|
||||||
|
*
|
||||||
|
* Browsers require user interaction before audio can play.
|
||||||
|
* 浏览器要求用户交互后才能播放音频。
|
||||||
|
*/
|
||||||
|
static async resumeAudioContext(): Promise<void> {
|
||||||
|
const ctx = AudioLoader.getAudioContext();
|
||||||
|
if (ctx.state === 'suspended') {
|
||||||
|
await ctx.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shared AudioContext instance
|
||||||
|
* 获取共享的 AudioContext 实例
|
||||||
|
*/
|
||||||
|
static get audioContext(): AudioContext {
|
||||||
|
return AudioLoader.getAudioContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,58 @@ export interface IResourceLoader {
|
|||||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||||
*/
|
*/
|
||||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载纹理资源(可选)
|
||||||
|
* Unload texture resource (optional)
|
||||||
|
*/
|
||||||
|
unloadTexture?(textureId: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载音频资源(可选)
|
||||||
|
* Unload audio resource (optional)
|
||||||
|
*/
|
||||||
|
unloadAudio?(audioId: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载数据资源(可选)
|
||||||
|
* Unload data resource (optional)
|
||||||
|
*/
|
||||||
|
unloadData?(dataId: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源引用计数条目
|
||||||
|
* Resource reference count entry
|
||||||
|
*/
|
||||||
|
interface ResourceRefCountEntry {
|
||||||
|
/** 资源路径 / Resource path */
|
||||||
|
path: string;
|
||||||
|
/** 资源类型 / Resource type */
|
||||||
|
type: ResourceReference['type'];
|
||||||
|
/** 运行时 ID / Runtime ID */
|
||||||
|
runtimeId: number;
|
||||||
|
/** 使用此资源的场景名称集合 / Set of scene names using this resource */
|
||||||
|
sceneNames: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SceneResourceManager {
|
export class SceneResourceManager {
|
||||||
private resourceLoader: IResourceLoader | null = null;
|
private resourceLoader: IResourceLoader | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源引用计数表
|
||||||
|
* Resource reference count table
|
||||||
|
*
|
||||||
|
* Key: resource path, Value: reference count entry
|
||||||
|
*/
|
||||||
|
private _resourceRefCounts = new Map<string, ResourceRefCountEntry>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景到其使用的资源路径的映射
|
||||||
|
* Map of scene name to resource paths used by that scene
|
||||||
|
*/
|
||||||
|
private _sceneResources = new Map<string, Set<string>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置资源加载器实现
|
* 设置资源加载器实现
|
||||||
* Set the resource loader implementation
|
* Set the resource loader implementation
|
||||||
@@ -53,6 +100,8 @@ export class SceneResourceManager {
|
|||||||
* Batch load each resource type
|
* Batch load each resource type
|
||||||
* 4. 将运行时 ID 分配回组件
|
* 4. 将运行时 ID 分配回组件
|
||||||
* Assign runtime IDs back to components
|
* Assign runtime IDs back to components
|
||||||
|
* 5. 更新引用计数
|
||||||
|
* Update reference counts
|
||||||
*
|
*
|
||||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
* @param scene 要加载资源的场景 / The scene to load resources for
|
||||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
||||||
@@ -63,6 +112,8 @@ export class SceneResourceManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sceneName = scene.name;
|
||||||
|
|
||||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
// 从组件收集所有资源引用 / Collect all resource references from components
|
||||||
const resourceRefs = this.collectResourceReferences(scene);
|
const resourceRefs = this.collectResourceReferences(scene);
|
||||||
|
|
||||||
@@ -91,6 +142,9 @@ export class SceneResourceManager {
|
|||||||
// 合并到总映射表 / Merge into combined map
|
// 合并到总映射表 / Merge into combined map
|
||||||
for (const [path, id] of resourceIds) {
|
for (const [path, id] of resourceIds) {
|
||||||
allResourceIds.set(path, id);
|
allResourceIds.set(path, id);
|
||||||
|
|
||||||
|
// 更新引用计数 / Update reference count
|
||||||
|
this.addResourceReference(path, type, id, sceneName);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||||
@@ -99,6 +153,58 @@ export class SceneResourceManager {
|
|||||||
|
|
||||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||||
this.assignResourceIds(scene, allResourceIds);
|
this.assignResourceIds(scene, allResourceIds);
|
||||||
|
|
||||||
|
// 记录场景使用的资源 / Record resources used by scene
|
||||||
|
const scenePaths = new Set<string>();
|
||||||
|
for (const ref of resourceRefs) {
|
||||||
|
scenePaths.add(ref.path);
|
||||||
|
}
|
||||||
|
this._sceneResources.set(sceneName, scenePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加资源引用
|
||||||
|
* Add resource reference
|
||||||
|
*/
|
||||||
|
private addResourceReference(
|
||||||
|
path: string,
|
||||||
|
type: ResourceReference['type'],
|
||||||
|
runtimeId: number,
|
||||||
|
sceneName: string
|
||||||
|
): void {
|
||||||
|
let entry = this._resourceRefCounts.get(path);
|
||||||
|
if (!entry) {
|
||||||
|
entry = {
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
runtimeId,
|
||||||
|
sceneNames: new Set()
|
||||||
|
};
|
||||||
|
this._resourceRefCounts.set(path, entry);
|
||||||
|
}
|
||||||
|
entry.sceneNames.add(sceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除资源引用
|
||||||
|
* Remove resource reference
|
||||||
|
*
|
||||||
|
* @returns true 如果资源引用计数归零 / true if resource reference count reaches zero
|
||||||
|
*/
|
||||||
|
private removeResourceReference(path: string, sceneName: string): boolean {
|
||||||
|
const entry = this._resourceRefCounts.get(path);
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.sceneNames.delete(sceneName);
|
||||||
|
|
||||||
|
if (entry.sceneNames.size === 0) {
|
||||||
|
this._resourceRefCounts.delete(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,15 +247,112 @@ export class SceneResourceManager {
|
|||||||
* 卸载场景使用的所有资源
|
* 卸载场景使用的所有资源
|
||||||
* Unload all resources used by a scene
|
* Unload all resources used by a scene
|
||||||
*
|
*
|
||||||
* 在场景销毁时调用
|
* 在场景销毁时调用,只会卸载不再被其他场景引用的资源
|
||||||
* Called when a scene is being destroyed
|
* Called when a scene is being destroyed, only unloads resources not referenced by other scenes
|
||||||
*
|
*
|
||||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||||
*/
|
*/
|
||||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
async unloadSceneResources(scene: Scene): Promise<void> {
|
||||||
// TODO: 实现资源卸载 / Implement resource unloading
|
const sceneName = scene.name;
|
||||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
|
||||||
// Need to track resource reference counts and only unload when no longer used
|
// 获取场景使用的资源路径 / Get resource paths used by scene
|
||||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
const scenePaths = this._sceneResources.get(sceneName);
|
||||||
|
if (!scenePaths) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 要卸载的资源 / Resources to unload
|
||||||
|
const toUnload: ResourceRefCountEntry[] = [];
|
||||||
|
|
||||||
|
// 移除引用并收集需要卸载的资源 / Remove references and collect resources to unload
|
||||||
|
for (const path of scenePaths) {
|
||||||
|
const entry = this._resourceRefCounts.get(path);
|
||||||
|
if (entry) {
|
||||||
|
const shouldUnload = this.removeResourceReference(path, sceneName);
|
||||||
|
if (shouldUnload) {
|
||||||
|
toUnload.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理场景资源记录 / Clean up scene resource record
|
||||||
|
this._sceneResources.delete(sceneName);
|
||||||
|
|
||||||
|
// 卸载不再使用的资源 / Unload resources no longer in use
|
||||||
|
if (this.resourceLoader && toUnload.length > 0) {
|
||||||
|
for (const entry of toUnload) {
|
||||||
|
this.unloadResource(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载单个资源
|
||||||
|
* Unload a single resource
|
||||||
|
*/
|
||||||
|
private unloadResource(entry: ResourceRefCountEntry): void {
|
||||||
|
if (!this.resourceLoader) return;
|
||||||
|
|
||||||
|
switch (entry.type) {
|
||||||
|
case 'texture':
|
||||||
|
if (this.resourceLoader.unloadTexture) {
|
||||||
|
this.resourceLoader.unloadTexture(entry.runtimeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'audio':
|
||||||
|
if (this.resourceLoader.unloadAudio) {
|
||||||
|
this.resourceLoader.unloadAudio(entry.runtimeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'data':
|
||||||
|
if (this.resourceLoader.unloadData) {
|
||||||
|
this.resourceLoader.unloadData(entry.runtimeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'font':
|
||||||
|
// 字体卸载暂未实现 / Font unloading not yet implemented
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源统计信息
|
||||||
|
* Get resource statistics
|
||||||
|
*/
|
||||||
|
getStatistics(): {
|
||||||
|
totalResources: number;
|
||||||
|
trackedScenes: number;
|
||||||
|
resourcesByType: Map<ResourceReference['type'], number>;
|
||||||
|
} {
|
||||||
|
const resourcesByType = new Map<ResourceReference['type'], number>();
|
||||||
|
|
||||||
|
for (const entry of this._resourceRefCounts.values()) {
|
||||||
|
const count = resourcesByType.get(entry.type) || 0;
|
||||||
|
resourcesByType.set(entry.type, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalResources: this._resourceRefCounts.size,
|
||||||
|
trackedScenes: this._sceneResources.size,
|
||||||
|
resourcesByType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源的引用计数
|
||||||
|
* Get reference count for a resource
|
||||||
|
*/
|
||||||
|
getResourceRefCount(path: string): number {
|
||||||
|
const entry = this._resourceRefCounts.get(path);
|
||||||
|
return entry ? entry.sceneNames.size : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有跟踪数据
|
||||||
|
* Clear all tracking data
|
||||||
|
*/
|
||||||
|
clearAll(): void {
|
||||||
|
this._resourceRefCounts.clear();
|
||||||
|
this._sceneResources.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user