diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..5b3a941 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/core/.DS_Store b/assets/core/.DS_Store new file mode 100644 index 0000000..5580667 Binary files /dev/null and b/assets/core/.DS_Store differ diff --git a/assets/core/Oops.ts b/assets/core/Oops.ts index 54cad50..e278ba8 100644 --- a/assets/core/Oops.ts +++ b/assets/core/Oops.ts @@ -1,11 +1,4 @@ -/* - * @Author: dgflash - * @Date: 2022-02-11 09:32:47 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-21 15:19:56 - */ import { DEBUG } from 'cc/env'; -import { EffectSingleCase } from '../libs/animator-effect/EffectSingleCase'; import { ecs } from '../libs/ecs/ECS'; import type { ECSRootSystem } from '../libs/ecs/ECSSystem'; import { LanguageManager } from '../libs/gui/language/Language'; @@ -57,8 +50,6 @@ export class oops { static ecs: ECSRootSystem = new ecs.RootSystem(); /** MVVM */ static mvvm = VM; - /** 对象池 */ - static pool = EffectSingleCase.instance; } // 引入oops全局变量以方便调试 diff --git a/assets/core/Root.ts b/assets/core/Root.ts index 67fdb5b..7c020de 100644 --- a/assets/core/Root.ts +++ b/assets/core/Root.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-28 10:02:57 - */ import { _decorator, Component, director, Game, game, JsonAsset, Node, profiler, resources, screen, sys } from 'cc'; import { GameConfig } from '../module/config/GameConfig'; import { GameQueryConfig } from '../module/config/GameQueryConfig'; @@ -28,14 +22,14 @@ export class Root extends Component { type: Node, tooltip: '游戏层' }) - game: Node = null!; // 可使用多摄像机自定义二维或三维游戏场景 + game: Node = null!; // 可使用多摄像机自定义二维或三维游戏场景 /** 界面层节点 */ @property({ type: Node, tooltip: '界面层' }) - gui: Node = null!; + gui: Node = null!; /** 框架常驻节点 */ private persist: Node = null!; @@ -46,7 +40,7 @@ export class Root extends Component { this.initModule(); this.iniStart(); - this.loadConfig().then(); + this.loadConfig(); } private initModule() { @@ -68,11 +62,11 @@ export class Root extends Component { oops.gui = new LayerManager(); } - private async loadConfig() { + private loadConfig() { const config_name = 'config'; resources.load(config_name, JsonAsset, (err, config) => { if (err) { - this.loadConfig().then(); + this.loadConfig(); return; } @@ -161,18 +155,18 @@ export class Root extends Component { } private onShow() { - oops.timer.load(); // 处理回到游戏时减去逝去时间 - oops.audio.resumeAll(); // 恢复所有暂停的音乐播放 - director.resume(); // 恢复暂停场景的游戏逻辑,如果当前场景没有暂停将没任何事情发生 - game.resume(); // 恢复游戏主循环。包含:游戏逻辑,渲染,事件处理,背景音乐和所有音效 + oops.timer.load(); // 处理回到游戏时减去逝去时间 + oops.audio.resumeAll(); // 恢复所有暂停的音乐播放 + director.resume(); // 恢复暂停场景的游戏逻辑,如果当前场景没有暂停将没任何事情发生 + game.resume(); // 恢复游戏主循环。包含:游戏逻辑,渲染,事件处理,背景音乐和所有音效 oops.message.dispatchEvent(EventMessage.GAME_SHOW); } private onHide() { - oops.timer.save(); // 处理切到后台后记录切出时间 - oops.audio.pauseAll(); // 暂停所有音乐播放 - director.pause(); // 暂停正在运行的场景,该暂停只会停止游戏逻辑执行,但是不会停止渲染和 UI 响应。 如果想要更彻底得暂停游戏,包含渲染,音频和事件 - game.pause(); // 暂停游戏主循环。包含:游戏逻辑、渲染、输入事件派发(Web 和小游戏平台除外) + oops.timer.save(); // 处理切到后台后记录切出时间 + oops.audio.pauseAll(); // 暂停所有音乐播放 + director.pause(); // 暂停正在运行的场景,该暂停只会停止游戏逻辑执行,但是不会停止渲染和 UI 响应。 如果想要更彻底得暂停游戏,包含渲染,音频和事件 + game.pause(); // 暂停游戏主循环。包含:游戏逻辑、渲染、输入事件派发(Web 和小游戏平台除外) oops.message.dispatchEvent(EventMessage.GAME_HIDE); } } diff --git a/assets/core/common/audio/AudioClipLoader.ts b/assets/core/common/audio/AudioClipLoader.ts deleted file mode 100644 index d7a99fa..0000000 --- a/assets/core/common/audio/AudioClipLoader.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { AudioClip } from 'cc'; -import { resLoader } from '../loader/ResLoader'; - -/** 加载结果 */ -export interface ILoadResult { - /** 加载成功的 AudioClip */ - clip: AudioClip; - /** 原始路径(URL 或 bundle 内路径) */ - path: string; - /** 资源包名(远程资源时为 null) */ - bundle: string | null; - /** 是否为远程资源 */ - isRemote: boolean; -} - -/** - * 音频资源加载器 - * 统一处理三种来源的 AudioClip 获取: - * 1. 直接传入 AudioClip 实例 - * 2. 远程 URL 加载 - * 3. Bundle 内资源加载 - */ -export class AudioClipLoader { - /** 加载中缓存,避免同一资源重复加载 */ - private loadingCache: Map> = new Map(); - /** 已加载的 AudioClip 缓存 */ - private clipCache: Map = new Map(); - - /** - * 从三种来源获取 AudioClip - * @param path - AudioClip 实例、远程 URL、或 bundle 内路径 - * @param bundle - 资源包名(path 为 AudioClip 或 URL 时忽略) - * @returns 加载结果,失败返回 null - */ - async load( - path: string | AudioClip, - bundle?: string - ): Promise { - if (path instanceof AudioClip) { - if (!path.isValid) { - console.warn(`AudioClip 实例已失效`); - return null; - } - // 外部传入的 AudioClip 实例,增加引擎引用计数 - path.addRef(); - return { clip: path, path: path.uuid, bundle: null, isRemote: false }; - } - - const cacheKey = this.getCacheKey(path, bundle); - const cached = this.clipCache.get(cacheKey); - - if (cached && cached.isValid) { - // 增加引擎引用计数 - cached.addRef(); - return { clip: cached, path, bundle: bundle || null, isRemote: path.indexOf('http') === 0 }; - } - - if (path.indexOf('http') === 0) { - return this.loadRemote(path); - } - - return this.loadBundle(path, bundle || resLoader.defaultBundleName); - } - - /** - * 释放指定路径的音频资源引用 - * @param path - 资源路径或 URL - * @param bundle - 资源包名(远程资源时忽略) - */ - release(path: string, bundle?: string): void { - const { key, entry } = this.getCacheEntry(path, bundle); - if (!entry) return; - - // 减少引擎引用计数 - if (entry.isValid) { - entry.decRef(); - } - } - - /** - * 立即释放指定路径的音频资源(不等待延迟) - * @param path - 资源路径或 URL - * @param bundle - 资源包名(远程资源时忽略) - */ - releaseImmediately(path: string, bundle?: string): void { - const { key, entry } = this.getCacheEntry(path, bundle); - if (!entry) return; - - this.doRelease(key, entry); - } - - /** - * 执行真正的资源释放 - * @param key - 缓存键值 - * @param entry - 缓存条目 - */ - private doRelease(key: string, entry: AudioClip): void { - if (entry && entry.isValid) { - entry.decRef(); - } - this.clipCache.delete(key); - } - - /** 清空所有缓存 */ - clearCache(): void { - this.clipCache.forEach((entry, key) => { - this.doRelease(key, entry); - }); - this.clipCache.clear(); - this.loadingCache.clear(); - } - - /** 销毁加载器,释放所有资源 */ - destroy(): void { - this.clearCache(); - } - - /** - * 获取缓存统计信息 - * @returns 缓存条目数量 - */ - getStats(): { total: number } { - return { total: this.clipCache.size }; - } - - /** - * 获取缓存 key - * @param path - 资源路径 - * @param bundle - 资源包名 - * @returns 缓存键值 - */ - private getCacheKey(path: string, bundle?: string): string { - if (path.indexOf('http') === 0) { - return `remote_${path}`; - } - return `bundle_${bundle || resLoader.defaultBundleName}_${path}`; - } - - /** - * 获取缓存条目 - * @param path - 资源路径 - * @param bundle - 资源包名 - * @returns 缓存键值和条目 - */ - private getCacheEntry(path: string, bundle?: string): { key: string; entry: AudioClip | undefined } { - const key = this.getCacheKey(path, bundle); - const entry = this.clipCache.get(key); - return { key, entry }; - } - - /** - * 设置缓存并返回加载结果 - * @param clip - 音频资源 - * @param path - 资源路径 - * @param cacheKey - 缓存键值 - * @param bundle - 资源包名 - * @param isRemote - 是否为远程资源 - * @returns 加载结果 - */ - private setCacheAndReturn( - clip: AudioClip, - path: string, - cacheKey: string, - bundle: string | null, - isRemote: boolean - ): ILoadResult { - clip.addRef(); - this.clipCache.set(cacheKey, clip); - return { clip, path, bundle, isRemote }; - } - - /** - * 加载远程资源 - * @param path - 远程 URL - * @returns 加载结果 - */ - private async loadRemote( - path: string - ): Promise { - let loadPromise = this.loadingCache.get(path); - if (!loadPromise) { - loadPromise = this.doLoadRemotePromise(path); - this.loadingCache.set(path, loadPromise); - } - - try { - const clip = await loadPromise; - if (!clip || !clip.isValid) { - console.warn(`远程音频资源加载失败: ${path}`); - return null; - } - - return this.setCacheAndReturn(clip, path, `remote_${path}`, null, true); - } - catch (e) { - console.warn(`远程音频资源加载异常: ${path}`, e); - return null; - } - finally { - this.loadingCache.delete(path); - } - } - - /** - * 执行远程资源加载 - * @param path - 远程 URL - * @returns AudioClip 加载 Promise - */ - private async doLoadRemotePromise(path: string): Promise { - const extension = path.split('.').pop(); - return resLoader.loadRemote(path, { ext: `.${extension}` }); - } - - /** - * 加载 Bundle 内资源 - * @param path - 资源路径 - * @param bundle - 资源包名 - * @returns 加载结果 - */ - private async loadBundle( - path: string, - bundle: string - ): Promise { - const cacheKey = `bundle_${bundle}_${path}`; - - let clip = resLoader.get(path, AudioClip, bundle); - if (clip) { - if (!clip.isValid) { - console.warn(`音频资源已失效: ${bundle}/${path}`); - return null; - } - - const entry = this.clipCache.get(cacheKey); - if (entry) { - clip.addRef(); - return { clip, path, bundle, isRemote: false }; - } - return this.setCacheAndReturn(clip, path, cacheKey, bundle, false); - } - - let loadPromise = this.loadingCache.get(cacheKey); - if (!loadPromise) { - loadPromise = resLoader.load(bundle, path, AudioClip); - this.loadingCache.set(cacheKey, loadPromise); - } - - try { - clip = await loadPromise; - if (!clip || !clip.isValid) { - console.warn(`音频资源加载失败: ${bundle}/${path}`); - return null; - } - - return this.setCacheAndReturn(clip, path, cacheKey, bundle, false); - } - catch (e) { - console.warn(`音频资源加载异常: ${bundle}/${path}`, e); - return null; - } - finally { - this.loadingCache.delete(cacheKey); - } - } -} \ No newline at end of file diff --git a/assets/core/common/audio/AudioEffect.ts b/assets/core/common/audio/AudioEffect.ts index 338db85..45cef16 100644 --- a/assets/core/common/audio/AudioEffect.ts +++ b/assets/core/common/audio/AudioEffect.ts @@ -1,15 +1,9 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 10:22:36 - */ import type { AudioClip } from 'cc'; import { AudioSource, _decorator } from 'cc'; import type { IAudioParams } from './IAudio'; const { ccclass } = _decorator; -/** 游戏音效 */ +/** 游戏音效播放器 */ @ccclass('AudioEffect') export class AudioEffect extends AudioSource { /** 唯一编号 */ diff --git a/assets/core/common/audio/AudioEffectPool.ts b/assets/core/common/audio/AudioEffectPool.ts index b384f5c..ac1ff22 100644 --- a/assets/core/common/audio/AudioEffectPool.ts +++ b/assets/core/common/audio/AudioEffectPool.ts @@ -1,10 +1,8 @@ -import { AudioClip, Node, NodePool } from 'cc'; +import { AudioClip, AudioSource, Node, NodePool } from 'cc'; import { oops } from '../../Oops'; -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; @@ -12,17 +10,14 @@ const AE_ID_MAX = 30000; /** * 音效池 * - * 内存管理思路: - * 1. 引用计数机制:每个 AudioClip 通过 addRef/decRef 管理生命周期 - * 2. 自动释放:界面关闭时调用 releaseResByPath 减少引用计数 - * 3. 永久缓存:通过预加载时额外增加一次引用,使资源不会被界面释放清理 - * 4. 缓存复用:引用计数 > 0 的资源保留在 clipCache 中供后续界面复用 + * 职责: + * 1. 不负责资源加载,只接收 AudioClip 实例进行播放 + * 2. 管理音效播放器对象池 + * 3. 资源加载与释放由外部(GameResModule + ResAutoTracker)管理 */ export class AudioEffectPool { /** 音效配置数据 */ - private data: { [node: string]: IAudioData } = null!; - /** 音频资源加载器 */ - private loader: AudioClipLoader = new AudioClipLoader(); + private data: { [node: string]: IAudioData } = {}; /** 音效播放器节点对象池 */ private pool: NodePool = new NodePool(); /** 正在播放的音效播放器集合 */ @@ -90,47 +85,31 @@ export class AudioEffectPool { } /** - * 加载与播放音效 - * @param path 音效资源地址与音效资源 + * 播放音效 + * @param clip AudioClip 实例 * @param params 音效附加参数 * @returns */ - async loadAndPlay(path: string | AudioClip, params?: IAudioParams): Promise { + play(clip: AudioClip, params?: IAudioParams): AudioEffect | null { const finalParams = this.mergeParams(params); const iad = this.data[finalParams.type!]; if (!iad) { console.error(`类型为【${finalParams.type!}】的音效配置不存在`); - return null!; + return null; } if (!iad.switch) { - return null!; + return null; } if (finalParams.volume == null) finalParams.volume = iad.volume; - const bundle = finalParams.bundle!; - let key: string; + const key = `${finalParams.type}_${clip.uuid}_${this.getAeId()}`; - if (path instanceof AudioClip) { - key = `${finalParams.type}_${path.uuid}`; - } - else { - key = `${finalParams.type}_${bundle}_${path}`; - } - - // 通过 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!; + return null; } // 获取音效播放器播放音乐 @@ -138,13 +117,8 @@ export class AudioEffectPool { let node: Node; if (this.pool.size() === 0) { - const aeid = this.getAeId(); - key = `${key}_${aeid}`; - node = new Node('AudioEffect'); ae = node.addComponent(AudioEffect)!; - ae.key = key; - ae.aeid = aeid; ae.onComplete = this.onAudioEffectPlayComplete.bind(this); } else { @@ -152,12 +126,15 @@ export class AudioEffectPool { ae = node.getComponent(AudioEffect)!; } + ae.key = key; + ae.aeid = this._aeId; + // 记录正在播放的音效播放器 this.effects.set(ae.key, ae); try { node.parent = oops.audio.node; - ae.path = path; + ae.path = clip; ae.params = finalParams; ae.loop = finalParams.loop!; ae.volume = finalParams.volume!; @@ -171,22 +148,14 @@ export class AudioEffectPool { this.effects.delete(ae.key); this.put(ae); console.warn(`音效播放异常,已回收: ${key}`, e); - return null!; + return null; } } /** 音效播放完成 */ private onAudioEffectPlayComplete(ae: AudioEffect) { - // 通过 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) { + // 非循环播放的音效,自动回收播放器 + if (!ae.params.loop) { ae.params && ae.params.onPlayComplete && ae.params.onPlayComplete(ae); this.put(ae); } @@ -194,7 +163,7 @@ export class AudioEffectPool { /** * 回收音效播放器 - * @param ae loadAndPlay 方法返回的音效播放器对象 + * @param ae play 方法返回的音效播放器对象 */ put(ae: AudioEffect) { const effect = this.effects.get(ae.key); @@ -218,9 +187,17 @@ export class AudioEffectPool { this.effects.clear(); } - /** 恢复所有音效 */ - play() { - this.effects.forEach((ae) => ae.play()); + /** 恢复或播放所有音效 */ + resume() { + this.effects.forEach((ae) => { + // 如果是暂停状态则恢复,如果是停止状态则播放 + if (ae.state === AudioSource.AudioState.PAUSED) { + ae.play(); + } + else if (ae.state === AudioSource.AudioState.INIT || ae.state === AudioSource.AudioState.STOPPED) { + ae.play(); + } + }); } /** 暂停所有音效 */ @@ -230,22 +207,12 @@ export class AudioEffectPool { for (let i = 0; i < effectsArray.length; i++) { const ae = effectsArray[i]; ae.pause(); - this.onAudioEffectPlayComplete(ae); + // 暂停时不回收音效播放器,只是暂停播放 } - this.effects.clear(); } - /** 释放所有音效资源与对象池中播放器 */ + /** 释放所有音效播放器 */ release() { - // 释放池中音乐播放器 - this.releasePool(); - - // 清空 loader 缓存(强制释放所有音频资源) - this.loader.clearCache(); - } - - /** 释放池中音乐播放器 */ - releasePool() { this.pool.clear(); // 释放正在播放的音效对象 @@ -276,38 +243,15 @@ export class AudioEffectPool { return destroyed; } - /** - * 释放指定远程音效资源(立即释放,不等待延迟) - * @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 + loop: false }; } -} \ No newline at end of file +} diff --git a/assets/core/common/audio/AudioManager.ts b/assets/core/common/audio/AudioManager.ts index 19180b0..3c3c96c 100644 --- a/assets/core/common/audio/AudioManager.ts +++ b/assets/core/common/audio/AudioManager.ts @@ -26,20 +26,20 @@ export class AudioManager extends Component { /** * 播放背景音乐 - * @param path 资源路径 + * @param clip AudioClip 实例 * @param params 音效参数 */ - playMusic(path: string, params?: IAudioParams) { - this.music.loadAndPlay(path, params); + playMusic(clip: AudioClip, params?: IAudioParams) { + this.music.play(clip, params); } /** * 播放音效 - * @param path 资源路径 + * @param clip AudioClip 实例 * @param params 音效参数 */ - playEffect(path: string | AudioClip, params?: IAudioParams): Promise { - return this.effect.loadAndPlay(path, params); + playEffect(clip: AudioClip, params?: IAudioParams): AudioEffect | null { + return this.effect.play(clip, params); } /** 回收音效播放器 */ @@ -47,18 +47,10 @@ 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(); + this.effect.resume(); } /** 暂停当前音乐与音效的播放 */ @@ -126,10 +118,9 @@ export class AudioManager extends Component { this.save(); } - /** 组件销毁时释放所有音频资源 */ + /** 组件销毁时停止所有音频 */ onDestroy() { this.stopAll(); - this.music?.release(); this.effect?.release(); this.music = null!; this.data = null!; diff --git a/assets/core/common/audio/AudioMusic.ts b/assets/core/common/audio/AudioMusic.ts index 2ba1055..c74dd05 100644 --- a/assets/core/common/audio/AudioMusic.ts +++ b/assets/core/common/audio/AudioMusic.ts @@ -1,36 +1,21 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:13 - * @LastEditors: dgflash - * @LastEditTime: 2023-05-16 09:11:30 - */ +import type { AudioClip } from 'cc'; import { Node } from 'cc'; -import { resLoader } from '../loader/ResLoader'; -import { AudioClipLoader } from './AudioClipLoader'; +import type { IAudioData, IAudioParams } from './IAudio'; import { AudioEffect } from './AudioEffect'; import { AudioEffectType } from './AudioEnum'; -import type { IAudioData, IAudioParams } from './IAudio'; /** * 背景音乐 - * 1、播放一个新背景音乐时,先加载音乐资源,然后停止正在播放的背景资源同时释放当前背景音乐资源,最后播放新的背景音乐 + * 1、播放一个新背景音乐时,先停止正在播放的背景资源,最后播放新的背景音乐 * 2、背景音乐循环播放时,不会触发播放完成事件 + * 3、不负责加载资源,也不负责释放资源,资源加载与引用计数完全由外部(GameResModule + ResAutoTracker)管理 */ 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; /** * 音效开关 @@ -94,76 +79,21 @@ export class AudioMusic extends Node { } /** - * 加载音乐并播放 - * @param path 音乐资源地址 + * 播放音乐 + * @param clip AudioClip 实例 * @param params 背景音乐资源播放参数 */ - async loadAndPlay(path: string, params?: IAudioParams) { + play(clip: AudioClip, params?: IAudioParams) { if (!this.getSwitch()) return; - if (this._isLoading) { - this._nextPath = path; - this._nextParams = params || null; - return; - } + if (this._ae.playing) this.stop(); - 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, - volume: params.volume ?? this.getVolume(), - destroy: params.destroy, - onPlayComplete: params.onPlayComplete - } : { - type: AudioEffectType.Music, - bundle: resLoader.defaultBundleName, - loop: true, - volume: this.getVolume() - }; + this._ae.params = params!; + this._ae.clip = clip; + this._ae.loop = params?.loop ?? true; + this._ae.volume = params?.volume ?? this.getVolume(); + this._ae.currentTime = 0; + this._ae.play(); } /** 恢复当前暂停的音乐与音效播放 */ @@ -181,28 +111,10 @@ export class AudioMusic extends Node { if (this._ae.playing) this._ae.stop(); } - /** 释放当前背景音乐资源 */ - release() { - if (this._ae && this._ae.clip) { - this.stop(); - this._ae.clip = null; - } - - // 通过 loader 释放资源引用(自动处理延迟释放) - if (this._currentPath) { - this.loader.release(this._currentPath, this._currentBundle || undefined); - this._currentPath = null; - this._currentBundle = null; - } - } - - /** 节点销毁时清理所有引用 */ + /** 节点销毁时清理 */ onDestroy() { - this.release(); - this._nextPath = null; - this._nextParams = null; + this.stop(); this._ae = null!; this.data = null!; - this.loader.destroy(); } -} \ No newline at end of file +} diff --git a/assets/core/common/audio/IAudio.ts b/assets/core/common/audio/IAudio.ts index b423ef1..21ac59c 100644 --- a/assets/core/common/audio/IAudio.ts +++ b/assets/core/common/audio/IAudio.ts @@ -1,14 +1,10 @@ export interface IAudioParams { - /** 音乐分类 */ - type?: string, - /** 资源包名 */ - bundle?: string, + /** 音效类型 */ + type?: string; /** 是否循环播放 */ loop?: boolean; /** 音效音量 */ volume?: number; - /** 是否在播放完后自动释放音乐资源(默认不释放) */ - destroy?: boolean; /** 播放完成事件 */ onPlayComplete?: Function; } @@ -18,4 +14,4 @@ export interface IAudioData { switch: boolean; /** 音量 */ volume: number; -} \ No newline at end of file +} diff --git a/assets/core/common/event/EventMessage.ts b/assets/core/common/event/EventMessage.ts index ebf9efe..6d91743 100644 --- a/assets/core/common/event/EventMessage.ts +++ b/assets/core/common/event/EventMessage.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 11:03:08 - */ - /** * 全局事件监听方法 * @param event 事件名 diff --git a/assets/core/common/pool.meta b/assets/core/common/pool.meta new file mode 100644 index 0000000..f555923 --- /dev/null +++ b/assets/core/common/pool.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "97818807-d408-4c88-8303-857111cc148c", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/pool/GameNodePool.ts b/assets/core/common/pool/GameNodePool.ts new file mode 100644 index 0000000..4346acb --- /dev/null +++ b/assets/core/common/pool/GameNodePool.ts @@ -0,0 +1,140 @@ +import type { Node } from 'cc'; +import { instantiate, NodePool, Prefab } from 'cc'; + +/** + * 通用对象池管理器 + * 基于 Prefab 的 UUID 管理对象池,支持全局单例访问 + * + * 使用场景: + * 1、特效对象池管理 + * 2、UI 对象池管理 + * 3、任意 Prefab 的对象池管理 + * + * 注意:本类只管理对象池,不管理资源加载与释放 + * 资源管理请使用各模块自己的资源管理系统 + */ +export class GameNodePool { + private static _instance: GameNodePool; + /** 获取单例实例 */ + static get instance(): GameNodePool { + if (this._instance == null) { + this._instance = new GameNodePool(); + } + return this._instance; + } + + /** 对象池集合 - key 为 Prefab 的 UUID */ + private _pools: Map = new Map(); + + /** + * 获取指定对象池中对象数量 + * @param prefab 预制体资源 + * @returns 对象池中可用对象数量 + */ + getCount(prefab: Prefab): number { + const pool = this._pools.get(prefab.uuid); + if (pool) { + return pool.size(); + } + return 0; + } + + /** + * 预加载对象到池中 + * @param count 预加载数量 + * @param prefab 预制体资源 + */ + preload(count: number, prefab: Prefab): void { + const uuid = prefab.uuid; + let pool = this._pools.get(uuid); + if (pool == null) { + pool = new NodePool(); + this._pools.set(uuid, pool); + } + + for (let i = 0; i < count; i++) { + const node = instantiate(prefab); + // @ts-ignore + node._pool_uuid = uuid; + pool.put(node); + } + } + + /** + * 从对象池获取对象 + * @param prefab 预制体资源 + * @param parent 父节点(可选) + * @returns 节点对象 + */ + get(prefab: Prefab, parent?: Node): Node { + const uuid = prefab.uuid; + let pool = this._pools.get(uuid); + if (pool == null) { + pool = new NodePool(); + this._pools.set(uuid, pool); + } + + let node: Node; + // 池中无可用对象时创建新对象 + if (pool.size() == 0) { + node = instantiate(prefab); + // @ts-ignore + node._pool_uuid = uuid; + } + // 从池中获取对象 + else { + node = pool.get()!; + } + + // 设置父节点 + if (parent) { + node.parent = parent; + } + + return node; + } + + /** + * 回收对象到池中 + * @param node 节点 + */ + put(node: Node) { + // @ts-ignore + const uuid = node._pool_uuid; + if (uuid) { + const pool = this._pools.get(uuid); + if (pool) { + // 从父节点移除 + if (node.parent) { + node.removeFromParent(); + } + + // 重置节点状态 + node.active = false; + + // 回收到池中 + pool.put(node); + } + } + } + + /** + * 清除对象池数据 + * @param prefab 预制体资源,为空时清除所有对象池数据 + */ + clear(prefab?: Prefab) { + if (prefab) { + const uuid = prefab.uuid; + const pool = this._pools.get(uuid); + if (pool) { + pool.clear(); + } + } + else { + this._pools.forEach((pool) => { + pool.clear(); + }); + this._pools.clear(); + } + } +} diff --git a/assets/core/common/pool/GameNodePool.ts.meta b/assets/core/common/pool/GameNodePool.ts.meta new file mode 100644 index 0000000..74f697c --- /dev/null +++ b/assets/core/common/pool/GameNodePool.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "fcd4f08b-a148-4303-918d-317135283b33", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/core/common/timer/Timer.ts b/assets/core/common/timer/Timer.ts index 98c4179..1872dd9 100644 --- a/assets/core/common/timer/Timer.ts +++ b/assets/core/common/timer/Timer.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2023-01-19 11:09:38 - * @LastEditors: dgflash - * @LastEditTime: 2023-01-19 14:28:05 - */ - /** * 定时触发组件 * @help https://gitee.com/dgflash/oops-framework/wikis/pages?sort_id=12037964&doc_id=2873565 diff --git a/assets/core/common/timer/TimerManager.ts b/assets/core/common/timer/TimerManager.ts index c159cfb..fe9df56 100644 --- a/assets/core/common/timer/TimerManager.ts +++ b/assets/core/common/timer/TimerManager.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2023-01-19 10:33:49 - * @LastEditors: dgflash - * @LastEditTime: 2023-01-19 14:37:19 - */ import { Component, game } from 'cc'; import { StringUtil } from '../../utils/StringUtil'; import { Timer } from './Timer'; diff --git a/assets/core/game/GameManager.ts b/assets/core/game/GameManager.ts index 1565430..9e80507 100644 --- a/assets/core/game/GameManager.ts +++ b/assets/core/game/GameManager.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-02-10 09:50:41 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 12:09:55 - */ import type { Node } from 'cc'; import { director, isValid } from 'cc'; import { GameComponent } from '../../module/common/GameComponent'; diff --git a/assets/core/gui/layer/LayerDialog.ts b/assets/core/gui/layer/LayerDialog.ts index 5db2ec9..2f14666 100644 --- a/assets/core/gui/layer/LayerDialog.ts +++ b/assets/core/gui/layer/LayerDialog.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2023-07-24 17:14:57 - */ import type { Node } from 'cc'; import { LayerPopUp } from './LayerPopup'; import type { UIParam, UIState } from './LayerUIElement'; diff --git a/assets/core/gui/layer/LayerGame.ts b/assets/core/gui/layer/LayerGame.ts index 2e73f32..6cc911d 100644 --- a/assets/core/gui/layer/LayerGame.ts +++ b/assets/core/gui/layer/LayerGame.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2025-08-15 10:06:47 - * @LastEditors: dgflash - * @LastEditTime: 2025-08-15 10:06:47 - */ import { Node, NodePool, Vec3, warn } from 'cc'; import { resLoader } from '../../common/loader/ResLoader'; import { ViewUtil } from '../../utils/ViewUtil'; diff --git a/assets/core/gui/layer/LayerManager.ts b/assets/core/gui/layer/LayerManager.ts index 381b7a8..79d3648 100644 --- a/assets/core/gui/layer/LayerManager.ts +++ b/assets/core/gui/layer/LayerManager.ts @@ -1,5 +1,4 @@ import { Camera, Node, warn } from 'cc'; -import { oops } from '../../Oops'; import { gui } from '../Gui'; import { LayerDialog } from './LayerDialog'; import type { UIConfigMap, Uiid } from './LayerEnum'; diff --git a/assets/core/gui/layer/LayerNotify.ts b/assets/core/gui/layer/LayerNotify.ts index be7f5d8..96c1e9c 100644 --- a/assets/core/gui/layer/LayerNotify.ts +++ b/assets/core/gui/layer/LayerNotify.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-08-15 10:06:47 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 13:44:12 - */ import { BlockInputEvents, Node, instantiate } from 'cc'; import { EDITOR } from 'cc/env'; import { ViewUtil } from '../../utils/ViewUtil'; diff --git a/assets/core/gui/layer/LayerPopup.ts b/assets/core/gui/layer/LayerPopup.ts index 4a92d64..c7f441b 100644 --- a/assets/core/gui/layer/LayerPopup.ts +++ b/assets/core/gui/layer/LayerPopup.ts @@ -1,8 +1,3 @@ -/* - * @Date: 2021-11-24 16:08:36 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 13:44:28 - */ import type { EventTouch } from 'cc'; import { BlockInputEvents, Node } from 'cc'; import { ViewUtil } from '../../utils/ViewUtil'; @@ -103,9 +98,9 @@ export class LayerPopUp extends LayerUI { this.black.enabled = false; } - if (config.mask) { - this.mask.parent = this; - this.mask.uiSprite.enabled = true; + if (config.mask) { + this.mask.parent = this; + this.mask.uiSprite.enabled = true; } } diff --git a/assets/core/gui/layer/LayerUIElement.ts b/assets/core/gui/layer/LayerUIElement.ts index ed15ce2..aebe678 100644 --- a/assets/core/gui/layer/LayerUIElement.ts +++ b/assets/core/gui/layer/LayerUIElement.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2023-01-09 11:55:03 - */ import type { Node } from 'cc'; import { Component, _decorator } from 'cc'; import { oops } from '../../Oops'; diff --git a/assets/core/gui/prompt/LoadingIndicator.ts b/assets/core/gui/prompt/LoadingIndicator.ts index 6f441ac..0a3ab09 100644 --- a/assets/core/gui/prompt/LoadingIndicator.ts +++ b/assets/core/gui/prompt/LoadingIndicator.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:08:39 - */ import { Component, Node, _decorator } from 'cc'; const { ccclass, property } = _decorator; diff --git a/assets/core/gui/prompt/Notify.ts b/assets/core/gui/prompt/Notify.ts index 004bc4f..d121d49 100644 --- a/assets/core/gui/prompt/Notify.ts +++ b/assets/core/gui/prompt/Notify.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: bansomin - * @LastEditTime: 2025-01-02 10:47:47 - */ import { Animation, Component, Label, _decorator } from 'cc'; import { LanguageLabel } from '../../../libs/gui/language/LanguageLabel'; @@ -48,4 +42,4 @@ export class Notify extends Component { this.lab_content.string = msg; } } -} +} diff --git a/assets/core/utils/ArrayUtil.ts b/assets/core/utils/ArrayUtil.ts index c463e48..0b56055 100644 --- a/assets/core/utils/ArrayUtil.ts +++ b/assets/core/utils/ArrayUtil.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-08-11 16:41:12 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:50:57 - */ - /** 数组工具 */ export class ArrayUtil { /** diff --git a/assets/core/utils/CameraUtil.ts b/assets/core/utils/CameraUtil.ts index 8f826f4..a81fb55 100644 --- a/assets/core/utils/CameraUtil.ts +++ b/assets/core/utils/CameraUtil.ts @@ -1,10 +1,4 @@ -/* - * @Author: dgflash - * @Date: 2022-07-26 15:29:57 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:50:16 - */ -import type { Camera } from 'cc'; +import type { Camera } from 'cc'; import { Vec3, view } from 'cc'; /** 摄像机工具 */ @@ -29,4 +23,4 @@ export class CameraUtil { && (viewPos.x <= viewportRect.width) && (viewPos.x >= 0) && (viewPos.y <= viewportRect.height) && (viewPos.y >= 0); } -} +} diff --git a/assets/core/utils/ImageUtil.ts b/assets/core/utils/ImageUtil.ts index 10474ca..ffeec5b 100644 --- a/assets/core/utils/ImageUtil.ts +++ b/assets/core/utils/ImageUtil.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:49:42 - */ import { assetManager, Color, ImageAsset, sys, Texture2D } from 'cc'; /** diff --git a/assets/core/utils/JsonUtil.ts b/assets/core/utils/JsonUtil.ts index 06b7fcd..947c825 100644 --- a/assets/core/utils/JsonUtil.ts +++ b/assets/core/utils/JsonUtil.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-08-18 17:00:59 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-22 15:48:02 - */ - import { JsonAsset } from 'cc'; import { ZipLoader } from 'db://oops-framework/core/common/loader/ZipLoader'; import { resLoader } from '../common/loader/ResLoader'; @@ -107,4 +100,4 @@ export class JsonUtil { static clear() { data.clear(); } -} +} diff --git a/assets/core/utils/ObjectUtil.ts b/assets/core/utils/ObjectUtil.ts index 5b1d86b..2832fd9 100644 --- a/assets/core/utils/ObjectUtil.ts +++ b/assets/core/utils/ObjectUtil.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-07-26 15:29:57 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 12:07:54 - */ - /** 对象工具 */ export class ObjectUtil { /** diff --git a/assets/core/utils/PhysicsUtil.ts b/assets/core/utils/PhysicsUtil.ts index 2b1438f..1b651b1 100644 --- a/assets/core/utils/PhysicsUtil.ts +++ b/assets/core/utils/PhysicsUtil.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-07-21 17:30:59 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:40:28 - */ import type { Node } from 'cc'; /** 物理分组数据 */ @@ -54,4 +48,4 @@ export class PhysicsUtil { PhysicsUtil.setNodeLayer(item, n); }); } -} +} diff --git a/assets/core/utils/PlatformUtil.ts b/assets/core/utils/PlatformUtil.ts index 7e30a0d..8f19f44 100644 --- a/assets/core/utils/PlatformUtil.ts +++ b/assets/core/utils/PlatformUtil.ts @@ -1,8 +1,3 @@ -/* - * @Date: 2021-08-14 16:17:03 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 14:39:03 - */ import type { __private } from 'cc'; import { native, sys } from 'cc'; diff --git a/assets/core/utils/RegexUtil.ts b/assets/core/utils/RegexUtil.ts index 8a6aaba..4416dbb 100644 --- a/assets/core/utils/RegexUtil.ts +++ b/assets/core/utils/RegexUtil.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-07-26 15:29:57 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 12:08:25 - */ - /** 正则工具 */ export class RegexUtil { /** diff --git a/assets/core/utils/RotateUtil.ts b/assets/core/utils/RotateUtil.ts index 27fdb7d..42ea7d0 100644 --- a/assets/core/utils/RotateUtil.ts +++ b/assets/core/utils/RotateUtil.ts @@ -1,10 +1,4 @@ -/* - * @Author: dgflash - * @Date: 2022-07-26 15:29:57 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 12:08:28 - */ -import type { Node } from 'cc'; +import type { Node } from 'cc'; import { Quat, toRadian, Vec3 } from 'cc'; import { Vec3Util } from './Vec3Util'; @@ -77,4 +71,4 @@ export class RotateUtil { return vec3; } -} +} diff --git a/assets/core/utils/ViewUtil.ts b/assets/core/utils/ViewUtil.ts index 65156a0..b08e9bc 100644 --- a/assets/core/utils/ViewUtil.ts +++ b/assets/core/utils/ViewUtil.ts @@ -1,10 +1,4 @@ -/* - * @Author: dgflash - * @Date: 2021-08-16 09:34:56 - * @LastEditors: dgflash - * @LastEditTime: 2023-01-19 14:52:12 - */ -import type { EventTouch, Node, Vec3 } from 'cc'; +import type { EventTouch, Node, Vec3 } from 'cc'; import { Animation, AnimationClip, instantiate, Prefab, Size, UITransform, v3 } from 'cc'; import { resLoader } from '../common/loader/ResLoader'; @@ -165,4 +159,4 @@ export class ViewUtil { anim.createState(clip, clip!.name); anim.play(clip!.name); } -} +} diff --git a/assets/libs/.DS_Store b/assets/libs/.DS_Store new file mode 100644 index 0000000..e5492b7 Binary files /dev/null and b/assets/libs/.DS_Store differ diff --git a/assets/libs/animator-effect/Effect2DFollow3D.ts b/assets/libs/animator-effect/Effect2DFollow3D.ts index 64cbbfd..f81454f 100644 --- a/assets/libs/animator-effect/Effect2DFollow3D.ts +++ b/assets/libs/animator-effect/Effect2DFollow3D.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-03-31 18:03:50 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-22 14:53:47 - */ - import type { Camera } from 'cc'; import { _decorator, Component, Node, Vec3 } from 'cc'; import { oops } from '../../core/Oops'; diff --git a/assets/libs/animator-effect/EffectDelayRelease.ts b/assets/libs/animator-effect/EffectDelayRelease.ts index f60a225..0eb37ed 100644 --- a/assets/libs/animator-effect/EffectDelayRelease.ts +++ b/assets/libs/animator-effect/EffectDelayRelease.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-08-11 16:41:12 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-22 14:54:17 - */ - import { Component, _decorator } from 'cc'; import { EffectSingleCase } from './EffectSingleCase'; const { ccclass, property } = _decorator; diff --git a/assets/libs/animator-effect/EffectFinishedRelease.ts b/assets/libs/animator-effect/EffectFinishedRelease.ts index 96cc00d..a6bb795 100644 --- a/assets/libs/animator-effect/EffectFinishedRelease.ts +++ b/assets/libs/animator-effect/EffectFinishedRelease.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-08-19 15:36:08 - * @LastEditors: dgflash - * @LastEditTime: 2023-03-01 18:28:55 - */ - import { Animation, Component, ParticleSystem, _decorator, sp } from 'cc'; import { EffectEvent } from './EffectEvent'; import { message } from '../../core/common/event/MessageManager'; diff --git a/assets/libs/animator-effect/EffectSingleCase.ts b/assets/libs/animator-effect/EffectSingleCase.ts index 8780098..e91a666 100644 --- a/assets/libs/animator-effect/EffectSingleCase.ts +++ b/assets/libs/animator-effect/EffectSingleCase.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-10-12 14:00:43 - * @LastEditors: dgflash - * @LastEditTime: 2023-03-06 14:40:34 - */ import type { Node, Vec3 } from 'cc'; import { Animation, NodePool, ParticleSystem, Prefab, sp } from 'cc'; import { message } from '../../core/common/event/MessageManager'; @@ -29,6 +23,7 @@ export interface IEffectParams { * 1、支持Spine动画 * 2、支持Cocos Animation动画 * 3、支持Cocos ParticleSystem粒子动画 + * @deprecated 已废弃,建议用 GameNodePool.ts */ export class EffectSingleCase { private static _instance: EffectSingleCase; diff --git a/assets/libs/animator-move/MoveRigidBody.ts b/assets/libs/animator-move/MoveRigidBody.ts index 61591cc..217d15d 100644 --- a/assets/libs/animator-move/MoveRigidBody.ts +++ b/assets/libs/animator-move/MoveRigidBody.ts @@ -15,10 +15,10 @@ const ROTATION_ANGLE_THRESHOLD = 10; @ccclass('MoveRigidBody') export class MoveRigidBody extends Component { @property({ tooltip: '阻尼' }) - damping = 0.5; + damping = 0.5; @property({ tooltip: '重力' }) - gravity = -10; + gravity = -10; @property private _speed = 5; diff --git a/assets/libs/animator-move/MoveTo.ts b/assets/libs/animator-move/MoveTo.ts index f4e7812..bfcef61 100644 --- a/assets/libs/animator-move/MoveTo.ts +++ b/assets/libs/animator-move/MoveTo.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-03-25 18:12:10 - * @LastEditors: dgflash - * @LastEditTime: 2023-01-19 14:59:50 - */ import { Component, error, Node, Vec3, _decorator } from 'cc'; import { Timer } from '../../core/common/timer/Timer'; diff --git a/assets/libs/animator-move/MoveTranslate.ts b/assets/libs/animator-move/MoveTranslate.ts index 0821501..58ae59e 100644 --- a/assets/libs/animator-move/MoveTranslate.ts +++ b/assets/libs/animator-move/MoveTranslate.ts @@ -1,14 +1,7 @@ - -/* - * @Author: dgflash - * @Date: 2022-03-25 18:12:10 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-25 11:52:23 - */ import { Component, Node, Vec3, _decorator } from 'cc'; import { Vec3Util } from '../../core/utils/Vec3Util'; -const { ccclass, property } = _decorator; +const { ccclass } = _decorator; /** 角色坐标方式移动 */ @ccclass('MoveTranslate') diff --git a/assets/libs/animator/AnimatorSkeletal.ts b/assets/libs/animator/AnimatorSkeletal.ts index 94cdc91..4f7605a 100644 --- a/assets/libs/animator/AnimatorSkeletal.ts +++ b/assets/libs/animator/AnimatorSkeletal.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-06-30 13:56:26 - * @LastEditors: dgflash - * @LastEditTime: 2021-11-04 10:46:00 - */ - import { AnimationClip, CCFloat, game, SkeletalAnimation, _decorator } from 'cc'; import AnimatorAnimation from './AnimatorAnimation'; diff --git a/assets/libs/behavior-tree/BTreeNode.ts b/assets/libs/behavior-tree/BTreeNode.ts index 854b716..550e47d 100644 --- a/assets/libs/behavior-tree/BTreeNode.ts +++ b/assets/libs/behavior-tree/BTreeNode.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:04:44 - */ import type { BTNodeJson } from './BTNodeJson'; import type { IControl } from './IControl'; diff --git a/assets/libs/behavior-tree/BranchNode.ts b/assets/libs/behavior-tree/BranchNode.ts index 2a78a5a..3b1ffb8 100644 --- a/assets/libs/behavior-tree/BranchNode.ts +++ b/assets/libs/behavior-tree/BranchNode.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 13:58:32 - */ import type { BTNodeJson } from './BTNodeJson'; import { BTreeNode } from './BTreeNode'; diff --git a/assets/libs/behavior-tree/Decorator.ts b/assets/libs/behavior-tree/Decorator.ts index f6f41f7..bee220c 100644 --- a/assets/libs/behavior-tree/Decorator.ts +++ b/assets/libs/behavior-tree/Decorator.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:05:02 - */ import { BehaviorTree } from './BehaviorTree'; import type { BTNodeJson } from './BTNodeJson'; import { BTreeNode } from './BTreeNode'; diff --git a/assets/libs/behavior-tree/IControl.ts b/assets/libs/behavior-tree/IControl.ts index 8621a3b..14a9c3f 100644 --- a/assets/libs/behavior-tree/IControl.ts +++ b/assets/libs/behavior-tree/IControl.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:04:27 - */ import type { BTreeNode } from './BTreeNode'; /** 行为控制接口 */ diff --git a/assets/libs/behavior-tree/Priority.ts b/assets/libs/behavior-tree/Priority.ts index 69dae58..e11f37c 100644 --- a/assets/libs/behavior-tree/Priority.ts +++ b/assets/libs/behavior-tree/Priority.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:08:10 - */ import { BranchNode } from './BranchNode'; /** 优先选择节点:首个成功的子节点即返回成功,全部失败则返回失败 */ diff --git a/assets/libs/behavior-tree/Selector.ts b/assets/libs/behavior-tree/Selector.ts index a930f44..6555bdf 100644 --- a/assets/libs/behavior-tree/Selector.ts +++ b/assets/libs/behavior-tree/Selector.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:05:40 - */ import { BranchNode } from './BranchNode'; /** diff --git a/assets/libs/behavior-tree/Sequence.ts b/assets/libs/behavior-tree/Sequence.ts index 01b2ed7..f9b29eb 100644 --- a/assets/libs/behavior-tree/Sequence.ts +++ b/assets/libs/behavior-tree/Sequence.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 14:05:22 - */ import { BranchNode } from './BranchNode'; /** diff --git a/assets/libs/behavior-tree/Task.ts b/assets/libs/behavior-tree/Task.ts index 9f686a9..d74cbed 100644 --- a/assets/libs/behavior-tree/Task.ts +++ b/assets/libs/behavior-tree/Task.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-06-21 12:05:14 - * @LastEditors: dgflash - * @LastEditTime: 2022-07-20 11:43:20 - */ import { BTreeNode } from './BTreeNode'; /** diff --git a/assets/libs/collection/Collection.ts b/assets/libs/collection/Collection.ts index 7c7412c..04f3387 100644 --- a/assets/libs/collection/Collection.ts +++ b/assets/libs/collection/Collection.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-07-22 15:54:51 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-22 14:47:59 - */ - /** 支持Map与Array功能的集合对象 */ export class Collection extends Map { private _array: V[] = []; diff --git a/assets/libs/ecs/ECSComp.ts b/assets/libs/ecs/ECSComp.ts index fa09d5e..618478b 100644 --- a/assets/libs/ecs/ECSComp.ts +++ b/assets/libs/ecs/ECSComp.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-05 14:03:54 - */ import type { ecs } from './ECS'; import type { ECSEntity } from './ECSEntity'; diff --git a/assets/libs/ecs/ECSGroup.ts b/assets/libs/ecs/ECSGroup.ts index 1e10a45..013c96e 100644 --- a/assets/libs/ecs/ECSGroup.ts +++ b/assets/libs/ecs/ECSGroup.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-05 14:21:54 - */ import type { ecs } from './ECS'; import type { ECSEntity } from './ECSEntity'; diff --git a/assets/libs/ecs/ECSMask.ts b/assets/libs/ecs/ECSMask.ts index 15850ff..785b7e7 100644 --- a/assets/libs/ecs/ECSMask.ts +++ b/assets/libs/ecs/ECSMask.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-05-12 14:18:44 - * @LastEditors: dgflash - * @LastEditTime: 2022-05-24 11:09:49 - */ import { ECSModel } from './ECSModel'; /** diff --git a/assets/libs/ecs/ECSModel.ts b/assets/libs/ecs/ECSModel.ts index fb21a77..59ba2b0 100644 --- a/assets/libs/ecs/ECSModel.ts +++ b/assets/libs/ecs/ECSModel.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-05-12 14:18:44 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-05 16:37:10 - */ import type { ecs } from './ECS'; import type { ECSEntity } from './ECSEntity'; import { ECSGroup } from './ECSGroup'; diff --git a/assets/libs/extension/ArrayExt.ts b/assets/libs/extension/ArrayExt.ts index ac8315c..1ec41e3 100644 --- a/assets/libs/extension/ArrayExt.ts +++ b/assets/libs/extension/ArrayExt.ts @@ -254,7 +254,7 @@ declare global { }, orderBy: { value: function () { - const mappers = []; + const mappers : any[] = []; for (let _i = 0; _i < arguments.length; _i++) { mappers[_i] = arguments[_i]; } @@ -275,7 +275,7 @@ declare global { }, orderByDesc: { value: function () { - const mappers = []; + const mappers : any[] = []; for (let _i = 0; _i < arguments.length; _i++) { mappers[_i] = arguments[_i]; } diff --git a/assets/libs/gui/button/ButtonEffect.ts b/assets/libs/gui/button/ButtonEffect.ts index ecc7b86..b99fdb0 100644 --- a/assets/libs/gui/button/ButtonEffect.ts +++ b/assets/libs/gui/button/ButtonEffect.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2023-01-30 14:00:41 - * @LastEditors: dgflash - * @LastEditTime: 2023-02-09 10:54:28 - */ import type { EventTouch } from 'cc'; import { Animation, AnimationClip, Node, Sprite, _decorator } from 'cc'; import { oops } from '../../../core/Oops'; @@ -11,9 +5,12 @@ import ButtonSimple from './ButtonSimple'; const { ccclass, property, menu } = _decorator; -/** 有特效按钮 */ +/** + * 有特效按钮 + * @deprecated 该组件已废弃,请使用 UIButton 替代,废弃 button_scale_start,button_scale_end 动画 + */ @ccclass('ButtonEffect') -@menu('OopsFramework/Button/ButtonEffect (有特效按钮)') +@menu('OopsFramework/Button/ButtonEffect (有特效按钮)[已废弃]') export default class ButtonEffect extends ButtonSimple { @property({ tooltip: '是否开启' diff --git a/assets/libs/gui/button/ButtonInterceptor.ts b/assets/libs/gui/button/ButtonInterceptor.ts new file mode 100644 index 0000000..66a3aca --- /dev/null +++ b/assets/libs/gui/button/ButtonInterceptor.ts @@ -0,0 +1,187 @@ +import { AudioClip, Button, Component, EventHandler, Node } from 'cc'; +import { oops } from 'db://oops-framework/core/Oops'; +import ButtonSimple from './ButtonSimple'; + +/** + * 按钮音效配置 + */ +export interface IButtonSoundConfig { + /** 按钮类构造函数,作为唯一标识 */ + class: typeof Component; + /** 音效资源 */ + clip: AudioClip; +} + +/** + * 按钮点击事件劫持器 + * 用于拦截所有按钮点击事件,根据按钮类型播放不同音效 + */ +export class ButtonInterceptor { + private _isActive = false; + /** 按钮类型配置映射,key 为组件类 */ + private _buttonConfigs: Map = new Map(); + /** 保存 EventHandler 原始 emitEvents 方法 */ + private _originalEmitEvents: Function | null = null; + + private static _instance: ButtonInterceptor | null = null; + /** 获取单例实例 */ + static get instance(): ButtonInterceptor { + if (!this._instance) { + this._instance = new ButtonInterceptor(); + } + return this._instance; + } + + /** 是否已激活 */ + get isActive(): boolean { + return this._isActive; + } + + /** ButtonSimple 原始 onClick 方法映射 */ + private _originalOnClickMap: Map = new Map(); + + /** + * 注册按钮音效配置 + * @param config 按钮音效配置 + */ + registerSound(config: IButtonSoundConfig): void { + this._buttonConfigs.set(config.class, config); + } + + /** + * 激活劫持器 + * 开始拦截所有按钮点击事件 + */ + activate(): void { + if (this._isActive) return; + this._isActive = true; + + // 劫持 EventHandler 的 emitEvents 方法 + this.hijackEmitEvents(); + + // 劫持 ButtonSimple 的 onClick 方法 + this.hijackButtonSimple(); + } + + /** + * 停用劫持器 + * 恢复所有按钮的原始点击事件 + */ + deactivate(): void { + if (!this._isActive) return; + this._isActive = false; + + // 恢复 EventHandler 原始方法 + if (this._originalEmitEvents) { + // @ts-ignore + EventHandler.emitEvents = this._originalEmitEvents; + this._originalEmitEvents = null; + } + + // 恢复所有 ButtonSimple 的原始 onClick 方法 + this.restoreButtonSimple(); + } + + /** + * 劫持 ButtonSimple 的 onClick 方法 + * 在触发点击时播放音效 + */ + private hijackButtonSimple(): void { + const self = this; + // @ts-ignore + const originalOnClick = ButtonSimple.prototype.onClick; + + // 保存原始方法 + // @ts-ignore + ButtonSimple.prototype._originalOnClick = originalOnClick; + + // 重写 onClick 方法 + // @ts-ignore + ButtonSimple.prototype.onClick = function (this: ButtonSimple) { + // 保存当前实例的原始方法引用 + if (!self._originalOnClickMap.has(this)) { + self._originalOnClickMap.set(this, originalOnClick); + } + + // 播放音效 + const config = self.getButtonConfig(this.node); + if (config) { + self.playButtonSound(config); + } + + // 调用原始方法 + return originalOnClick.apply(this); + }; + } + + /** + * 恢复 ButtonSimple 的原始 onClick 方法 + */ + private restoreButtonSimple(): void { + // @ts-ignore + if (ButtonSimple.prototype._originalOnClick) { + // @ts-ignore + ButtonSimple.prototype.onClick = ButtonSimple.prototype._originalOnClick; + // @ts-ignore + ButtonSimple.prototype._originalOnClick = null; + } + this._originalOnClickMap.clear(); + } + + /** + * 劫持 EventHandler 的 emitEvents 方法 + * 在触发点击事件时播放音效 + */ + private hijackEmitEvents(): void { + const self = this; + + // 保存原始方法 + // @ts-ignore + this._originalEmitEvents = EventHandler.emitEvents; + + EventHandler.emitEvents = function (...args: any[]) { + // 调用原始方法 + const result = self._originalEmitEvents!.apply(this, args); + + // 检查是否是 touch-end 事件 + const event = args?.[1]; + if (event?.type === 'touch-end') { + // 获取按钮节点 + const target = event?.currentTarget as Node; + if (target) { + const config = self.getButtonConfig(target); + if (config) { + self.playButtonSound(config); + } + } + } + + return result; + }; + } + + /** + * 通过节点获取按钮配置 + * @param node 节点 + * @returns 按钮音效配置,未找到返回 null + */ + private getButtonConfig(node: Node): IButtonSoundConfig | null { + // 遍历所有注册的配置,检查节点上是否有对应的组件 + for (const [classType, config] of this._buttonConfigs) { + if (node.getComponent(classType)) { + return config; + } + } + return null; + } + + /** + * 播放按钮音效 + * @param config 按钮音效配置 + */ + private playButtonSound(config: IButtonSoundConfig): void { + if (typeof oops !== 'undefined' && oops.audio) { + oops.audio.playEffect(config.clip); + } + } +} diff --git a/assets/libs/gui/button/ButtonInterceptor.ts.meta b/assets/libs/gui/button/ButtonInterceptor.ts.meta new file mode 100644 index 0000000..25dd416 --- /dev/null +++ b/assets/libs/gui/button/ButtonInterceptor.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "2d8b38ba-88ad-4e5f-8ec2-85c73dbc13f8", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/libs/gui/button/ButtonSimple.ts b/assets/libs/gui/button/ButtonSimple.ts index ccb0643..8358227 100644 --- a/assets/libs/gui/button/ButtonSimple.ts +++ b/assets/libs/gui/button/ButtonSimple.ts @@ -1,6 +1,5 @@ import type { EventTouch } from 'cc'; -import { AudioClip, Component, Node, _decorator, game } from 'cc'; -import { oops } from '../../../core/Oops'; +import { Component, Node, _decorator, game } from 'cc'; const { ccclass, property, menu } = _decorator; @@ -18,23 +17,11 @@ export default class ButtonSimple extends Component { }) private interval = 500; - @property({ - tooltip: '触摸音效', - type: AudioClip - }) - private effect: AudioClip = null!; - /** 触摸次数计数 */ private touchCount = 0; /** 上次触摸结束时间 */ private touchEndTime = 0; - private static effectPath: string = null!; - /** 批量设置触摸音效 */ - static setBatchEffect(path: string) { - this.effectPath = path; - } - onLoad() { this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); @@ -56,28 +43,17 @@ export default class ButtonSimple extends Component { } else { this.touchEndTime = game.totalTime; - - // 短按触摸音效 - this.playEffect(); + this.onClick(); } } - /** 短按触摸音效 */ - protected playEffect() { - if (ButtonSimple.effectPath) { - oops.audio.playEffect(ButtonSimple.effectPath); - } - else if (this.effect) { - oops.audio.playEffect(this.effect); - } + protected onClick(){ + } /** 组件销毁时的清理工作 */ onDestroy() { this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); - - // 清理音效引用 - this.effect = null!; } } diff --git a/assets/libs/gui/button/ButtonTouchLong.ts b/assets/libs/gui/button/ButtonTouchLong.ts index 4a33c29..7f187a9 100644 --- a/assets/libs/gui/button/ButtonTouchLong.ts +++ b/assets/libs/gui/button/ButtonTouchLong.ts @@ -1,18 +1,15 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2022-04-14 18:15:42 - */ import type { EventTouch } from 'cc'; import { EventHandler, _decorator } from 'cc'; import ButtonEffect from './ButtonEffect'; const { ccclass, property, menu } = _decorator; -/** 长按按钮 */ +/** + * 长按按钮 + * @deprecated 该组件已废弃,请使用 UIButton 替代 + */ @ccclass('ButtonTouchLong') -@menu('OopsFramework/Button/ButtonTouchLong (长按按钮)') +@menu('OopsFramework/Button/ButtonTouchLong (长按按钮)[已废弃]') export class ButtonTouchLong extends ButtonEffect { @property({ tooltip: '长按时间(秒)' @@ -76,9 +73,6 @@ export class ButtonTouchLong extends ButtonEffect { event.emit([event.customEventData]); }); - // 长按触摸音效(只播放一次) - this.playEffect(); - this.removeTouchLong(); } } diff --git a/assets/libs/gui/button/UIButton.ts b/assets/libs/gui/button/UIButton.ts index bb2c359..2b22dae 100644 --- a/assets/libs/gui/button/UIButton.ts +++ b/assets/libs/gui/button/UIButton.ts @@ -1,19 +1,21 @@ import type { EventTouch } from 'cc'; -import { AudioClip, Button, EventHandler, _decorator, game } from 'cc'; -import { oops } from '../../../core/Oops'; +import { Button, Component, EventHandler, _decorator, game } from 'cc'; -const { ccclass, property, menu } = _decorator; +const { ccclass, property, menu, requireComponent } = _decorator; /** * 通用按钮 * 1、防连点 - * 2、按钮点击触发音效 + * 2、支持只触发一次 + * + * 注意:此组件需要配合 Button 组件使用,会自动添加 Button 组件 */ @ccclass('UIButton') @menu('OopsFramework/Button/UIButton (通用按钮)') -export default class UIButton extends Button { +@requireComponent(Button) +export default class UIButton extends Component { @property({ - tooltip: '每次触发间隔' + tooltip: '每次触发间隔(毫秒)' }) private interval = 500; @@ -22,31 +24,43 @@ export default class UIButton extends Button { }) private once = false; - @property({ - tooltip: '触摸音效', - type: AudioClip - }) - private effect: AudioClip = null!; - /** 触摸次数 */ private _touchCount = 0; /** 触摸结束时间 */ private _touchEndTime = 0; + /** 按钮组件引用 */ + private _button: Button | null = null; + /** 原始触摸结束回调 */ + private _originalTouchEnded: Function | null = null; - private static effectPath: string = null!; - /** 批量设置触摸音效 */ - static setBatchEffect(path: string) { - this.effectPath = path; - } - - /** 触摸结束 */ - protected _onTouchEnded(event: EventTouch) { - if (!this._interactable || !this.enabledInHierarchy) { + onLoad() { + this._button = this.getComponent(Button); + if (!this._button) { + console.warn('[UIButton] 未找到 Button 组件,请确保节点上有 Button 组件'); return; } - //@ts-ignore - if (this._pressed) { + // 保存原始回调并劫持 + // @ts-ignore + this._originalTouchEnded = this._button._onTouchEnded; + // @ts-ignore + this._button._onTouchEnded = this._onTouchEnded.bind(this); + } + + /** + * 触摸结束事件处理 + * @param event 触摸事件 + */ + private _onTouchEnded(event: EventTouch) { + if (!this._button) return; + + // @ts-ignore + if (!this._button._interactable || !this._button.enabledInHierarchy) { + return; + } + + // @ts-ignore + if (this._button._pressed) { // 是否只触发一次 if (this.once) { if (this._touchCount > 0) { @@ -62,37 +76,28 @@ export default class UIButton extends Button { } else { this._touchEndTime = game.totalTime; - EventHandler.emitEvents(this.clickEvents, event); - this.node.emit(Button.EventType.CLICK, this); - - // 短按触摸音效 - this.playEffect(); + EventHandler.emitEvents(this._button.clickEvents, event); + this.node.emit(Button.EventType.CLICK, this._button); } } - //@ts-ignore - this._pressed = false; - this._updateState(); + // @ts-ignore + this._button._pressed = false; + // @ts-ignore + this._button._updateState(); if (event) { event.propagationStopped = true; } } - /** 短按触摸音效 */ - protected playEffect() { - if (UIButton.effectPath) { - oops.audio.playEffect(UIButton.effectPath); - } - else if (this.effect) { - oops.audio.playEffect(this.effect); - } - } - - /** 组件销毁时的清理工作 */ onDestroy() { - // 清理音效引用 - this.effect = null!; - super.onDestroy(); + // 恢复原始回调 + if (this._button && this._originalTouchEnded) { + // @ts-ignore + this._button._onTouchEnded = this._originalTouchEnded; + } + this._button = null; + this._originalTouchEnded = null; } } diff --git a/assets/libs/gui/label/LabelChange.ts b/assets/libs/gui/label/LabelChange.ts index 9cf9f6e..4d0a080 100644 --- a/assets/libs/gui/label/LabelChange.ts +++ b/assets/libs/gui/label/LabelChange.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-11 10:00:51 - */ import { _decorator } from 'cc'; import LabelNumber from './LabelNumber'; diff --git a/assets/libs/gui/label/LabelNumber.ts b/assets/libs/gui/label/LabelNumber.ts index 7c22c9d..08ed59e 100644 --- a/assets/libs/gui/label/LabelNumber.ts +++ b/assets/libs/gui/label/LabelNumber.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-11 10:27:04 - */ import { Label, _decorator } from 'cc'; const { ccclass, property, menu } = _decorator; diff --git a/assets/libs/gui/language/LanguageData.ts b/assets/libs/gui/language/LanguageData.ts index ec8464e..344c6b7 100644 --- a/assets/libs/gui/language/LanguageData.ts +++ b/assets/libs/gui/language/LanguageData.ts @@ -1,12 +1,5 @@ import type { TTFFont } from 'cc'; -/* - * @Author: dgflash - * @Date: 2022-02-11 09:31:52 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-22 16:37:40 - */ - /** 框架支持的语言数据类型 */ export enum LanguageDataType { /** Json格式配置 */ diff --git a/assets/libs/gui/language/LanguagePack.ts b/assets/libs/gui/language/LanguagePack.ts index 371e8e9..aba7336 100644 --- a/assets/libs/gui/language/LanguagePack.ts +++ b/assets/libs/gui/language/LanguagePack.ts @@ -1,13 +1,6 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2023-08-22 16:34:28 - */ import { director, error, JsonAsset, TTFFont } from 'cc'; import { resLoader } from '../../../core/common/loader/ResLoader'; import { Logger } from '../../../core/common/log/Logger'; -import { JsonUtil } from '../../../core/utils/JsonUtil'; import { LanguageData, LanguageDataType, LanguageType } from './LanguageData'; export class LanguagePack { diff --git a/assets/libs/gui/language/LanguageSpine.ts b/assets/libs/gui/language/LanguageSpine.ts index cd1a1ec..dd429c5 100644 --- a/assets/libs/gui/language/LanguageSpine.ts +++ b/assets/libs/gui/language/LanguageSpine.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2023-07-25 10:44:38 - * @LastEditors: dgflash - * @LastEditTime: 2023-07-25 11:48:52 - */ import { CCString, Component, _decorator, sp } from 'cc'; import { EDITOR } from 'cc/env'; import { resLoader } from '../../../core/common/loader/ResLoader'; diff --git a/assets/libs/gui/language/LanguageSprite.ts b/assets/libs/gui/language/LanguageSprite.ts index 4c13822..15125a5 100644 --- a/assets/libs/gui/language/LanguageSprite.ts +++ b/assets/libs/gui/language/LanguageSprite.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-11-24 15:51:01 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-02 10:04:57 - */ -import type { Size } from 'cc'; import { CCString, Component, Sprite, SpriteFrame, UITransform, _decorator } from 'cc'; import { EDITOR } from 'cc/env'; import { resLoader } from '../../../core/common/loader/ResLoader'; diff --git a/assets/libs/gui/window/PromptBase.ts b/assets/libs/gui/window/PromptBase.ts index 476d19d..2ccd0c7 100644 --- a/assets/libs/gui/window/PromptBase.ts +++ b/assets/libs/gui/window/PromptBase.ts @@ -95,7 +95,7 @@ export class PromptBase extends GameComponent { } protected onLoad(): void { - this.button.setButton(); + this.button.bind(); } /** 确认按钮点击事件 */ diff --git a/assets/libs/model-view/JsonOb.ts b/assets/libs/model-view/JsonOb.ts index f174320..c8cec99 100644 --- a/assets/libs/model-view/JsonOb.ts +++ b/assets/libs/model-view/JsonOb.ts @@ -1,9 +1,4 @@ /* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2024-03-08 10:00:00 - * * JsonOb 性能优化版本(默认实现) * * 优化特性: diff --git a/assets/libs/network/HttpRequest.ts b/assets/libs/network/HttpRequest.ts index 20342f7..8dd7da5 100644 --- a/assets/libs/network/HttpRequest.ts +++ b/assets/libs/network/HttpRequest.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-09 18:10:50 - */ import { error, warn } from 'cc'; /** diff --git a/assets/libs/network/NetInterface.ts b/assets/libs/network/NetInterface.ts index 2f2523b..4e743d3 100644 --- a/assets/libs/network/NetInterface.ts +++ b/assets/libs/network/NetInterface.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-09 18:31:18 - */ - /* * 网络相关接口定义 */ diff --git a/assets/libs/network/NetManager.ts b/assets/libs/network/NetManager.ts index b9636b2..303b031 100644 --- a/assets/libs/network/NetManager.ts +++ b/assets/libs/network/NetManager.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-09-01 18:00:28 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-09 18:10:50 - */ import type { CallbackObject, IRequestProtocol, NetData } from './NetInterface'; import type { NetConnectOptions, NetNode } from './NetNode'; diff --git a/assets/libs/network/NetProtocolPako.ts b/assets/libs/network/NetProtocolPako.ts index 4dde123..9615fb1 100644 --- a/assets/libs/network/NetProtocolPako.ts +++ b/assets/libs/network/NetProtocolPako.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-21 13:45:51 - * @LastEditors: dgflash - * @LastEditTime: 2022-04-21 13:51:33 - */ import type { IProtocolHelper, IRequestProtocol, IResponseProtocol, NetData } from './NetInterface'; const unzip = function (str: string) { diff --git a/assets/libs/network/WebSock.ts b/assets/libs/network/WebSock.ts index af61596..a407f70 100644 --- a/assets/libs/network/WebSock.ts +++ b/assets/libs/network/WebSock.ts @@ -1,10 +1,5 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-09 17:42:19 - */ import { Logger } from '../../core/common/log/Logger'; +import { oops } from '../../core/Oops'; import type { ISocket, MessageFunc, NetData } from './NetInterface'; type Connected = (event: any) => void; @@ -31,7 +26,7 @@ export class WebSock implements ISocket { connect(options: any) { if (this._ws) { if (this._ws.readyState === WebSocket.CONNECTING) { - Logger.logNet('websocket connecting, wait for a moment...'); + oops.log.logNet('websocket connecting, wait for a moment...'); return false; } } diff --git a/assets/module/.DS_Store b/assets/module/.DS_Store new file mode 100644 index 0000000..37cf278 Binary files /dev/null and b/assets/module/.DS_Store differ diff --git a/assets/module/common/CCBusiness.ts b/assets/module/common/CCBusiness.ts index 9652e11..ecc7811 100644 --- a/assets/module/common/CCBusiness.ts +++ b/assets/module/common/CCBusiness.ts @@ -1,30 +1,14 @@ -/* - * @Author: dgflash - * @Date: 2025-09-18 10:20:51 - * @LastEditors: dgflash - * @LastEditTime: 2025-09-18 17:20:51 - */ - -import { EventDispatcher } from '../../core/common/event/EventDispatcher'; import type { ListenerFunc, ListenerFuncTyped } from '../../core/common/event/EventMessage'; +import { GamePartEvent } from './part/GamePartEvent'; +import { GamePartRegistry, GamePartKey, createPart } from './GamePartRegistry'; import type { CCEntity } from './CCEntity'; /** 业务逻辑 */ export class CCBusiness { - private _destroyed: boolean = false; - - /** 当前业务逻辑是否有效(未销毁) */ - get isValid(): boolean { - return !this._destroyed; - } - private _ent: T | null = null; /** 所属实体引用 */ get ent(): T { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试访问已销毁的业务逻辑的实体引用'); - } return this._ent!; } @@ -32,42 +16,35 @@ export class CCBusiness { this._ent = value; } + private _parts: GamePartRegistry | null = null; + + /** 获取模块注册表(懒加载) */ + private get parts(): GamePartRegistry { + return (this._parts ??= createPart(this)); + } + + /** 获取事件模块 */ + get event(): GamePartEvent { + return this.parts.get(GamePartKey.Event); + } + /** 业务逻辑初始化(由 CCEntity.addBusiness 自动调用) */ protected init() { } destroy() { - if (this._destroyed) { - console.warn('[OopsFramework]', '业务逻辑已销毁,无需重复销毁'); - return; - } - - this._destroyed = true; - - // 释放消息对象 - if (this._event) { - this._event.clear(); - this._event = null; + // 销毁所有模块 + if (this._parts) { + this._parts.destroy(); + this._parts = null; } // 清空实体引用,避免循环引用导致的内存泄漏 this._ent = null; } - //#region 全局事件管理 - - private _event: EventDispatcher | null = null; - /** 全局事件管理器 */ - private get event(): EventDispatcher { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试访问已销毁的业务逻辑的事件管理器'); - } - if (this._event == null) this._event = new EventDispatcher(); - return this._event; - } - - //#region 强类型事件方法 + //#region ========== 兼容旧版本 API 如果是新项目可以把注释包起来的代码都删除 ========== /** * 注册全局事件(强类型) @@ -76,11 +53,7 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象 */ watch(event: K, listener: ListenerFuncTyped, object: any): void { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上注册事件'); - return; - } - this.event.on(event as string, listener as ListenerFunc, object); + this.event.watch(event, listener, object); } /** @@ -90,11 +63,7 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象 */ watchOnce(event: K, listener: ListenerFuncTyped, object: any): void { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上注册一次性事件'); - return; - } - this.event.once(event as string, listener as ListenerFunc, object); + this.event.watchOnce(event, listener, object); } /** @@ -104,8 +73,7 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象(可选) */ unwatch(event: K, listener?: ListenerFuncTyped, object?: any): void { - if (this._destroyed) return; - this.event.off(event as string, listener as ListenerFunc, object); + this.event.unwatch(event, listener, object); } /** @@ -114,10 +82,6 @@ export class CCBusiness { * @param data 事件数据 */ emit(event: K, data?: OopsFramework.TypedEventMap[K]): void { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上触发事件'); - return; - } this.event.emit(event, data); } @@ -127,17 +91,9 @@ export class CCBusiness { * @param data 事件数据(必须完全匹配类型定义) */ emitAsync(event: K, data: OopsFramework.TypedEventMap[K]): Promise { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上触发异步事件'); - return Promise.resolve(); - } return this.event.emitAsync(event, data); } - //#endregion - - //#region 弱类型事件方法 - /** * 注册全局事件 * @param event 事件名 @@ -145,10 +101,6 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象 */ on(event: string, listener: ListenerFunc, object: object) { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上注册事件'); - return; - } this.event.on(event, listener, object); } @@ -159,10 +111,6 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象 */ once(event: string, listener: ListenerFunc, object: object) { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上注册一次性事件'); - return; - } this.event.once(event, listener, object); } @@ -173,7 +121,6 @@ export class CCBusiness { * @param object 侦听函数绑定的this对象(可选) */ off(event: string, listener?: ListenerFunc, object?: object) { - if (this._destroyed) return; this.event.off(event, listener, object); } @@ -183,10 +130,6 @@ export class CCBusiness { * @param args 事件参数 */ dispatchEvent(event: string, ...args: unknown[]) { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上触发事件'); - return; - } this.event.dispatchEvent(event, ...args); } @@ -196,10 +139,6 @@ export class CCBusiness { * @param args 事件参数 */ dispatchEventAsync(event: string, ...args: unknown[]): Promise { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上触发异步事件'); - return Promise.resolve(); - } return this.event.dispatchEventAsync(event, ...args); } @@ -212,22 +151,8 @@ export class CCBusiness { * onGlobal(event: string, args: unknown) { console.log(args) }; */ protected setEvent(...args: string[]) { - if (this._destroyed) { - console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上批量设置事件'); - return; - } - for (const name of args) { - const func = (this as Record)[name]; - if (typeof func === 'function') { - this.on(name, func as ListenerFunc, this); - } - else { - console.error('[OopsFramework]', `名为【${name}】的全局事方法不存在`); - } - } + this.event.setEvent(...args); } //#endregion - - //#endregion } diff --git a/assets/module/common/CCEntity.ts b/assets/module/common/CCEntity.ts index 2542f4c..dfe2038 100644 --- a/assets/module/common/CCEntity.ts +++ b/assets/module/common/CCEntity.ts @@ -106,30 +106,36 @@ export abstract class CCEntity extends ecs.Entity { node = result; + const comp = node.getComponent(ctor); + if (comp) this.add(comp as unknown as ecs.Comp); + // 检查实体是否已销毁 if ( !this.isValid) { console.warn('[OopsFramework]', `实体已销毁,取消添加预制体组件: ${(ctor as any).name}`); + // 移除已添加的 ECS 组件 + if (comp) this.remove(ctor as unknown as CompType); node.destroy(); return null; } - const comp = node.getComponent(ctor); - if (comp) this.add(comp as unknown as ecs.Comp); node.parent = parent.node; } // 手动内存管理 else { node = await ViewUtil.createPrefabNodeAsync(path, bundleName); + const comp = node.getComponent(ctor); + if (comp) this.add(comp as unknown as ecs.Comp); + // 检查实体是否已销毁 if (!this.isValid) { console.warn('[OopsFramework]', `实体已销毁,取消添加预制体组件: ${(ctor as any).name}`); + // 移除已添加的 ECS 组件 + if (comp) this.remove(ctor as unknown as CompType); node.destroy(); return null; } - const comp = node.getComponent(ctor); - if (comp) this.add(comp as unknown as ecs.Comp); node.parent = parent; } @@ -175,16 +181,18 @@ export abstract class CCEntity extends ecs.Entity { const node = await oops.gui.open(key, params); + const comp = node.getComponent(ctor) as unknown as ecs.Comp; + if (comp) this.add(comp); + // 检查实体是否已销毁 if (!this.isValid) { console.warn('[OopsFramework]', `实体已销毁,取消添加界面组件: ${key}`); + // 移除已添加的 ECS 组件 + if (comp) this.remove(ctor as unknown as CompType); oops.gui.remove(key); return null; } - const comp = node.getComponent(ctor) as unknown as ecs.Comp; - if (comp) this.add(comp); - oops.gui.show(key); return node; } @@ -288,7 +296,7 @@ export abstract class CCEntity extends ecs.Entity { this.businesss.delete(cls); // 清理实体上的业务逻辑组件引用 - Reflect.set(this, cls.name, null); + delete (this as any)[cls.name]; } } } @@ -299,6 +307,7 @@ export abstract class CCEntity extends ecs.Entity { if (this.singletons) { this.singletons.forEach((entity) => { if (entity && typeof entity.destroy === 'function') { + this.removeChild(entity); entity.destroy(); } }); @@ -308,7 +317,11 @@ export abstract class CCEntity extends ecs.Entity { // 2. 再销毁所有业务组件 if (this.businesss) { - this.businesss.forEach((business) => business.destroy()); + this.businesss.forEach((business, cls) => { + business.destroy(); + // 清理实体上的业务逻辑组件引用 + delete (this as any)[cls.name]; + }); this.businesss.clear(); this.businesss = null!; } diff --git a/assets/module/common/CCView.ts b/assets/module/common/CCView.ts index d3cdb33..7c5af10 100644 --- a/assets/module/common/CCView.ts +++ b/assets/module/common/CCView.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-11-11 19:05:32 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-06 17:20:51 - */ - import type { ecs } from '../../libs/ecs/ECS'; import { ECSModel } from '../../libs/ecs/ECSModel'; import { VM } from '../../libs/model-view/ViewModel'; @@ -94,7 +87,7 @@ export abstract class CCView extends GameComponent implement const data = this.data; if (data === undefined || data === null) { - console.warn('[OopsFramework]', `${this.constructor.name}: mvvm=true 但 data 未定义,VM 绑定已跳过`); + console.warn('[OopsFramework]', `${this.constructor.name}: mvvm = true 但 data 未定义,VM 绑定已跳过`); return; } @@ -195,13 +188,13 @@ export abstract class CCView extends GameComponent implement const tid = this.tid; if (tid < 0) { - console.error('[OopsFramework]', `组件 ${this.name} 移除失败,组件未注册 (tid=${tid})`); + console.error('[OopsFramework]', `组件 ${this.name} 移除失败,组件未注册 (tid = ${tid})`); return; } const cct = ECSModel.compCtors[tid]; if (!cct) { - console.error('[OopsFramework]', `组件 ${this.name} 移除失败,组件构造函数不存在 (tid=${tid})`); + console.error('[OopsFramework]', `组件 ${this.name} 移除失败,组件构造函数不存在 (tid = ${tid})`); return; } diff --git a/assets/module/common/CCViewVM.ts b/assets/module/common/CCViewVM.ts index 35d4c9d..124ecc8 100644 --- a/assets/module/common/CCViewVM.ts +++ b/assets/module/common/CCViewVM.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-11-11 19:05:32 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-06 17:20:51 - */ - import type { CCEntity } from './CCEntity'; import { CCView } from './CCView'; diff --git a/assets/module/common/GameComponent.ts b/assets/module/common/GameComponent.ts index 17bc80a..152aec9 100644 --- a/assets/module/common/GameComponent.ts +++ b/assets/module/common/GameComponent.ts @@ -1,25 +1,19 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2022-12-13 11:36:00 - */ 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 type { ListenerFunc, ListenerFuncTyped } 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 { 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'; +import type { GamePartAudio } from './part/GamePartAudio'; +import type { GamePartButton } from './part/GamePartButton'; +import type { GamePartNodePool } from './part/GamePartNodePool'; +import type { GamePartEvent } from './part/GamePartEvent'; +import type { GamePartKeyboard } from './part/GamePartKeyboard'; +import type { GamePartNode } from './part/GamePartNode'; +import type { GamePartRes } from './part/GamePartRes'; +import { createPart, GamePartRegistry, GamePartKey } from './GamePartRegistry'; const { ccclass } = _decorator; @@ -47,40 +41,45 @@ const { ccclass } = _decorator; */ @ccclass('GameComponent') export class GameComponent extends Component { - private _viewRegistry: GameViewModuleRegistry | null = null; + private _parts: GamePartRegistry | null = null; - private get viewRegistry(): GameViewModuleRegistry { - return (this._viewRegistry ??= new GameViewModuleRegistry(this)); + private get parts(): GamePartRegistry { + return (this._parts ??= createPart(this)); } /** 获取事件模块 */ - get event(): GameEventModule { - return this.viewRegistry.get(ViewModuleKey.Event); + get event(): GamePartEvent { + return this.parts.get(GamePartKey.Event); } /** 获取节点模块 */ - get nodes(): GameNodeModule { - return this.viewRegistry.get(ViewModuleKey.Nodes); + get nodes(): GamePartNode { + return this.parts.get(GamePartKey.Nodes); } /** 获取资源模块 */ - get res(): GameResModule { - return this.viewRegistry.get(ViewModuleKey.Res); + get res(): GamePartRes { + return this.parts.get(GamePartKey.Res); } /** 获取音频模块 */ - get audio(): GameAudioModule { - return this.viewRegistry.get(ViewModuleKey.Audio); + get audio(): GamePartAudio { + return this.parts.get(GamePartKey.Audio); } /** 获取按钮模块 */ - get button(): GameButtonModule { - return this.viewRegistry.get(ViewModuleKey.Button); + get button(): GamePartButton { + return this.parts.get(GamePartKey.Button); } /** 获取键盘模块 */ - get keyboard(): GameKeyboardModule { - return this.viewRegistry.get(ViewModuleKey.Keyboard); + get keyboard(): GamePartKeyboard { + return this.parts.get(GamePartKey.Keyboard); + } + + /** 游戏节点池模块 */ + get pool(): GamePartNodePool { + return this.parts.get(GamePartKey.Pool); } /** 移除当前节点 */ @@ -90,20 +89,10 @@ export class GameComponent extends Component { /** 组件销毁时调用 */ protected onDestroy() { - this._viewRegistry?.destroy(); + this._parts?.destroy(); } - /** 打印全局资源状态 */ - static printGlobalResStatus() { - resAutoTracker.printStatus(); - } - - /** 设置资源调试模式 */ - static setResDebugMode(enabled: boolean) { - resAutoTracker.enableDebug(enabled); - } - - //#region ========== 兼容旧版本 API ========== + //#region ========== 兼容旧版本 API 如果是新项目可以把注释包起来的代码都删除 ========== //#region 全局事件管理(兼容旧版本) /** @deprecated 请使用 this.event.watch() */ @@ -160,7 +149,7 @@ export class GameComponent extends Component { //#region 预制节点管理(兼容旧版本) /** @deprecated 请使用 this.nodes.getNode() */ getNode(name: string): Node | undefined { - return this.nodes.getNode(name); + return this.nodes.get(name); } /** @deprecated 请使用 this.nodes.nodeTreeInfoLite() */ @@ -177,7 +166,7 @@ export class GameComponent extends Component { //#region 资源加载管理(兼容旧版本) /** @deprecated 请使用 this.res.getRes() */ getRes(path: string, type?: __private.__types_globals__Constructor | null, bundleName?: string): T | null { - return this.res.getRes(path, type, bundleName); + return this.res.get(path, type, bundleName); } /** @deprecated 请使用 this.res.load() */ @@ -216,12 +205,12 @@ export class GameComponent extends Component { //#endregion //#region 音频播放管理(兼容旧版本) - /** @deprecated 请使用 this.audio.playMusic() */ - playMusic(url: string, params?: IAudioParams): void { - this.audio.playMusic(url, params); + /** @deprecated 请使用 await this.audio.playMusic() */ + async playMusic(url: string, params?: IAudioParams): Promise { + return this.audio.playMusic(url, params); } - /** @deprecated 请使用 this.audio.playEffect() */ + /** @deprecated 请使用 await this.audio.playEffect() */ playEffect(url: string, params?: IAudioParams): Promise { return this.audio.playEffect(url, params); } @@ -230,7 +219,7 @@ export class GameComponent extends Component { //#region 游戏逻辑事件(兼容旧版本) /** @deprecated 请使用 this.button.setButton() */ protected setButton(bindRootEvent = true): void { - this.button.setButton(bindRootEvent); + this.button.bind(bindRootEvent); } /** @deprecated 请使用 this.event.setEvent() */ diff --git a/assets/module/common/GamePartBase.ts b/assets/module/common/GamePartBase.ts new file mode 100644 index 0000000..3592cfb --- /dev/null +++ b/assets/module/common/GamePartBase.ts @@ -0,0 +1,10 @@ +/** GameComponent 子模块基类(可用于任意宿主对象) */ +export abstract class GamePartBase { + /** 构造函数 + * @param comp 宿主对象(如 GameComponent 或 CCEntity) + */ + constructor(protected readonly comp: object) {} + + /** 组件销毁时回调,子类按需覆盖 */ + destroy(): void {} +} diff --git a/assets/module/common/GamePartBase.ts.meta b/assets/module/common/GamePartBase.ts.meta new file mode 100644 index 0000000..41b08fd --- /dev/null +++ b/assets/module/common/GamePartBase.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "59760005-af28-46f1-9fe5-f0db224c0f82", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/GamePartRegistry.ts b/assets/module/common/GamePartRegistry.ts new file mode 100644 index 0000000..93b5488 --- /dev/null +++ b/assets/module/common/GamePartRegistry.ts @@ -0,0 +1,100 @@ +import { GamePartAudio } from './part/GamePartAudio'; +import { GamePartButton } from './part/GamePartButton'; +import { GamePartNodePool } from './part/GamePartNodePool'; +import { GamePartEvent } from './part/GamePartEvent'; +import { GamePartKeyboard } from './part/GamePartKeyboard'; +import { GamePartNode } from './part/GamePartNode'; +import { GamePartRes } from './part/GamePartRes'; +import { GamePartBase } from './GamePartBase'; + +/** + * 子模块注册键 + * @remarks 枚举顺序即销毁顺序(先输入/音频/资源/特效,最后事件) + */ +export enum GamePartKey { + /** 按钮 */ + Button = 'button', + /** 键盘 */ + Keyboard = 'keyboard', + /** 音频 */ + Audio = 'audio', + /** 资源 */ + Res = 'res', + /** 节点池 */ + Pool = 'pool', + /** 节点树 */ + Nodes = 'nodes', + /** 全局事件 */ + Event = 'event', +} + +/** 模块创建器类型 */ +type ModuleCreator = (comp: object) => GamePartBase; + +/** 子模块懒加载注册表(统一登记、按序批量销毁) */ +export class GamePartRegistry { + private instances: Map | null = null; + private creators: Map; + + /** 构造函数 + * @param comp 宿主对象 + * @param creators 模块创建器映射表 + */ + constructor( + private readonly comp: object, + creators: Map + ) { + this.creators = creators; + } + + /** 获取实例映射表(延迟创建) */ + private getInstances(): Map { + if (!this.instances) { + this.instances = new Map(); + } + return this.instances; + } + + /** 获取模块实例 + * @param key 模块键 + * @returns 模块实例 + */ + get(key: GamePartKey): M { + const instances = this.getInstances(); + let module = instances.get(key) as M | undefined; + if (!module) { + const creator = this.creators.get(key); + if (!creator) { + throw new Error(`未找到模块创建器: ${key}`); + } + module = creator(this.comp) as M; + instances.set(key, module); + } + return module; + } + + /** 销毁所有模块 */ + destroy(): void { + if (this.instances) { + for (const key of Object.values(GamePartKey)) { + this.instances.get(key)?.destroy(); + } + this.instances.clear(); + this.instances = null; + } + } +} + +const GAME_PART = new Map([ + [GamePartKey.Button, (comp) => new GamePartButton(comp)], + [GamePartKey.Keyboard, (comp) => new GamePartKeyboard(comp)], + [GamePartKey.Audio, (comp) => new GamePartAudio(comp)], + [GamePartKey.Res, (comp) => new GamePartRes(comp)], + [GamePartKey.Pool, (comp) => new GamePartNodePool(comp)], + [GamePartKey.Nodes, (comp) => new GamePartNode(comp)], + [GamePartKey.Event, (comp) => new GamePartEvent(comp)], +]); + +export function createPart(comp: object): GamePartRegistry { + return new GamePartRegistry(comp, GAME_PART); +} diff --git a/assets/module/common/GamePartRegistry.ts.meta b/assets/module/common/GamePartRegistry.ts.meta new file mode 100644 index 0000000..e90c5c8 --- /dev/null +++ b/assets/module/common/GamePartRegistry.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "f0d4afee-df23-475e-871c-72a80c535ede", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/part.meta b/assets/module/common/part.meta new file mode 100644 index 0000000..4f93f32 --- /dev/null +++ b/assets/module/common/part.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "cc86c2b1-3471-46ff-878d-b9efc9feee0f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameAudioModule.ts.meta b/assets/module/common/part/GameAudioModule.ts.meta similarity index 100% rename from assets/module/common/view/GameAudioModule.ts.meta rename to assets/module/common/part/GameAudioModule.ts.meta diff --git a/assets/module/common/view/GameButtonModule.ts.meta b/assets/module/common/part/GameButtonModule.ts.meta similarity index 100% rename from assets/module/common/view/GameButtonModule.ts.meta rename to assets/module/common/part/GameButtonModule.ts.meta diff --git a/assets/module/common/view/GameEventModule.ts.meta b/assets/module/common/part/GameEventModule.ts.meta similarity index 100% rename from assets/module/common/view/GameEventModule.ts.meta rename to assets/module/common/part/GameEventModule.ts.meta diff --git a/assets/module/common/view/GameKeyboardModule.ts.meta b/assets/module/common/part/GameKeyboardModule.ts.meta similarity index 100% rename from assets/module/common/view/GameKeyboardModule.ts.meta rename to assets/module/common/part/GameKeyboardModule.ts.meta diff --git a/assets/module/common/view/GameNodeModule.ts.meta b/assets/module/common/part/GameNodeModule.ts.meta similarity index 100% rename from assets/module/common/view/GameNodeModule.ts.meta rename to assets/module/common/part/GameNodeModule.ts.meta diff --git a/assets/module/common/part/GamePartAudio.ts b/assets/module/common/part/GamePartAudio.ts new file mode 100644 index 0000000..faa891a --- /dev/null +++ b/assets/module/common/part/GamePartAudio.ts @@ -0,0 +1,128 @@ +import { AudioClip } from 'cc'; +import type { GameComponent } from '../GameComponent'; +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 { GamePartBase } from '../GamePartBase'; + +/** 音频播放选项 */ +export interface IAudioPlayOptions extends IAudioParams { + /** 资源包名 */ + bundle?: string; + /** 是否为远程资源 */ + isRemote?: boolean; +} + +/** 音频播放 + * 资源内存由 ResAutoTracker 自动管理,界面销毁时自动释放 + */ +export class GamePartAudio extends GamePartBase { + /** 宿主组件 */ + protected declare comp: GameComponent; + + /** 当前播放的背景音乐资源 */ + private currentMusic: AudioClip | null = null; + + /** 检查音乐功能是否启用 */ + private isMusicEnabled(): boolean { + return oops.audio?.music?.getSwitch?.() ?? true; + } + + /** 检查音效功能是否启用 */ + private isEffectEnabled(): boolean { + return oops.audio?.effect?.getSwitch?.() ?? true; + } + + /** 播放背景音乐 + * @param url 音频资源路径或URL + * @param options 音频播放选项 + */ + async playMusic(url: string, options?: IAudioPlayOptions): Promise { + // 音乐功能被禁用时直接返回 + if (!this.isMusicEnabled()) return; + + const clip = await this.loadAudioClip(url, options); + + if (!clip) { + console.warn(`背景音乐资源加载失败: ${url}`); + return; + } + + // 使用 this.comp.isValid 检查组件是否有效 + if (!this.comp.isValid) { + // 界面已销毁,不播放 + return; + } + + // 再次检查音乐功能是否启用(异步加载期间可能被禁用) + if (!this.isMusicEnabled()) return; + + // 记录当前背景音乐 + this.currentMusic = clip; + + // 使用已加载的 AudioClip 播放 + oops.audio.playMusic(clip, options); + } + + /** 播放音效 + * @param url 音频资源路径或URL + * @param options 音频播放选项 + * @returns 音效对象 + */ + async playEffect(url: string, options?: IAudioPlayOptions): Promise { + // 音效功能被禁用时直接返回 + if (!this.isEffectEnabled()) return null; + + const clip = await this.loadAudioClip(url, options); + + if (!clip) { + console.warn(`音效资源加载失败: ${url}`); + return null; + } + + // 使用 this.comp.isValid 检查组件是否有效 + if (!this.comp.isValid) { + // 界面已销毁,不播放 + return null; + } + + // 再次检查音效功能是否启用(异步加载期间可能被禁用) + if (!this.isEffectEnabled()) return null; + + // 使用已加载的 AudioClip 播放 + const ae = oops.audio.playEffect(clip, options); + return ae; + } + + /** + * 组件销毁时停止音乐并清理 + * 资源释放由 ResAutoTracker 自动处理 + */ + destroy(): void { + // 停止当前背景音乐 + if (this.currentMusic) { + oops.audio.music.stop(); + this.currentMusic = null; + } + } + + /** + * 加载音频资源 + * @param url 资源路径或URL + * @param options 音频播放选项 + * @returns 音频资源 + */ + private async loadAudioClip(url: string, options?: IAudioPlayOptions): Promise { + const isRemote = options?.isRemote ?? false; + const bundle = options?.bundle ?? resLoader.defaultBundleName; + + if (isRemote) { + // 加载远程资源(ResAutoTracker 自动管理) + return await this.comp.res.loadRemote(url); + } else { + // 加载本地资源(ResAutoTracker 自动管理) + return await this.comp.res.load(bundle, url, AudioClip); + } + } +} diff --git a/assets/module/common/part/GamePartAudio.ts.meta b/assets/module/common/part/GamePartAudio.ts.meta new file mode 100644 index 0000000..df3d4d6 --- /dev/null +++ b/assets/module/common/part/GamePartAudio.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "adfbddd7-4af6-4b88-8da9-20c924249f08", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameButtonModule.ts b/assets/module/common/part/GamePartButton.ts similarity index 84% rename from assets/module/common/view/GameButtonModule.ts rename to assets/module/common/part/GamePartButton.ts index 6c3a35a..b7372e3 100644 --- a/assets/module/common/view/GameButtonModule.ts +++ b/assets/module/common/part/GamePartButton.ts @@ -1,18 +1,17 @@ -/* - * @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'; +import type { GameComponent } from '../GameComponent'; +import { GamePartBase } from '../GamePartBase'; /** 界面按钮批量绑定 */ -export class GameButtonModule extends GameViewModule { +export class GamePartButton extends GamePartBase { + /** 宿主组件 */ + protected declare comp: GameComponent; + /** 设置按钮事件绑定 * @param bindRootEvent 是否绑定根节点事件,默认为 true */ - setButton(bindRootEvent = true): void { + bind(bindRootEvent = true): void { if (bindRootEvent) { this.comp.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => { const self: any = this.comp; diff --git a/assets/module/common/part/GamePartButton.ts.meta b/assets/module/common/part/GamePartButton.ts.meta new file mode 100644 index 0000000..1499036 --- /dev/null +++ b/assets/module/common/part/GamePartButton.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "1443b95a-646b-46d6-a15c-286b7f586b81", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameEventModule.ts b/assets/module/common/part/GamePartEvent.ts similarity index 96% rename from assets/module/common/view/GameEventModule.ts rename to assets/module/common/part/GamePartEvent.ts index 1ccdbc6..06fd069 100644 --- a/assets/module/common/view/GameEventModule.ts +++ b/assets/module/common/part/GamePartEvent.ts @@ -1,14 +1,9 @@ -/* - * @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'; +import { GamePartBase } from '../GamePartBase'; /** 全局事件管理(含游戏前后台、画布、全屏、旋转等生命周期) */ -export class GameEventModule extends GameViewModule { +export class GamePartEvent extends GamePartBase { private _event: EventDispatcher | null = null; /** 获取事件分发器 */ @@ -178,4 +173,4 @@ export class GameEventModule extends GameViewModule { this._event = null; } } -} \ No newline at end of file +} diff --git a/assets/module/common/part/GamePartEvent.ts.meta b/assets/module/common/part/GamePartEvent.ts.meta new file mode 100644 index 0000000..f57279a --- /dev/null +++ b/assets/module/common/part/GamePartEvent.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "33227a0d-d0a5-4c97-970d-b082c713cd1e", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameKeyboardModule.ts b/assets/module/common/part/GamePartKeyboard.ts similarity index 92% rename from assets/module/common/view/GameKeyboardModule.ts rename to assets/module/common/part/GamePartKeyboard.ts index 1d1b920..a92582c 100644 --- a/assets/module/common/view/GameKeyboardModule.ts +++ b/assets/module/common/part/GamePartKeyboard.ts @@ -1,11 +1,6 @@ -/* - * @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'; +import { GamePartBase } from '../GamePartBase'; /** 键盘事件回调 */ export interface KeyboardCallbacks { @@ -15,7 +10,7 @@ export interface KeyboardCallbacks { } /** 键盘输入 */ -export class GameKeyboardModule extends GameViewModule { +export class GamePartKeyboard extends GamePartBase { private _enabled = false; private _callbacks: KeyboardCallbacks | null = null; diff --git a/assets/module/common/part/GamePartKeyboard.ts.meta b/assets/module/common/part/GamePartKeyboard.ts.meta new file mode 100644 index 0000000..0a903fa --- /dev/null +++ b/assets/module/common/part/GamePartKeyboard.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "fdfe6071-4b80-4bef-aa7a-80c6807f3153", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/part/GamePartNode.ts b/assets/module/common/part/GamePartNode.ts new file mode 100644 index 0000000..a994bc5 --- /dev/null +++ b/assets/module/common/part/GamePartNode.ts @@ -0,0 +1,99 @@ +import { instantiate, Node, Prefab, Vec3 } from 'cc'; +import type { GameComponent } from '../GameComponent'; +import { ViewUtil } from '../../../core/utils/ViewUtil'; +import { GamePartBase } from '../GamePartBase'; +import { MoveTo } from '../../../libs/animator-move/MoveTo'; +import { resLoader } from 'db://oops-framework/core/common/loader/ResLoader'; + +/** 预制节点与节点树管理 */ +export class GamePartNode extends GamePartBase { + /** 宿主组件 */ + protected declare comp: GameComponent; + + /** 摊平的节点集合(所有节点不能重名)- 延迟初始化 */ + private _nodes: Map | null = null; + + /** 获取节点集合 */ + get nodes(): Map { + if (!this._nodes) { + this._nodes = new Map(); + } + return this._nodes; + } + + /** 获取节点 + * @param name 节点名称 + * @returns 节点对象 + */ + get(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 = resLoader.defaultBundleName): Promise { + const prefab = await this.comp.res.load(bundleName, path, Prefab); + if (!prefab) { + console.warn('[OopsFramework]', `预制体加载失败: ${path}`); + return null; + } + return instantiate(prefab); + } + + /** + * 移动节点到指定目标位置 + * @param node 要移动的节点 + * @param target 目标位置(Vec3)或目标节点(Node) + * @param speed 移动速度(每秒移动的像素距离) + * @param options 可选参数配置 + * @returns MoveTo组件实例 + */ + moveTo( + node: Node, + target: Vec3 | Node, + speed: number, + options?: { + hasYAxis?: boolean; + offset?: number; + offsetVector?: Vec3; + onStart?: () => void; + onComplete?: () => void; + onChange?: () => void; + } + ): MoveTo | null { + let moveTo = node.getComponent(MoveTo); + if (!moveTo) { + moveTo = node.addComponent(MoveTo); + } + + moveTo.target = target; + moveTo.speed = speed; + + if (options) { + if (options.hasYAxis !== undefined) moveTo.hasYAxis = options.hasYAxis; + if (options.offset !== undefined) moveTo.offset = options.offset; + if (options.offsetVector !== undefined) moveTo.offsetVector = options.offsetVector; + if (options.onStart !== undefined) moveTo.onStart = options.onStart; + if (options.onComplete !== undefined) moveTo.onComplete = options.onComplete; + if (options.onChange !== undefined) moveTo.onChange = options.onChange; + } + + moveTo.move(); + return moveTo; + } + + /** 销毁节点模块 */ + override destroy(): void { + this._nodes?.clear(); + this._nodes = null; + } +} \ No newline at end of file diff --git a/assets/module/common/part/GamePartNode.ts.meta b/assets/module/common/part/GamePartNode.ts.meta new file mode 100644 index 0000000..1edd551 --- /dev/null +++ b/assets/module/common/part/GamePartNode.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "ce34048b-ae58-40a6-8564-17a916eaa2f9", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/part/GamePartNodePool.ts b/assets/module/common/part/GamePartNodePool.ts new file mode 100644 index 0000000..b4ff43c --- /dev/null +++ b/assets/module/common/part/GamePartNodePool.ts @@ -0,0 +1,375 @@ +import type { Node, Vec3 } from 'cc'; +import { Animation, ParticleSystem, Prefab, sp } from 'cc'; +import type { GameComponent } from '../GameComponent'; +import { GameNodePool } from '../../../core/common/pool/GameNodePool'; +import { resLoader } from '../../../core/common/loader/ResLoader'; +import { GamePartBase } from '../GamePartBase'; + +/** + * 自动释放接口 + * 实现此接口的组件可以自动播放并在播放完成后自动回收到对象池 + * + * 使用场景: + * 1、Spine 动画组件 + * 2、Cocos Animation 动画组件 + * 3、ParticleSystem 粒子组件 + * 4、自定义动画组件 + * + * 实现示例: + * ```typescript + * class MyAnimation extends Component implements IAutoRelease { + * onAutoRelease(callback: () => void): void { + * // 监听动画完成事件 + * this.animation.once(Animation.EventType.FINISHED, callback); + * } + * + * play(): void { + * // 开始播放动画 + * this.animation.play(); + * } + * } + * ``` + */ +export interface IAutoRelease { + /** + * 设置自动释放回调 + * 当动画播放完成时调用 callback,对象池会自动回收节点 + * @param callback 释放回调函数 + */ + onAutoRelease(callback: () => void): void; + + /** + * 播放动画 + * 对象池获取节点后自动调用此方法播放动画 + */ + play(): void; +} + +/** 特效参数 */ +export interface IEffectParams { + /** 初始空间坐标 */ + pos?: Vec3, + /** 初始世界坐标 */ + worldPos?: Vec3, + /** 是否播放完成后删除 */ + isPlayFinishedRelease?: boolean, + /** 资源包名 */ + bundle?: string, +} + +/** + * 游戏节点池模块 + * 基于 GameNodePool 统一管理对象池,本模块负责资源加载与自动释放 + * 1、支持Spine动画 + * 2、支持Cocos Animation动画 + * 3、支持Cocos ParticleSystem粒子动画 + * 4、资源由 GameResModule 管理,自动释放 + */ +export class GamePartNodePool extends GamePartBase { + /** 宿主组件 */ + protected declare comp: GameComponent; + + /** 本模块加载的 Prefab 资源记录,用于自动释放 */ + private _loadedPrefabs: Set = new Set(); + /** 全局动画播放速度 */ + private _speed = 1; + + /** + * 获取全局动画播放速度 + */ + get speed(): number { + return this._speed; + } + + /** + * 设置全局动画播放速度 + */ + set speed(value: number) { + this._speed = value; + } + + /** + * 获取指定资源池中对象数量 + * @param path 预制体资源路径 + * @param bundle 资源包名,默认为 resources + */ + getCount(path: string, bundle?: string): number { + const bundleName = bundle ?? resLoader.defaultBundleName; + const prefab = this.comp.res.get(path, Prefab, bundleName); + if (!prefab) { + return 0; + } + return GameNodePool.instance.getCount(prefab); + } + + /** + * 池中预加载显示对象 + * @param count 预加载数量 + * @param path 预制资源路径 + * @param params 特效参数(包含 bundleName 等) + */ + async preload(count: number, path: string, params?: IEffectParams): Promise { + const bundleName = params?.bundle ?? resLoader.defaultBundleName; + + // 使用 GameResModule 加载资源,自动管理引用计数 + const prefab = await this.comp.res.load(bundleName, path, Prefab); + + // 记录已加载的资源 + this._loadedPrefabs.add(prefab); + + // 使用 GameNodePool 预加载到对象池 + GameNodePool.instance.preload(count, prefab); + } + + /** + * 显示预制对象(需确保资源已加载) + * @param path 预制体资源路径 + * @param parent 父节点 + * @param params 特效参数(包含 pos、worldPos、isPlayFinishedRelease、bundle 等) + */ + show(path: string, parent?: Node, params?: IEffectParams): Node { + const bundleName = params?.bundle ?? resLoader.defaultBundleName; + + // 获取已加载的预制资源 + const prefab = this.comp.res.get(path, Prefab, bundleName); + if (!prefab) { + console.warn(`[GamePartNodePool] 预制资源未加载: ${bundleName}/${path}`); + return null!; + } + + // 记录已加载的资源 + this._loadedPrefabs.add(prefab); + + // 使用 GameNodePool 获取节点 + const node = GameNodePool.instance.get(prefab, parent); + + // 应用特效参数 + this._applyEffectParams(node, params); + + return node; + } + + /** + * 回收对象 + * @param node 节点 + */ + put(node: Node) { + GameNodePool.instance.put(node); + } + + /** + * 清除对象池数据(只清除本模块管理的) + * @param path 预制体资源路径,为空时清除本模块管理的所有对象池数据 + * @param bundle 资源包名,默认为 resources + */ + clear(path?: string, bundle?: string) { + if (path) { + // 只清除本模块管理的指定对象池 + const bundleName = bundle ?? resLoader.defaultBundleName; + const prefab = this.comp.res.get(path, Prefab, bundleName); + if (prefab && this._loadedPrefabs.has(prefab)) { + GameNodePool.instance.clear(prefab); + } + } + else { + // 只清除本模块管理的所有对象池 + this._loadedPrefabs.forEach((p) => { + GameNodePool.instance.clear(p); + }); + } + } + + /** + * 释放对象池中显示对象的资源内存(只释放本模块管理的) + * @param path 预制体资源路径,为空时释放所有本模块管理的资源 + * @param bundle 资源包名,默认为 resources + */ + release(path?: string, bundle?: string) { + if (path) { + // 只释放本模块管理的指定资源 + const bundleName = bundle ?? resLoader.defaultBundleName; + const prefab = this.comp.res.get(path, Prefab, bundleName); + if (prefab && this._loadedPrefabs.has(prefab)) { + // 清除对象池 + GameNodePool.instance.clear(prefab); + // 释放资源 + this.comp.res.releaseRes(prefab.uuid); + this._loadedPrefabs.delete(prefab); + } + } + else { + // 释放本模块加载的所有资源 + this._loadedPrefabs.forEach((p) => { + GameNodePool.instance.clear(p); + this.comp.res.releaseRes(p.uuid); + }); + this._loadedPrefabs.clear(); + } + } + + /** 销毁特效模块 */ + override destroy(): void { + // 释放本模块管理的所有资源 + this.release(); + } + + /** + * 应用特效参数 + * @param node 节点 + * @param params 特效参数 + */ + private _applyEffectParams(node: Node, params?: IEffectParams) { + if (!params) return; + + // 设置位置 + if (params.pos) node.position = params.pos; + if (params.worldPos) node.worldPosition = params.worldPos; + + // 播放完成后自动回收 + if (params.isPlayFinishedRelease) { + // 监听动画完成事件,自动回收 + this.setupAutoRelease(node); + } + + // 设置动画速度并播放 + this._setSpeed(node); + this._playAnimation(node); + } + + /** + * 设置动画速度 + * @param node 节点 + */ + private _setSpeed(node: Node) { + // Spine动画 + const spine = node.getComponent(sp.Skeleton); + if (spine) { + spine.timeScale = this._speed; + return; + } + + // Cocos动画 + const anims = node.getComponentsInChildren(Animation); + if (anims.length > 0) { + anims.forEach((animator) => { + const aniName = animator.defaultClip?.name; + if (aniName) { + const aniState = animator.getState(aniName); + if (aniState) { + aniState.speed = this._speed; + } + } + }); + return; + } + + // 粒子动画 + const particles = node.getComponentsInChildren(ParticleSystem); + particles.forEach((particle) => { + particle.simulationSpeed = this._speed; + }); + } + + /** + * 播放动画 + * @param node 节点 + */ + private _playAnimation(node: Node) { + // Spine动画 + const spine = node.getComponent(sp.Skeleton); + if (spine) { + // @ts-ignore + const animationName = spine.defaultAnimation ?? spine.animation; + if (animationName) { + spine.setAnimation(0, animationName, false); + } + return; + } + + // Cocos Animation动画 + const anim = node.getComponent(Animation); + if (anim && anim.defaultClip) { + anim.play(); + return; + } + + // 粒子动画 + const particles = node.getComponentsInChildren(ParticleSystem); + if (particles.length > 0) { + particles.forEach((particle) => { + particle.play(); + }); + } + } + + /** + * 设置自动回收 + * 优先使用 IAutoRelease 接口,其次使用内置动画检测 + * 每个节点只设置一次事件监听,避免重复设置 + * @param node 节点 + */ + private setupAutoRelease(node: Node) { + // 检查是否已经设置过自动回收 + // @ts-ignore + if (node._autoRelease) return; + // @ts-ignore + node._autoRelease = true; + + // 优先检查是否实现了 IAutoRelease 接口 + const components = node.components; + for (const comp of components) { + if (this.isAutoRelease(comp)) { + comp.onAutoRelease(() => this.put(node)); + comp.play(); + return; + } + } + + // 内置动画类型检测 + this.setupBuiltinAutoRelease(node); + } + + /** + * 判断组件是否实现 IAutoRelease 接口 + * @param component 组件 + * @returns 是否实现接口 + */ + private isAutoRelease(component: any): component is IAutoRelease { + return component && typeof component.onAutoRelease === 'function'; + } + + /** + * 设置内置动画类型的自动回收 + * 每个节点只调用一次,通过 _autoRelease 标记控制 + * @param node 节点 + */ + private setupBuiltinAutoRelease(node: Node) { + // Spine动画 + const spine = node.getComponent(sp.Skeleton); + if (spine) { + spine.setCompleteListener(() => { + this.put(node); + }); + return; + } + + // Cocos Animation动画 + const anim = node.getComponent(Animation); + if (anim) { + anim.once(Animation.EventType.FINISHED, () => { + this.put(node); + }); + return; + } + + // 粒子动画 + const particle = node.getComponent(ParticleSystem); + if (particle) { + // 粒子没有完成事件,使用持续时间估算 + const duration = particle.duration; + setTimeout(() => { + this.put(node); + }, duration * 1000); + } + } +} diff --git a/assets/module/common/part/GamePartNodePool.ts.meta b/assets/module/common/part/GamePartNodePool.ts.meta new file mode 100644 index 0000000..397d189 --- /dev/null +++ b/assets/module/common/part/GamePartNodePool.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8bdddd97-d4f3-43c4-a384-606902c61d50", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameResModule.ts b/assets/module/common/part/GamePartRes.ts similarity index 72% rename from assets/module/common/view/GameResModule.ts rename to assets/module/common/part/GamePartRes.ts index 97c18a5..9bc182d 100644 --- a/assets/module/common/view/GameResModule.ts +++ b/assets/module/common/part/GamePartRes.ts @@ -1,27 +1,25 @@ -/* - * @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 { SpriteFrame, isValid } from 'cc'; +import type { GameComponent } from '../GameComponent'; +import type { AssetType, CompleteCallback, IRemoteOptions, 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'; +import { GamePartBase } from '../GamePartBase'; import { DEBUG } from 'cc/env'; /** 资源加载与引用计数管理 */ -export class GameResModule extends GameViewModule { +export class GamePartRes extends GamePartBase { + /** 宿主组件 */ + protected declare comp: GameComponent; + /** 获取资源 * @param path 资源路径 * @param type 资源类型 * @param bundleName 资源包名称 * @returns 资源对象 */ - getRes(path: string, type?: __private.__types_globals__Constructor | null, bundleName?: string): T | null { - return oops.res.get(path, type, bundleName); + get(path: string, type?: __private.__types_globals__Constructor | null, bundleName?: string): T | null { + return resLoader.get(path, type, bundleName); } /** 加载资源 @@ -31,7 +29,7 @@ export class GameResModule extends GameViewModule { * @returns 资源对象 */ async load(bundleName: string, paths: Paths | AssetType, type?: AssetType): Promise { - const result = await oops.res.load(bundleName, paths, type); + const result = await resLoader.load(bundleName, paths, type); if (result) { resAutoTracker.acquire(this.comp, result); } @@ -59,7 +57,7 @@ export class GameResModule extends GameViewModule { originalComplete?.(err, data); }; - oops.res.loadAny(bundleName, paths, onProgress, wrappedComplete); + resLoader.loadAny(bundleName, paths, onProgress, wrappedComplete); } /** 加载目录资源 @@ -84,7 +82,7 @@ export class GameResModule extends GameViewModule { originalComplete?.(err, data); }; - oops.res.loadDir(bundleName, dir, type, onProgress, wrappedComplete); + resLoader.loadDir(bundleName, dir, type, onProgress, wrappedComplete); } /** 释放资源 @@ -103,6 +101,26 @@ export class GameResModule extends GameViewModule { } } + /** 加载远程资源 + * @param url 资源URL + * @param options 加载选项 + * @returns 资源对象 + */ + async loadRemote(url: string, options?: IRemoteOptions): Promise { + const result = await resLoader.loadRemote(url, options); + if (result) { + resAutoTracker.acquire(this.comp, result); + } + return result; + } + + /** 释放远程资源 + * @param url 资源URL + */ + releaseRemote(url: string): void { + resLoader.releaseRemote(url); + } + /** 设置精灵图片 * @param target 精灵组件 * @param path 图片路径 diff --git a/assets/module/common/part/GamePartRes.ts.meta b/assets/module/common/part/GamePartRes.ts.meta new file mode 100644 index 0000000..c84394e --- /dev/null +++ b/assets/module/common/part/GamePartRes.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "00b88873-9408-4ab9-bccf-15fde0fca53b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/module/common/view/GameResModule.ts.meta b/assets/module/common/part/GameResModule.ts.meta similarity index 100% rename from assets/module/common/view/GameResModule.ts.meta rename to assets/module/common/part/GameResModule.ts.meta diff --git a/assets/module/common/view/GameViewModuleBase.ts.meta b/assets/module/common/part/GameViewModuleBase.ts.meta similarity index 100% rename from assets/module/common/view/GameViewModuleBase.ts.meta rename to assets/module/common/part/GameViewModuleBase.ts.meta diff --git a/assets/module/common/view/GameViewModuleRegistry.ts.meta b/assets/module/common/part/GameViewModuleRegistry.ts.meta similarity index 100% rename from assets/module/common/view/GameViewModuleRegistry.ts.meta rename to assets/module/common/part/GameViewModuleRegistry.ts.meta diff --git a/assets/module/common/view/GameAudioModule.ts b/assets/module/common/view/GameAudioModule.ts deleted file mode 100644 index 4f7cdbe..0000000 --- a/assets/module/common/view/GameAudioModule.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * @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 { - 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 = []; - } -} \ No newline at end of file diff --git a/assets/module/common/view/GameNodeModule.ts b/assets/module/common/view/GameNodeModule.ts deleted file mode 100644 index 6b3f6f3..0000000 --- a/assets/module/common/view/GameNodeModule.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * @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 = 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 { - 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(); - } -} \ No newline at end of file diff --git a/assets/module/common/view/GameViewModuleBase.ts b/assets/module/common/view/GameViewModuleBase.ts deleted file mode 100644 index 9a216cf..0000000 --- a/assets/module/common/view/GameViewModuleBase.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * @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 {} -} \ No newline at end of file diff --git a/assets/module/common/view/GameViewModuleRegistry.ts b/assets/module/common/view/GameViewModuleRegistry.ts deleted file mode 100644 index 5fd8254..0000000 --- a/assets/module/common/view/GameViewModuleRegistry.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * @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(); - - /** 构造函数 - * @param comp 游戏组件 - */ - constructor(private readonly comp: GameComponent) {} - - /** 获取模块实例 - * @param key 模块键 - * @returns 模块实例 - */ - get(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; - } - } - } -} \ No newline at end of file diff --git a/assets/module/config/BuildTimeConstants.ts b/assets/module/config/BuildTimeConstants.ts index 0358fb1..b21fde5 100644 --- a/assets/module/config/BuildTimeConstants.ts +++ b/assets/module/config/BuildTimeConstants.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2022-08-02 14:25:27 - */ import * as buildTimeConstants from 'cc/env'; const keys = (Object.keys(buildTimeConstants) as (keyof typeof buildTimeConstants)[]).sort(); @@ -20,4 +14,4 @@ export class BuildTimeConstants { console.log(enviroment); } -} +} diff --git a/assets/module/config/Config.ts b/assets/module/config/Config.ts index 6dcec0c..ab05c4a 100644 --- a/assets/module/config/Config.ts +++ b/assets/module/config/Config.ts @@ -1,10 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2022-11-01 15:47:16 - */ - import { BuildTimeConstants } from './BuildTimeConstants'; import type { GameConfig } from './GameConfig'; import type { GameQueryConfig } from './GameQueryConfig'; @@ -19,4 +12,4 @@ export class Config { /** 浏览器查询参数 */ query!: GameQueryConfig; -} +} diff --git a/assets/module/config/GameConfig.ts b/assets/module/config/GameConfig.ts index bdd99d8..bdbcf7a 100644 --- a/assets/module/config/GameConfig.ts +++ b/assets/module/config/GameConfig.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2021-07-03 16:13:17 - * @LastEditors: dgflash - * @LastEditTime: 2023-02-14 14:27:22 - */ import { oops } from '../../core/Oops'; /** 游戏自定义参数分组类型 */ diff --git a/assets/module/config/GameQueryConfig.ts b/assets/module/config/GameQueryConfig.ts index 6fcf6a0..6722c4c 100644 --- a/assets/module/config/GameQueryConfig.ts +++ b/assets/module/config/GameQueryConfig.ts @@ -1,9 +1,3 @@ -/* - * @Author: dgflash - * @Date: 2022-04-14 17:08:01 - * @LastEditors: dgflash - * @LastEditTime: 2022-09-06 17:29:45 - */ import { sys } from 'cc'; import { oops } from '../../core/Oops'; import { StringUtil } from '../../core/utils/StringUtil'; @@ -75,4 +69,4 @@ export class GameQueryConfig { return {}; } } -} +}