消消方块阵换皮表情
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

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);
});
}
}