You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
17 KiB
504 lines
17 KiB
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<string, Node[]> = new Map();
|
|
/**资源缓存 */
|
|
private _resCache: Map<string, ResPakage> = new Map();
|
|
/** bundle in-flight 去重 */
|
|
private _bundleLoading: Map<string, Promise<AssetManager.Bundle | null>> = new Map();
|
|
|
|
/**获取资源
|
|
* @param path 资源路径
|
|
* @param bundle 资源所在bundle
|
|
* @returns 资源
|
|
*/
|
|
getRes<T>(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<string, Promise<Prefab | null>> = 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<Prefab | null> {
|
|
const cached = this.getRes<Prefab>(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<Prefab | null> {
|
|
const b = await this.loadBundleAsync(bundle);
|
|
if (!b) {
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', bundle);
|
|
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);
|
|
gg.sdk?.pfLogKey?.('RES:PREFAB_FAIL', `${bundle}/${assetPath}`);
|
|
resolve(null);
|
|
return;
|
|
}
|
|
asset.addRef();
|
|
this._registerDynamicPrefab(name, path, bundle, asset);
|
|
resolve(asset);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 优先对象池,池空则按需加载 prefab 再实例化。
|
|
*/
|
|
async getNodeAsync(path: string, name: string, bundle: string): Promise<Node | null> {
|
|
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);
|
|
gg.sdk?.pfLogKey?.('RES:GET_NODE_FAIL', key);
|
|
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<string, Promise<AssetManager.Bundle | null>>();
|
|
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<Asset>(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) {
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', conf.bundle);
|
|
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<string, Promise<AssetManager.Bundle | null>>();
|
|
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<Asset>(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) {
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', conf.bundle);
|
|
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);
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', bundleName);
|
|
if (onComplete) onComplete(null);
|
|
} else {
|
|
if (onComplete) onComplete(bundle);
|
|
}
|
|
})
|
|
}
|
|
|
|
private loadBundleAsync(bundleName: string): Promise<AssetManager.Bundle | null> {
|
|
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<AssetManager.Bundle | null>((resolve) => {
|
|
assetManager.loadBundle(bundleName, (err, bundle) => {
|
|
if (err) {
|
|
console.log(`加载Bundle:${bundleName}错误`, err);
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', bundleName);
|
|
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);
|
|
gg.sdk?.pfLogKey?.('RES:ASSET_FAIL', `${bundle.name}/${assetPath}`);
|
|
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<string, Promise<AssetManager.Bundle | null>>();
|
|
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) {
|
|
gg.sdk?.pfLogKey?.('RES:BUNDLE_FAIL', conf.bundle);
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|