import { _decorator, Asset, AssetManager, assetManager, Component, director, instantiate, Node, ParticleSystem2D, Prefab, SpriteFrame } from 'cc'; import { Singleton } from '../../tools/Singleton'; import { ResPkgName } from 'db://assets/script/game/ConfigRes'; const { ccclass, property } = _decorator; /**资源信息 */ export interface ResInfo { /**资源名称 */ name: string, /**资源所在bundle */ bundle: string, /**资源路径 */ path: string, /**资源类型 */ type: typeof Asset /**资源实例 */ res?: Asset } /**资源包 */ export interface ResPakage { /**资源包名称 */ name: string, /**是否可释放 */ isReleaseable: boolean, /**资源列表 */ ress: ResInfo[] } @ccclass('ResourcesManager') export class ResourcesManager extends Singleton { /**节点池 */ private _nodePool: Map = new Map(); /**资源缓存 */ private _resCache: Map = new Map(); /** bundle in-flight 去重 */ private _bundleLoading: Map> = new Map(); /**获取资源 * @param path 资源路径 * @param bundle 资源所在bundle * @returns 资源 */ getRes(name: string, path: string, bundle: string, type: typeof Asset): T extends Asset ? T : null { if (type == SpriteFrame) { name = name + "/spriteframe"; } for (let resArr of this._resCache.values()) { for (let i = 0; i < resArr.ress.length; i++) { let resInfo = resArr.ress[i]; if (resInfo.name == name && resInfo.path == path && resInfo.bundle == bundle && resInfo.type == type) { if (resInfo.res) return resInfo.res as T extends Asset ? T : null; } } } let b = assetManager.getBundle(bundle); if (b) { let res = b.get(path + name); if (res) { return res as T extends Asset ? T : null; } } return null; } /** prefab 异步加载去重 */ private _prefabInflight: Map> = new Map(); private _resKey(bundle: string, path: string, name: string): string { return bundle + path + name; } private _registerDynamicPrefab(name: string, path: string, bundle: string, prefab: Prefab): void { let pkg = this._resCache.get(ResPkgName.BattleRes); if (!pkg) { pkg = { name: ResPkgName.BattleRes, isReleaseable: true, ress: [] }; this._resCache.set(ResPkgName.BattleRes, pkg); } const exists = pkg.ress.some( (r) => r.name === name && r.path === path && r.bundle === bundle, ); if (!exists) { pkg.ress.push({ name, path, bundle, type: Prefab, res: prefab }); } } /** * 确保 prefab 已加载并登记缓存(不实例化)。 * 同一资源并发请求会合并为一次 load。 */ async ensurePrefab(name: string, path: string, bundle: string): Promise { const cached = this.getRes(name, path, bundle, Prefab); if (cached) { return cached; } const key = this._resKey(bundle, path, name); let task = this._prefabInflight.get(key); if (!task) { task = this._loadPrefabInternal(name, path, bundle); this._prefabInflight.set(key, task); task.then( () => { this._prefabInflight.delete(key); }, () => { this._prefabInflight.delete(key); }, ); } return task; } private async _loadPrefabInternal(name: string, path: string, bundle: string): Promise { const b = await this.loadBundleAsync(bundle); if (!b) { return null; } const assetPath = path + name; const hit = b.get(assetPath) as Prefab | null; if (hit) { hit.addRef(); this._registerDynamicPrefab(name, path, bundle, hit); return hit; } return new Promise((resolve) => { b.load(assetPath, Prefab, (err, asset) => { if (err || !asset) { console.warn(`[Res] load prefab failed ${bundle}/${assetPath}`, err); resolve(null); return; } asset.addRef(); this._registerDynamicPrefab(name, path, bundle, asset); resolve(asset); }); }); } /** * 优先对象池,池空则按需加载 prefab 再实例化。 */ async getNodeAsync(path: string, name: string, bundle: string): Promise { const node = this.getNode(path, name, bundle); if (node) { return node; } const prefab = await this.ensurePrefab(name, path, bundle); if (!prefab) { return null; } return this.getNode(path, name, bundle); } /**获取节点(从节点池获取,没有就从预制件缓存实例化) */ getNode(path: string, name: string, bundle: string) { let key = this._resKey(bundle, path, name); try { let pool = this._nodePool.get(key); if (!pool) { pool = []; this._nodePool.set(key, pool); } if (pool.length <= 0) { let p = this.getRes(name, path, bundle, Prefab) as Prefab; if (p) { let n = instantiate(p); n["resKey"] = key; pool.push(n); } else { // 后台预热,下次 getNode / getNodeAsync 可直接命中 void this.ensurePrefab(name, path, bundle); } } if (pool.length > 0) { return pool.shift(); } return null; } catch (error) { console.error("UIBase getNode error", key, error); return null; } } /** 单类预制体对象池上限,超出则销毁,避免战斗中内存只增不减 */ private static readonly POOL_MAX_PER_KEY = 14; /**回收节点 */ putNode(node: Node) { let psss = node.getComponentsInChildren(ParticleSystem2D); for (let ps of psss) { ps.stopSystem(); } let pool = this._nodePool.get(node["resKey"]); if (!pool) { node.destroy(); } else { node.removeFromParent(); // 优先使用 UI 相机计时器;不可用时回退到 setTimeout,避免场景切换时回收失败 const pushBack = () => { if (!node || !node.isValid) return; if (pool.length >= ResourcesManager.POOL_MAX_PER_KEY) { node.destroy(); return; } pool.push(node); }; const camera = gg?.ui?.UICamera; if (camera && camera.isValid && camera.scheduleOnce) { camera.scheduleOnce(() => { pushBack(); }, 0.3); } else { setTimeout(() => { pushBack(); }, 300); } } } /**加载资源包 * @param resPkg 资源数组 * @param onProgress 进度回调 * @param onComplete 完成回调 */ loadResPakg(resPkg: ResPakage, onProgress: Function = null, onComplete: Function = null) { // 同名包重复加载前先清理旧引用,避免 _resCache 覆盖后旧引用无法释放 if (this._resCache.has(resPkg.name)) { this.clearResPkg(resPkg.name); } this._resCache.set(resPkg.name, resPkg); let totalCount = resPkg.ress.length; let loadCount = 0; let allCount = totalCount; if (totalCount <= 0) { if (onComplete) onComplete(); return; } let fun = () => { loadCount++; // console.log("加载资源:" + loadCount + " / " + allCount); if (onProgress) onProgress(loadCount, allCount); if (loadCount >= allCount) { if (onComplete) onComplete(); } } const bundleTasks = new Map>(); for (let i = 0; i < totalCount; i++) { const bundleName = resPkg.ress[i].bundle; if (!bundleTasks.has(bundleName)) { bundleTasks.set(bundleName, this.loadBundleAsync(bundleName)); } } for (let i = 0; i < totalCount; i++) { let conf = resPkg.ress[i]; let path = conf.path + conf.name; let res = this.getRes(conf.name, conf.path, conf.bundle, conf.type) as Asset; if (res) { // 命中缓存时也要登记本包引用,保证 clearResPkg 可以对称释放 res.addRef(); conf.res = res; fun(); continue; } bundleTasks.get(conf.bundle)?.then((b) => { if (!b) { fun(); return; } this.loadRes(b, path, (res) => { if (res) { res.addRef(); conf.res = res; } fun(); }, conf.type); }); } } /** * 向已有资源包增量加载条目(不清除已加载项),用于按波次预加载怪物等。 */ appendToResPkg( respkgName: ResPkgName | string, items: ResInfo[], onProgress: Function = null, onComplete: Function = null, ) { if (!items?.length) { if (onComplete) onComplete(); return; } let pkg = this._resCache.get(respkgName); if (!pkg) { pkg = { name: respkgName, isReleaseable: true, ress: [] }; this._resCache.set(respkgName, pkg); } const pending: ResInfo[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; const exists = pkg.ress.some( (r) => r.name === item.name && r.path === item.path && r.bundle === item.bundle, ); if (!exists) { pending.push(item); pkg.ress.push(item); } else { const cached = pkg.ress.find( (r) => r.name === item.name && r.path === item.path && r.bundle === item.bundle, ); if (cached?.res) { item.res = cached.res; } } } if (pending.length === 0) { if (onComplete) onComplete(); return; } let loadCount = 0; const allCount = pending.length; const done = () => { loadCount++; if (onProgress) onProgress(loadCount, allCount); if (loadCount >= allCount && onComplete) onComplete(); }; const bundleTasks = new Map>(); for (let i = 0; i < pending.length; i++) { const bundleName = pending[i].bundle; if (!bundleTasks.has(bundleName)) { bundleTasks.set(bundleName, this.loadBundleAsync(bundleName)); } } for (let i = 0; i < pending.length; i++) { const conf = pending[i]; const path = conf.path + conf.name; const hit = this.getRes(conf.name, conf.path, conf.bundle, conf.type) as Asset; if (hit) { hit.addRef(); conf.res = hit; done(); continue; } bundleTasks.get(conf.bundle)?.then((b) => { if (!b) { done(); return; } this.loadRes(b, path, (res) => { if (res) { res.addRef(); conf.res = res; } done(); }, conf.type); }); } } /**清理资源 */ clearResPkg(respkgName: ResPkgName | string) { let respkg = this._resCache.get(respkgName); if (!respkg) return; for (let res of respkg.ress) { res.res?.decRef(); if (res.res && res.res.refCount <= 0) { res.res = null; } } this._resCache.delete(respkg.name); } clearNodePool() { for (let pool of this._nodePool.values()) { for (let node of pool) { node.destroy(); } pool = []; } this._nodePool.clear(); } loadBundle(bundleName, onComplete: Function = null) { let bundle = assetManager.getBundle(bundleName); if (bundle) { if (onComplete) onComplete(bundle); return; } assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.log(`加载Bundle:${bundleName}错误`, err); if (onComplete) onComplete(null); } else { if (onComplete) onComplete(bundle); } }) } private loadBundleAsync(bundleName: string): Promise { const existed = assetManager.getBundle(bundleName); if (existed) { return Promise.resolve(existed); } const inflight = this._bundleLoading.get(bundleName); if (inflight) { return inflight; } const task = new Promise((resolve) => { assetManager.loadBundle(bundleName, (err, bundle) => { if (err) { console.log(`加载Bundle:${bundleName}错误`, err); resolve(null); } else { resolve(bundle); } }); }); this._bundleLoading.set(bundleName, task); task.then(() => { this._bundleLoading.delete(bundleName); }).catch(() => { this._bundleLoading.delete(bundleName); }); return task; } private loadRes(bundle: AssetManager.Bundle, assetPath, onComplete: Function = null, type = Asset) { let res = bundle.get(assetPath); if (res) { if (onComplete) onComplete(res); return; } bundle.load(assetPath, type, (err, asset) => { if (err) { console.log(`加载Bundle:${bundle.name},path:${assetPath}资源错误`, err); if (onComplete) onComplete(null); } else { if (onComplete) onComplete(asset); } }) } /**预加载资源包 */ preLoadResPakg(resPkg: ResPakage, onProgress: Function = null, onComplete: Function = null) { let totalCount = resPkg.ress.length; let loadCount = 0; let allCount = totalCount; console.log("预加载资源包", resPkg.name, JSON.stringify(resPkg)); if (totalCount <= 0) { if (onComplete) onComplete(); return; } let fun = () => { loadCount++; if (onProgress) onProgress(loadCount, allCount); if (loadCount >= allCount) { if (onComplete) onComplete(); } } const bundleTasks = new Map>(); for (let i = 0; i < totalCount; i++) { const bundleName = resPkg.ress[i].bundle; if (!bundleTasks.has(bundleName)) { bundleTasks.set(bundleName, this.loadBundleAsync(bundleName)); } } for (let i = 0; i < totalCount; i++) { let conf = resPkg.ress[i]; let path = conf.path + conf.name; let res = this.getRes(conf.name, conf.path, conf.bundle, conf.type); if (res) { fun(); continue; } bundleTasks.get(conf.bundle)?.then((b) => { if (!b) { fun(); return; } this.preLoadRes(b, path, (res) => { fun(); }); }); } } /**预加载资源 */ preLoadRes(bundle: AssetManager.Bundle, assetPath, onComplete: Function = null) { bundle.preload(assetPath, (err, asset) => { if (onComplete) onComplete(asset); }); } }