diff --git a/assets/core/common/audio/AudioClipLoader.ts b/assets/core/common/audio/AudioClipLoader.ts new file mode 100644 index 0000000..d7a99fa --- /dev/null +++ b/assets/core/common/audio/AudioClipLoader.ts @@ -0,0 +1,264 @@ +import { AudioClip } from 'cc'; +import { resLoader } from '../loader/ResLoader'; + +/** 加载结果 */ +export interface ILoadResult { + /** 加载成功的 AudioClip */ + clip: AudioClip; + /** 原始路径(URL 或 bundle 内路径) */ + path: string; + /** 资源包名(远程资源时为 null) */ + bundle: string | null; + /** 是否为远程资源 */ + isRemote: boolean; +} + +/** + * 音频资源加载器 + * 统一处理三种来源的 AudioClip 获取: + * 1. 直接传入 AudioClip 实例 + * 2. 远程 URL 加载 + * 3. Bundle 内资源加载 + */ +export class AudioClipLoader { + /** 加载中缓存,避免同一资源重复加载 */ + private loadingCache: Map> = new Map(); + /** 已加载的 AudioClip 缓存 */ + private clipCache: Map = new Map(); + + /** + * 从三种来源获取 AudioClip + * @param path - AudioClip 实例、远程 URL、或 bundle 内路径 + * @param bundle - 资源包名(path 为 AudioClip 或 URL 时忽略) + * @returns 加载结果,失败返回 null + */ + async load( + path: string | AudioClip, + bundle?: string + ): Promise { + if (path instanceof AudioClip) { + if (!path.isValid) { + console.warn(`AudioClip 实例已失效`); + return null; + } + // 外部传入的 AudioClip 实例,增加引擎引用计数 + path.addRef(); + return { clip: path, path: path.uuid, bundle: null, isRemote: false }; + } + + const cacheKey = this.getCacheKey(path, bundle); + const cached = this.clipCache.get(cacheKey); + + if (cached && cached.isValid) { + // 增加引擎引用计数 + cached.addRef(); + return { clip: cached, path, bundle: bundle || null, isRemote: path.indexOf('http') === 0 }; + } + + if (path.indexOf('http') === 0) { + return this.loadRemote(path); + } + + return this.loadBundle(path, bundle || resLoader.defaultBundleName); + } + + /** + * 释放指定路径的音频资源引用 + * @param path - 资源路径或 URL + * @param bundle - 资源包名(远程资源时忽略) + */ + release(path: string, bundle?: string): void { + const { key, entry } = this.getCacheEntry(path, bundle); + if (!entry) return; + + // 减少引擎引用计数 + if (entry.isValid) { + entry.decRef(); + } + } + + /** + * 立即释放指定路径的音频资源(不等待延迟) + * @param path - 资源路径或 URL + * @param bundle - 资源包名(远程资源时忽略) + */ + releaseImmediately(path: string, bundle?: string): void { + const { key, entry } = this.getCacheEntry(path, bundle); + if (!entry) return; + + this.doRelease(key, entry); + } + + /** + * 执行真正的资源释放 + * @param key - 缓存键值 + * @param entry - 缓存条目 + */ + private doRelease(key: string, entry: AudioClip): void { + if (entry && entry.isValid) { + entry.decRef(); + } + this.clipCache.delete(key); + } + + /** 清空所有缓存 */ + clearCache(): void { + this.clipCache.forEach((entry, key) => { + this.doRelease(key, entry); + }); + this.clipCache.clear(); + this.loadingCache.clear(); + } + + /** 销毁加载器,释放所有资源 */ + destroy(): void { + this.clearCache(); + } + + /** + * 获取缓存统计信息 + * @returns 缓存条目数量 + */ + getStats(): { total: number } { + return { total: this.clipCache.size }; + } + + /** + * 获取缓存 key + * @param path - 资源路径 + * @param bundle - 资源包名 + * @returns 缓存键值 + */ + private getCacheKey(path: string, bundle?: string): string { + if (path.indexOf('http') === 0) { + return `remote_${path}`; + } + return `bundle_${bundle || resLoader.defaultBundleName}_${path}`; + } + + /** + * 获取缓存条目 + * @param path - 资源路径 + * @param bundle - 资源包名 + * @returns 缓存键值和条目 + */ + private getCacheEntry(path: string, bundle?: string): { key: string; entry: AudioClip | undefined } { + const key = this.getCacheKey(path, bundle); + const entry = this.clipCache.get(key); + return { key, entry }; + } + + /** + * 设置缓存并返回加载结果 + * @param clip - 音频资源 + * @param path - 资源路径 + * @param cacheKey - 缓存键值 + * @param bundle - 资源包名 + * @param isRemote - 是否为远程资源 + * @returns 加载结果 + */ + private setCacheAndReturn( + clip: AudioClip, + path: string, + cacheKey: string, + bundle: string | null, + isRemote: boolean + ): ILoadResult { + clip.addRef(); + this.clipCache.set(cacheKey, clip); + return { clip, path, bundle, isRemote }; + } + + /** + * 加载远程资源 + * @param path - 远程 URL + * @returns 加载结果 + */ + private async loadRemote( + path: string + ): Promise { + let loadPromise = this.loadingCache.get(path); + if (!loadPromise) { + loadPromise = this.doLoadRemotePromise(path); + this.loadingCache.set(path, loadPromise); + } + + try { + const clip = await loadPromise; + if (!clip || !clip.isValid) { + console.warn(`远程音频资源加载失败: ${path}`); + return null; + } + + return this.setCacheAndReturn(clip, path, `remote_${path}`, null, true); + } + catch (e) { + console.warn(`远程音频资源加载异常: ${path}`, e); + return null; + } + finally { + this.loadingCache.delete(path); + } + } + + /** + * 执行远程资源加载 + * @param path - 远程 URL + * @returns AudioClip 加载 Promise + */ + private async doLoadRemotePromise(path: string): Promise { + const extension = path.split('.').pop(); + return resLoader.loadRemote(path, { ext: `.${extension}` }); + } + + /** + * 加载 Bundle 内资源 + * @param path - 资源路径 + * @param bundle - 资源包名 + * @returns 加载结果 + */ + private async loadBundle( + path: string, + bundle: string + ): Promise { + const cacheKey = `bundle_${bundle}_${path}`; + + let clip = resLoader.get(path, AudioClip, bundle); + if (clip) { + if (!clip.isValid) { + console.warn(`音频资源已失效: ${bundle}/${path}`); + return null; + } + + const entry = this.clipCache.get(cacheKey); + if (entry) { + clip.addRef(); + return { clip, path, bundle, isRemote: false }; + } + return this.setCacheAndReturn(clip, path, cacheKey, bundle, false); + } + + let loadPromise = this.loadingCache.get(cacheKey); + if (!loadPromise) { + loadPromise = resLoader.load(bundle, path, AudioClip); + this.loadingCache.set(cacheKey, loadPromise); + } + + try { + clip = await loadPromise; + if (!clip || !clip.isValid) { + console.warn(`音频资源加载失败: ${bundle}/${path}`); + return null; + } + + return this.setCacheAndReturn(clip, path, cacheKey, bundle, false); + } + catch (e) { + console.warn(`音频资源加载异常: ${bundle}/${path}`, e); + return null; + } + finally { + this.loadingCache.delete(cacheKey); + } + } +} \ No newline at end of file diff --git a/assets/core/common/audio/AudioClipLoader.ts.meta b/assets/core/common/audio/AudioClipLoader.ts.meta new file mode 100644 index 0000000..a331819 --- /dev/null +++ b/assets/core/common/audio/AudioClipLoader.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "7e9304f5-fa80-43d0-883a-2fd8734e7bbb", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/audio/AudioEffect.ts b/assets/core/common/audio/AudioEffect.ts index 10ab6bc..338db85 100644 --- a/assets/core/common/audio/AudioEffect.ts +++ b/assets/core/common/audio/AudioEffect.ts @@ -42,7 +42,7 @@ export class AudioEffect extends AudioSource { /** 组件销毁时清理资源 */ onDestroy() { - this.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this); + if (this.node) this.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this); this.reset(); } -} +} \ No newline at end of file diff --git a/assets/core/common/audio/AudioEffectPool.ts b/assets/core/common/audio/AudioEffectPool.ts index 91385f0..b384f5c 100644 --- a/assets/core/common/audio/AudioEffectPool.ts +++ b/assets/core/common/audio/AudioEffectPool.ts @@ -1,27 +1,32 @@ import { AudioClip, Node, NodePool } from 'cc'; import { oops } from '../../Oops'; -import { resLoader } from '../loader/ResLoader'; +import { AudioClipLoader } from './AudioClipLoader'; import { AudioEffect } from './AudioEffect'; import { AudioEffectType } from './AudioEnum'; import type { IAudioData, IAudioParams } from './IAudio'; +import { resLoader } from '../loader/ResLoader'; /** 音乐效缓冲编号最大值 */ const AE_ID_MAX = 30000; -/** 音效池 */ +/** + * 音效池 + * + * 内存管理思路: + * 1. 引用计数机制:每个 AudioClip 通过 addRef/decRef 管理生命周期 + * 2. 自动释放:界面关闭时调用 releaseResByPath 减少引用计数 + * 3. 永久缓存:通过预加载时额外增加一次引用,使资源不会被界面释放清理 + * 4. 缓存复用:引用计数 > 0 的资源保留在 clipCache 中供后续界面复用 + */ export class AudioEffectPool { /** 音效配置数据 */ private data: { [node: string]: IAudioData } = null!; + /** 音频资源加载器 */ + private loader: AudioClipLoader = new AudioClipLoader(); /** 音效播放器节点对象池 */ private pool: NodePool = new NodePool(); - /** 对象池集合 */ + /** 正在播放的音效播放器集合 */ private effects: Map = new Map(); - /** 记录项目资源库中使用过的音乐资源 */ - private res_project: Map> = new Map(); - /** 外网远程资源记录(地址、音效对象) */ - private res_remote: Map = new Map(); - /** 正在加载的资源Promise缓存,避免重复加载 */ - private loading_cache: Map> = new Map(); private _aeId = 0; /** 获取请求唯一编号 */ @@ -71,7 +76,6 @@ export class AudioEffectPool { if (iad == null) console.error(`类型为【${type}】的音效配置不存在`); return iad.volume; } - /** * 音效音量设置 * @param value 音效音量 @@ -92,20 +96,7 @@ export class AudioEffectPool { * @returns */ async loadAndPlay(path: string | AudioClip, params?: IAudioParams): Promise { - // 合并默认参数(减少对象创建) - const finalParams: IAudioParams = params ? { - type: params.type ?? AudioEffectType.Effect, - bundle: params.bundle ?? resLoader.defaultBundleName, - loop: params.loop ?? false, - destroy: params.destroy ?? false, - volume: params.volume, - onPlayComplete: params.onPlayComplete - } : { - type: AudioEffectType.Effect, - bundle: resLoader.defaultBundleName, - loop: false, - destroy: false - }; + const finalParams = this.mergeParams(params); const iad = this.data[finalParams.type!]; if (!iad) { @@ -121,70 +112,31 @@ export class AudioEffectPool { const bundle = finalParams.bundle!; let key: string; - let clip: AudioClip | null = null; - - // 通过预制自动加载的音效资源(音效内存跟随预制体的内存一并释放) + if (path instanceof AudioClip) { key = `${finalParams.type}_${path.uuid}`; - clip = path; } - // 非引擎管理的远程资源加载 - else if (path.indexOf('http') === 0) { - key = `${finalParams.type}_${path}`; - clip = this.res_remote.get(path) || null; - - if (!clip) { - // 检查是否正在加载,避免重复请求 - let loadPromise = this.loading_cache.get(path); - if (!loadPromise) { - const extension = path.split('.').pop(); - loadPromise = resLoader.loadRemote(path, { ext: `.${extension}` }); - this.loading_cache.set(path, loadPromise); - } - - clip = await loadPromise; - this.res_remote.set(path, clip); - this.loading_cache.delete(path); // 加载完成后清除缓存 - } - } - // 资源加载 else { key = `${finalParams.type}_${bundle}_${path}`; - clip = resLoader.get(path, AudioClip, bundle); - - // 加载音效资源 - if (!clip) { - const cacheKey = `${bundle}_${path}`; - let loadPromise = this.loading_cache.get(cacheKey); - - if (!loadPromise) { - loadPromise = resLoader.load(bundle, path, AudioClip); - this.loading_cache.set(cacheKey, loadPromise); - - // 记录资源路径(使用Set避免重复) - let paths = this.res_project.get(bundle); - if (!paths) { - paths = new Set(); - this.res_project.set(bundle, paths); - } - paths.add(path); - } - - clip = await loadPromise; - this.loading_cache.delete(cacheKey); // 加载完成后清除缓存 - } } - // 资源已被释放或加载失败 - if (!clip || !clip.isValid) { - console.warn(`音效资源【${key!}】已被释放或加载失败`); + // 通过 loader 加载/获取资源(自动处理缓存和引用计数) + const result = await this.loader.load(path, bundle); + if (!result) { + console.warn(`音效资源加载失败: ${key}`); + return null!; + } + + const clip = result.clip; + if (!clip.isValid) { + console.warn(`音效资源【${key}】已失效`); return null!; } // 获取音效播放器播放音乐 let ae: AudioEffect; let node: Node; - + if (this.pool.size() === 0) { const aeid = this.getAeId(); key = `${key}_${aeid}`; @@ -203,33 +155,40 @@ export class AudioEffectPool { // 记录正在播放的音效播放器 this.effects.set(ae.key, ae); - node.parent = oops.audio.node; - ae.path = path; - ae.params = finalParams; - ae.loop = finalParams.loop!; - ae.volume = finalParams.volume!; - ae.clip = clip; - ae.play(); + try { + node.parent = oops.audio.node; + ae.path = path; + ae.params = finalParams; + ae.loop = finalParams.loop!; + ae.volume = finalParams.volume!; + ae.clip = clip; + ae.play(); - return ae; + return ae; + } + catch (e) { + // 播放异常时清理 effects 条目,防止残留 + this.effects.delete(ae.key); + this.put(ae); + console.warn(`音效播放异常,已回收: ${key}`, e); + return null!; + } } /** 音效播放完成 */ private onAudioEffectPlayComplete(ae: AudioEffect) { - if (ae.params.destroy) { - if (ae.path instanceof AudioClip) { - ae.path.decRef(); - } - else { - resLoader.release(ae.path, ae.params!.bundle); - } + // 通过 loader 释放资源引用(自动处理延迟释放) + if (ae.path instanceof AudioClip) { + this.loader.release(ae.path.uuid); + } + else { + this.loader.release(ae.path as string, ae.params?.bundle); } // 循环播放的音效或自动释放音乐资源的音效,自动回收音乐播放器 if (!ae.params.loop || ae.params.destroy) { ae.params && ae.params.onPlayComplete && ae.params.onPlayComplete(ae); this.put(ae); - // console.log(`【音效】回收,池中剩余音效播放器【${this.pool.size()}】`); } } @@ -281,14 +240,8 @@ export class AudioEffectPool { // 释放池中音乐播放器 this.releasePool(); - // 释放各个资源包中的音效资源 - this.releaseRes(); - - // 释放外网远程音效资源 - this.releaseResRemote(); - - // 清空加载缓存 - this.loading_cache.clear(); + // 清空 loader 缓存(强制释放所有音频资源) + this.loader.clearCache(); } /** 释放池中音乐播放器 */ @@ -303,22 +256,58 @@ export class AudioEffectPool { this.effects.clear(); } - /** 释放各个资源包中的音效资源 */ - releaseRes() { - this.res_project.forEach((paths: Set, bundleName: string) => { - paths.forEach((path) => resLoader.release(path, bundleName)); - paths.clear(); // 清空Set - }); - this.res_project.clear(); + /** + * 按容量释放对象池中的空闲节点 + * @param maxSize 保留的最大空闲节点数,超出部分销毁 + * @returns 实际销毁的节点数 + */ + releasePoolBySize(maxSize: number): number { + let destroyed = 0; + while (this.pool.size() > maxSize) { + const node = this.pool.get(); + if (node) { + node.destroy(); + destroyed++; + } + else { + break; + } + } + return destroyed; } - /** 释放外网远程音效资源 */ - releaseResRemote() { - this.res_remote.forEach((clip: AudioClip) => { - if (clip && clip.isValid) { - clip.decRef(); - } - }); - this.res_remote.clear(); + /** + * 释放指定远程音效资源(立即释放,不等待延迟) + * @param path 远程资源 URL + * @returns 是否成功释放 + */ + releaseResRemoteByPath(path: string): boolean { + this.loader.releaseImmediately(path); + return true; + } + + /** + * 释放指定路径的音效资源引用 + * @param path 资源路径 + * @param bundle 资源包名(可选) + */ + releaseResByPath(path: string, bundle?: string): void { + this.loader.release(path, bundle); + } + + private mergeParams(params?: IAudioParams): IAudioParams { + return params ? { + type: params.type ?? AudioEffectType.Effect, + bundle: params.bundle ?? resLoader.defaultBundleName, + loop: params.loop ?? false, + destroy: params.destroy ?? false, + volume: params.volume, + onPlayComplete: params.onPlayComplete + } : { + type: AudioEffectType.Effect, + bundle: resLoader.defaultBundleName, + loop: false, + destroy: false + }; } } \ No newline at end of file diff --git a/assets/core/common/audio/AudioManager.ts b/assets/core/common/audio/AudioManager.ts index 7454d5d..19180b0 100644 --- a/assets/core/common/audio/AudioManager.ts +++ b/assets/core/common/audio/AudioManager.ts @@ -47,6 +47,15 @@ export class AudioManager extends Component { this.effect.put(ae); } + /** + * 释放指定远程音效资源 + * @param path 远程资源 URL + * @returns 是否成功释放 + */ + releaseEffectRemote(path: string): boolean { + return this.effect.releaseResRemoteByPath(path); + } + /** 恢复当前暂停的音乐与音效播放 */ resumeAll() { this.music.resume(); @@ -125,4 +134,4 @@ export class AudioManager extends Component { this.music = null!; this.data = null!; } -} +} \ No newline at end of file diff --git a/assets/core/common/audio/AudioMusic.ts b/assets/core/common/audio/AudioMusic.ts index d28a6a2..2ba1055 100644 --- a/assets/core/common/audio/AudioMusic.ts +++ b/assets/core/common/audio/AudioMusic.ts @@ -4,8 +4,9 @@ * @LastEditors: dgflash * @LastEditTime: 2023-05-16 09:11:30 */ -import { AudioClip, Node } from 'cc'; +import { Node } from 'cc'; import { resLoader } from '../loader/ResLoader'; +import { AudioClipLoader } from './AudioClipLoader'; import { AudioEffect } from './AudioEffect'; import { AudioEffectType } from './AudioEnum'; import type { IAudioData, IAudioParams } from './IAudio'; @@ -19,11 +20,17 @@ export class AudioMusic extends Node { /** 音效配置数据 */ private data: { [node: string]: IAudioData } = null!; + /** 音频资源加载器(统一管理引用计数与延迟释放) */ + private loader: AudioClipLoader = new AudioClipLoader(); private _progress = 0; private _isLoading = false; private _nextPath: string | null = null; private _nextParams: IAudioParams | null = null; private _ae: AudioEffect = null!; + /** 当前播放的音乐路径(用于释放引用) */ + private _currentPath: string | null = null; + /** 当前播放的音乐 bundle(用于释放引用) */ + private _currentBundle: string | null = null; /** * 音效开关 @@ -92,17 +99,59 @@ export class AudioMusic extends Node { * @param params 背景音乐资源播放参数 */ async loadAndPlay(path: string, params?: IAudioParams) { - if (!this.getSwitch()) return; // 禁止播放音乐 + if (!this.getSwitch()) return; - // 下一个加载的背景音乐资源(避免重复存储,直接覆盖) if (this._isLoading) { this._nextPath = path; this._nextParams = params || null; return; } - // 合并默认参数(减少对象创建) - const finalParams: IAudioParams = params ? { + const finalParams = this.mergeParams(params); + this._isLoading = true; + + const result = await this.loader.load(path, finalParams.bundle); + this._isLoading = false; + + if (!result) { + console.warn(`音乐资源加载失败: ${path}`); + return; + } + + if (this._nextPath !== null) { + const nextPath = this._nextPath; + const nextParams = this._nextParams; + this._nextPath = null; + // 清理回调引用,防止闭包持有外部对象 + this._nextParams = null; + + // 释放刚加载的资源引用(未实际播放) + this.loader.release(path, finalParams.bundle); + + this.loadAndPlay(nextPath, nextParams || undefined); + } + else { + if (this._ae.playing) this.stop(); + + // 释放当前播放的资源引用 + this.release(); + + this._ae.params = finalParams; + this._ae.path = path; + this._ae.clip = result.clip; + this._ae.loop = finalParams.loop!; + this._ae.volume = finalParams.volume!; + this._ae.currentTime = 0; + this._ae.play(); + + // 记录当前播放的资源路径,用于后续释放 + this._currentPath = path; + this._currentBundle = finalParams.bundle || null; + } + } + + private mergeParams(params?: IAudioParams): IAudioParams { + return params ? { type: params.type ?? AudioEffectType.Music, bundle: params.bundle ?? resLoader.defaultBundleName, loop: params.loop ?? true, @@ -115,56 +164,6 @@ export class AudioMusic extends Node { loop: true, volume: this.getVolume() }; - - this._isLoading = true; - - let clip: AudioClip | null = null; - if (path.indexOf('http') === 0) { - const extension = path.split('.').pop(); - clip = await resLoader.loadRemote(path, { ext: `.${extension}` }); - } - else { - clip = await resLoader.load(finalParams.bundle!, path, AudioClip); - } - - this._isLoading = false; - - // 加载失败处理 - if (!clip) { - console.warn(`音乐资源加载失败: ${path}`); - return; - } - - // 处理等待加载的背景音乐 - if (this._nextPath !== null) { - const nextPath = this._nextPath; - const nextParams = this._nextParams; - // 立即清空引用,减少内存占用 - this._nextPath = null; - this._nextParams = null; - - // 释放刚加载的音乐资源(因为有新的音乐要播放) - clip.decRef(); - - // 加载等待播放的背景音乐 - this.loadAndPlay(nextPath, nextParams || undefined); - } - else { - // 正在播放的时候先关闭 - if (this._ae.playing) this.stop(); - - // 删除当前正在播放的音乐 - this.release(); - - // 播放背景音乐 - this._ae.params = finalParams; - this._ae.path = path; - this._ae.clip = clip; - this._ae.loop = finalParams.loop!; - this._ae.volume = finalParams.volume!; - this._ae.currentTime = 0; - this._ae.play(); - } } /** 恢复当前暂停的音乐与音效播放 */ @@ -186,9 +185,15 @@ export class AudioMusic extends Node { release() { if (this._ae && this._ae.clip) { this.stop(); - this._ae.clip.decRef(); this._ae.clip = null; } + + // 通过 loader 释放资源引用(自动处理延迟释放) + if (this._currentPath) { + this.loader.release(this._currentPath, this._currentBundle || undefined); + this._currentPath = null; + this._currentBundle = null; + } } /** 节点销毁时清理所有引用 */ @@ -198,5 +203,6 @@ export class AudioMusic extends Node { this._nextParams = null; this._ae = null!; this.data = null!; + this.loader.destroy(); } -} +} \ No newline at end of file diff --git a/assets/core/common/audio/IAudio.ts b/assets/core/common/audio/IAudio.ts index 83d058e..b423ef1 100644 --- a/assets/core/common/audio/IAudio.ts +++ b/assets/core/common/audio/IAudio.ts @@ -18,4 +18,4 @@ export interface IAudioData { switch: boolean; /** 音量 */ volume: number; -} +} \ No newline at end of file diff --git a/assets/core/common/loader/ResAutoTracker.ts b/assets/core/common/loader/ResAutoTracker.ts new file mode 100644 index 0000000..249fa30 --- /dev/null +++ b/assets/core/common/loader/ResAutoTracker.ts @@ -0,0 +1,247 @@ +import type { Asset, Component } from 'cc'; +import { assetManager } from 'cc'; + +/** + * 一次 acquire 记录的根资源及其递归依赖(均已在引擎缓存中执行过 addRef) + */ +export interface TrackedResEntry { + /** 本轮加载的根资源(如 Prefab、SpriteFrame 等) */ + asset: Asset; + /** 通过 dependUtil.getDepsRecursively 得到的依赖(不含 asset 本身) */ + deps: Asset[]; +} + +/** + * 基于引擎 Asset.refCount 的自动引用管理:生命周期内 acquire,销毁时统一 release。 + * - 不显式自建「第二层」计数器,仅以 addRef/decRef + 条目列表对齐逻辑所有权 + */ +class ResAutoTracker { + /** 持有者 -> 该车持有过的所有条目 */ + private readonly ownerEntries = new Map(); + private debugMode = false; + + enableDebug(enabled: boolean): void { + this.debugMode = enabled; + } + + /** 持有者是否已通过本追踪器占用过资源 */ + isTracking(owner: Component): boolean { + const list = this.ownerEntries.get(owner); + return list != null && list.length > 0; + } + + /** + * 为持有者增加对资源及其递归依赖的引用(主资源 + deps 各自 addRef) + */ + acquire(owner: Component, asset: Asset | null | undefined): void { + if (!owner || !asset) { + return; + } + + const deps = this.collectDependencyAssets(asset); + asset.addRef(); + const n = deps.length; + for (let i = 0; i < n; i++) { + deps[i].addRef(); + } + + const entry: TrackedResEntry = { asset, deps }; + let arr = this.ownerEntries.get(owner); + if (!arr) { + arr = []; + this.ownerEntries.set(owner, arr); + } + arr.push(entry); + + if (this.debugMode) { + console.log(`[ResAutoTracker] acquire owner=${this.ownerLabel(owner)} asset=${asset.name} deps=${deps.length} ref(main)=${asset.refCount}`); + } + } + + /** + * 为持有者批量登记资源(常用于 loadDir / loadAny) + */ + acquireMany(owner: Component, assets: (Asset | null | undefined)[] | null | undefined): void { + if (!owner || !assets || assets.length === 0) { + return; + } + const len = assets.length; + for (let i = 0; i < len; i++) { + this.acquire(owner, assets[i]); + } + } + + /** + * 释放持有者登记的一条「与给定 asset uuid 匹配的」条目(若同 asset 有多条仅移除最先匹配的一条) + */ + releaseByAsset(owner: Component, asset: Asset | null | undefined): boolean { + if (!owner || !asset) { + return false; + } + const arr = this.ownerEntries.get(owner); + if (!arr || arr.length === 0) { + return false; + } + const uuid = asset.uuid; + const idx = arr.findIndex(e => e.asset.uuid === uuid); + if (idx < 0) { + return false; + } + const [removed] = arr.splice(idx, 1); + this.releaseEntry(owner, removed); + if (arr.length === 0) { + this.ownerEntries.delete(owner); + } + return true; + } + + /** 路径 + bundle:从缓存取资源后解除一条逻辑引用(供 releaseRes(path) 使用) */ + releaseByPath(owner: Component, path: string, bundleName: string): boolean { + if (!owner || !path) { + return false; + } + const bundle = bundleName ? assetManager.getBundle(bundleName) : null; + if (!bundle) { + return false; + } + const a = bundle.get(path) as Asset | null; + if (!a) { + return false; + } + return this.releaseByAsset(owner, a); + } + + /** + * 释放持有者名下全部条目,返回被逻辑释放的条目数 + */ + releaseAll(owner: Component | null | undefined): number { + if (!owner) { + return 0; + } + const arr = this.ownerEntries.get(owner); + if (!arr || arr.length === 0) { + this.ownerEntries.delete(owner); + return 0; + } + const count = arr.length; + const copy = arr.splice(0, arr.length); + this.ownerEntries.delete(owner); + + let i = copy.length - 1; + for (; i >= 0; i--) { + this.releaseEntry(owner, copy[i]!); + } + return count; + } + + /** 持有者当前登记的根资源条目数 */ + getOwnerEntryCount(owner: Component): number { + return this.ownerEntries.get(owner)?.length ?? 0; + } + + getStats(): { totalOwners: number; totalTrackedRoots: number; totalDepAssetsInEntries: number } { + let totalTrackedRoots = 0; + let totalDepAssetsInEntries = 0; + + const owners = [...this.ownerEntries.keys()]; + const totalOwners = owners.length; + + owners.forEach((owner) => { + const entries = this.ownerEntries.get(owner); + const len = entries?.length ?? 0; + if (!entries || len === 0) return; + totalTrackedRoots += len; + for (let i = 0; i < len; i++) { + totalDepAssetsInEntries += entries[i]!.deps.length; + } + }); + + return { totalOwners, totalTrackedRoots, totalDepAssetsInEntries }; + } + + printOwnerStatus(owner: Component): void { + const entries = this.ownerEntries.get(owner); + console.log(`\n===== ResAutoTracker ${this.ownerLabel(owner)} =====`); + if (!entries || entries.length === 0) { + console.log(' (无)'); + } + else { + entries.forEach((e, idx) => { + console.log(` [${idx}] ${e.asset.constructor.name} name=${e.asset.name} uuid=${e.asset.uuid} refCount=${e.asset.refCount} deps=${e.deps.length}`); + }); + } + console.log('========================================\n'); + } + + printStatus(): void { + console.log('\n========== ResAutoTracker 全局 =========='); + const stats = this.getStats(); + console.log(` 持有者数: ${stats.totalOwners} | 根资源条目: ${stats.totalTrackedRoots} | 条目内依赖条数之和: ${stats.totalDepAssetsInEntries}`); + + this.ownerEntries.forEach((entries, owner) => { + console.log(`\n ▸ ${this.ownerLabel(owner)} — ${entries.length} 条`); + entries.forEach((e, i) => { + console.log(` [${i}] ${e.asset.constructor.name} ref=${e.asset.refCount} deps=${e.deps.length}`); + }); + }); + + console.log('=========================================\n'); + } + + /** 清空记录(不推荐运行时使用;不传参清空全部持有者) */ + clear(): void { + this.ownerEntries.clear(); + if (this.debugMode) { + console.warn('[ResAutoTracker] clear() — 已与引擎 refCount 不同步:仅清空表,不负责 decRef'); + } + } + + private ownerLabel(owner: Component): string { + const ctor = owner?.constructor?.name ?? 'Unknown'; + const nodeName = owner?.node?.name ?? '?'; + const uuidShort = owner?.uuid?.slice?.(0, 8) ?? '?'; + return `${ctor}<${nodeName}>@${uuidShort}`; + } + + private collectDependencyAssets(root: Asset): Asset[] { + const out: Asset[] = []; + const seen = new Set(); + seen.add(root.uuid); + + const uuidList = assetManager?.dependUtil?.getDepsRecursively?.(root.uuid); + if (!uuidList?.length) { + return out; + } + + const n = uuidList.length; + for (let i = 0; i < n; i++) { + const id = uuidList[i] as string; + if (!id || seen.has(id)) { + continue; + } + const dep = assetManager.assets.get(id); + if (dep && !seen.has(dep.uuid)) { + seen.add(dep.uuid); + out.push(dep); + } + } + + return out; + } + + private releaseEntry(owner: Component, entry: TrackedResEntry): void { + const deps = entry.deps; + const dLen = deps.length; + let i = dLen - 1; + for (; i >= 0; i--) { + deps[i]?.decRef(); + } + entry.asset.decRef(); + + if (this.debugMode) { + console.log(`[ResAutoTracker] release owner=${this.ownerLabel(owner)} asset=${entry.asset.name}`); + } + } +} + +export const resAutoTracker = new ResAutoTracker(); \ No newline at end of file diff --git a/assets/core/common/loader/ResAutoTracker.ts.meta b/assets/core/common/loader/ResAutoTracker.ts.meta new file mode 100644 index 0000000..7c628a6 --- /dev/null +++ b/assets/core/common/loader/ResAutoTracker.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "2a5b8165-c540-4e3a-abf7-5ef91e6f2c3b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/loader/ResDebug.ts b/assets/core/common/loader/ResDebug.ts new file mode 100644 index 0000000..2a2db36 --- /dev/null +++ b/assets/core/common/loader/ResDebug.ts @@ -0,0 +1,121 @@ +import { Asset, assetManager } from 'cc'; + +/** + * 资源调试工具类 + * 用于打印和分析资源加载情况 + */ +export class ResDebug { + /** + * 打印缓存中所有资源信息 + */ + static dump() { + const builtinBundles = new Set(['internal', 'main', '未分类资源']); + const uuidToInfo: { [uuid: string]: { bundle: string, path: string } } = {}; + + // 从 bundle 配置中收集 uuid 映射 + assetManager.bundles.forEach((bundle) => { + if (builtinBundles.has(bundle.name)) return; + const config = (bundle as any)._config; + if (!config) return; + + if (Array.isArray(config.uuids)) { + config.uuids.forEach((uuid: string) => { uuidToInfo[uuid] = { bundle: bundle.name, path: '' }; }); + } + if (config.paths) { + for (const path in config.paths) { + const arr = config.paths[path]; + if (Array.isArray(arr)) { + arr.forEach((info: any) => { + if (info?.uuid) uuidToInfo[info.uuid] = { bundle: bundle.name, path: info.path || path || '' }; + }); + } + } + } + if (config.scenes) { + for (const scene in config.scenes) { + const info = config.scenes[scene]; + if (info?.uuid) uuidToInfo[info.uuid] = { bundle: bundle.name, path: info.path || scene || '' }; + } + } + // 处理依赖资源 + if (config.dependAssets) { + for (const uuid in config.dependAssets) { + if (!uuidToInfo[uuid]) { + uuidToInfo[uuid] = { bundle: bundle.name, path: '' }; + } + } + } + }); + + // 兜底:用 getAssetInfo 反向查找仍未映射的资源 + assetManager.assets.forEach((value: Asset, key: string) => { + if (value.refCount <= 0 || uuidToInfo[key]) return; + assetManager.bundles.forEach((bundle) => { + if (builtinBundles.has(bundle.name)) return; + const info = bundle.getAssetInfo(key); + if (info) uuidToInfo[key] = { bundle: bundle.name, path: (info as any).path || '' }; + }); + }); + + // 按 bundle 分组 - 包括所有有引用的资源 + const bundleGroups: { [bundleName: string]: { uuid: string, path: string, refCount: number, asset: Asset }[] } = {}; + + // 先处理已映射的资源 + assetManager.assets.forEach((value: Asset, key: string) => { + if (value.refCount <= 0) return; + const info = uuidToInfo[key]; + if (info) { + (bundleGroups[info.bundle] ||= []).push({ uuid: key, refCount: value.refCount, path: info.path, asset: value }); + } + }); + + // 处理未映射但有引用的资源(放入 unknown 分组) + const unknownAssets: { uuid: string, path: string, refCount: number, asset: Asset }[] = []; + assetManager.assets.forEach((value: Asset, key: string) => { + if (value.refCount <= 0) return; + if (!uuidToInfo[key]) { + unknownAssets.push({ uuid: key, refCount: value.refCount, path: '', asset: value }); + } + }); + + // 将未分类资源归入 bundleGroups + if (unknownAssets.length > 0) { + bundleGroups['未分类资源'] = unknownAssets; + } + + // 打印结果(过滤掉 builtinBundles) + for (const bundleName in bundleGroups) { + if (builtinBundles.has(bundleName)) continue; + const items = bundleGroups[bundleName]; + console.group(`[ResLoader] Bundle: ${bundleName} (${items.length})`); + console.log(`[ResLoader] ----- ${bundleName} -----`); + console.log(items); + console.groupEnd(); + } + + console.log(`[ResLoader] 当前资源总数: ${assetManager.assets.count}`); + } + + /** + * 获取资源统计信息 + */ + static getStats(): { totalAssets: number, totalBundles: number, bundleStats: { [name: string]: number } } { + const bundleStats: { [name: string]: number } = {}; + let totalAssets = 0; + + assetManager.bundles.forEach((bundle) => { + const count = bundle.getDirWithPath('').length; + bundleStats[bundle.name] = count; + }); + + assetManager.assets.forEach((asset: Asset) => { + if (asset.refCount > 0) totalAssets++; + }); + + return { + totalAssets, + totalBundles: (assetManager.bundles as any).size || 0, + bundleStats + }; + } +} \ No newline at end of file diff --git a/assets/core/common/loader/ResDebug.ts.meta b/assets/core/common/loader/ResDebug.ts.meta new file mode 100644 index 0000000..7caa03e --- /dev/null +++ b/assets/core/common/loader/ResDebug.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "96c108eb-8f7e-408d-8a6c-00d5cf5be314", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/loader/ResErrors.ts b/assets/core/common/loader/ResErrors.ts new file mode 100644 index 0000000..8f1bcd6 --- /dev/null +++ b/assets/core/common/loader/ResErrors.ts @@ -0,0 +1,26 @@ +/** 资源加载错误类,包含上下文信息 */ +export class ResourceError extends Error { + /** 资源路径 */ + readonly path?: string; + /** 资源包名 */ + readonly bundle?: string; + /** 原始错误 */ + readonly cause?: Error | string; + + constructor(message: string, options?: { path?: string; bundle?: string; cause?: Error | string }) { + super(message); + this.name = 'ResourceError'; + this.path = options?.path; + this.bundle = options?.bundle; + this.cause = options?.cause; + } + + /** 格式化错误信息 */ + toString(): string { + let msg = `[ResourceError] ${this.message}`; + if (this.bundle) msg += `\n Bundle: ${this.bundle}`; + if (this.path) msg += `\n Path: ${this.path}`; + if (this.cause) msg += `\n Cause: ${this.cause instanceof Error ? this.cause.message : this.cause}`; + return msg; + } +} \ No newline at end of file diff --git a/assets/core/common/loader/ResErrors.ts.meta b/assets/core/common/loader/ResErrors.ts.meta new file mode 100644 index 0000000..55c700b --- /dev/null +++ b/assets/core/common/loader/ResErrors.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "54c4ac22-3d70-4716-bd8e-335d919c6ce4", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/loader/ResLoader.ts b/assets/core/common/loader/ResLoader.ts index dd0ed98..438e812 100644 --- a/assets/core/common/loader/ResLoader.ts +++ b/assets/core/common/loader/ResLoader.ts @@ -1,137 +1,190 @@ -import type { __private, AssetManager } from 'cc'; -import { AnimationClip, Asset, assetManager, AudioClip, Font, ImageAsset, js, JsonAsset, Material, Mesh, Prefab, resources, sp, SpriteFrame, Texture2D } from 'cc'; +import { Asset, assetManager, resources } from 'cc'; +import { AssetType, ILoadResArgs, IRemoteOptions, Paths, ProgressCallback, CompleteCallback } from './ResTypes'; +import { ResourceError } from './ResErrors'; +import { isValidString, warn, error, createError, releasePrefabDepsRecursively } from './ResUtils'; -export type AssetType = __private.__types_globals__Constructor | null; -export type Paths = string | string[]; -export type ProgressCallback = ((finished: number, total: number, item: AssetManager.RequestItem) => void) | null; -export type CompleteCallback = any; -export type IRemoteOptions = { [k: string]: any; ext?: string; } | null; +// 类型导出 +export type { AssetType, Paths, ProgressCallback, CompleteCallback, IRemoteOptions, ILoadResArgs } from './ResTypes'; -interface ILoadResArgs { - /** 资源包名 */ - bundle?: string; - /** 资源文件夹名 */ - dir?: string; - /** 资源路径 */ - paths: Paths; - /** 资源类型 */ - type: AssetType; - /** 资源加载进度 */ - onProgress: ProgressCallback; - /** 资源加载完成 */ - onComplete: CompleteCallback; - /** 是否为预加载 */ - preload?: boolean; -} +// 错误类导出 +export { ResourceError } from './ResErrors'; + +// 工具方法导出 +export { isValidString, warn, error, createError, releasePrefabDepsRecursively } from './ResUtils'; + +// 调试工具导出 +export { ResDebug } from './ResDebug'; /** - * 游戏资源管理 - * 1、加载默认resources文件夹中资源 - * 2、加载默认bundle远程资源 - * 3、主动传递bundle名时,优先加载传递bundle名资源包中的资源 - * - * @help https://gitee.com/dgflash/oops-framework/wikis/pages?sort_id=12037901&doc_id=2873565 + * 资源加载器核心类 + * 负责底层资源加载、释放、缓存管理 */ export class ResLoader { - //#region 资源配置数据 /** 全局默认加载的资源包名 */ defaultBundleName = 'resources'; - /** 下载时的最大并发数 - 项目设置 -> 项目数据 -> 资源下载并发数,设置默认值;初始值为15 */ + /** 正在加载的 Bundle Promise 缓存,防止并发重复加载 */ + private _loadingBundles: Map> = new Map(); + + /** 正在加载的资源 Promise 缓存,防止并发重复加载 */ + private _loadingAssets: Map> = new Map(); + + /** 已加载的远程资源缓存,用于统一管理释放 */ + private _remoteAssets: Map = new Map(); + + //#region 下载配置 + /** 获取最大并发下载数 */ get maxConcurrency(): number { return assetManager.downloader.maxConcurrency; } + + /** 设置最大并发下载数 */ set maxConcurrency(value: number) { assetManager.downloader.maxConcurrency = value; } - /** 下载时每帧可以启动的最大请求数 - 默认值为15 */ + /** 获取每帧最大请求数 */ get maxRequestsPerFrame(): number { return assetManager.downloader.maxRequestsPerFrame; } + + /** 设置每帧最大请求数 */ set maxRequestsPerFrame(value: number) { assetManager.downloader.maxRequestsPerFrame = value; } - /** 失败重试次数 - 默认值为0 */ + /** 获取最大重试次数 */ get maxRetryCount(): number { return assetManager.downloader.maxRetryCount; } + + /** 设置最大重试次数 */ set maxRetryCount(value: number) { assetManager.downloader.maxRetryCount = value; } - /** 重试的间隔时间,单位为毫秒 - 默认值为2000毫秒 */ + /** 获取重试间隔(毫秒) */ get retryInterval(): number { return assetManager.downloader.retryInterval; } + + /** 设置重试间隔(毫秒) */ set retryInterval(value: number) { assetManager.downloader.retryInterval = value; } //#endregion - //#region 加载远程资源 + //#region 远程资源加载 /** * 加载远程资源 - * @param url 资源地址 - * @param options 资源参数,例:{ ext: ".png" } - * @example - var opt: IRemoteOptions = { ext: ".png" }; - var data = await oops.res.loadRemote(this.url, opt); - const texture = new Texture2D(); - texture.image = data; - - const spriteFrame = new SpriteFrame(); - spriteFrame.texture = texture; - - var sprite = this.sprite.addComponent(Sprite); - sprite.spriteFrame = spriteFrame; + * @param url 资源URL + * @param options 加载选项 + * @returns 资源Promise */ loadRemote(url: string, options: IRemoteOptions | null = null): Promise { return new Promise((resolve, reject) => { + if (!isValidString(url)) { + reject(createError('loadRemote', 'url 不能为空')); + return; + } + + const cachedAsset = this._remoteAssets.get(url); + if (cachedAsset) { + resolve(cachedAsset as T); + return; + } + assetManager.loadRemote(url, options, (err, data: T) => { if (err) { - reject(err); + reject(createError('loadRemote', `加载远程资源失败: ${url}`, err)); return; } + this._remoteAssets.set(url, data); resolve(data); }); }); } - //#endregion - - //#region 资源包管理 /** - * 获取资源包 - * @param name 资源包名 + * 释放指定远程资源 + * @param url 资源URL + */ + releaseRemote(url: string) { + if (!isValidString(url)) { + warn('releaseRemote', 'url 不能为空'); + return; + } + + const asset = this._remoteAssets.get(url); + if (!asset) { + warn('releaseRemote', `远程资源 "${url}" 不存在`); + return; + } + + asset.decRef(); + this._remoteAssets.delete(url); + } + + /** 释放所有远程资源 */ + releaseRemoteAll() { + this._remoteAssets.forEach((asset) => asset.decRef()); + this._remoteAssets.clear(); + } + + /** + * 获取远程资源数量 + * @returns 资源数量 + */ + getRemoteAssetCount(): number { + return this._remoteAssets.size; + } + //#endregion + + //#region Bundle 管理 + /** + * 获取已加载的Bundle + * @param name Bundle名称 + * @returns Bundle对象或null */ getBundle(name: string) { return assetManager.bundles.get(name); } /** - * 加载资源包 - * @param name 资源地址 - * @param options 资源参数,例:{ version: "74fbe" } - * @example - await oops.res.loadBundle(name, options); + * 加载Bundle + * @param name Bundle名称 + * @param options 加载选项 + * @returns Bundle Promise */ - loadBundle(name: string, options: { [k: string]: any; version?: string; } | null = null): Promise { - return new Promise((resolve, reject) => { - assetManager.loadBundle(name, options, (err, bundle: AssetManager.Bundle) => { + loadBundle(name: string, options: { [k: string]: any; version?: string; } | null = null): Promise { + if (!isValidString(name)) { + return Promise.reject(createError('loadBundle', 'name 不能为空')); + } + + const existingBundle = assetManager.bundles.get(name); + if (existingBundle) return Promise.resolve(existingBundle); + + const loadingPromise = this._loadingBundles.get(name); + if (loadingPromise) return loadingPromise; + + const promise = new Promise((resolve, reject) => { + assetManager.loadBundle(name, options, (err, bundle) => { + this._loadingBundles.delete(name); if (err) { - reject(err); + reject(createError('loadBundle', `加载资源包失败: ${name}`, err)); return; } resolve(bundle); }); }); + + this._loadingBundles.set(name, promise); + return promise; } /** - * 释放资源包与包中所有资源 - * @param bundleName 资源地址 + * 移除并释放Bundle + * @param bundleName Bundle名称 */ removeBundle(bundleName: string) { const bundle = assetManager.bundles.get(bundleName); @@ -142,31 +195,103 @@ export class ResLoader { } //#endregion - //#region 预加载资源 + //#region 资源加载 /** - * 加载一个资源 - * @param bundleName 远程包名 - * @param paths 资源路径 - * @param type 资源类型 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 + * 加载资源 + * @param bundleName Bundle名称 + * @param paths 资源路径或类型 + * @param type 资源类型 + * @returns 资源Promise */ - preload(bundleName: string, paths: Paths, type: AssetType, onProgress: ProgressCallback): Promise; - preload(bundleName: string, paths: Paths, onProgress: ProgressCallback): Promise; - preload(bundleName: string, paths: Paths): Promise; - preload(bundleName: string, paths: Paths, type: AssetType): Promise; - preload(paths: Paths, type: AssetType, onProgress: ProgressCallback): Promise; - preload(paths: Paths, onProgress: ProgressCallback): Promise; - preload(paths: Paths): Promise; - preload(paths: Paths, type: AssetType): Promise; - preload( - bundleName: string, - paths?: Paths | AssetType | ProgressCallback, - type?: AssetType | ProgressCallback, - onProgress?: ProgressCallback - ) { + load(bundleName: string, paths: Paths | AssetType, type?: AssetType): Promise { + let args: ILoadResArgs | null = null; + if (typeof paths === 'string' || paths instanceof Array) { + args = this.parseLoadResArgs(paths, type, null); + args.bundle = bundleName; + } + else { + args = this.parseLoadResArgs(bundleName, paths, null); + args.bundle = this.defaultBundleName; + } + + const pathsKey = Array.isArray(args.paths) ? args.paths.join(',') : args.paths; + const typeKey = args.type ? (args.type as any).name || 'Asset' : 'Asset'; + const cacheKey = `${args.bundle}:${pathsKey}:${typeKey}`; + + const loadingPromise = this._loadingAssets.get(cacheKey); + if (loadingPromise) return loadingPromise as Promise; + + const promise = new Promise((resolve, reject) => { + const onComplete = (err: Error | null, data: T) => { + this._loadingAssets.delete(cacheKey); + if (err) { + reject(err); + return; + } + resolve(data); + }; + + args!.onComplete = onComplete; + this.loadByArgs(args!); + }); + + this._loadingAssets.set(cacheKey, promise); + return promise; + } + + /** + * 加载任意资源(支持多种参数组合) + * @param bundleName Bundle名称或路径数组 + * @param paths 路径数组或进度回调 + * @param onProgress 进度回调 + * @param onComplete 完成回调 + */ + loadAny(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void { + let args: ILoadResArgs | null = null; + if (typeof bundleName === 'string' && paths instanceof Array) { + args = this.parseLoadResArgs(paths, onProgress, onComplete); + args.bundle = bundleName; + } + else { + args = this.parseLoadResArgs(bundleName, paths, onProgress); + args.bundle = this.defaultBundleName; + } + this.loadByArgs(args); + } + + /** + * 加载目录资源 + * @param bundleName Bundle名称 + * @param dir 目录路径或类型或回调 + * @param type 资源类型或回调 + * @param onProgress 进度回调 + * @param onComplete 完成回调 + */ + loadDir(bundleName: string, dir?: string | AssetType | ProgressCallback | CompleteCallback, type?: AssetType | ProgressCallback | CompleteCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void { + let args: ILoadResArgs | null = null; + if (typeof dir === 'string') { + args = this.parseLoadResArgs(dir, type, onProgress, onComplete); + args.bundle = bundleName; + } + else { + args = this.parseLoadResArgs(bundleName, dir, type, onProgress); + args.bundle = this.defaultBundleName; + } + args.dir = args.paths as string; + this.loadByArgs(args); + } + + /** + * 预加载资源 + * @param bundleName Bundle名称 + * @param paths 资源路径或类型或回调 + * @param type 资源类型或回调 + * @param onProgress 进度回调 + * @returns Promise + */ + preload(bundleName: string, paths?: Paths | AssetType | ProgressCallback, type?: AssetType | ProgressCallback, onProgress?: ProgressCallback): Promise { return new Promise((resolve, reject) => { - const onComplete = (err: Error | null, data: AssetManager.RequestItem) => { + const onComplete = (err: Error | null, data: any) => { if (err) { reject(err); return; @@ -189,28 +314,14 @@ export class ResLoader { } /** - * 预加载文件夹中的资源 - * @param bundleName 远程包名 - * @param dir 文件夹名 - * @param type 资源类型 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 + * 预加载目录资源 + * @param bundleName Bundle名称 + * @param dir 目录路径或类型或回调 + * @param type 资源类型或回调 + * @param onProgress 进度回调 + * @param onComplete 完成回调 */ - preloadDir(bundleName: string, dir: string, type: AssetType, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - preloadDir(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - preloadDir(bundleName: string, dir: string, onComplete?: CompleteCallback): void; - preloadDir(bundleName: string, dir: string, type: AssetType, onComplete?: CompleteCallback): void; - preloadDir(dir: string, type: AssetType, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - preloadDir(dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - preloadDir(dir: string, onComplete?: CompleteCallback): void; - preloadDir(dir: string, type: AssetType, onComplete?: CompleteCallback): void; - preloadDir( - bundleName: string, - dir?: string | AssetType | ProgressCallback | CompleteCallback, - type?: AssetType | ProgressCallback | CompleteCallback, - onProgress?: ProgressCallback | CompleteCallback, - onComplete?: CompleteCallback, - ) { + preloadDir(bundleName: string, dir?: string | AssetType | ProgressCallback | CompleteCallback, type?: AssetType | ProgressCallback | CompleteCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void { let args: ILoadResArgs | null = null; if (typeof dir === 'string') { args = this.parseLoadResArgs(dir, type, onProgress, onComplete); @@ -226,141 +337,48 @@ export class ResLoader { } //#endregion - //#region 资源加载、获取、释放 + //#region 资源释放 /** - * 加载一个资源 - * @param bundleName 远程包名 - * @param paths 资源路径 - * @param type 资源类型 - * @example - const sd = await oops.res.load("spine_path", sp.SkeletonData); - */ - load(bundleName: string, paths: Paths | AssetType, type?: AssetType) { - return new Promise((resolve, reject) => { - const onComplete = (err: Error | null, data: T) => { - if (err) { - reject(err); - return; - } - // 增加引用计数,防止资源被意外释放 - // if (data) { - // data.addRef(); - // } - resolve(data); - }; - - let args: ILoadResArgs | null = null; - if (typeof paths === 'string' || paths instanceof Array) { - args = this.parseLoadResArgs(paths, type, onComplete); - args.bundle = bundleName; - } - else { - args = this.parseLoadResArgs(bundleName, paths, onComplete); - args.bundle = this.defaultBundleName; - } - this.loadByArgs(args); - }); - } - - /** - * 加载指定资源包中的多个任意类型资源 - * @param bundleName 远程包名 - * @param paths 资源路径 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 - */ - loadAny(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void { - let args: ILoadResArgs | null = null; - if (typeof bundleName === 'string' && paths instanceof Array) { - args = this.parseLoadResArgs(paths, onProgress, onComplete); - args.bundle = bundleName; - } - else { - args = this.parseLoadResArgs(bundleName, paths, onProgress); - args.bundle = this.defaultBundleName; - } - this.loadByArgs(args); - } - - /** - * 加载文件夹中的资源 - * @param bundleName 远程包名 - * @param dir 文件夹名 - * @param type 资源类型 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 - * @example - // 加载进度事件 - var onProgressCallback = (finished: number, total: number, item: any) => { - console.log("资源加载进度", finished, total); - } - - // 加载完成事件 - var onCompleteCallback = () => { - console.log("资源加载完成"); - } - oops.res.loadDir("game", onProgressCallback, onCompleteCallback); - */ - loadDir(bundleName: string, dir: string, type: AssetType, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - loadDir(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - loadDir(bundleName: string, dir: string, onComplete?: CompleteCallback): void; - loadDir(bundleName: string, dir: string, type: AssetType, onComplete?: CompleteCallback): void; - loadDir(dir: string, type: AssetType, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - loadDir(dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void; - loadDir(dir: string, onComplete?: CompleteCallback): void; - loadDir(dir: string, type: AssetType, onComplete?: CompleteCallback): void; - loadDir( - bundleName: string, - dir?: string | AssetType | ProgressCallback | CompleteCallback, - type?: AssetType | ProgressCallback | CompleteCallback, - onProgress?: ProgressCallback | CompleteCallback, - onComplete?: CompleteCallback, - ) { - let args: ILoadResArgs | null = null; - if (typeof dir === 'string') { - args = this.parseLoadResArgs(dir, type, onProgress, onComplete); - args.bundle = bundleName; - } - else { - args = this.parseLoadResArgs(bundleName, dir, type, onProgress); - args.bundle = this.defaultBundleName; - } - args.dir = args.paths as string; - this.loadByArgs(args); - } - - /** - * 通过资源相对路径释放资源 - * @param path 资源路径 - * @param bundleName 远程资源包名 + * 释放指定资源 + * @param path 资源路径 + * @param bundleName Bundle名称 */ release(path: string, bundleName?: string) { + if (!isValidString(path)) { + warn('release', 'path 不能为空'); + return; + } + if (bundleName == undefined) bundleName = this.defaultBundleName; const bundle = assetManager.getBundle(bundleName); - if (bundle) { - const asset = bundle.get(path); - if (asset) { - this.releasePrefabDepsRecursively(asset); - } + if (!bundle) { + warn('release', `资源包 "${bundleName}" 不存在`); + return; } + + const asset = bundle.get(path); + if (!asset) { + warn('release', `资源 "${path}" 在资源包 "${bundleName}" 中不存在`); + return; + } + + releasePrefabDepsRecursively(asset); } /** - * 通过相对文件夹路径删除所有文件夹中资源 - * @param path 资源文件夹路径 - * @param bundleName 远程资源包名 + * 释放目录资源 + * @param path 目录路径 + * @param bundleName Bundle名称 */ releaseDir(path: string, bundleName?: string) { if (bundleName == undefined) bundleName = this.defaultBundleName; - const bundle: AssetManager.Bundle | null = assetManager.getBundle(bundleName); + const bundle = assetManager.getBundle(bundleName); if (bundle) { const infos = bundle.getDirWithPath(path); if (infos) { - infos.forEach((info) => { - this.releasePrefabDepsRecursively(info.uuid); - }); + infos.forEach((info: any) => releasePrefabDepsRecursively(info.uuid)); } if (path == '' && bundleName != 'resources') { @@ -368,12 +386,36 @@ export class ResLoader { } } } + //#endregion + + //#region 资源获取 + /** + * 获取已加载的资源 + * @param path 资源路径 + * @param type 资源类型 + * @param bundleName Bundle名称 + * @returns 资源对象或null + */ + get(path: string, type?: AssetType, bundleName: string = this.defaultBundleName): T | null { + if (!isValidString(path)) { + warn('get', 'path 不能为空'); + return null; + } + + const bundle = assetManager.getBundle(bundleName); + if (!bundle) { + warn('get', `资源包 "${bundleName}" 不存在`); + return null; + } + + return bundle.get(path, type); + } /** * 获取资源路径 - * @param bundleName 资源包名 - * @param uuid 资源唯一编号 - * @returns + * @param bundleName Bundle名称 + * @param uuid 资源UUID + * @returns 资源路径 */ getAssetPath(bundleName: string, uuid: string): string { const b = this.getBundle(bundleName); @@ -382,60 +424,36 @@ export class ResLoader { if (!info) return ''; return (info as any).path || ''; } - - /** 释放预制依赖资源 */ - private releasePrefabDepsRecursively(uuid: string | Asset) { - let asset: Asset | null | undefined; - if (uuid instanceof Asset) { - asset = uuid; - uuid.decRef(); - } - else { - asset = assetManager.assets.get(uuid); - if (asset) asset.decRef(); - } - - // 释放预制引用资源(防止内存泄漏) - // if (asset instanceof Prefab) { - // const uuids: string[] = assetManager.dependUtil.getDepsRecursively(asset.uuid)!; - // uuids.forEach(depUuid => { - // const depAsset = assetManager.assets.get(depUuid); - // if (depAsset) depAsset.decRef(); - // }); - // } - } - - /** - * 获取资源 - * @param path 资源路径 - * @param type 资源类型 - * @param bundleName 远程资源包名 - */ - get(path: string, type?: AssetType, bundleName: string = this.defaultBundleName): T | null { - const bundle: AssetManager.Bundle = assetManager.getBundle(bundleName)!; - return bundle.get(path, type); - } //#endregion - private parseLoadResArgs(paths: Paths, type?: AssetType | ProgressCallback | CompleteCallback, onProgress?: AssetType | ProgressCallback | CompleteCallback, onComplete?: ProgressCallback | CompleteCallback) { + //#region 私有方法 + /** + * 解析加载资源参数 + * @param paths 资源路径 + * @param type 资源类型或回调 + * @param onProgress 进度回调或类型 + * @param onComplete 完成回调 + * @returns 解析后的参数对象 + */ + private parseLoadResArgs(paths: Paths, type?: AssetType | ProgressCallback | CompleteCallback, onProgress?: AssetType | ProgressCallback | CompleteCallback, onComplete?: ProgressCallback | CompleteCallback): ILoadResArgs { const pathsOut: any = paths; let typeOut: any = type; let onProgressOut: any = onProgress; let onCompleteOut: any = onComplete; if (onComplete === undefined) { - const isValidType = js.isChildClassOf(type as AssetType, Asset); + const isValidType = (t: any) => t && typeof t === 'function' && t.prototype instanceof Asset; if (onProgress) { onCompleteOut = onProgress as CompleteCallback; - if (isValidType) { + if (isValidType(type)) { onProgressOut = null; } } - else if (onProgress === undefined && !isValidType) { + else if (onProgress === undefined && !isValidType(type)) { onCompleteOut = type as CompleteCallback; onProgressOut = null; typeOut = null; } - if (onProgress !== undefined && !isValidType) { + if (onProgress !== undefined && !isValidType(type)) { onProgressOut = type as ProgressCallback; typeOut = null; } @@ -443,7 +461,12 @@ export class ResLoader { return { paths: pathsOut, type: typeOut, onProgress: onProgressOut, onComplete: onCompleteOut }; } - private loadByBundleAndArgs(bundle: AssetManager.Bundle, args: ILoadResArgs): void { + /** + * 根据Bundle和参数加载资源 + * @param Bundle Bundle对象 + * @param args 加载参数 + */ + private loadByBundleAndArgs(bundle: any, args: ILoadResArgs): void { if (args.dir) { if (args.preload) { bundle.preloadDir(args.paths as string, args.type, args.onProgress, args.onComplete); @@ -462,91 +485,46 @@ export class ResLoader { } } + /** + * 根据参数加载资源 + * @param args 加载参数 + */ private async loadByArgs(args: ILoadResArgs) { try { if (args.bundle) { let bundle = assetManager.bundles.get(args.bundle); - // 自动加载资源包 if (bundle == null) { bundle = await this.loadBundle(args.bundle); if (!bundle) { - const error = new Error(`加载资源包失败: ${args.bundle}`); - console.error(error.message); - if (args.onComplete) { - args.onComplete(error, null); - } + const resError = new ResourceError(`加载资源包失败`, { bundle: args.bundle }); + error('loadByArgs', `加载资源包失败: ${args.bundle}`); + if (args.onComplete) args.onComplete(resError, null); return; } } - // 加载指定资源包中的资源 this.loadByBundleAndArgs(bundle, args); } - // 默认资源包 else { this.loadByBundleAndArgs(resources, args); } } - catch (error) { - console.error('loadByArgs 错误:', error); - if (args.onComplete) { - args.onComplete(error as Error, null); - } - } - } - - /** 打印缓存中所有资源信息 */ - dump() { - assetManager.assets.forEach((value: Asset, key: string) => { - console.log(`[${key}] 引用数量: ${value.refCount}`, value); - }); - console.log(`当前资源总数: ${assetManager.assets.count}`); - } - - private debugLogReleasedAsset(bundleName: string, asset: Asset) { - if (asset.refCount == 0) { - const path = this.getAssetPath(bundleName, asset.uuid); - let content = ''; - if (asset instanceof JsonAsset) { - content = '【释放资源】Json【路径】' + path; - } - else if (asset instanceof Prefab) { - content = '【释放资源】Prefab【路径】' + path; - } - else if (asset instanceof SpriteFrame) { - content = '【释放资源】SpriteFrame【路径】' + path; - } - else if (asset instanceof Texture2D) { - content = '【释放资源】Texture2D【路径】' + path; - } - else if (asset instanceof ImageAsset) { - content = '【释放资源】ImageAsset【路径】' + path; - } - else if (asset instanceof AudioClip) { - content = '【释放资源】AudioClip【路径】' + path; - } - else if (asset instanceof AnimationClip) { - content = '【释放资源】AnimationClip【路径】' + path; - } - else if (asset instanceof Font) { - content = '【释放资源】Font【路径】' + path; - } - else if (asset instanceof Material) { - content = '【释放资源】Material【路径】' + path; - } - else if (asset instanceof Mesh) { - content = '【释放资源】Mesh【路径】' + path; - } - else if (asset instanceof sp.SkeletonData) { - content = '【释放资源】Spine【路径】' + path; - } - else { - content = '【释放资源】未知【路径】' + path; - } - console.log(content); + catch (err) { + const pathsStr = Array.isArray(args.paths) ? args.paths.join(',') : args.paths; + const resError = err instanceof ResourceError + ? err + : new ResourceError(`资源加载失败`, { + path: pathsStr, + bundle: args.bundle, + cause: err instanceof Error ? err : String(err) + }); + error('loadByArgs', `资源加载失败: ${pathsStr}`, resError); + if (args.onComplete) args.onComplete(resError, null); } } + //#endregion } -export const resLoader = new ResLoader(); +/** 资源加载器单例实例 */ +export const resLoader = new ResLoader(); \ No newline at end of file diff --git a/assets/core/common/loader/ResRefManager.ts b/assets/core/common/loader/ResRefManager.ts deleted file mode 100644 index 0eb8a2d..0000000 --- a/assets/core/common/loader/ResRefManager.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { resLoader } from './ResLoader'; - -export interface ResRefRecord { - bundle: string; - path: string; - refCount: number; - referrers: Set; - lastAccessTime: number; -} - -export interface ComponentResInfo { - resKeys: Set; -} - -class ResRefManager { - private resRefs: Map = new Map(); - private componentRefs: Map = new Map(); - private debugMode: boolean = false; - - enableDebug(enabled: boolean = true): void { - this.debugMode = enabled; - } - - private getResKey(bundle: string, path: string): string { - return `${bundle}::${path}`; - } - - private getComponentKey(component: any): string { - if (!component) return 'unknown'; - const node = component.node; - if (!node) return 'unknown'; - const uuid = node.uuid || 'no-uuid'; - const name = node.name || 'unnamed'; - return `${name}(${uuid.substring(0, 8)})`; - } - - addRef(bundle: string, path: string, component: any): string { - const resKey = this.getResKey(bundle, path); - const compKey = this.getComponentKey(component); - - let record = this.resRefs.get(resKey); - if (!record) { - record = { - bundle, - path, - refCount: 0, - referrers: new Set(), - lastAccessTime: Date.now(), - }; - this.resRefs.set(resKey, record); - } - - if (!record.referrers.has(compKey)) { - record.refCount++; - record.referrers.add(compKey); - record.lastAccessTime = Date.now(); - - if (!this.componentRefs.has(compKey)) { - this.componentRefs.set(compKey, { resKeys: new Set() }); - } - this.componentRefs.get(compKey)!.resKeys.add(resKey); - - if (this.debugMode) { - console.log(`[ResRef] +1 引用: ${resKey} (总引用: ${record.refCount}, 引用者: ${compKey})`); - } - } - - return resKey; - } - - removeRef(bundle: string, path: string, component: any): boolean { - const resKey = this.getResKey(bundle, path); - const compKey = this.getComponentKey(component); - - const record = this.resRefs.get(resKey); - if (!record) { - if (this.debugMode) { - console.warn(`[ResRef] 尝试移除不存在的资源引用: ${resKey}`); - } - return false; - } - - if (!record.referrers.has(compKey)) { - if (this.debugMode) { - console.warn(`[ResRef] 组件 ${compKey} 未引用资源 ${resKey}`); - } - return false; - } - - record.refCount--; - record.referrers.delete(compKey); - - const compInfo = this.componentRefs.get(compKey); - if (compInfo) { - compInfo.resKeys.delete(resKey); - } - - if (this.debugMode) { - console.log(`[ResRef] -1 引用: ${resKey} (剩余引用: ${record.refCount}, 引用者: ${compKey})`); - } - - if (record.refCount <= 0) { - this.releaseResource(resKey, record); - return true; - } - - return false; - } - - releaseAllByComponent(component: any): string[] { - const compKey = this.getComponentKey(component); - const compInfo = this.componentRefs.get(compKey); - - if (!compInfo || compInfo.resKeys.size === 0) { - if (this.debugMode) { - console.log(`[ResRef] 组件 ${compKey} 没有资源引用`); - } - return []; - } - - const releasedResources: string[] = []; - const resKeysToProcess = Array.from(compInfo.resKeys); - - for (const resKey of resKeysToProcess) { - const record = this.resRefs.get(resKey); - if (!record) continue; - - record.refCount--; - record.referrers.delete(compKey); - - if (this.debugMode) { - console.log(`[ResRef] -1 引用: ${resKey} (剩余引用: ${record.refCount}, 组件销毁: ${compKey})`); - } - - if (record.refCount <= 0) { - this.releaseResource(resKey, record); - releasedResources.push(resKey); - } - } - - this.componentRefs.delete(compKey); - - return releasedResources; - } - - private releaseResource(resKey: string, record: ResRefRecord): void { - if (this.debugMode) { - console.log(`[ResRef] 🗑️ 释放资源: ${resKey} (引用者: [${Array.from(record.referrers).join(', ')}])`); - } - - resLoader.release(record.path, record.bundle); - this.resRefs.delete(resKey); - - for (const compKey of record.referrers) { - const compInfo = this.componentRefs.get(compKey); - if (compInfo) { - compInfo.resKeys.delete(resKey); - } - } - } - - getRefCount(bundle: string, path: string): number { - const resKey = this.getResKey(bundle, path); - const record = this.resRefs.get(resKey); - return record ? record.refCount : 0; - } - - getReferrers(bundle: string, path: string): string[] { - const resKey = this.getResKey(bundle, path); - const record = this.resRefs.get(resKey); - return record ? Array.from(record.referrers) : []; - } - - hasRef(bundle: string, path: string): boolean { - const resKey = this.getResKey(bundle, path); - return this.resRefs.has(resKey); - } - - getComponentResCount(component: any): number { - const compKey = this.getComponentKey(component); - const compInfo = this.componentRefs.get(compKey); - return compInfo ? compInfo.resKeys.size : 0; - } - - printStatus(): void { - console.log('\n========== 全局资源引用状态 =========='); - console.log(`总资源数: ${this.resRefs.size}`); - console.log(`总组件数: ${this.componentRefs.size}`); - - if (this.resRefs.size > 0) { - console.log('\n[资源引用详情]'); - const sortedRecords = Array.from(this.resRefs.entries()).sort((a, b) => b[1].refCount - a[1].refCount); - - for (const [key, record] of sortedRecords) { - console.log(` ${key}`); - console.log(` 引用计数: ${record.refCount}`); - console.log(` 引用者: [${Array.from(record.referrers).join(', ')}]`); - } - } - - if (this.componentRefs.size > 0) { - console.log('\n[组件资源详情]'); - for (const [compKey, compInfo] of this.componentRefs) { - console.log(` ${compKey}: ${compInfo.resKeys.size} 个资源`); - } - } - - console.log('=====================================\n'); - } - - printComponentStatus(component: any): void { - const compKey = this.getComponentKey(component); - const compInfo = this.componentRefs.get(compKey); - - console.log(`\n===== 组件资源状态: ${compKey} =====`); - if (!compInfo || compInfo.resKeys.size === 0) { - console.log(' 无资源引用'); - } - else { - console.log(` 引用资源数: ${compInfo.resKeys.size}`); - for (const resKey of compInfo.resKeys) { - const record = this.resRefs.get(resKey); - if (record) { - console.log(` - ${resKey} (全局引用: ${record.refCount})`); - } - } - } - console.log('================================\n'); - } - - getTotalStats(): { totalResources: number; totalComponents: number; totalRefs: number } { - let totalRefs = 0; - for (const record of this.resRefs.values()) { - totalRefs += record.refCount; - } - return { - totalResources: this.resRefs.size, - totalComponents: this.componentRefs.size, - totalRefs, - }; - } - - clear(): void { - if (this.debugMode) { - console.log('[ResRef] 清空所有资源引用记录'); - } - this.resRefs.clear(); - this.componentRefs.clear(); - } -} - -export const resRef = new ResRefManager(); diff --git a/assets/core/common/loader/ResRefManager.ts.meta b/assets/core/common/loader/ResRefManager.ts.meta new file mode 100644 index 0000000..4b8adc4 --- /dev/null +++ b/assets/core/common/loader/ResRefManager.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ed3a8f2f-f783-4fed-bffe-6a5f3a8efc39", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/loader/ResTypes.ts b/assets/core/common/loader/ResTypes.ts new file mode 100644 index 0000000..6b430c4 --- /dev/null +++ b/assets/core/common/loader/ResTypes.ts @@ -0,0 +1,35 @@ +import type { __private, AssetManager } from 'cc'; +import { Asset } from 'cc'; + +/** 资源类型 */ +export type AssetType = __private.__types_globals__Constructor | null; + +/** 资源路径(单路径或多路径) */ +export type Paths = string | string[]; + +/** 加载进度回调 */ +export type ProgressCallback = ((finished: number, total: number, item: AssetManager.RequestItem) => void) | null; + +/** 加载完成回调 */ +export type CompleteCallback = any; + +/** 远程资源加载选项 */ +export type IRemoteOptions = { [k: string]: any; ext?: string; } | null; + +/** 资源加载参数接口 */ +export interface ILoadResArgs { + /** 资源包名 */ + bundle?: string; + /** 资源文件夹名 */ + dir?: string; + /** 资源路径 */ + paths: Paths; + /** 资源类型 */ + type: AssetType; + /** 资源加载进度回调 */ + onProgress: ProgressCallback; + /** 资源加载完成回调 */ + onComplete: CompleteCallback; + /** 是否为预加载 */ + preload?: boolean; +} \ No newline at end of file diff --git a/assets/core/common/loader/ResTypes.ts.meta b/assets/core/common/loader/ResTypes.ts.meta new file mode 100644 index 0000000..f044bf5 --- /dev/null +++ b/assets/core/common/loader/ResTypes.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "4f5307a4-662a-4f67-8779-81ef5a28c540", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/loader/ResUtils.ts b/assets/core/common/loader/ResUtils.ts new file mode 100644 index 0000000..e4f4e5d --- /dev/null +++ b/assets/core/common/loader/ResUtils.ts @@ -0,0 +1,49 @@ +import { Asset, assetManager } from 'cc'; + +/** 检查字符串是否有效 */ +export function isValidString(str: any): str is string { + return typeof str === 'string' && str.trim() !== ''; +} + +/** 输出警告日志 */ +export function warn(method: string, msg: string) { + console.warn(`[ResLoader] ${method}: ${msg}`); +} + +/** 输出错误日志 */ +export function error(method: string, msg: string, cause?: Error | string) { + const message = cause ? `${msg}\n原因: ${cause instanceof Error ? cause.message : cause}` : msg; + console.error(`[ResLoader] ${method}: ${message}`); +} + +/** 创建错误对象 */ +export function createError(method: string, msg: string, cause?: Error | string): Error { + const message = cause ? `${msg}\n原因: ${cause instanceof Error ? cause.message : cause}` : msg; + return new Error(`[ResLoader] ${method}: ${message}`); +} + +/** 释放预制依赖资源(递归释放所有依赖) */ +export function releasePrefabDepsRecursively(uuid: string | Asset, visited: Set = new Set()) { + let asset: Asset | null | undefined; + if (uuid instanceof Asset) { + asset = uuid; + } + else { + asset = assetManager.assets.get(uuid); + } + + if (!asset) return; + + const assetUuid = (asset as any).uuid || ''; + if (assetUuid && visited.has(assetUuid)) return; + if (assetUuid) visited.add(assetUuid); + + const dependentAssets = (asset as any).dependentAssets; + if (dependentAssets && dependentAssets.size > 0) { + dependentAssets.forEach((depAsset: Asset) => { + releasePrefabDepsRecursively(depAsset, visited); + }); + } + + asset.decRef(); +} \ No newline at end of file diff --git a/assets/core/common/loader/ResUtils.ts.meta b/assets/core/common/loader/ResUtils.ts.meta new file mode 100644 index 0000000..9d4e4ca --- /dev/null +++ b/assets/core/common/loader/ResUtils.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9b2e2186-a60a-467a-a770-c51bb46c31a1", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/game/GameManager.ts b/assets/core/game/GameManager.ts index 84e9e4c..1565430 100644 --- a/assets/core/game/GameManager.ts +++ b/assets/core/game/GameManager.ts @@ -9,7 +9,6 @@ import { director, isValid } from 'cc'; import { GameComponent } from '../../module/common/GameComponent'; import { resLoader } from '../common/loader/ResLoader'; import { ViewUtil } from '../utils/ViewUtil'; -import { View } from '../../types/Module'; /** 游戏元素打开参数 */ export interface ElementParams { @@ -38,7 +37,7 @@ export class GameManager { * @param params 可选参数据 * @returns Promise 成功返回节点,失败返回 null */ - async open(parent: View, prefabPath: string, params?: ElementParams): Promise { + async open(parent: OopsFramework.View, prefabPath: string, params?: ElementParams): Promise { try { // 简化 bundleName 获取逻辑 const bundleName = params?.bundle || resLoader.defaultBundleName; @@ -47,7 +46,7 @@ export class GameManager { // 自动内存管理 if (parent instanceof GameComponent) { - node = await parent.createPrefabNode(prefabPath, bundleName); + node = await parent.nodes.createPrefabNode(prefabPath, bundleName); if (!node || !isValid(node)) { console.error(`[GameManager] 创建预制失败: ${prefabPath}`); return null; @@ -149,4 +148,4 @@ export class GameManager { // 清理引用 this.root = null!; } -} +} \ No newline at end of file diff --git a/assets/core/gui/layer/LayerUI.ts b/assets/core/gui/layer/LayerUI.ts index d8b12b7..20ac8ca 100644 --- a/assets/core/gui/layer/LayerUI.ts +++ b/assets/core/gui/layer/LayerUI.ts @@ -1,6 +1,8 @@ import { instantiate, Node, Prefab, SafeArea } from 'cc'; import { Collection } from 'db://oops-framework/libs/collection/Collection'; +import { resAutoTracker } from '../../common/loader/ResAutoTracker'; import { resLoader } from '../../common/loader/ResLoader'; +import { GameComponent } from '../../../module/common/GameComponent'; import { oops } from '../../Oops'; import type { Uiid } from './LayerEnum'; import { LayerHelper } from './LayerHelper'; @@ -114,6 +116,9 @@ export class LayerUI extends Node { // 检查加载完成后 state 是否已被标记为移除,避免创建僵尸节点 if (!state.valid) { console.log(`界面【${state.config.prefab}】在加载过程中已被移除,取消实例化`); + if (res) { + res.decRef(); + } resolve(null!); return; } @@ -127,6 +132,15 @@ export class LayerUI extends Node { // 窗口事件委托 const comp = state.node.addComponent(LayerUIElement); comp.state = state; + + const viewRoot = state.node.getComponent(GameComponent); + if (viewRoot) { + resAutoTracker.acquire(viewRoot, res); + state.prefabTrackedByView = true; + } + else { + state.prefabTrackedByView = false; + } } else { console.warn(`路径为【${state.config.prefab}】的预制加载失败`); @@ -295,4 +309,4 @@ export class LayerUI extends Node { }); } } -} +} \ No newline at end of file diff --git a/assets/core/gui/layer/LayerUIElement.ts b/assets/core/gui/layer/LayerUIElement.ts index cfc12e0..ed15ce2 100644 --- a/assets/core/gui/layer/LayerUIElement.ts +++ b/assets/core/gui/layer/LayerUIElement.ts @@ -81,8 +81,10 @@ export class LayerUIElement extends Component { // 释放界面显示对象 this.node.destroy(); - // 释放界面相关资源 - oops.res.release(this.state.config.prefab, this.state.config.bundle); + // 预制已由根节点 GameComponent + ResAutoTracker 管理时可不额外 release,否则会与 decRef 重复 + if (!this.state.prefabTrackedByView) { + oops.res.release(this.state.config.prefab, this.state.config.bundle); + } // oops.log.logView(`【界面管理】释放【${uip.config.prefab}】界面资源`); } @@ -122,6 +124,10 @@ export class UIState { valid = true; /** 界面根节点 */ node: Node = null!; + /** + * 根节点上存在 GameComponent 时,LayerUI 已将预制资源交给 ResAutoTracker,关闭界面时不再调用 oops.res.release + */ + prefabTrackedByView = false; } /*** 界面打开参数 */ @@ -154,4 +160,4 @@ export interface UIParam { * @param params 外部传递参数 */ onRemoved?: (node: Node, params: any) => void -} +} \ No newline at end of file diff --git a/assets/libs/animator-move/MoveTo.ts b/assets/libs/animator-move/MoveTo.ts index c5be5f2..f4e7812 100644 --- a/assets/libs/animator-move/MoveTo.ts +++ b/assets/libs/animator-move/MoveTo.ts @@ -1,4 +1,3 @@ - /* * @Author: dgflash * @Date: 2022-03-25 18:12:10 @@ -7,9 +6,8 @@ */ import { Component, error, Node, Vec3, _decorator } from 'cc'; import { Timer } from '../../core/common/timer/Timer'; -import { Vec3Util } from '../../core/utils/Vec3Util'; -const { ccclass, property } = _decorator; +const { ccclass } = _decorator; /** 移动到指定目标位置 */ @ccclass('MoveTo') @@ -17,7 +15,7 @@ export class MoveTo extends Component { /** 目标位置 */ target: Vec3 | Node | null = null; /** 移动方向 */ - velocity: Vec3 = Vec3Util.zero; + velocity: Vec3 = new Vec3(); /** 移动速度(每秒移动的像素距离) */ speed = 0; /** 是否计算将 Y 轴带入计算 */ @@ -40,6 +38,8 @@ export class MoveTo extends Component { private timer: Timer = new Timer(); /** 终点备份 */ private end: Vec3 | null = null; + /** 复用向量——避免每帧分配 Vec3 对象 */ + private _temp: Vec3 = new Vec3(); protected onLoad(): void { this.enabled = false; @@ -86,9 +86,10 @@ export class MoveTo extends Component { target.y = 0; } - // 移动方向与移动速度 + // 移动方向与移动速度(直接写入 velocity,避免 new Vec3) const start = this.ns === Node.NodeSpace.WORLD ? this.node.worldPosition : this.node.position; - this.velocity = Vec3Util.sub(target, start).normalize(); + Vec3.subtract(this.velocity, target, start); + this.velocity.normalize(); // 移动时间与目标偏位置计算 const distance = Vec3.distance(start, target) - this.offset; @@ -108,12 +109,19 @@ export class MoveTo extends Component { } if (this.speed > 0) { - const trans = Vec3Util.mul(this.velocity, this.speed * dt); + // _temp = velocity * speed * dt(写入预分配向量,零分配) + Vec3.multiplyScalar(this._temp, this.velocity, this.speed * dt); + const curPos = this.ns === Node.NodeSpace.WORLD + ? this.node.worldPosition + : this.node.position; + this._temp.x += curPos.x; + this._temp.y += curPos.y; + this._temp.z += curPos.z; if (this.ns === Node.NodeSpace.WORLD) { - this.node.worldPosition = Vec3Util.add(this.node.worldPosition, trans); + this.node.worldPosition = this._temp; } else { - this.node.position = Vec3Util.add(this.node.position, trans); + this.node.position = this._temp; } } @@ -138,7 +146,7 @@ export class MoveTo extends Component { this.enabled = false; this.target = null; - this.velocity = Vec3Util.zero; + this.velocity.set(0, 0, 0); this.speed = 0; this.hasYAxis = true; this.ns = Node.NodeSpace.LOCAL; @@ -164,4 +172,4 @@ export class MoveTo extends Component { this.end = null; this.timer.reset(); } -} +} \ No newline at end of file diff --git a/assets/libs/gui/window/PromptBase.ts b/assets/libs/gui/window/PromptBase.ts index 3ff0dd1..476d19d 100644 --- a/assets/libs/gui/window/PromptBase.ts +++ b/assets/libs/gui/window/PromptBase.ts @@ -95,7 +95,7 @@ export class PromptBase extends GameComponent { } protected onLoad(): void { - this.setButton(); + this.button.setButton(); } /** 确认按钮点击事件 */ @@ -146,4 +146,4 @@ export class PromptBase extends GameComponent { super.onDestroy(); } -} +} \ No newline at end of file diff --git a/assets/module/common/CCEntity.ts b/assets/module/common/CCEntity.ts index f894959..2542f4c 100644 --- a/assets/module/common/CCEntity.ts +++ b/assets/module/common/CCEntity.ts @@ -101,7 +101,7 @@ export abstract class CCEntity extends ecs.Entity { // 跟随父节点释放自动释放当前资源 if (parent instanceof GameComponent) { - const result = await parent.createPrefabNode(path, bundleName); + const result = await parent.nodes.createPrefabNode(path, bundleName); if (result == null) return null; node = result; @@ -315,4 +315,4 @@ export abstract class CCEntity extends ecs.Entity { super.destroy(); } -} +} \ No newline at end of file diff --git a/assets/module/common/GameComponent.ts b/assets/module/common/GameComponent.ts index 986c16c..17bc80a 100644 --- a/assets/module/common/GameComponent.ts +++ b/assets/module/common/GameComponent.ts @@ -4,18 +4,22 @@ * @LastEditors: dgflash * @LastEditTime: 2022-12-13 11:36:00 */ -import type { Asset, EventKeyboard, EventTouch, Sprite, __private } from 'cc'; -import { Button, Component, EventHandler, Input, Node, Prefab, SpriteFrame, _decorator, input, instantiate, isValid } from 'cc'; -import { oops } from '../../core/Oops'; +import type { Asset, EventKeyboard, Node, Sprite, __private } from 'cc'; +import { Component, _decorator } from 'cc'; import type { AudioEffect } from '../../core/common/audio/AudioEffect'; import type { IAudioParams } from '../../core/common/audio/IAudio'; -import { EventDispatcher } from '../../core/common/event/EventDispatcher'; import type { ListenerFunc, ListenerFuncTyped } from '../../core/common/event/EventMessage'; -import { EventMessage } from '../../core/common/event/EventMessage'; +import { resAutoTracker } from '../../core/common/loader/ResAutoTracker'; import type { AssetType, CompleteCallback, Paths, ProgressCallback } from '../../core/common/loader/ResLoader'; import { resLoader } from '../../core/common/loader/ResLoader'; -import { ViewUtil } from '../../core/utils/ViewUtil'; -import { resRef } from '../../core/common/loader/ResRefManager'; +import { oops } from '../../core/Oops'; +import type { GameAudioModule } from './view/GameAudioModule'; +import type { GameButtonModule } from './view/GameButtonModule'; +import type { GameEventModule } from './view/GameEventModule'; +import type { GameKeyboardModule } from './view/GameKeyboardModule'; +import type { GameNodeModule } from './view/GameNodeModule'; +import type { GameResModule } from './view/GameResModule'; +import { GameViewModuleRegistry, ViewModuleKey } from './view/GameViewModuleRegistry'; const { ccclass } = _decorator; @@ -23,275 +27,170 @@ const { ccclass } = _decorator; * 游戏显示对象组件模板 * * 特性: - * 1. 自动管理资源引用计数 - 多组件共享资源时不会错误释放 - * 2. 组件销毁时自动释放资源引用 - 开发者无需手动管理 - * 3. 全局资源追踪 - 可查看任意资源的引用者和引用计数 + * 1. 基于引擎 Asset.addRef/decRef + 递归依赖保护,与其它持有者共享时不误释放 + * 2. 组件销毁时自动 release 本产品登记的资源条目 + * 3. ResAutoTracker 全局调试视图(持有者 / 条目数) * * 使用示例: * ```typescript - * // 加载资源(自动注册引用) - * const spriteFrame = await this.load('textures/avatar', SpriteFrame); + * const spriteFrame = await this.res.load('common', 'textures/avatar', SpriteFrame); + * this.nodes.nodeTreeInfoLite(); + * this.event.on('MyEvent', this.onMyEvent, this); + * this.event.setEvent('onGlobal'); + * this.button.setButton(); + * this.keyboard.setKeyboard(true, { onKeyDown: (e) => {} }); + * this.event.setGameShow(() => {}); * - * // 组件销毁时自动释放引用(无需手动调用) - * // 只有当所有引用者都销毁时,资源才会被真正释放 - * - * // 调试:查看资源引用情况 * GameComponent.printGlobalResStatus(); - * GameComponent.setResDebugMode(true); // 开启详细日志 + * GameComponent.setResDebugMode(true); * ``` */ @ccclass('GameComponent') export class GameComponent extends Component { - //#region 全局事件管理 - private _event: EventDispatcher | null = null; - /** 全局事件管理器 */ - private get event(): EventDispatcher { - if (this._event == null) this._event = new EventDispatcher(); - return this._event; + private _viewRegistry: GameViewModuleRegistry | null = null; + + private get viewRegistry(): GameViewModuleRegistry { + return (this._viewRegistry ??= new GameViewModuleRegistry(this)); } - /** 标记是否已注册键盘事件 */ - private _keyboardEnabled = false; - /** 标记是否已注册按钮事件 */ - private _buttonEnabled = false; + /** 获取事件模块 */ + get event(): GameEventModule { + return this.viewRegistry.get(ViewModuleKey.Event); + } - //#region 强类型事件方法(提供给 Agent 自动生成用) + /** 获取节点模块 */ + get nodes(): GameNodeModule { + return this.viewRegistry.get(ViewModuleKey.Nodes); + } - /** - * 注册全局事件(强类型) - * @param event 事件名(枚举) - * @param listener 处理事件的侦听器函数 - * @param object 侦听函数绑定的this对象 - */ + /** 获取资源模块 */ + get res(): GameResModule { + return this.viewRegistry.get(ViewModuleKey.Res); + } + + /** 获取音频模块 */ + get audio(): GameAudioModule { + return this.viewRegistry.get(ViewModuleKey.Audio); + } + + /** 获取按钮模块 */ + get button(): GameButtonModule { + return this.viewRegistry.get(ViewModuleKey.Button); + } + + /** 获取键盘模块 */ + get keyboard(): GameKeyboardModule { + return this.viewRegistry.get(ViewModuleKey.Keyboard); + } + + /** 移除当前节点 */ + remove() { + oops.gui.removeByNode(this.node); + } + + /** 组件销毁时调用 */ + protected onDestroy() { + this._viewRegistry?.destroy(); + } + + /** 打印全局资源状态 */ + static printGlobalResStatus() { + resAutoTracker.printStatus(); + } + + /** 设置资源调试模式 */ + static setResDebugMode(enabled: boolean) { + resAutoTracker.enableDebug(enabled); + } + + //#region ========== 兼容旧版本 API ========== + + //#region 全局事件管理(兼容旧版本) + /** @deprecated 请使用 this.event.watch() */ watch(event: K, listener: ListenerFuncTyped, object: any): void { - this.event.on(event as string, listener as ListenerFunc, object); + this.event.watch(event, listener, object); } - /** - * 监听一次事件,事件响应后,该监听自动移除(强类型) - * @param event 事件名(枚举) - * @param listener 事件触发回调方法 - * @param object 侦听函数绑定的this对象 - */ + /** @deprecated 请使用 this.event.watchOnce() */ watchOnce(event: K, listener: ListenerFuncTyped, object: any): void { - this.event.once(event as string, listener as ListenerFunc, object); + this.event.watchOnce(event, listener, object); } - /** - * 移除全局事件(强类型) - * @param event 事件名(枚举) - * @param listener 处理事件的侦听器函数(可选) - * @param object 侦听函数绑定的this对象(可选) - */ + /** @deprecated 请使用 this.event.unwatch() */ unwatch(event: K, listener?: ListenerFuncTyped, object?: any): void { - this.event.off(event as string, listener as ListenerFunc, object); + this.event.unwatch(event, listener, object); } - /** - * 触发强类型全局事件 - * @param event 事件名(枚举) - * @param data 事件数据 - */ + /** @deprecated 请使用 this.event.emit() */ emit(event: K, data?: OopsFramework.TypedEventMap[K]): void { this.event.emit(event, data); } - /** - * 触发强类型异步全局事件(严格类型检查) - * @param event 事件名(枚举) - * @param data 事件数据(必须完全匹配类型定义) - */ + /** @deprecated 请使用 this.event.emitAsync() */ emitAsync(event: K, data: OopsFramework.TypedEventMap[K]): Promise { return this.event.emitAsync(event, data); } - //#endregion - - //#region 弱类型事件方法 - /** - * 注册全局事件 - * @param event 事件名 - * @param listener 处理事件的侦听器函数 - * @param object 侦听函数绑定的this对象 - */ + /** @deprecated 请使用 this.event.on() */ on(event: string, listener: ListenerFunc, object: any): void { this.event.on(event, listener, object); } - /** - * 监听一次事件,事件响应后,该监听自动移除 - * @param event 事件名 - * @param listener 事件触发回调方法 - * @param object 侦听函数绑定的this对象 - */ + /** @deprecated 请使用 this.event.once() */ once(event: string, listener: ListenerFunc, object: any): void { this.event.once(event, listener, object); } - /** - * 移除全局事件 - * @param event 事件名 - * @param listener 处理事件的侦听器函数(可选) - * @param object 侦听函数绑定的this对象(可选) - */ + /** @deprecated 请使用 this.event.off() */ off(event: string, listener?: ListenerFunc, object?: object): void { this.event.off(event, listener, object); } - /** - * 触发全局事件 - * @param event 事件名 - * @param args 事件参数 - */ + /** @deprecated 请使用 this.event.dispatchEvent() */ dispatchEvent(event: string, ...args: any[]): void { this.event.dispatchEvent(event, ...args); } - /** - * 触发全局事件,支持同步与异步处理 - * @param event 事件名 - * @param args 事件参数 - */ + /** @deprecated 请使用 this.event.dispatchEventAsync() */ dispatchEventAsync(event: string, ...args: any[]): Promise { return this.event.dispatchEventAsync(event, ...args); } //#endregion - //#endregion - - //#region 预制节点管理 - - /** 摊平的节点集合(所有节点不能重名) */ - nodes: Map = null!; - - /** 通过节点名获取预制上的节点,整个预制不能有重名节点 */ + //#region 预制节点管理(兼容旧版本) + /** @deprecated 请使用 this.nodes.getNode() */ getNode(name: string): Node | undefined { - if (this.nodes) { - return this.nodes.get(name); - } - return undefined; + return this.nodes.getNode(name); } - /** 平摊所有节点存到Map中通过get(name: string)方法获取 */ - nodeTreeInfoLite() { - this.nodes = new Map(); - ViewUtil.nodeTreeInfoLite(this.node, this.nodes); + /** @deprecated 请使用 this.nodes.nodeTreeInfoLite() */ + nodeTreeInfoLite(): void { + this.nodes.nodeTreeInfoLite(); } - /** - * 从资源缓存中找到预制资源名并创建一个显示对象 - * @param path 资源路径 - * @param bundleName 资源包名 - * @returns 预制节点,加载失败返回 null - */ + /** @deprecated 请使用 this.nodes.createPrefabNode() */ async createPrefabNode(path: string, bundleName: string = oops.res.defaultBundleName): Promise { - const prefab = await this.load(bundleName, path, Prefab); - if (!prefab) { - console.warn('[OopsFramework]', `预制体加载失败: ${path}`); - return null; - } - return instantiate(prefab); + return this.nodes.createPrefabNode(path, bundleName); } //#endregion - //#region 资源加载管理 - /** - * 获取资源 - * @param path 资源路径 - * @param type 资源类型 - * @param bundleName 远程资源包名 - */ + //#region 资源加载管理(兼容旧版本) + /** @deprecated 请使用 this.res.getRes() */ getRes(path: string, type?: __private.__types_globals__Constructor | null, bundleName?: string): T | null { - return oops.res.get(path, type, bundleName); + return this.res.getRes(path, type, bundleName); } - /** - * 加载一个资源(自动管理引用计数) - * @param bundleName 远程包名 - * @param paths 资源路径 - * @param type 资源类型 - * @param onProgress 加载进度回调 - * @remarks - * - 资源引用会自动注册到全局管理器 - * - 组件销毁时会自动减少引用计数 - * - 只有引用计数为0时才会真正释放资源 - */ + /** @deprecated 请使用 this.res.load() */ async load(bundleName: string, paths: Paths | AssetType, type?: AssetType): Promise { - let realBundle: string; - let realPath: string; - - if (typeof paths === 'string') { - realBundle = bundleName; - realPath = paths; - } - else { - realBundle = oops.res.defaultBundleName; - realPath = bundleName; - } - - resRef.addRef(realBundle, realPath, this); - - try { - const result = await oops.res.load(bundleName, paths, type); - if (!result) { - resRef.removeRef(realBundle, realPath, this); - } - return result; - } - catch (error) { - resRef.removeRef(realBundle, realPath, this); - throw error; - } + return this.res.load(bundleName, paths, type); } - /** - * 加载指定资源包中的多个任意类型资源(回调模式) - * @param bundleName 远程包名或资源路径数组 - * @param paths 资源路径数组或进度回调 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 - */ + /** @deprecated 请使用 this.res.loadAny() */ loadAny(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void { - const originalComplete = onComplete as ((err: Error | null, data: Asset[]) => void) | undefined; - const pathsToTrack: { bundle: string; path: string }[] = []; - - if (typeof bundleName === 'string' && Array.isArray(paths)) { - paths.forEach(p => { - resRef.addRef(bundleName, p, this); - pathsToTrack.push({ bundle: bundleName, path: p }); - }); - } - else if (Array.isArray(bundleName)) { - bundleName.forEach(p => { - resRef.addRef(resLoader.defaultBundleName, p, this); - pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: p }); - }); - } - else if (typeof bundleName === 'string' && typeof paths === 'function') { - resRef.addRef(resLoader.defaultBundleName, bundleName, this); - pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: bundleName }); - } - - const wrappedComplete = (err: Error | null, data: Asset[]) => { - if (err || !data) { - pathsToTrack.forEach(({ bundle, path }) => { - resRef.removeRef(bundle, path, this); - }); - } - originalComplete?.(err, data); - }; - - oops.res.loadAny(bundleName, paths, onProgress, wrappedComplete); + this.res.loadAny(bundleName, paths, onProgress, onComplete); } - /** - * 加载文件夹中的资源(回调模式) - * @param bundleName 远程包名 - * @param dir 文件夹名 - * @param type 资源类型 - * @param onProgress 加载进度回调 - * @param onComplete 加载完成回调 - */ + /** @deprecated 请使用 this.res.loadDir() */ loadDir(bundleName: string, dir: string, type: AssetType, onProgress: ProgressCallback, onComplete: CompleteCallback): void; loadDir(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void; loadDir(bundleName: string, dir: string, onComplete?: CompleteCallback): void; @@ -307,333 +206,101 @@ export class GameComponent extends Component { onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback, ): void { - let realDir: string; - let realBundle: string; - if (typeof dir === 'string') { - realDir = dir; - realBundle = bundleName; - } - else { - realDir = bundleName; - realBundle = oops.res.defaultBundleName; - } - - resRef.addRef(realBundle, realDir, this); - - const originalComplete = onComplete as ((err: Error | null, data: T[]) => void) | undefined; - const wrappedComplete = (err: Error | null, data: T[]) => { - if (err || !data) { - resRef.removeRef(realBundle, realDir, this); - } - originalComplete?.(err, data); - }; - - oops.res.loadDir(bundleName, dir, type, onProgress, wrappedComplete); + this.res.loadDir(bundleName, dir, type, onProgress, onComplete); } - /** - * 手动释放指定资源引用 - * @param path 资源路径 - * @param bundleName 资源包名 - * @remarks - * - 只减少当前组件对该资源的引用计数 - * - 只有引用计数为0时才会真正释放资源 - * - 其他组件的引用不受影响 - */ - releaseRes(path: string, bundleName: string = resLoader.defaultBundleName) { - resRef.removeRef(bundleName, path, this); - } - - /** - * 释放当前组件所有资源引用 - * @remarks - * - 自动减少所有资源的引用计数 - * - 只有引用计数为0的资源才会被真正释放 - * - 共享资源不会被错误释放 - */ - release() { - const released = resRef.releaseAllByComponent(this); - if (released.length > 0) { - console.log(`[GameComponent] ${this.node?.name} 释放了 ${released.length} 个资源:`, released); - } - } - - /** - * 释放所有文件夹资源引用 - * @deprecated 文件夹资源现在也通过全局引用计数管理,直接调用 release() 即可 - */ - releaseDir() { - console.warn('[GameComponent] releaseDir() 已废弃,请直接使用 release()'); - } - - /** - * 获取资源的全局引用计数 - * @param path 资源路径 - * @param bundleName 资源包名 - * @returns 全局引用计数 - */ - getResRefCount(path: string, bundleName: string = resLoader.defaultBundleName): number { - return resRef.getRefCount(bundleName, path); - } - - /** - * 获取资源的所有引用者 - * @param path 资源路径 - * @param bundleName 资源包名 - * @returns 引用者列表 - */ - getResReferrers(path: string, bundleName: string = resLoader.defaultBundleName): string[] { - return resRef.getReferrers(bundleName, path); - } - - /** - * 打印当前组件的资源引用情况 - */ - printResUsage() { - resRef.printComponentStatus(this); - } - - /** - * 打印全局资源引用状态(调试用) - */ - static printGlobalResStatus() { - resRef.printStatus(); - } - - /** - * 开启/关闭全局资源调试模式 - * @param enabled 是否开启 - */ - static setResDebugMode(enabled: boolean) { - resRef.enableDebug(enabled); - } - - /** - * 设置图片资源 - * @param target 目标精灵对象 - * @param path 图片资源地址 - * @param bundle 资源包名 - * @returns 是否设置成功 - * @remarks 资源引用计数由 load 方法自动管理,加载失败时会自动回滚 - */ + /** @deprecated 请使用 this.res.setSprite() */ async setSprite(target: Sprite, path: string, bundle: string = resLoader.defaultBundleName): Promise { - const spriteFrame = await this.load(bundle, path, SpriteFrame); - if (!spriteFrame) { - return false; - } - if (!isValid(target)) { - this.releaseRes(path, bundle); - return false; - } - target.spriteFrame = spriteFrame; - return true; + return this.res.setSprite(target, path, bundle); } //#endregion - //#region 音频播放管理 - /** - * 播放背景音乐(不受自动释放资源管理) - * @param url 资源地址 - * @param params 背景音乐资源播放参数 - */ - playMusic(url: string, params?: IAudioParams) { - oops.audio.music.loadAndPlay(url, params); + //#region 音频播放管理(兼容旧版本) + /** @deprecated 请使用 this.audio.playMusic() */ + playMusic(url: string, params?: IAudioParams): void { + this.audio.playMusic(url, params); } - /** - * 播放音效 - * @param url 资源地址 - * @param params 音效播放参数 - * @returns 音效实例,播放失败返回 null - * @remarks 注意:音效资源由 AudioEffectPool 自动管理,不需要在此组件中记录 - */ + /** @deprecated 请使用 this.audio.playEffect() */ playEffect(url: string, params?: IAudioParams): Promise { - return new Promise((resolve) => { - if (params == null) { - params = { bundle: resLoader.defaultBundleName }; - } - else if (params.bundle == null) { - params.bundle = resLoader.defaultBundleName; - } - - oops.audio.playEffect(url, params).then((ae) => { - resolve(ae ?? null); - }); - }); + return this.audio.playEffect(url, params); } //#endregion - //#region 游戏逻辑事件 - /** - * 批量设置当前界面按钮事件 - * @param bindRootEvent 是否对预制根节点绑定触摸事件 - * @example - * 注:按钮节点Label1、Label2必须绑定UIButton等类型的按钮组件才会生效,方法名必须与节点名一致 - * this.setButton(); - * - * Label1(event: EventTouch) { console.log(event.target.name); } - * Label2(event: EventTouch) { console.log(event.target.name); } - */ - protected setButton(bindRootEvent = true) { - this._buttonEnabled = true; - - // 自定义按钮批量绑定触摸事件 - if (bindRootEvent) { - this.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => { - const self: any = this; - const func = self[event.target.name]; - if (func) { - func.call(this, event); - } - // 不触发界面根节点触摸事件、不触发长按钮组件的触摸事件 - // else if (event.target != this.node && event.target.getComponent(ButtonTouchLong) == null) { - // console.warn(`名为【${event.target.name}】的按钮事件方法不存在`); - // } - }, this); - } - - // Cocos Creator Button组件批量绑定触摸事件(使用UIButton支持放连点功能) - const regex = /<([^>]+)>/; - const match = this.name.match(regex); - if (!match || !match[1]) { - console.warn('[OopsFramework]', `组件名 "${this.name}" 不符合 "<组件名>" 格式,跳过按钮事件绑定`); - return; - } - const componentName = match[1]; - const buttons = this.node.getComponentsInChildren