feat (core): 完善框架核心功能模块 - 音频、对象池、资源加载与组件系统

新增与优化核心模块
🎯 GUI 交互增强
ButtonInterceptor:按钮点击事件劫持器
劫持 EventHandler.emitEvents / ButtonSimple.onClick 核心方法
支持多类型按钮音效配置注册,按按钮类型自动匹配音效
单例模式管理,支持全局动态激活 / 停用拦截功能
🧩 对象池管理系统
GameNodePool:通用游戏节点对象池管理器
基于预制体(Prefab)UUID 实现池化隔离管理
支持特效、UI 元素等高频对象预加载、复用与回收
全局单例访问,统一管控对象创建 / 销毁逻辑
🔊 音频管理系统
AudioManager:音频核心管理器
独立背景音乐(AudioMusic)+ 音效池(AudioEffectPool)双模块
支持全局音量控制、播放状态恢复与场景切换管理
AudioEffect:基础音效播放器
AudioEnum:音频类型、音效分类枚举定义
📦 资源加载系统
ResLoader:资源加载核心控制器
资源包(Bundle)统一管理 + 并发加载限流控制
远程资源本地缓存、自动释放与内存优化
完整加载进度回调 + 错误异常处理机制
ResTypes:资源类型规范定义
ResErrors:资源加载错误码与异常处理
ResUtils:资源路径、格式转换等工具方法
🧱 模块化组件系统
GameComponent:游戏显示对象组件基类
集成资源自动引用计数(RC)管理
模块化部件(GamePartRegistry)插拔式架构
内置音频、按钮、事件、键盘、节点、对象池、资源七大部件
部件封装:
GamePartAudio:音频功能部件
GamePartButton:按钮交互部件
GamePartEvent:全局事件部件
GamePartKeyboard:键盘输入部件
GamePartNode:节点操作部件
GamePartNodePool:对象池调用部件
GamePartRes:资源加载部件
📝 类型定义完善
新增 IAudio 音频数据与播放参数接口
统一全模块 TS 类型定义与导出规范
优化接口注释与类型约束,提升开发体验
This commit is contained in:
dgflash
2026-05-29 22:55:13 +08:00
parent 0484015e7a
commit d4f140077e
118 changed files with 1452 additions and 1440 deletions

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/core/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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全局变量以方便调试

View File

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

View File

@@ -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<string, Promise<AudioClip>> = new Map();
/** 已加载的 AudioClip 缓存 */
private clipCache: Map<string, AudioClip> = new Map();
/**
* 从三种来源获取 AudioClip
* @param path - AudioClip 实例、远程 URL、或 bundle 内路径
* @param bundle - 资源包名path 为 AudioClip 或 URL 时忽略)
* @returns 加载结果,失败返回 null
*/
async load(
path: string | AudioClip,
bundle?: string
): Promise<ILoadResult | null> {
if (path instanceof AudioClip) {
if (!path.isValid) {
console.warn(`AudioClip 实例已失效`);
return null;
}
// 外部传入的 AudioClip 实例,增加引擎引用计数
path.addRef();
return { clip: path, path: path.uuid, bundle: null, isRemote: false };
}
const cacheKey = this.getCacheKey(path, bundle);
const cached = this.clipCache.get(cacheKey);
if (cached && cached.isValid) {
// 增加引擎引用计数
cached.addRef();
return { clip: cached, path, bundle: bundle || null, isRemote: path.indexOf('http') === 0 };
}
if (path.indexOf('http') === 0) {
return this.loadRemote(path);
}
return this.loadBundle(path, bundle || resLoader.defaultBundleName);
}
/**
* 释放指定路径的音频资源引用
* @param path - 资源路径或 URL
* @param bundle - 资源包名(远程资源时忽略)
*/
release(path: string, bundle?: string): void {
const { key, entry } = this.getCacheEntry(path, bundle);
if (!entry) return;
// 减少引擎引用计数
if (entry.isValid) {
entry.decRef();
}
}
/**
* 立即释放指定路径的音频资源(不等待延迟)
* @param path - 资源路径或 URL
* @param bundle - 资源包名(远程资源时忽略)
*/
releaseImmediately(path: string, bundle?: string): void {
const { key, entry } = this.getCacheEntry(path, bundle);
if (!entry) return;
this.doRelease(key, entry);
}
/**
* 执行真正的资源释放
* @param key - 缓存键值
* @param entry - 缓存条目
*/
private doRelease(key: string, entry: AudioClip): void {
if (entry && entry.isValid) {
entry.decRef();
}
this.clipCache.delete(key);
}
/** 清空所有缓存 */
clearCache(): void {
this.clipCache.forEach((entry, key) => {
this.doRelease(key, entry);
});
this.clipCache.clear();
this.loadingCache.clear();
}
/** 销毁加载器,释放所有资源 */
destroy(): void {
this.clearCache();
}
/**
* 获取缓存统计信息
* @returns 缓存条目数量
*/
getStats(): { total: number } {
return { total: this.clipCache.size };
}
/**
* 获取缓存 key
* @param path - 资源路径
* @param bundle - 资源包名
* @returns 缓存键值
*/
private getCacheKey(path: string, bundle?: string): string {
if (path.indexOf('http') === 0) {
return `remote_${path}`;
}
return `bundle_${bundle || resLoader.defaultBundleName}_${path}`;
}
/**
* 获取缓存条目
* @param path - 资源路径
* @param bundle - 资源包名
* @returns 缓存键值和条目
*/
private getCacheEntry(path: string, bundle?: string): { key: string; entry: AudioClip | undefined } {
const key = this.getCacheKey(path, bundle);
const entry = this.clipCache.get(key);
return { key, entry };
}
/**
* 设置缓存并返回加载结果
* @param clip - 音频资源
* @param path - 资源路径
* @param cacheKey - 缓存键值
* @param bundle - 资源包名
* @param isRemote - 是否为远程资源
* @returns 加载结果
*/
private setCacheAndReturn(
clip: AudioClip,
path: string,
cacheKey: string,
bundle: string | null,
isRemote: boolean
): ILoadResult {
clip.addRef();
this.clipCache.set(cacheKey, clip);
return { clip, path, bundle, isRemote };
}
/**
* 加载远程资源
* @param path - 远程 URL
* @returns 加载结果
*/
private async loadRemote(
path: string
): Promise<ILoadResult | null> {
let loadPromise = this.loadingCache.get(path);
if (!loadPromise) {
loadPromise = this.doLoadRemotePromise(path);
this.loadingCache.set(path, loadPromise);
}
try {
const clip = await loadPromise;
if (!clip || !clip.isValid) {
console.warn(`远程音频资源加载失败: ${path}`);
return null;
}
return this.setCacheAndReturn(clip, path, `remote_${path}`, null, true);
}
catch (e) {
console.warn(`远程音频资源加载异常: ${path}`, e);
return null;
}
finally {
this.loadingCache.delete(path);
}
}
/**
* 执行远程资源加载
* @param path - 远程 URL
* @returns AudioClip 加载 Promise
*/
private async doLoadRemotePromise(path: string): Promise<AudioClip> {
const extension = path.split('.').pop();
return resLoader.loadRemote<AudioClip>(path, { ext: `.${extension}` });
}
/**
* 加载 Bundle 内资源
* @param path - 资源路径
* @param bundle - 资源包名
* @returns 加载结果
*/
private async loadBundle(
path: string,
bundle: string
): Promise<ILoadResult | null> {
const cacheKey = `bundle_${bundle}_${path}`;
let clip = resLoader.get(path, AudioClip, bundle);
if (clip) {
if (!clip.isValid) {
console.warn(`音频资源已失效: ${bundle}/${path}`);
return null;
}
const entry = this.clipCache.get(cacheKey);
if (entry) {
clip.addRef();
return { clip, path, bundle, isRemote: false };
}
return this.setCacheAndReturn(clip, path, cacheKey, bundle, false);
}
let loadPromise = this.loadingCache.get(cacheKey);
if (!loadPromise) {
loadPromise = resLoader.load(bundle, path, AudioClip);
this.loadingCache.set(cacheKey, loadPromise);
}
try {
clip = await loadPromise;
if (!clip || !clip.isValid) {
console.warn(`音频资源加载失败: ${bundle}/${path}`);
return null;
}
return this.setCacheAndReturn(clip, path, cacheKey, bundle, false);
}
catch (e) {
console.warn(`音频资源加载异常: ${bundle}/${path}`, e);
return null;
}
finally {
this.loadingCache.delete(cacheKey);
}
}
}

View File

@@ -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 {
/** 唯一编号 */

View File

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

View File

@@ -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<AudioEffect> {
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!;

View File

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

View File

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

View File

@@ -1,10 +1,3 @@
/*
* @Author: dgflash
* @Date: 2021-07-03 16:13:17
* @LastEditors: dgflash
* @LastEditTime: 2022-09-02 11:03:08
*/
/**
* 全局事件监听方法
* @param event 事件名

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "97818807-d408-4c88-8303-857111cc148c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fcd4f08b-a148-4303-918d-317135283b33",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
/**

View File

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

View File

@@ -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';
/**

View File

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

View File

@@ -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 {
/**

View File

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

View File

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

View File

@@ -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 {
/**

View File

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

View File

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

BIN
assets/libs/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
/** 行为控制接口 */

View File

@@ -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';
/** 优先选择节点:首个成功的子节点即返回成功,全部失败则返回失败 */

View File

@@ -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';
/**

View File

@@ -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';
/**

View File

@@ -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';
/**

View File

@@ -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<K, V> extends Map<K, V> {
private _array: V[] = [];

View File

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

View File

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

View File

@@ -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';
/**

View File

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

View File

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

View File

@@ -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_startbutton_scale_end 动画
*/
@ccclass('ButtonEffect')
@menu('OopsFramework/Button/ButtonEffect (有特效按钮)')
@menu('OopsFramework/Button/ButtonEffect (有特效按钮)[已废弃]')
export default class ButtonEffect extends ButtonSimple {
@property({
tooltip: '是否开启'

View File

@@ -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<typeof Component, IButtonSoundConfig> = 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<ButtonSimple, Function> = 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);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "2d8b38ba-88ad-4e5f-8ec2-85c73dbc13f8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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格式配置 */

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ export class PromptBase extends GameComponent {
}
protected onLoad(): void {
this.button.setButton();
this.button.bind();
}
/** 确认按钮点击事件 */

View File

@@ -1,9 +1,4 @@
/*
* @Author: dgflash
* @Date: 2022-09-01 18:00:28
* @LastEditors: dgflash
* @LastEditTime: 2024-03-08 10:00:00
*
* JsonOb 性能优化版本(默认实现)
*
* 优化特性:

View File

@@ -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';
/**

View File

@@ -1,10 +1,3 @@
/*
* @Author: dgflash
* @Date: 2022-09-01 18:00:28
* @LastEditors: dgflash
* @LastEditTime: 2022-09-09 18:31:18
*/
/*
* 网络相关接口定义
*/

View File

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

View File

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

View File

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

BIN
assets/module/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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<T extends CCEntity> {
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<T extends CCEntity> {
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<T extends CCEntity> {
* @param object 侦听函数绑定的this对象
*/
watch<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<T extends CCEntity> {
* @param object 侦听函数绑定的this对象
*/
watchOnce<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<T extends CCEntity> {
* @param object 侦听函数绑定的this对象可选
*/
unwatch<K extends keyof OopsFramework.TypedEventMap>(event: K, listener?: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, 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<T extends CCEntity> {
* @param data 事件数据
*/
emit<K extends keyof OopsFramework.TypedEventMap>(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<T extends CCEntity> {
* @param data 事件数据(必须完全匹配类型定义)
*/
emitAsync<K extends keyof OopsFramework.TypedEventMap>(event: K, data: OopsFramework.TypedEventMap[K]): Promise<void> {
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<T extends CCEntity> {
* @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<T extends CCEntity> {
* @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<T extends CCEntity> {
* @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<T extends CCEntity> {
* @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<T extends CCEntity> {
* @param args 事件参数
*/
dispatchEventAsync(event: string, ...args: unknown[]): Promise<void> {
if (this._destroyed) {
console.warn('[OopsFramework]', '尝试在已销毁的业务逻辑上触发异步事件');
return Promise.resolve();
}
return this.event.dispatchEventAsync(event, ...args);
}
@@ -212,22 +151,8 @@ export class CCBusiness<T extends CCEntity> {
* 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<string, unknown>)[name];
if (typeof func === 'function') {
this.on(name, func as ListenerFunc, this);
}
else {
console.error('[OopsFramework]', `名为【${name}】的全局事方法不存在`);
}
}
this.event.setEvent(...args);
}
//#endregion
//#endregion
}

View File

@@ -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<ecs.IComp>);
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<ecs.IComp>);
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<ecs.IComp>);
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!;
}

View File

@@ -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<T extends CCEntity> 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<T extends CCEntity> 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;
}

View File

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

View File

@@ -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<T extends Asset>(path: string, type?: __private.__types_globals__Constructor<T> | 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<void> {
return this.audio.playMusic(url, params);
}
/** @deprecated 请使用 this.audio.playEffect() */
/** @deprecated 请使用 await this.audio.playEffect() */
playEffect(url: string, params?: IAudioParams): Promise<AudioEffect | null> {
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() */

View File

@@ -0,0 +1,10 @@
/** GameComponent 子模块基类(可用于任意宿主对象) */
export abstract class GamePartBase {
/** 构造函数
* @param comp 宿主对象(如 GameComponent 或 CCEntity
*/
constructor(protected readonly comp: object) {}
/** 组件销毁时回调,子类按需覆盖 */
destroy(): void {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "59760005-af28-46f1-9fe5-f0db224c0f82",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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<GamePartKey, GamePartBase> | null = null;
private creators: Map<GamePartKey, ModuleCreator>;
/** 构造函数
* @param comp 宿主对象
* @param creators 模块创建器映射表
*/
constructor(
private readonly comp: object,
creators: Map<GamePartKey, ModuleCreator>
) {
this.creators = creators;
}
/** 获取实例映射表(延迟创建) */
private getInstances(): Map<GamePartKey, GamePartBase> {
if (!this.instances) {
this.instances = new Map<GamePartKey, GamePartBase>();
}
return this.instances;
}
/** 获取模块实例
* @param key 模块键
* @returns 模块实例
*/
get<M extends GamePartBase = GamePartBase>(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, ModuleCreator>([
[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);
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f0d4afee-df23-475e-871c-72a80c535ede",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "cc86c2b1-3471-46ff-878d-b9efc9feee0f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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<void> {
// 音乐功能被禁用时直接返回
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<AudioEffect | null> {
// 音效功能被禁用时直接返回
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<AudioClip | null> {
const isRemote = options?.isRemote ?? false;
const bundle = options?.bundle ?? resLoader.defaultBundleName;
if (isRemote) {
// 加载远程资源ResAutoTracker 自动管理)
return await this.comp.res.loadRemote<AudioClip>(url);
} else {
// 加载本地资源ResAutoTracker 自动管理)
return await this.comp.res.load(bundle, url, AudioClip);
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "adfbddd7-4af6-4b88-8da9-20c924249f08",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "1443b95a-646b-46d6-a15c-286b7f586b81",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "33227a0d-d0a5-4c97-970d-b082c713cd1e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More