BundleManager.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import { _decorator, Asset, assetManager, AssetManager,Constructor, SpriteFrame} from 'cc';
  2. import { Singleton } from './Singleton';
  3. import { Logger } from '../../extend/Logger';
  4. import { bundleConfig } from '../configs/BundleConfig';
  5. const { ccclass, property } = _decorator;
  6. /** wws
  7. * Bundle 中的单个资源定义
  8. */
  9. /** wws
  10. * Bundle 中的单个资源定义
  11. */
  12. export interface BundleAsset {
  13. /**资源路径(相对于Bundle根目录*/
  14. path: string;
  15. /**资源类型(可选)*/
  16. type?: Constructor<Asset>;
  17. /** 是否跳过加载(默认false*/
  18. skipLoading?: boolean;
  19. /** 是否是目录(默认false)*/
  20. isDirectory?: boolean;
  21. }
  22. /**Bundle 配置项*/
  23. export interface BundleSetting {
  24. /**是否在游戏启动时加载 */
  25. loadAtLaunch: boolean;
  26. /**需要预加载的资源列表(当autoLoadAll为false时生效*/
  27. preloadAssets?: BundleAsset[];
  28. /** 是否自动加载Bundle内所有资源 */
  29. autoLoadAll?: boolean;
  30. /**要排除的资源路径前缀(当autoLoadAll为true时生效*/
  31. excludePaths?: string[];
  32. /**要排除的文件扩展名(如['.meta']*/
  33. excludeExtensions?: string[];
  34. }
  35. /**Bundle 配置表类型(Map结构) Key: Bundle名称 Value: Bundle配置*/
  36. export type bundleConfig = Map<string, BundleSetting>;
  37. /**加载进度信息*/
  38. export interface LoadProgress {
  39. /**总加载步骤数*/
  40. totalSteps: number;
  41. /**已完成步骤数*/
  42. completedSteps: number;
  43. /**当前正在加载的Bundle名称*/
  44. currentBundle: string;
  45. /**当前正在加载的资源路径*/
  46. currentAsset: string;
  47. /**当前Bundle的加载进度(0-1)*/
  48. bundleProgress: number;
  49. /**总体加载进度(0-1)*/
  50. totalProgress: number;
  51. }
  52. /**
  53. * 进度回调函数类型
  54. * @param progress 当前加载进度信息
  55. */
  56. export type ProgressCallback = (progress: LoadProgress) => void;
  57. /**
  58. * 调用实例
  59. * bundleMgr.preloadConfigAllRes((progress) => {
  60. const percent = Math.floor(progress.totalProgress * 100);
  61. this.loadUI.progressLabel.string = `加载进度: ${percent}%`;
  62. this.loadUI.tipLabel.string = `${progress.currentBundle} (${progress.completedSteps}/${progress.totalSteps})`;
  63. // 更新进度条
  64. this.progressBar.progress = progress.totalProgress;
  65. this.progressBar.progress = progress.bundleProgress;
  66. //调试信息(可选)
  67. Logger.log(`当前加载: ${progress.currentAsset}`);
  68. });
  69. */
  70. @ccclass('BundleManager')
  71. class BundleManager extends Singleton {
  72. //已加载的Bundle缓存(包含引用计数)
  73. private _bundles: Map<string, { bundle: AssetManager.Bundle; refCount: number }> = new Map();
  74. //资源结果缓存cache (path -> asset)
  75. private _assetCache: Map<string, Asset> = new Map();
  76. //Bundle配置表
  77. private _settings: bundleConfig = new Map();
  78. //当前加载进度状态
  79. private _loadProgress: LoadProgress = {
  80. totalSteps: 0,
  81. completedSteps: 0,
  82. currentBundle: '',
  83. currentAsset: '',
  84. bundleProgress: 0,
  85. totalProgress: 0
  86. };
  87. /**
  88. * 加载配置bundle下的所有资源
  89. * @param onProgress 加载的进度回调
  90. * @example 调用事例:
  91. bundleMgr.preloadConfigAllRes((progress) => {
  92. //更新UI进度显示
  93. const percent = Math.floor(progress.totalProgress * 100);
  94. this.loadUI.progressLabel.string = `加载进度: ${percent}%`;
  95. this.loadUI.tipLabel.string = `${progress.currentBundle} | ${progress.currentAsset}
  96. (${progress.completedSteps}/${progress.totalSteps})`;
  97. //更新进度条
  98. this.progressBar.progress = progress.totalProgress;
  99. //this.progressBar.progress = progress.bundleProgress;
  100. //调试信息(可选)
  101. Logger.log(`当前加载: ${progress.currentBundle}` + `${progress.currentAsset}`);
  102. if(progress.totalProgress == 1){
  103. this.loadComplete = true;
  104. }
  105. });
  106. */
  107. public async preloadConfigAllRes(
  108. onProgress?: ProgressCallback,
  109. config: bundleConfig = bundleConfig
  110. ): Promise<void> {
  111. //初始化Bundle配置
  112. this._settings = config;
  113. await this._calculateTotalSteps();
  114. //开始加载并显示进度
  115. await bundleMgr.loadLaunchBundles(onProgress);
  116. }
  117. /**
  118. * 加载启动时必须的Bundle(根据配置表中loadAtLaunch=true的配置)
  119. * @param onProgress 进度回调函数(可选)
  120. */
  121. public async loadLaunchBundles(onProgress?: ProgressCallback): Promise<void> {
  122. //重置进度状态
  123. this._resetProgress();
  124. //获取需要加载的Bundle配置
  125. const bundlesToLoad = Array.from(this._settings.entries())
  126. .filter(([_, setting]) => setting.loadAtLaunch);
  127. //顺序加载每个Bundle
  128. for (const [bundleName, setting] of bundlesToLoad) {
  129. this._loadProgress.currentBundle = bundleName;
  130. if(setting?.autoLoadAll) {
  131. await this._loadAutoBundle(bundleName, setting, onProgress);
  132. }else if (setting?.preloadAssets) {
  133. await this._loadConfiguredBundle(bundleName, setting.preloadAssets, onProgress);
  134. }
  135. }
  136. }
  137. async getBundleAssetList(bundleName: string): Promise<string[]> {
  138. // 远程 Bundle 路径
  139. const configUrl = `${bundleName}/config.json`;
  140. // 使用原生 XMLHttpRequest 或 fetch 获取 config.json
  141. const response = await fetch(configUrl);
  142. const config = await response.json();
  143. // 从 config.json 中提取资源路径
  144. if (config && config.packages && config.packages[0] && config.packages[0].pathMap) {
  145. return Object.keys(config.packages[0].pathMap);
  146. }
  147. return [];
  148. }
  149. /**
  150. * 计算总加载步骤数(用于进度计算)
  151. */
  152. private async _calculateTotalSteps(): Promise<void> {
  153. let total = 0;
  154. for (const [bundleName, setting] of this._settings) {
  155. if(setting?.loadAtLaunch){
  156. const bundle = await this.getBundle(bundleName);
  157. if(setting?.autoLoadAll) {//整个bundle包下的资源
  158. if(bundle && bundle['config']?.paths?._map) {
  159. total += Object.keys(bundle['config'].paths._map).length;
  160. }
  161. }else if (setting?.preloadAssets) {//只统计需要加载的资源(skipLoading=false的)
  162. let pathsToLoad = await this._expandDirectoryAssets(bundle, setting.preloadAssets);
  163. total += pathsToLoad.length;
  164. }
  165. }
  166. }
  167. this._loadProgress.totalSteps = total;
  168. }
  169. /**
  170. * 自动加载Bundle内所有资源
  171. */
  172. private async _loadAutoBundle(
  173. bundleName: string,
  174. setting: BundleSetting,
  175. onProgress?: ProgressCallback
  176. ): Promise<void> {
  177. //加载Bundle
  178. const bundle = await this.getBundle(bundleName);
  179. //获取Bundle内所有资源路径(带类型信息)
  180. const assets = await this._getBundleAssets(bundle, {
  181. excludeExtensions: setting.excludeExtensions
  182. });
  183. //过滤时使用资源的path字段
  184. let filteredAssets = assets.filter(asset =>
  185. !setting.excludePaths?.some(exclude => asset.path.startsWith(exclude))
  186. );
  187. //直接传递完整资源对象(包含path和type)
  188. await this._loadBundleAssets(
  189. bundleName,
  190. filteredAssets, // 直接传递完整资源对象
  191. onProgress
  192. );
  193. }
  194. /**
  195. * 根据配置加载Bundle资源
  196. */
  197. private async _loadConfiguredBundle(
  198. bundleName: string,
  199. assets: BundleAsset[],
  200. onProgress?: ProgressCallback
  201. ): Promise<void> {
  202. //加载Bundle 处理目录资源
  203. const bundle = await this.getBundle(bundleName);
  204. //一个bundle下的目录资源
  205. let pathsToLoad = await this._expandDirectoryAssets(bundle, assets);
  206. //加载资源
  207. await this._loadBundleAssets(
  208. bundleName,
  209. pathsToLoad.map(asset => ({
  210. path: asset.path,
  211. ...(asset.type && { type: asset.type })
  212. })),
  213. onProgress
  214. );
  215. }
  216. /**
  217. * 加载指定Bundle
  218. * @param bundleName Bundle名称
  219. * @param onComplete 加载完成回调
  220. */
  221. public loadBundle(
  222. bundleName: string,
  223. onComplete?: (err: Error | null, bundle?: AssetManager.Bundle) => void
  224. ): void {
  225. //检查是否已加载
  226. const bundleInfo = this._bundles.get(bundleName);
  227. if (bundleInfo) {
  228. bundleInfo.refCount++;
  229. onComplete?.(null, bundleInfo.bundle);
  230. return;
  231. }
  232. //加载新Bundle
  233. assetManager.loadBundle(bundleName, (err, bundle) => {
  234. if (err) {
  235. console.error(`加载Bundle失败 [${bundleName}]:`, err);
  236. onComplete?.(err);
  237. return;
  238. }
  239. //缓存Bundle并设置引用计数
  240. this._bundles.set(bundleName, {
  241. bundle: bundle!,
  242. refCount: 1,
  243. });
  244. onComplete?.(null, bundle);
  245. });
  246. }
  247. /**
  248. * 获取Bundle内资源路径列表
  249. */
  250. private _getBundleAssets(
  251. bundle: AssetManager.Bundle,
  252. options: {
  253. excludeFolders?: boolean;
  254. excludeExtensions?: string[];
  255. } = {}
  256. ): { path: string, type?: Constructor<Asset> }[] {
  257. const { excludeFolders = true, excludeExtensions = [] } = options;
  258. let assets: Array<{ path: string, type?: Constructor<Asset> }> = [];
  259. //更安全的类型断言和路径过滤
  260. const pathMap = bundle['_config']?.assetInfos?._map as Record<string, {
  261. path: string;
  262. ctor?: Constructor<Asset>;
  263. }> | undefined;
  264. if(pathMap) {
  265. //过滤无效路径和带的衍生资源
  266. Object.values(pathMap).forEach(info => {
  267. if (info.path && !info.path.includes('@')) {
  268. assets.push({
  269. path: info.path,
  270. type: info.ctor
  271. });
  272. }
  273. });
  274. }
  275. //过滤处理 排除文件夹路径
  276. let result = assets.filter(asset => {
  277. //排除文件夹
  278. if(excludeFolders && asset.path.endsWith('/')) return false;
  279. //排除指定扩展名
  280. if (excludeExtensions?.length && excludeExtensions.some(ext =>
  281. asset.path.endsWith(ext)
  282. )) return false;
  283. return true;
  284. });
  285. return result;
  286. }
  287. /**
  288. * 加载Bundle内的多个资源
  289. */
  290. private async _loadBundleAssets(
  291. bundleName: string,
  292. assets: {path: string, type?: Constructor<Asset>}[],
  293. onProgress?: ProgressCallback,
  294. ): Promise<void> {
  295. const totalAssets = assets.length;
  296. if (totalAssets == 0) return;
  297. let loadedAssets = 0;
  298. //创建所有资源的加载Promise数组
  299. const loadPromises = assets.map(asset => {
  300. return new Promise<void>((resolve, reject) => {
  301. this.loadAsset(bundleName, asset.path, asset.type, (err) => {
  302. if (err) {
  303. console.error(`[${bundleName}] 加载资源失败: ${asset.path}`, err);
  304. reject(err);
  305. return;
  306. }
  307. loadedAssets++;
  308. this._loadProgress.completedSteps++;
  309. this._loadProgress.currentBundle = bundleName;
  310. this._loadProgress.currentAsset = asset.path;
  311. this._loadProgress.bundleProgress = loadedAssets / totalAssets;
  312. this._loadProgress.totalProgress = Math.min(
  313. this._loadProgress.completedSteps / this._loadProgress.totalSteps,
  314. 1// 避免提前显示100%
  315. );
  316. this._updateProgress(onProgress);
  317. resolve();
  318. });
  319. });
  320. });
  321. //等待所有资源加载完成
  322. await Promise.all(loadPromises)
  323. .catch(err => {
  324. console.error(`[${bundleName}] 部分资源加载失败`, err);
  325. throw err; // 重新抛出错误让上层处理
  326. });
  327. }
  328. /**调用事例
  329. await this.loadMultipleAssets("levels",
  330. [{ path: "map", type: Prefab },
  331. { path: "textures/trees", type: SpriteFrame },
  332. ],
  333. (progress) => {//直接更新UI进度条(0~1)
  334. this.loadingBar.progress = progress;
  335. }
  336. (err, asset, path) => {
  337. if (err) {
  338. Logger.error(`资源 ${path} 加载失败:`, err);
  339. } else {
  340. Logger.log(`资源 ${path} 加载成功:`, asset);
  341. }
  342. }
  343. );
  344. * 加载Bundle内的多个资源(仅计算当前Bundle的进度)
  345. * @param bundleName Bundle名称
  346. * @param assets 资源列表
  347. * @param onProgress 进度回调(当前Bundle的进度,范围0~1)
  348. */
  349. public async loadMultipleAssets(
  350. bundleName: string,
  351. assets: { path: string; type?: Constructor<Asset> }[],
  352. onProgress?: (progress: number) => void,
  353. onComplete?: (err: Error | null, asset?: Asset, path?: string) => void,
  354. ): Promise<void> {
  355. const totalAssets = assets.length;
  356. let loadedAssets = 0;
  357. for (const asset of assets) {
  358. await new Promise<void>((resolve) => {
  359. this.loadAsset(bundleName,asset.path,asset.type,(err, loadedAsset) => {
  360. if (err) {
  361. console.error(`加载资源失败 [${bundleName}] ${asset.path}:`, err);
  362. onComplete?.(err, undefined, asset.path);
  363. } else {
  364. loadedAssets++;
  365. const bundleProgress = loadedAssets / totalAssets;
  366. onProgress?.(bundleProgress);
  367. onComplete?.(null, loadedAsset, asset.path);
  368. }
  369. resolve();
  370. })
  371. });
  372. }
  373. }
  374. /**调用事例
  375. await bundleMgr.loadAsset(bundleName,path,AudioClip,(err: Error, clip: AudioClip) => {
  376. if(err) {
  377. Logger.error("加载资源失败:", err);
  378. return;
  379. }});
  380. * 加载Bundle内的指定资源
  381. * @param bundleName Bundle名称
  382. * @param assetPath 资源路径
  383. * @param type 资源类型(可选)
  384. * @param onComplete 加载完成回调
  385. */
  386. public loadAsset<T extends Asset>(
  387. bundleName: string,
  388. assetPath: string,
  389. type: Constructor<T> | null,
  390. onComplete?: (err: Error | null, asset?: T) => void
  391. ): Promise<T> | void {
  392. const cacheKey = `${assetPath}-${type?.name}`;
  393. //从资源缓存中caches取
  394. const cachedAsset = this._assetCache.get(cacheKey);
  395. if (cachedAsset) {
  396. //console.log(`[Cache Hit] Using cached asset: ${cacheKey}`);
  397. onComplete?.(null, cachedAsset as T);
  398. return Promise.resolve(cachedAsset as T);
  399. }
  400. return new Promise((resolve, reject) => {
  401. this.loadBundle(bundleName, (err, bundle) => {
  402. if (err) {
  403. onComplete?.(err);
  404. reject(err);
  405. return;
  406. }
  407. //特殊处理SpriteFrame的路径提示
  408. const isSpriteFrame = type && (type as any).name === 'SpriteFrame';
  409. if(isSpriteFrame && !assetPath.endsWith('/spriteFrame')) {
  410. console.warn(
  411. `SpriteFrame路径建议: ${assetPath} -> ${assetPath}/spriteFrame`,
  412. `\n(请确认是否使用完整SpriteFrame路径)`
  413. );
  414. }
  415. bundle!.load(assetPath, type,(err, asset) => {
  416. if(err || !asset) {
  417. console.error(`加载失败 [${bundleName}] ${assetPath}:`, err.message);
  418. const warning = isSpriteFrame
  419. ? `\n可能原因:\n1. 使用完整路径如 ${assetPath}/spriteFrame 类型为SpriteFrame\n2. 或者加载Texture: ${assetPath}/texture 类型为Texture`
  420. : `\n请检查资源路径是否正确`;
  421. console.warn(`空资源 [${bundleName}] ${assetPath}`, warning);
  422. onComplete?.(err);
  423. reject(err ?? new Error("Asset is null"));
  424. return;
  425. }
  426. this._assetCache.set(cacheKey, asset);
  427. console.log(`加载资源 ${assetPath} 成功: ${cacheKey} + 类型为: ${asset?.constructor.name}`);
  428. onComplete?.(err, asset as T);
  429. resolve(asset as T);
  430. });
  431. });
  432. });
  433. }
  434. /**
  435. * 释放Bundle
  436. * @param bundleName Bundle名称
  437. * @param force 是否强制释放(即使还有引用)
  438. */
  439. public releaseBundle(bundleName: string, force: boolean = false): void {
  440. const bundleInfo = this._bundles.get(bundleName);
  441. if (!bundleInfo) return;
  442. //减少引用计数
  443. bundleInfo.refCount--;
  444. // 当引用计数归零或强制释放时
  445. if (bundleInfo.refCount <= 0 || force) {
  446. assetManager.removeBundle(bundleInfo.bundle);
  447. this._bundles.delete(bundleName);
  448. console.log(`已释放Bundle: ${bundleName}`);
  449. }
  450. }
  451. /**
  452. * 获取已加载的Bundle实例
  453. * @param bundleName Bundle名称
  454. * @returns Bundle实例或null
  455. */
  456. public async getBundle(bundleName: string): Promise<AssetManager.Bundle | null> {
  457. //先从缓存中查找
  458. const cachedBundle = this._bundles.get(bundleName)?.bundle;
  459. if(cachedBundle) {
  460. return cachedBundle;
  461. }
  462. try {//如果缓存中没有,则加载bundle
  463. const bundle = await new Promise<AssetManager.Bundle>((resolve, reject) => {
  464. assetManager.loadBundle(bundleName, (err, bundle) => {
  465. err ? reject(err) : resolve(bundle);
  466. });
  467. });
  468. return bundle;
  469. } catch (error) {
  470. console.error(`加载Bundle ${bundleName} 失败:`, error);
  471. return null;
  472. }
  473. }
  474. /**
  475. * 检查Bundle是否已加载
  476. * @param bundleName Bundle名称
  477. */
  478. public hasBundle(bundleName: string): boolean {
  479. return this._bundles.has(bundleName);
  480. }
  481. /**
  482. * 释放所有Bundle
  483. * @param force 是否强制释放
  484. */
  485. public releaseAll(force: boolean = false): void {
  486. this._bundles.forEach((_, name) => this.releaseBundle(name, force));
  487. }
  488. /**
  489. * 展开目录资源为具体资源列表
  490. * @param bundle Bundle实例
  491. * @param assets 原始资源列表
  492. */
  493. private async _expandDirectoryAssets(
  494. bundle: AssetManager.Bundle,
  495. assets: BundleAsset[]
  496. ): Promise<BundleAsset[]> {
  497. let result: BundleAsset[] = [];
  498. for(let idx = 0; idx < assets.length; idx++) {
  499. const asset = assets[idx];
  500. if (!asset?.isDirectory) {
  501. if(!asset?.skipLoading){
  502. result.push(asset);
  503. }
  504. }else{//获取目录下所有资源路径
  505. const dirPath = asset.path.endsWith('/') ? asset.path : `${asset.path}/`;
  506. //获取带类型信息的资源列表
  507. const allPaths = await this._getBundleAssets(bundle, {
  508. excludeFolders: true,
  509. excludeExtensions: ['.meta']
  510. });
  511. //保留原始类型信息
  512. const dirResources = allPaths.filter(res =>
  513. res.path.startsWith(dirPath) &&
  514. !res.path.substring(dirPath.length).includes('/')
  515. );
  516. //转换时携带类型信息
  517. dirResources.forEach(res => {
  518. result.push({
  519. path: res.path,
  520. type: res.type, // 保留类型信息
  521. skipLoading: false
  522. });
  523. });
  524. }
  525. }
  526. result = result.filter(asset => !asset?.skipLoading);
  527. return result;
  528. }
  529. /**
  530. * 触发进度回调
  531. */
  532. private _updateProgress(onProgress?: ProgressCallback): void {
  533. //传递进度状态的副本,避免外部修改
  534. onProgress?.({...this._loadProgress});
  535. }
  536. /**
  537. * 重置加载进度状态
  538. */
  539. private _resetProgress(): void {
  540. this._loadProgress = {
  541. totalSteps: this._loadProgress.totalSteps, // 保持总步骤数
  542. completedSteps: 0,
  543. currentBundle: '',
  544. currentAsset: '',
  545. bundleProgress: 0,
  546. totalProgress: 0
  547. };
  548. }
  549. /**
  550. * 明确指定元组类型
  551. * @param asset 未使用这个方法 之前在测试
  552. * { path: 'texture/msg_hint/spriteFrame', type: SpriteFrame },
  553. { path: 'texture/test_01/texture', type: Texture2D },
  554. 这两种的时候有写到
  555. */
  556. private _getLoadArguments(asset: {
  557. path: string,
  558. type?: Constructor<Asset>
  559. }): [string, Constructor<Asset>?] { // 返回元组类型
  560. if (asset.type === SpriteFrame) {
  561. return [asset.path,asset.type];
  562. }
  563. return asset.type ? [asset.path, asset.type] : [asset.path];
  564. }
  565. }
  566. //全局单例
  567. export const bundleMgr = BundleManager.ins();