This commit is contained in:
dgflash
2026-05-22 22:22:33 +08:00
parent d33b668810
commit 58d145b7dc
44 changed files with 2285 additions and 1275 deletions

View File

@@ -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<string, Promise<AudioClip>> = new Map();
/** 已加载的 AudioClip 缓存 */
private clipCache: Map<string, AudioClip> = new Map();
/**
* 从三种来源获取 AudioClip
* @param path - AudioClip 实例、远程 URL、或 bundle 内路径
* @param bundle - 资源包名path 为 AudioClip 或 URL 时忽略)
* @returns 加载结果,失败返回 null
*/
async load(
path: string | AudioClip,
bundle?: string
): Promise<ILoadResult | null> {
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<ILoadResult | null> {
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<AudioClip> {
const extension = path.split('.').pop();
return resLoader.loadRemote<AudioClip>(path, { ext: `.${extension}` });
}
/**
* 加载 Bundle 内资源
* @param path - 资源路径
* @param bundle - 资源包名
* @returns 加载结果
*/
private async loadBundle(
path: string,
bundle: string
): Promise<ILoadResult | null> {
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);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7e9304f5-fa80-43d0-883a-2fd8734e7bbb",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -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<string, AudioEffect> = new Map();
/** 记录项目资源库中使用过的音乐资源 */
private res_project: Map<string, Set<string>> = new Map();
/** 外网远程资源记录(地址、音效对象) */
private res_remote: Map<string, AudioClip> = new Map();
/** 正在加载的资源Promise缓存避免重复加载 */
private loading_cache: Map<string, Promise<AudioClip>> = 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<AudioEffect> {
// 合并默认参数(减少对象创建)
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<AudioClip>(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<string>();
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<string>, 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
};
}
}

View File

@@ -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!;
}
}
}

View File

@@ -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<AudioClip>(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();
}
}
}

View File

@@ -18,4 +18,4 @@ export interface IAudioData {
switch: boolean;
/** 音量 */
volume: number;
}
}

View File

@@ -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<Component, TrackedResEntry[]>();
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<string>();
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();

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "2a5b8165-c540-4e3a-abf7-5ef91e6f2c3b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "96c108eb-8f7e-408d-8a6c-00d5cf5be314",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "54c4ac22-3d70-4716-bd8e-335d919c6ce4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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<T = Asset> = __private.__types_globals__Constructor<T> | 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<T extends Asset> {
/** 资源包名 */
bundle?: string;
/** 资源文件夹名 */
dir?: string;
/** 资源路径 */
paths: Paths;
/** 资源类型 */
type: AssetType<T>;
/** 资源加载进度 */
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<string, Promise<any>> = new Map();
/** 正在加载的资源 Promise 缓存,防止并发重复加载 */
private _loadingAssets: Map<string, Promise<any>> = new Map();
/** 已加载的远程资源缓存,用于统一管理释放 */
private _remoteAssets: Map<string, Asset> = 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<ImageAsset>(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<T extends Asset>(url: string, options: IRemoteOptions | null = null): Promise<T> {
return new Promise<T>((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<T>(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<AssetManager.Bundle> {
return new Promise<AssetManager.Bundle>((resolve, reject) => {
assetManager.loadBundle(name, options, (err, bundle: AssetManager.Bundle) => {
loadBundle(name: string, options: { [k: string]: any; version?: string; } | null = null): Promise<any> {
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<T extends Asset>(bundleName: string, paths: Paths, type: AssetType<T>, onProgress: ProgressCallback): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(bundleName: string, paths: Paths, onProgress: ProgressCallback): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(bundleName: string, paths: Paths): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(bundleName: string, paths: Paths, type: AssetType<T>): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(paths: Paths, type: AssetType<T>, onProgress: ProgressCallback): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(paths: Paths, onProgress: ProgressCallback): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(paths: Paths): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(paths: Paths, type: AssetType<T>): Promise<AssetManager.RequestItem>;
preload<T extends Asset>(
bundleName: string,
paths?: Paths | AssetType<T> | ProgressCallback,
type?: AssetType<T> | ProgressCallback,
onProgress?: ProgressCallback
) {
load<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>): Promise<T> {
let args: ILoadResArgs<T> | 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<T>;
const promise = new Promise<T>((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<T extends Asset>(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void {
let args: ILoadResArgs<T> | 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<T extends Asset>(bundleName: string, dir?: string | AssetType<T> | ProgressCallback | CompleteCallback, type?: AssetType<T> | ProgressCallback | CompleteCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void {
let args: ILoadResArgs<T> | 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<T extends Asset>(bundleName: string, paths?: Paths | AssetType<T> | ProgressCallback, type?: AssetType<T> | ProgressCallback, onProgress?: ProgressCallback): Promise<any> {
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<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
preloadDir<T extends Asset>(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
preloadDir<T extends Asset>(bundleName: string, dir: string, onComplete?: CompleteCallback): void;
preloadDir<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onComplete?: CompleteCallback): void;
preloadDir<T extends Asset>(dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
preloadDir<T extends Asset>(dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
preloadDir<T extends Asset>(dir: string, onComplete?: CompleteCallback): void;
preloadDir<T extends Asset>(dir: string, type: AssetType<T>, onComplete?: CompleteCallback): void;
preloadDir<T extends Asset>(
bundleName: string,
dir?: string | AssetType<T> | ProgressCallback | CompleteCallback,
type?: AssetType<T> | ProgressCallback | CompleteCallback,
onProgress?: ProgressCallback | CompleteCallback,
onComplete?: CompleteCallback,
) {
preloadDir<T extends Asset>(bundleName: string, dir?: string | AssetType<T> | ProgressCallback | CompleteCallback, type?: AssetType<T> | ProgressCallback | CompleteCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void {
let args: ILoadResArgs<T> | 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<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>) {
return new Promise<T>((resolve, reject) => {
const onComplete = (err: Error | null, data: T) => {
if (err) {
reject(err);
return;
}
// 增加引用计数,防止资源被意外释放
// if (data) {
// data.addRef();
// }
resolve(data);
};
let args: ILoadResArgs<T> | 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<T extends Asset>(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void {
let args: ILoadResArgs<T> | 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<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, onComplete?: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onComplete?: CompleteCallback): void;
loadDir<T extends Asset>(dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(dir: string, onComplete?: CompleteCallback): void;
loadDir<T extends Asset>(dir: string, type: AssetType<T>, onComplete?: CompleteCallback): void;
loadDir<T extends Asset>(
bundleName: string,
dir?: string | AssetType<T> | ProgressCallback | CompleteCallback,
type?: AssetType<T> | ProgressCallback | CompleteCallback,
onProgress?: ProgressCallback | CompleteCallback,
onComplete?: CompleteCallback,
) {
let args: ILoadResArgs<T> | 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<T extends Asset>(path: string, type?: AssetType<T>, 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<T extends Asset>(path: string, type?: AssetType<T>, bundleName: string = this.defaultBundleName): T | null {
const bundle: AssetManager.Bundle = assetManager.getBundle(bundleName)!;
return bundle.get(path, type);
}
//#endregion
private parseLoadResArgs<T extends Asset>(paths: Paths, type?: AssetType<T> | ProgressCallback | CompleteCallback, onProgress?: AssetType<T> | ProgressCallback | CompleteCallback, onComplete?: ProgressCallback | CompleteCallback) {
//#region 私有方法
/**
* 解析加载资源参数
* @param paths 资源路径
* @param type 资源类型或回调
* @param onProgress 进度回调或类型
* @param onComplete 完成回调
* @returns 解析后的参数对象
*/
private parseLoadResArgs<T extends Asset>(paths: Paths, type?: AssetType<T> | ProgressCallback | CompleteCallback, onProgress?: AssetType<T> | ProgressCallback | CompleteCallback, onComplete?: ProgressCallback | CompleteCallback): ILoadResArgs<T> {
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<T extends Asset>(bundle: AssetManager.Bundle, args: ILoadResArgs<T>): void {
/**
* 根据Bundle和参数加载资源
* @param Bundle Bundle对象
* @param args 加载参数
*/
private loadByBundleAndArgs<T extends Asset>(bundle: any, args: ILoadResArgs<T>): 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<T extends Asset>(args: ILoadResArgs<T>) {
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();

View File

@@ -1,252 +0,0 @@
import { resLoader } from './ResLoader';
export interface ResRefRecord {
bundle: string;
path: string;
refCount: number;
referrers: Set<string>;
lastAccessTime: number;
}
export interface ComponentResInfo {
resKeys: Set<string>;
}
class ResRefManager {
private resRefs: Map<string, ResRefRecord> = new Map();
private componentRefs: Map<string, ComponentResInfo> = 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();

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ed3a8f2f-f783-4fed-bffe-6a5f3a8efc39",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,35 @@
import type { __private, AssetManager } from 'cc';
import { Asset } from 'cc';
/** 资源类型 */
export type AssetType<T = Asset> = __private.__types_globals__Constructor<T> | 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<T extends Asset> {
/** 资源包名 */
bundle?: string;
/** 资源文件夹名 */
dir?: string;
/** 资源路径 */
paths: Paths;
/** 资源类型 */
type: AssetType<T>;
/** 资源加载进度回调 */
onProgress: ProgressCallback;
/** 资源加载完成回调 */
onComplete: CompleteCallback;
/** 是否为预加载 */
preload?: boolean;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4f5307a4-662a-4f67-8779-81ef5a28c540",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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<string> = 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();
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9b2e2186-a60a-467a-a770-c51bb46c31a1",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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<Node | null> 成功返回节点,失败返回 null
*/
async open(parent: View, prefabPath: string, params?: ElementParams): Promise<Node | null> {
async open(parent: OopsFramework.View, prefabPath: string, params?: ElementParams): Promise<Node | null> {
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!;
}
}
}

View File

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

View File

@@ -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
}
}

View File

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

View File

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

View File

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

View File

@@ -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<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<K extends keyof OopsFramework.TypedEventMap>(event: K, listener?: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<K extends keyof OopsFramework.TypedEventMap>(event: K, data?: OopsFramework.TypedEventMap[K]): void {
this.event.emit(event, data);
}
/**
* 触发强类型异步全局事件(严格类型检查)
* @param event 事件名(枚举)
* @param data 事件数据(必须完全匹配类型定义)
*/
/** @deprecated 请使用 this.event.emitAsync() */
emitAsync<K extends keyof OopsFramework.TypedEventMap>(event: K, data: OopsFramework.TypedEventMap[K]): Promise<void> {
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<void> {
return this.event.dispatchEventAsync(event, ...args);
}
//#endregion
//#endregion
//#region 预制节点管理
/** 摊平的节点集合(所有节点不能重名) */
nodes: Map<string, Node> = 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<string, Node>中通过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<Node | null> {
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<T extends Asset>(path: string, type?: __private.__types_globals__Constructor<T> | 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<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>): Promise<T> {
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<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(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<boolean> {
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<AudioEffect | null> {
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<Button>(Button);
buttons.forEach((b: Button) => {
const node = b.node;
const self: any = this;
const func = self[node.name];
if (func) {
const event = new EventHandler();
event.target = this.node;
event.handler = b.node.name;
event.component = componentName;
b.clickEvents.push(event);
}
// else {
// console.warn(`名为【${node.name}】的按钮事件方法不存在`);
// }
});
//#region 游戏逻辑事件(兼容旧版本)
/** @deprecated 请使用 this.button.setButton() */
protected setButton(bindRootEvent = true): void {
this.button.setButton(bindRootEvent);
}
/**
* 批量设置全局事件
* @example
* this.setEvent("onGlobal");
* this.dispatchEvent("onGlobal", "全局事件");
*
* onGlobal(event: string, args: any) { console.log(args) };
*/
protected setEvent(...args: string[]) {
const self: any = this;
for (const name of args) {
const func = self[name];
if (func)
this.on(name, func, this);
else
console.error(`名为【${name}】的全局事方法不存在`);
}
/** @deprecated 请使用 this.event.setEvent() */
protected setEvent(...args: string[]): void {
this.event.setEvent(...args);
}
/**
* 键盘事件开关
* @param on 打开键盘事件为true
*/
setKeyboard(on: boolean) {
/** @deprecated 请使用 this.keyboard.setKeyboard() */
setKeyboard(on: boolean): void {
if (on) {
this._keyboardEnabled = true;
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
input.on(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this.keyboard.setKeyboard(true, {
onKeyDown: this.onKeyDown.bind(this),
onKeyUp: this.onKeyUp.bind(this),
onKeyPressing: this.onKeyPressing.bind(this)
});
}
else {
this._keyboardEnabled = false;
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
input.off(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this.keyboard.setKeyboard(false);
}
}
/** 键按下 */
protected onKeyDown(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyDown(event: EventKeyboard): void { }
/** 键放开 */
protected onKeyUp(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyUp(event: EventKeyboard): void { }
/** 键长按 */
protected onKeyPressing(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyPressing(event: EventKeyboard): void { }
/** 监听游戏从后台进入事件 */
protected setGameShow() {
this.on(EventMessage.GAME_SHOW, this.onGameShow, this);
/** @deprecated 请使用 this.event.setGameShow() */
protected setGameShow(): void {
this.event.setGameShow(this.onGameShow.bind(this));
}
/** 监听游戏切到后台事件 */
protected setGameHide() {
this.on(EventMessage.GAME_HIDE, this.onGameHide, this);
/** @deprecated 请使用 this.event.setGameHide() */
protected setGameHide(): void {
this.event.setGameHide(this.onGameHide.bind(this));
}
/** 监听游戏画笔尺寸变化事件 */
protected setGameResize() {
this.on(EventMessage.GAME_RESIZE, this.onGameResize, this);
/** @deprecated 请使用 this.event.setGameResize() */
protected setGameResize(): void {
this.event.setGameResize(this.onGameResize.bind(this));
}
/** 监听游戏全屏事件 */
protected setGameFullScreen() {
this.on(EventMessage.GAME_FULL_SCREEN, this.onGameFullScreen, this);
/** @deprecated 请使用 this.event.setGameFullScreen() */
protected setGameFullScreen(): void {
this.event.setGameFullScreen(this.onGameFullScreen.bind(this));
}
/** 监听游戏旋转屏幕事件 */
protected setGameOrientation() {
this.on(EventMessage.GAME_ORIENTATION, this.onGameOrientation, this);
/** @deprecated 请使用 this.event.setGameOrientation() */
protected setGameOrientation(): void {
this.event.setGameOrientation(this.onGameOrientation.bind(this));
}
/** 游戏从后台进入事件回调 */
/** @deprecated 请配合 setGameShow() 使用 */
protected onGameShow(): void { }
/** 游戏切到后台事件回调 */
/** @deprecated 请配合 setGameHide() 使用 */
protected onGameHide(): void { }
/** 游戏画笔尺寸变化事件回调 */
/** @deprecated 请配合 setGameResize() 使用 */
protected onGameResize(): void { }
/** 游戏全屏事件回调 */
/** @deprecated 请配合 setGameFullScreen() 使用 */
protected onGameFullScreen(): void { }
/** 游戏旋转屏幕事件回调 */
/** @deprecated 请配合 setGameOrientation() 使用 */
protected onGameOrientation(): void { }
//#endregion
/** 移除自己 */
remove() {
oops.gui.removeByNode(this.node);
}
protected onDestroy() {
if (this._keyboardEnabled) {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
input.off(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this._keyboardEnabled = false;
}
if (this._buttonEnabled) {
this.node.off(Node.EventType.TOUCH_END);
this._buttonEnabled = false;
}
if (this._event) {
this._event.clear();
this._event = null;
}
if (this.nodes) {
this.nodes.clear();
this.nodes = null!;
}
this.release();
}
}
//#endregion
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d2d11e7b-7d36-4417-8e97-537c7219488a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,84 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import { oops } from '../../../core/Oops';
import type { AudioEffect } from '../../../core/common/audio/AudioEffect';
import type { IAudioParams } from '../../../core/common/audio/IAudio';
import { resLoader } from '../../../core/common/loader/ResLoader';
import { GameViewModule } from './GameViewModuleBase';
/** 音频资源使用记录 */
interface IAudioUsage {
/** 资源路径 */
path: string;
/** 资源包名 */
bundle: string | null;
}
/** 音频播放 */
export class GameAudioModule extends GameViewModule {
/** 当前界面使用的音效资源记录 */
private usedAudios: IAudioUsage[] = [];
/** 是否已销毁 */
private isDestroyed = false;
/** 播放背景音乐(全局唯一,不由界面管理生命周期)
* @param url 音频资源路径
* @param params 音频参数
*/
playMusic(url: string, params?: IAudioParams): void {
oops.audio.music.loadAndPlay(url, params);
}
/** 播放音效
* @param url 音频资源路径
* @param params 音频参数
* @returns 音效对象
*/
playEffect(url: string, params?: IAudioParams): Promise<AudioEffect | null> {
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) => {
// 资源加载成功且界面未销毁时才记录
if (ae && !this.isDestroyed) {
this.recordAudioUsage(url, params!.bundle);
}
resolve(ae ?? null);
});
});
}
/**
* 记录音频资源使用
* @param path 资源路径
* @param bundle 资源包名
*/
private recordAudioUsage(path: string, bundle?: string): void {
const usage: IAudioUsage = {
path,
bundle: bundle || null
};
this.usedAudios.push(usage);
}
/**
* 组件销毁时释放所有使用的音效资源
*/
destroy(): void {
this.isDestroyed = true;
// 释放音效资源
for (const usage of this.usedAudios) {
oops.audio.effect.releaseResByPath(usage.path, usage.bundle || undefined);
}
this.usedAudios = [];
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "68e344ff-705b-4472-9053-bc70bd7363da",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,47 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import type { EventTouch } from 'cc';
import { Button, EventHandler, Node } from 'cc';
import { GameViewModule } from './GameViewModuleBase';
/** 界面按钮批量绑定 */
export class GameButtonModule extends GameViewModule {
/** 设置按钮事件绑定
* @param bindRootEvent 是否绑定根节点事件,默认为 true
*/
setButton(bindRootEvent = true): void {
if (bindRootEvent) {
this.comp.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => {
const self: any = this.comp;
const func = self[event.target.name];
if (func) {
func.call(this.comp, event);
}
}, this.comp);
}
const regex = /<([^>]+)>/;
const match = this.comp.name.match(regex);
if (!match || !match[1]) {
console.warn('[OopsFramework]', `组件名 "${this.comp.name}" 不符合 "<组件名>" 格式,跳过按钮事件绑定`);
return;
}
const componentName = match[1];
const buttons = this.comp.node.getComponentsInChildren<Button>(Button);
buttons.forEach((b: Button) => {
const node = b.node;
const self: any = this.comp;
const func = self[node.name];
if (func) {
const event = new EventHandler();
event.target = this.comp.node;
event.handler = b.node.name;
event.component = componentName;
b.clickEvents.push(event);
}
});
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "48bfef5e-fe8b-4ad4-88be-cd1872153689",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,181 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import { EventDispatcher } from '../../../core/common/event/EventDispatcher';
import { EventMessage, type ListenerFunc, type ListenerFuncTyped } from '../../../core/common/event/EventMessage';
import { GameViewModule } from './GameViewModuleBase';
/** 全局事件管理(含游戏前后台、画布、全屏、旋转等生命周期) */
export class GameEventModule extends GameViewModule {
private _event: EventDispatcher | null = null;
/** 获取事件分发器 */
private get event(): EventDispatcher {
if (this._event == null) {
this._event = new EventDispatcher();
}
return this._event;
}
/** 监听事件
* @param event 事件类型
* @param listener 监听回调
* @param object 监听对象
*/
watch<K extends keyof OopsFramework.TypedEventMap>(
event: K,
listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>,
object: object,
): void {
this.event.on(event as string, listener as ListenerFunc, object);
}
/** 监听事件(只触发一次)
* @param event 事件类型
* @param listener 监听回调
* @param object 监听对象
*/
watchOnce<K extends keyof OopsFramework.TypedEventMap>(
event: K,
listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>,
object: object,
): void {
this.event.once(event as string, listener as ListenerFunc, object);
}
/** 取消监听事件
* @param event 事件类型
* @param listener 监听回调
* @param object 监听对象
*/
unwatch<K extends keyof OopsFramework.TypedEventMap>(
event: K,
listener?: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>,
object?: object,
): void {
this.event.off(event as string, listener as ListenerFunc, object);
}
/** 触发事件
* @param event 事件类型
* @param data 事件数据
*/
emit<K extends keyof OopsFramework.TypedEventMap>(event: K, data?: OopsFramework.TypedEventMap[K]): void {
this.event.emit(event, data);
}
/** 异步触发事件
* @param event 事件类型
* @param data 事件数据
*/
emitAsync<K extends keyof OopsFramework.TypedEventMap>(event: K, data: OopsFramework.TypedEventMap[K]): Promise<void> {
return this.event.emitAsync(event, data);
}
/** 监听事件
* @param event 事件名称
* @param listener 监听回调
* @param object 监听对象
*/
on(event: string, listener: ListenerFunc, object: object): void {
this.event.on(event, listener, object);
}
/** 监听事件(只触发一次)
* @param event 事件名称
* @param listener 监听回调
* @param object 监听对象
*/
once(event: string, listener: ListenerFunc, object: object): void {
this.event.once(event, listener, object);
}
/** 取消监听事件
* @param event 事件名称
* @param listener 监听回调
* @param object 监听对象
*/
off(event: string, listener?: ListenerFunc, object?: object): void {
this.event.off(event, listener, object);
}
/** 分发事件
* @param event 事件名称
* @param args 事件参数
*/
dispatchEvent(event: string, ...args: any[]): void {
this.event.dispatchEvent(event, ...args);
}
/** 异步分发事件
* @param event 事件名称
* @param args 事件参数
*/
dispatchEventAsync(event: string, ...args: any[]): Promise<void> {
return this.event.dispatchEventAsync(event, ...args);
}
/**
* 批量设置全局事件(按组件上的方法名绑定)
* @example
* this.event.setEvent('onGlobal');
* onGlobal(event: string, args: any) { console.log(args); }
*/
setEvent(...args: string[]): void {
const self: any = this.comp;
for (const name of args) {
const func = self[name];
if (func) {
this.on(name, func, this.comp);
}
else {
console.error(`名为【${name}】的全局事方法不存在`);
}
}
}
/** 设置游戏显示回调
* @param callback 回调函数
*/
setGameShow(callback: () => void): void {
this.on(EventMessage.GAME_SHOW, callback, this.comp);
}
/** 设置游戏隐藏回调
* @param callback 回调函数
*/
setGameHide(callback: () => void): void {
this.on(EventMessage.GAME_HIDE, callback, this.comp);
}
/** 设置游戏尺寸变化回调
* @param callback 回调函数
*/
setGameResize(callback: () => void): void {
this.on(EventMessage.GAME_RESIZE, callback, this.comp);
}
/** 设置游戏全屏回调
* @param callback 回调函数
*/
setGameFullScreen(callback: () => void): void {
this.on(EventMessage.GAME_FULL_SCREEN, callback, this.comp);
}
/** 设置游戏方向变化回调
* @param callback 回调函数
*/
setGameOrientation(callback: () => void): void {
this.on(EventMessage.GAME_ORIENTATION, callback, this.comp);
}
/** 销毁事件模块 */
override destroy(): void {
if (this._event) {
this._event.clear();
this._event = null;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b05268d5-1e17-4d87-bc27-47e100a55206",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,86 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import type { EventKeyboard } from 'cc';
import { Input, input } from 'cc';
import { GameViewModule } from './GameViewModuleBase';
/** 键盘事件回调 */
export interface KeyboardCallbacks {
onKeyDown?: (event: EventKeyboard) => void;
onKeyUp?: (event: EventKeyboard) => void;
onKeyPressing?: (event: EventKeyboard) => void;
}
/** 键盘输入 */
export class GameKeyboardModule extends GameViewModule {
private _enabled = false;
private _callbacks: KeyboardCallbacks | null = null;
/**
* 键盘事件开关
* @param on 是否开启
* @param callbacks 开启时传入对应键事件回调(可只传需要的项)
*/
setKeyboard(on: boolean, callbacks?: KeyboardCallbacks): void {
if (on) {
if (callbacks) {
this._callbacks = callbacks;
}
if (!this._callbacks) {
console.warn('[OopsFramework]', 'setKeyboard(true) 需传入 callbacks');
return;
}
this._register(this._callbacks);
this._enabled = true;
}
else {
if (this._enabled && this._callbacks) {
this._unregister(this._callbacks);
}
this._enabled = false;
this._callbacks = null;
}
}
/** 销毁键盘模块 */
override destroy(): void {
if (this._enabled && this._callbacks) {
this._unregister(this._callbacks);
this._enabled = false;
this._callbacks = null;
}
}
/** 注册键盘事件
* @param callbacks 键盘事件回调
*/
private _register(callbacks: KeyboardCallbacks): void {
if (callbacks.onKeyDown) {
input.on(Input.EventType.KEY_DOWN, callbacks.onKeyDown, this.comp);
}
if (callbacks.onKeyUp) {
input.on(Input.EventType.KEY_UP, callbacks.onKeyUp, this.comp);
}
if (callbacks.onKeyPressing) {
input.on(Input.EventType.KEY_PRESSING, callbacks.onKeyPressing, this.comp);
}
}
/** 注销键盘事件
* @param callbacks 键盘事件回调
*/
private _unregister(callbacks: KeyboardCallbacks): void {
if (callbacks.onKeyDown) {
input.off(Input.EventType.KEY_DOWN, callbacks.onKeyDown, this.comp);
}
if (callbacks.onKeyUp) {
input.off(Input.EventType.KEY_UP, callbacks.onKeyUp, this.comp);
}
if (callbacks.onKeyPressing) {
input.off(Input.EventType.KEY_PRESSING, callbacks.onKeyPressing, this.comp);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "bc13cdc4-f4d7-4425-8397-27382d30cc9c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,48 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import { instantiate, Node, Prefab } from 'cc';
import { oops } from '../../../core/Oops';
import { ViewUtil } from '../../../core/utils/ViewUtil';
import { GameViewModule } from './GameViewModuleBase';
/** 预制节点与节点树管理 */
export class GameNodeModule extends GameViewModule {
/** 摊平的节点集合(所有节点不能重名) */
readonly nodes: Map<string, Node> = new Map();
/** 获取节点
* @param name 节点名称
* @returns 节点对象
*/
getNode(name: string): Node | undefined {
return this.nodes.get(name);
}
/** 获取节点树信息(轻量版) */
nodeTreeInfoLite(): void {
this.nodes.clear();
ViewUtil.nodeTreeInfoLite(this.comp.node, this.nodes);
}
/** 创建预制体节点
* @param path 预制体路径
* @param bundleName 资源包名称
* @returns 节点对象
*/
async createPrefabNode(path: string, bundleName: string = oops.res.defaultBundleName): Promise<Node | null> {
const prefab = await this.comp.res.load(bundleName, path, Prefab);
if (!prefab) {
console.warn('[OopsFramework]', `预制体加载失败: ${path}`);
return null;
}
return instantiate(prefab);
}
/** 销毁节点模块 */
override destroy(): void {
this.nodes.clear();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d5ac263c-8b18-45d3-9e66-60ddb1346365",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,147 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import type { Asset, Sprite, __private } from 'cc';
import { SpriteFrame, assetManager, isValid } from 'cc';
import { oops } from '../../../core/Oops';
import type { AssetType, CompleteCallback, Paths, ProgressCallback } from '../../../core/common/loader/ResLoader';
import { resLoader } from '../../../core/common/loader/ResLoader';
import { resAutoTracker } from '../../../core/common/loader/ResAutoTracker';
import { GameViewModule } from './GameViewModuleBase';
/** 资源加载与引用计数管理 */
export class GameResModule extends GameViewModule {
/** 获取资源
* @param path 资源路径
* @param type 资源类型
* @param bundleName 资源包名称
* @returns 资源对象
*/
getRes<T extends Asset>(path: string, type?: __private.__types_globals__Constructor<T> | null, bundleName?: string): T | null {
return oops.res.get(path, type, bundleName);
}
/** 加载资源
* @param bundleName 资源包名称
* @param paths 资源路径
* @param type 资源类型
* @returns 资源对象
*/
async load<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>): Promise<T> {
const result = await oops.res.load(bundleName, paths, type);
if (result) {
resAutoTracker.acquire(this.comp, result);
}
return result;
}
/** 加载任意资源
* @param bundleName 资源包名称
* @param paths 资源路径数组
* @param onProgress 进度回调
* @param onComplete 完成回调
*/
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 wrappedComplete = (err: Error | null, data: Asset[]) => {
if (!err && data?.length) {
resAutoTracker.acquireMany(this.comp, data);
}
originalComplete?.(err, data);
};
oops.res.loadAny(bundleName, paths, onProgress, wrappedComplete);
}
/** 加载目录资源
* @param bundleName 资源包名称
* @param dir 目录路径
* @param type 资源类型
* @param onProgress 进度回调
* @param onComplete 完成回调
*/
loadDir<T extends Asset>(
bundleName: string,
dir?: string | AssetType<T> | ProgressCallback | CompleteCallback,
type?: AssetType<T> | ProgressCallback | CompleteCallback,
onProgress?: ProgressCallback | CompleteCallback,
onComplete?: CompleteCallback,
): void {
const originalComplete = onComplete as ((err: Error | null, data: T[]) => void) | undefined;
const wrappedComplete = (err: Error | null, data: T[]) => {
if (!err && data?.length) {
resAutoTracker.acquireMany(this.comp, data);
}
originalComplete?.(err, data);
};
oops.res.loadDir(bundleName, dir, type, onProgress, wrappedComplete);
}
/** 释放资源
* @param path 资源路径
* @param bundleName 资源包名称
*/
releaseRes(path: string, bundleName: string = resLoader.defaultBundleName): void {
resAutoTracker.releaseByPath(this.comp, path, bundleName);
}
/** 销毁资源模块 */
override destroy(): void {
const released = resAutoTracker.releaseAll(this.comp);
if (released > 0) {
console.log(`[GameComponent] ${this.comp.node?.name} 释放 ${released} 条资源登记`);
}
}
/** 获取资源引用计数
* @param path 资源路径
* @param bundleName 资源包名称
* @returns 引用计数
*/
getResRefCount(path: string, bundleName: string = resLoader.defaultBundleName): number {
const bundle = assetManager.getBundle(bundleName);
const a = bundle?.get(path) as Asset | null;
return a ? a.refCount : 0;
}
/** 获取追踪的资源根节点数量
* @returns 资源根节点数量
*/
getTrackedResRootCount(): number {
return resAutoTracker.getOwnerEntryCount(this.comp);
}
/** 打印资源使用情况 */
printResUsage(): void {
resAutoTracker.printOwnerStatus(this.comp);
}
/** 设置精灵图片
* @param target 精灵组件
* @param path 图片路径
* @param bundle 资源包名称
* @returns 是否设置成功
*/
async setSprite(target: Sprite, path: string, bundle: string = resLoader.defaultBundleName): Promise<boolean> {
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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b834447c-db2b-48c2-b13d-7d986331afcd",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,17 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import type { GameComponent } from '../GameComponent';
/** GameComponent 下 view 子模块基类 */
export abstract class GameViewModule {
/** 构造函数
* @param comp 游戏组件
*/
constructor(protected readonly comp: GameComponent) {}
/** 组件销毁时回调,子类按需覆盖 */
destroy(): void {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f8bc2eb5-9b07-480d-a77a-321f288bde2f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,90 @@
/*
* @Author: dgflash
* @Date: 2022-04-14 17:08:01
* @LastEditors: dgflash
*/
import type { GameComponent } from '../GameComponent';
import { GameAudioModule } from './GameAudioModule';
import { GameButtonModule } from './GameButtonModule';
import { GameEventModule } from './GameEventModule';
import { GameKeyboardModule } from './GameKeyboardModule';
import { GameNodeModule } from './GameNodeModule';
import { GameResModule } from './GameResModule';
import type { GameViewModule } from './GameViewModuleBase';
export { GameViewModule } from './GameViewModuleBase';
/**
* view 子模块注册键
* @remarks 枚举顺序即销毁顺序(先输入/音频/资源,最后事件)
*/
export enum ViewModuleKey {
/** 按钮 */
Button = 'button',
/** 键盘 */
Keyboard = 'keyboard',
/** 音频 */
Audio = 'audio',
/** 资源 */
Res = 'res',
/** 节点树 */
Nodes = 'nodes',
/** 全局事件 */
Event = 'event',
}
/** view 子模块懒加载注册表(统一登记、按序批量销毁) */
export class GameViewModuleRegistry {
private readonly instances = new Map<ViewModuleKey, GameViewModule>();
/** 构造函数
* @param comp 游戏组件
*/
constructor(private readonly comp: GameComponent) {}
/** 获取模块实例
* @param key 模块键
* @returns 模块实例
*/
get<T extends GameViewModule = GameViewModule>(key: ViewModuleKey): T {
let module = this.instances.get(key) as T | undefined;
if (!module) {
module = this.createViewModule(key) as T;
this.instances.set(key, module);
}
return module;
}
/** 销毁所有模块 */
destroy(): void {
for (const key of Object.values(ViewModuleKey)) {
this.instances.get(key)?.destroy();
}
this.instances.clear();
}
/** 创建视图模块
* @param key 模块键
* @returns 模块实例
*/
private createViewModule(key: ViewModuleKey): GameViewModule {
switch (key) {
case ViewModuleKey.Event:
return new GameEventModule(this.comp);
case ViewModuleKey.Nodes:
return new GameNodeModule(this.comp);
case ViewModuleKey.Res:
return new GameResModule(this.comp);
case ViewModuleKey.Audio:
return new GameAudioModule(this.comp);
case ViewModuleKey.Button:
return new GameButtonModule(this.comp);
case ViewModuleKey.Keyboard:
return new GameKeyboardModule(this.comp);
default: {
const _exhaustive: never = key;
return _exhaustive;
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ae82d7cd-1564-4c48-855f-4e9a0fc9e96f",
"files": [],
"subMetas": {},
"userData": {}
}