mirror of
https://gitee.com/dgflash/oops-plugin-framework.git
synced 2026-05-30 18:39:18 +08:00
优化
This commit is contained in:
264
assets/core/common/audio/AudioClipLoader.ts
Normal file
264
assets/core/common/audio/AudioClipLoader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/core/common/audio/AudioClipLoader.ts.meta
Normal file
9
assets/core/common/audio/AudioClipLoader.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7e9304f5-fa80-43d0-883a-2fd8734e7bbb",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,4 @@ export interface IAudioData {
|
||||
switch: boolean;
|
||||
/** 音量 */
|
||||
volume: number;
|
||||
}
|
||||
}
|
||||
247
assets/core/common/loader/ResAutoTracker.ts
Normal file
247
assets/core/common/loader/ResAutoTracker.ts
Normal 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();
|
||||
9
assets/core/common/loader/ResAutoTracker.ts.meta
Normal file
9
assets/core/common/loader/ResAutoTracker.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "2a5b8165-c540-4e3a-abf7-5ef91e6f2c3b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
121
assets/core/common/loader/ResDebug.ts
Normal file
121
assets/core/common/loader/ResDebug.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
9
assets/core/common/loader/ResDebug.ts.meta
Normal file
9
assets/core/common/loader/ResDebug.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "96c108eb-8f7e-408d-8a6c-00d5cf5be314",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
26
assets/core/common/loader/ResErrors.ts
Normal file
26
assets/core/common/loader/ResErrors.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
assets/core/common/loader/ResErrors.ts.meta
Normal file
9
assets/core/common/loader/ResErrors.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "54c4ac22-3d70-4716-bd8e-335d919c6ce4",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
9
assets/core/common/loader/ResRefManager.ts.meta
Normal file
9
assets/core/common/loader/ResRefManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ed3a8f2f-f783-4fed-bffe-6a5f3a8efc39",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
35
assets/core/common/loader/ResTypes.ts
Normal file
35
assets/core/common/loader/ResTypes.ts
Normal 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;
|
||||
}
|
||||
9
assets/core/common/loader/ResTypes.ts.meta
Normal file
9
assets/core/common/loader/ResTypes.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4f5307a4-662a-4f67-8779-81ef5a28c540",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
49
assets/core/common/loader/ResUtils.ts
Normal file
49
assets/core/common/loader/ResUtils.ts
Normal 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();
|
||||
}
|
||||
9
assets/core/common/loader/ResUtils.ts.meta
Normal file
9
assets/core/common/loader/ResUtils.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9b2e2186-a60a-467a-a770-c51bb46c31a1",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
9
assets/module/common/view.meta
Normal file
9
assets/module/common/view.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "d2d11e7b-7d36-4417-8e97-537c7219488a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
84
assets/module/common/view/GameAudioModule.ts
Normal file
84
assets/module/common/view/GameAudioModule.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameAudioModule.ts.meta
Normal file
9
assets/module/common/view/GameAudioModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "68e344ff-705b-4472-9053-bc70bd7363da",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
47
assets/module/common/view/GameButtonModule.ts
Normal file
47
assets/module/common/view/GameButtonModule.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameButtonModule.ts.meta
Normal file
9
assets/module/common/view/GameButtonModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "48bfef5e-fe8b-4ad4-88be-cd1872153689",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
181
assets/module/common/view/GameEventModule.ts
Normal file
181
assets/module/common/view/GameEventModule.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameEventModule.ts.meta
Normal file
9
assets/module/common/view/GameEventModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b05268d5-1e17-4d87-bc27-47e100a55206",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
86
assets/module/common/view/GameKeyboardModule.ts
Normal file
86
assets/module/common/view/GameKeyboardModule.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameKeyboardModule.ts.meta
Normal file
9
assets/module/common/view/GameKeyboardModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "bc13cdc4-f4d7-4425-8397-27382d30cc9c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
48
assets/module/common/view/GameNodeModule.ts
Normal file
48
assets/module/common/view/GameNodeModule.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameNodeModule.ts.meta
Normal file
9
assets/module/common/view/GameNodeModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d5ac263c-8b18-45d3-9e66-60ddb1346365",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
147
assets/module/common/view/GameResModule.ts
Normal file
147
assets/module/common/view/GameResModule.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameResModule.ts.meta
Normal file
9
assets/module/common/view/GameResModule.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b834447c-db2b-48c2-b13d-7d986331afcd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
17
assets/module/common/view/GameViewModuleBase.ts
Normal file
17
assets/module/common/view/GameViewModuleBase.ts
Normal 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 {}
|
||||
}
|
||||
9
assets/module/common/view/GameViewModuleBase.ts.meta
Normal file
9
assets/module/common/view/GameViewModuleBase.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f8bc2eb5-9b07-480d-a77a-321f288bde2f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
90
assets/module/common/view/GameViewModuleRegistry.ts
Normal file
90
assets/module/common/view/GameViewModuleRegistry.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/module/common/view/GameViewModuleRegistry.ts.meta
Normal file
9
assets/module/common/view/GameViewModuleRegistry.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ae82d7cd-1564-4c48-855f-4e9a0fc9e96f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user