import { _decorator, Asset, assetManager, AssetManager,Constructor, SpriteFrame} from 'cc'; import { Singleton } from './Singleton'; import { Logger } from '../../extend/Logger'; import { bundleConfig } from '../configs/BundleConfig'; const { ccclass, property } = _decorator; /** wws * Bundle 中的单个资源定义 */ /** wws * Bundle 中的单个资源定义 */ export interface BundleAsset { /**资源路径(相对于Bundle根目录*/ path: string; /**资源类型(可选)*/ type?: Constructor; /** 是否跳过加载(默认false*/ skipLoading?: boolean; /** 是否是目录(默认false)*/ isDirectory?: boolean; } /**Bundle 配置项*/ export interface BundleSetting { /**是否在游戏启动时加载 */ loadAtLaunch: boolean; /**需要预加载的资源列表(当autoLoadAll为false时生效*/ preloadAssets?: BundleAsset[]; /** 是否自动加载Bundle内所有资源 */ autoLoadAll?: boolean; /**要排除的资源路径前缀(当autoLoadAll为true时生效*/ excludePaths?: string[]; /**要排除的文件扩展名(如['.meta']*/ excludeExtensions?: string[]; } /**Bundle 配置表类型(Map结构) Key: Bundle名称 Value: Bundle配置*/ export type bundleConfig = Map; /**加载进度信息*/ export interface LoadProgress { /**总加载步骤数*/ totalSteps: number; /**已完成步骤数*/ completedSteps: number; /**当前正在加载的Bundle名称*/ currentBundle: string; /**当前正在加载的资源路径*/ currentAsset: string; /**当前Bundle的加载进度(0-1)*/ bundleProgress: number; /**总体加载进度(0-1)*/ totalProgress: number; } /** * 进度回调函数类型 * @param progress 当前加载进度信息 */ export type ProgressCallback = (progress: LoadProgress) => void; /** * 调用实例 * bundleMgr.preloadConfigAllRes((progress) => { const percent = Math.floor(progress.totalProgress * 100); this.loadUI.progressLabel.string = `加载进度: ${percent}%`; this.loadUI.tipLabel.string = `${progress.currentBundle} (${progress.completedSteps}/${progress.totalSteps})`; // 更新进度条 this.progressBar.progress = progress.totalProgress; this.progressBar.progress = progress.bundleProgress; //调试信息(可选) Logger.log(`当前加载: ${progress.currentAsset}`); }); */ @ccclass('BundleManager') class BundleManager extends Singleton { //已加载的Bundle缓存(包含引用计数) private _bundles: Map = new Map(); //资源结果缓存cache (path -> asset) private _assetCache: Map = new Map(); //Bundle配置表 private _settings: bundleConfig = new Map(); //当前加载进度状态 private _loadProgress: LoadProgress = { totalSteps: 0, completedSteps: 0, currentBundle: '', currentAsset: '', bundleProgress: 0, totalProgress: 0 }; /** * 加载配置bundle下的所有资源 * @param onProgress 加载的进度回调 * @example 调用事例: bundleMgr.preloadConfigAllRes((progress) => { //更新UI进度显示 const percent = Math.floor(progress.totalProgress * 100); this.loadUI.progressLabel.string = `加载进度: ${percent}%`; this.loadUI.tipLabel.string = `${progress.currentBundle} | ${progress.currentAsset} (${progress.completedSteps}/${progress.totalSteps})`; //更新进度条 this.progressBar.progress = progress.totalProgress; //this.progressBar.progress = progress.bundleProgress; //调试信息(可选) Logger.log(`当前加载: ${progress.currentBundle}` + `${progress.currentAsset}`); if(progress.totalProgress == 1){ this.loadComplete = true; } }); */ public async preloadConfigAllRes( onProgress?: ProgressCallback, config: bundleConfig = bundleConfig ): Promise { //初始化Bundle配置 this._settings = config; await this._calculateTotalSteps(); //开始加载并显示进度 await bundleMgr.loadLaunchBundles(onProgress); } /** * 加载启动时必须的Bundle(根据配置表中loadAtLaunch=true的配置) * @param onProgress 进度回调函数(可选) */ public async loadLaunchBundles(onProgress?: ProgressCallback): Promise { //重置进度状态 this._resetProgress(); //获取需要加载的Bundle配置 const bundlesToLoad = Array.from(this._settings.entries()) .filter(([_, setting]) => setting.loadAtLaunch); //顺序加载每个Bundle for (const [bundleName, setting] of bundlesToLoad) { this._loadProgress.currentBundle = bundleName; if(setting?.autoLoadAll) { await this._loadAutoBundle(bundleName, setting, onProgress); }else if (setting?.preloadAssets) { await this._loadConfiguredBundle(bundleName, setting.preloadAssets, onProgress); } } } async getBundleAssetList(bundleName: string): Promise { // 远程 Bundle 路径 const configUrl = `${bundleName}/config.json`; // 使用原生 XMLHttpRequest 或 fetch 获取 config.json const response = await fetch(configUrl); const config = await response.json(); // 从 config.json 中提取资源路径 if (config && config.packages && config.packages[0] && config.packages[0].pathMap) { return Object.keys(config.packages[0].pathMap); } return []; } /** * 计算总加载步骤数(用于进度计算) */ private async _calculateTotalSteps(): Promise { let total = 0; for (const [bundleName, setting] of this._settings) { if(setting?.loadAtLaunch){ const bundle = await this.getBundle(bundleName); if(setting?.autoLoadAll) {//整个bundle包下的资源 if(bundle && bundle['config']?.paths?._map) { total += Object.keys(bundle['config'].paths._map).length; } }else if (setting?.preloadAssets) {//只统计需要加载的资源(skipLoading=false的) let pathsToLoad = await this._expandDirectoryAssets(bundle, setting.preloadAssets); total += pathsToLoad.length; } } } this._loadProgress.totalSteps = total; } /** * 自动加载Bundle内所有资源 */ private async _loadAutoBundle( bundleName: string, setting: BundleSetting, onProgress?: ProgressCallback ): Promise { //加载Bundle const bundle = await this.getBundle(bundleName); //获取Bundle内所有资源路径(带类型信息) const assets = await this._getBundleAssets(bundle, { excludeExtensions: setting.excludeExtensions }); //过滤时使用资源的path字段 let filteredAssets = assets.filter(asset => !setting.excludePaths?.some(exclude => asset.path.startsWith(exclude)) ); //直接传递完整资源对象(包含path和type) await this._loadBundleAssets( bundleName, filteredAssets, // 直接传递完整资源对象 onProgress ); } /** * 根据配置加载Bundle资源 */ private async _loadConfiguredBundle( bundleName: string, assets: BundleAsset[], onProgress?: ProgressCallback ): Promise { //加载Bundle 处理目录资源 const bundle = await this.getBundle(bundleName); //一个bundle下的目录资源 let pathsToLoad = await this._expandDirectoryAssets(bundle, assets); //加载资源 await this._loadBundleAssets( bundleName, pathsToLoad.map(asset => ({ path: asset.path, ...(asset.type && { type: asset.type }) })), onProgress ); } /** * 加载指定Bundle * @param bundleName Bundle名称 * @param onComplete 加载完成回调 */ public loadBundle( bundleName: string, onComplete?: (err: Error | null, bundle?: AssetManager.Bundle) => void ): void { //检查是否已加载 const bundleInfo = this._bundles.get(bundleName); if (bundleInfo) { bundleInfo.refCount++; onComplete?.(null, bundleInfo.bundle); return; } //加载新Bundle assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.error(`加载Bundle失败 [${bundleName}]:`, err); onComplete?.(err); return; } //缓存Bundle并设置引用计数 this._bundles.set(bundleName, { bundle: bundle!, refCount: 1, }); onComplete?.(null, bundle); }); } /** * 获取Bundle内资源路径列表 */ private _getBundleAssets( bundle: AssetManager.Bundle, options: { excludeFolders?: boolean; excludeExtensions?: string[]; } = {} ): { path: string, type?: Constructor }[] { const { excludeFolders = true, excludeExtensions = [] } = options; let assets: Array<{ path: string, type?: Constructor }> = []; //更安全的类型断言和路径过滤 const pathMap = bundle['_config']?.assetInfos?._map as Record; }> | undefined; if(pathMap) { //过滤无效路径和带的衍生资源 Object.values(pathMap).forEach(info => { if (info.path && !info.path.includes('@')) { assets.push({ path: info.path, type: info.ctor }); } }); } //过滤处理 排除文件夹路径 let result = assets.filter(asset => { //排除文件夹 if(excludeFolders && asset.path.endsWith('/')) return false; //排除指定扩展名 if (excludeExtensions?.length && excludeExtensions.some(ext => asset.path.endsWith(ext) )) return false; return true; }); return result; } /** * 加载Bundle内的多个资源 */ private async _loadBundleAssets( bundleName: string, assets: {path: string, type?: Constructor}[], onProgress?: ProgressCallback, ): Promise { const totalAssets = assets.length; if (totalAssets == 0) return; let loadedAssets = 0; //创建所有资源的加载Promise数组 const loadPromises = assets.map(asset => { return new Promise((resolve, reject) => { this.loadAsset(bundleName, asset.path, asset.type, (err) => { if (err) { console.error(`[${bundleName}] 加载资源失败: ${asset.path}`, err); reject(err); return; } loadedAssets++; this._loadProgress.completedSteps++; this._loadProgress.currentBundle = bundleName; this._loadProgress.currentAsset = asset.path; this._loadProgress.bundleProgress = loadedAssets / totalAssets; this._loadProgress.totalProgress = Math.min( this._loadProgress.completedSteps / this._loadProgress.totalSteps, 1// 避免提前显示100% ); this._updateProgress(onProgress); resolve(); }); }); }); //等待所有资源加载完成 await Promise.all(loadPromises) .catch(err => { console.error(`[${bundleName}] 部分资源加载失败`, err); throw err; // 重新抛出错误让上层处理 }); } /**调用事例 await this.loadMultipleAssets("levels", [{ path: "map", type: Prefab }, { path: "textures/trees", type: SpriteFrame }, ], (progress) => {//直接更新UI进度条(0~1) this.loadingBar.progress = progress; } (err, asset, path) => { if (err) { Logger.error(`资源 ${path} 加载失败:`, err); } else { Logger.log(`资源 ${path} 加载成功:`, asset); } } ); * 加载Bundle内的多个资源(仅计算当前Bundle的进度) * @param bundleName Bundle名称 * @param assets 资源列表 * @param onProgress 进度回调(当前Bundle的进度,范围0~1) */ public async loadMultipleAssets( bundleName: string, assets: { path: string; type?: Constructor }[], onProgress?: (progress: number) => void, onComplete?: (err: Error | null, asset?: Asset, path?: string) => void, ): Promise { const totalAssets = assets.length; let loadedAssets = 0; for (const asset of assets) { await new Promise((resolve) => { this.loadAsset(bundleName,asset.path,asset.type,(err, loadedAsset) => { if (err) { console.error(`加载资源失败 [${bundleName}] ${asset.path}:`, err); onComplete?.(err, undefined, asset.path); } else { loadedAssets++; const bundleProgress = loadedAssets / totalAssets; onProgress?.(bundleProgress); onComplete?.(null, loadedAsset, asset.path); } resolve(); }) }); } } /**调用事例 await bundleMgr.loadAsset(bundleName,path,AudioClip,(err: Error, clip: AudioClip) => { if(err) { Logger.error("加载资源失败:", err); return; }}); * 加载Bundle内的指定资源 * @param bundleName Bundle名称 * @param assetPath 资源路径 * @param type 资源类型(可选) * @param onComplete 加载完成回调 */ public loadAsset( bundleName: string, assetPath: string, type: Constructor | null, onComplete?: (err: Error | null, asset?: T) => void ): Promise | void { const cacheKey = `${assetPath}-${type?.name}`; //从资源缓存中caches取 const cachedAsset = this._assetCache.get(cacheKey); if (cachedAsset) { //console.log(`[Cache Hit] Using cached asset: ${cacheKey}`); onComplete?.(null, cachedAsset as T); return Promise.resolve(cachedAsset as T); } return new Promise((resolve, reject) => { this.loadBundle(bundleName, (err, bundle) => { if (err) { onComplete?.(err); reject(err); return; } //特殊处理SpriteFrame的路径提示 const isSpriteFrame = type && (type as any).name === 'SpriteFrame'; if(isSpriteFrame && !assetPath.endsWith('/spriteFrame')) { console.warn( `SpriteFrame路径建议: ${assetPath} -> ${assetPath}/spriteFrame`, `\n(请确认是否使用完整SpriteFrame路径)` ); } bundle!.load(assetPath, type,(err, asset) => { if(err || !asset) { console.error(`加载失败 [${bundleName}] ${assetPath}:`, err.message); const warning = isSpriteFrame ? `\n可能原因:\n1. 使用完整路径如 ${assetPath}/spriteFrame 类型为SpriteFrame\n2. 或者加载Texture: ${assetPath}/texture 类型为Texture` : `\n请检查资源路径是否正确`; console.warn(`空资源 [${bundleName}] ${assetPath}`, warning); onComplete?.(err); reject(err ?? new Error("Asset is null")); return; } this._assetCache.set(cacheKey, asset); console.log(`加载资源 ${assetPath} 成功: ${cacheKey} + 类型为: ${asset?.constructor.name}`); onComplete?.(err, asset as T); resolve(asset as T); }); }); }); } /** * 释放Bundle * @param bundleName Bundle名称 * @param force 是否强制释放(即使还有引用) */ public releaseBundle(bundleName: string, force: boolean = false): void { const bundleInfo = this._bundles.get(bundleName); if (!bundleInfo) return; //减少引用计数 bundleInfo.refCount--; // 当引用计数归零或强制释放时 if (bundleInfo.refCount <= 0 || force) { assetManager.removeBundle(bundleInfo.bundle); this._bundles.delete(bundleName); console.log(`已释放Bundle: ${bundleName}`); } } /** * 获取已加载的Bundle实例 * @param bundleName Bundle名称 * @returns Bundle实例或null */ public async getBundle(bundleName: string): Promise { //先从缓存中查找 const cachedBundle = this._bundles.get(bundleName)?.bundle; if(cachedBundle) { return cachedBundle; } try {//如果缓存中没有,则加载bundle const bundle = await new Promise((resolve, reject) => { assetManager.loadBundle(bundleName, (err, bundle) => { err ? reject(err) : resolve(bundle); }); }); return bundle; } catch (error) { console.error(`加载Bundle ${bundleName} 失败:`, error); return null; } } /** * 检查Bundle是否已加载 * @param bundleName Bundle名称 */ public hasBundle(bundleName: string): boolean { return this._bundles.has(bundleName); } /** * 释放所有Bundle * @param force 是否强制释放 */ public releaseAll(force: boolean = false): void { this._bundles.forEach((_, name) => this.releaseBundle(name, force)); } /** * 展开目录资源为具体资源列表 * @param bundle Bundle实例 * @param assets 原始资源列表 */ private async _expandDirectoryAssets( bundle: AssetManager.Bundle, assets: BundleAsset[] ): Promise { let result: BundleAsset[] = []; for(let idx = 0; idx < assets.length; idx++) { const asset = assets[idx]; if (!asset?.isDirectory) { if(!asset?.skipLoading){ result.push(asset); } }else{//获取目录下所有资源路径 const dirPath = asset.path.endsWith('/') ? asset.path : `${asset.path}/`; //获取带类型信息的资源列表 const allPaths = await this._getBundleAssets(bundle, { excludeFolders: true, excludeExtensions: ['.meta'] }); //保留原始类型信息 const dirResources = allPaths.filter(res => res.path.startsWith(dirPath) && !res.path.substring(dirPath.length).includes('/') ); //转换时携带类型信息 dirResources.forEach(res => { result.push({ path: res.path, type: res.type, // 保留类型信息 skipLoading: false }); }); } } result = result.filter(asset => !asset?.skipLoading); return result; } /** * 触发进度回调 */ private _updateProgress(onProgress?: ProgressCallback): void { //传递进度状态的副本,避免外部修改 onProgress?.({...this._loadProgress}); } /** * 重置加载进度状态 */ private _resetProgress(): void { this._loadProgress = { totalSteps: this._loadProgress.totalSteps, // 保持总步骤数 completedSteps: 0, currentBundle: '', currentAsset: '', bundleProgress: 0, totalProgress: 0 }; } /** * 明确指定元组类型 * @param asset 未使用这个方法 之前在测试 * { path: 'texture/msg_hint/spriteFrame', type: SpriteFrame }, { path: 'texture/test_01/texture', type: Texture2D }, 这两种的时候有写到 */ private _getLoadArguments(asset: { path: string, type?: Constructor }): [string, Constructor?] { // 返回元组类型 if (asset.type === SpriteFrame) { return [asset.path,asset.type]; } return asset.type ? [asset.path, asset.type] : [asset.path]; } } //全局单例 export const bundleMgr = BundleManager.ins();