123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- 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<Asset>;
- /** 是否跳过加载(默认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<string, BundleSetting>;
- /**加载进度信息*/
- 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<string, { bundle: AssetManager.Bundle; refCount: number }> = new Map();
- //资源结果缓存cache (path -> asset)
- private _assetCache: Map<string, Asset> = 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<void> {
- //初始化Bundle配置
- this._settings = config;
- await this._calculateTotalSteps();
- //开始加载并显示进度
- await bundleMgr.loadLaunchBundles(onProgress);
- }
- /**
- * 加载启动时必须的Bundle(根据配置表中loadAtLaunch=true的配置)
- * @param onProgress 进度回调函数(可选)
- */
- public async loadLaunchBundles(onProgress?: ProgressCallback): Promise<void> {
- //重置进度状态
- 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<string[]> {
- // 远程 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<void> {
- 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<void> {
- //加载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<void> {
- //加载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<Asset> }[] {
- const { excludeFolders = true, excludeExtensions = [] } = options;
- let assets: Array<{ path: string, type?: Constructor<Asset> }> = [];
- //更安全的类型断言和路径过滤
- const pathMap = bundle['_config']?.assetInfos?._map as Record<string, {
- path: string;
- ctor?: Constructor<Asset>;
- }> | 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<Asset>}[],
- onProgress?: ProgressCallback,
- ): Promise<void> {
- const totalAssets = assets.length;
- if (totalAssets == 0) return;
- let loadedAssets = 0;
- //创建所有资源的加载Promise数组
- const loadPromises = assets.map(asset => {
- return new Promise<void>((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<Asset> }[],
- onProgress?: (progress: number) => void,
- onComplete?: (err: Error | null, asset?: Asset, path?: string) => void,
- ): Promise<void> {
- const totalAssets = assets.length;
- let loadedAssets = 0;
- for (const asset of assets) {
- await new Promise<void>((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<T extends Asset>(
- bundleName: string,
- assetPath: string,
- type: Constructor<T> | null,
- onComplete?: (err: Error | null, asset?: T) => void
- ): Promise<T> | 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<AssetManager.Bundle | null> {
- //先从缓存中查找
- const cachedBundle = this._bundles.get(bundleName)?.bundle;
- if(cachedBundle) {
- return cachedBundle;
- }
- try {//如果缓存中没有,则加载bundle
- const bundle = await new Promise<AssetManager.Bundle>((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<BundleAsset[]> {
- 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<Asset>
- }): [string, Constructor<Asset>?] { // 返回元组类型
- if (asset.type === SpriteFrame) {
- return [asset.path,asset.type];
- }
- return asset.type ? [asset.path, asset.type] : [asset.path];
- }
- }
- //全局单例
- export const bundleMgr = BundleManager.ins();
|