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

View File

@@ -101,7 +101,7 @@ export abstract class CCEntity extends ecs.Entity {
// 跟随父节点释放自动释放当前资源
if (parent instanceof GameComponent) {
const result = await parent.createPrefabNode(path, bundleName);
const result = await parent.nodes.createPrefabNode(path, bundleName);
if (result == null) return null;
node = result;
@@ -315,4 +315,4 @@ export abstract class CCEntity extends ecs.Entity {
super.destroy();
}
}
}

View File

@@ -4,18 +4,22 @@
* @LastEditors: dgflash
* @LastEditTime: 2022-12-13 11:36:00
*/
import type { Asset, EventKeyboard, EventTouch, Sprite, __private } from 'cc';
import { Button, Component, EventHandler, Input, Node, Prefab, SpriteFrame, _decorator, input, instantiate, isValid } from 'cc';
import { oops } from '../../core/Oops';
import type { Asset, EventKeyboard, Node, Sprite, __private } from 'cc';
import { Component, _decorator } from 'cc';
import type { AudioEffect } from '../../core/common/audio/AudioEffect';
import type { IAudioParams } from '../../core/common/audio/IAudio';
import { EventDispatcher } from '../../core/common/event/EventDispatcher';
import type { ListenerFunc, ListenerFuncTyped } from '../../core/common/event/EventMessage';
import { EventMessage } from '../../core/common/event/EventMessage';
import { resAutoTracker } from '../../core/common/loader/ResAutoTracker';
import type { AssetType, CompleteCallback, Paths, ProgressCallback } from '../../core/common/loader/ResLoader';
import { resLoader } from '../../core/common/loader/ResLoader';
import { ViewUtil } from '../../core/utils/ViewUtil';
import { resRef } from '../../core/common/loader/ResRefManager';
import { oops } from '../../core/Oops';
import type { GameAudioModule } from './view/GameAudioModule';
import type { GameButtonModule } from './view/GameButtonModule';
import type { GameEventModule } from './view/GameEventModule';
import type { GameKeyboardModule } from './view/GameKeyboardModule';
import type { GameNodeModule } from './view/GameNodeModule';
import type { GameResModule } from './view/GameResModule';
import { GameViewModuleRegistry, ViewModuleKey } from './view/GameViewModuleRegistry';
const { ccclass } = _decorator;
@@ -23,275 +27,170 @@ const { ccclass } = _decorator;
* 游戏显示对象组件模板
*
* 特性:
* 1. 自动管理资源引用计数 - 多组件共享资源时不会错误释放
* 2. 组件销毁时自动释放资源引用 - 开发者无需手动管理
* 3. 全局资源追踪 - 可查看任意资源的引用者和引用计数
* 1. 基于引擎 Asset.addRef/decRef + 递归依赖保护,与其它持有者共享时不误释放
* 2. 组件销毁时自动 release 本产品登记的资源条目
* 3. ResAutoTracker 全局调试视图(持有者 / 条目数)
*
* 使用示例:
* ```typescript
* // 加载资源(自动注册引用)
* const spriteFrame = await this.load('textures/avatar', SpriteFrame);
* const spriteFrame = await this.res.load('common', 'textures/avatar', SpriteFrame);
* this.nodes.nodeTreeInfoLite();
* this.event.on('MyEvent', this.onMyEvent, this);
* this.event.setEvent('onGlobal');
* this.button.setButton();
* this.keyboard.setKeyboard(true, { onKeyDown: (e) => {} });
* this.event.setGameShow(() => {});
*
* // 组件销毁时自动释放引用(无需手动调用)
* // 只有当所有引用者都销毁时,资源才会被真正释放
*
* // 调试:查看资源引用情况
* GameComponent.printGlobalResStatus();
* GameComponent.setResDebugMode(true); // 开启详细日志
* GameComponent.setResDebugMode(true);
* ```
*/
@ccclass('GameComponent')
export class GameComponent extends Component {
//#region 全局事件管理
private _event: EventDispatcher | null = null;
/** 全局事件管理器 */
private get event(): EventDispatcher {
if (this._event == null) this._event = new EventDispatcher();
return this._event;
private _viewRegistry: GameViewModuleRegistry | null = null;
private get viewRegistry(): GameViewModuleRegistry {
return (this._viewRegistry ??= new GameViewModuleRegistry(this));
}
/** 标记是否已注册键盘事件 */
private _keyboardEnabled = false;
/** 标记是否已注册按钮事件 */
private _buttonEnabled = false;
/** 获取事件模块 */
get event(): GameEventModule {
return this.viewRegistry.get(ViewModuleKey.Event);
}
//#region 强类型事件方法(提供给 Agent 自动生成用)
/** 获取节点模块 */
get nodes(): GameNodeModule {
return this.viewRegistry.get(ViewModuleKey.Nodes);
}
/**
* 注册全局事件(强类型)
* @param event 事件名(枚举)
* @param listener 处理事件的侦听器函数
* @param object 侦听函数绑定的this对象
*/
/** 获取资源模块 */
get res(): GameResModule {
return this.viewRegistry.get(ViewModuleKey.Res);
}
/** 获取音频模块 */
get audio(): GameAudioModule {
return this.viewRegistry.get(ViewModuleKey.Audio);
}
/** 获取按钮模块 */
get button(): GameButtonModule {
return this.viewRegistry.get(ViewModuleKey.Button);
}
/** 获取键盘模块 */
get keyboard(): GameKeyboardModule {
return this.viewRegistry.get(ViewModuleKey.Keyboard);
}
/** 移除当前节点 */
remove() {
oops.gui.removeByNode(this.node);
}
/** 组件销毁时调用 */
protected onDestroy() {
this._viewRegistry?.destroy();
}
/** 打印全局资源状态 */
static printGlobalResStatus() {
resAutoTracker.printStatus();
}
/** 设置资源调试模式 */
static setResDebugMode(enabled: boolean) {
resAutoTracker.enableDebug(enabled);
}
//#region ========== 兼容旧版本 API ==========
//#region 全局事件管理(兼容旧版本)
/** @deprecated 请使用 this.event.watch() */
watch<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, object: any): void {
this.event.on(event as string, listener as ListenerFunc, object);
this.event.watch(event, listener, object);
}
/**
* 监听一次事件,事件响应后,该监听自动移除(强类型)
* @param event 事件名(枚举)
* @param listener 事件触发回调方法
* @param object 侦听函数绑定的this对象
*/
/** @deprecated 请使用 this.event.watchOnce() */
watchOnce<K extends keyof OopsFramework.TypedEventMap>(event: K, listener: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, object: any): void {
this.event.once(event as string, listener as ListenerFunc, object);
this.event.watchOnce(event, listener, object);
}
/**
* 移除全局事件(强类型)
* @param event 事件名(枚举)
* @param listener 处理事件的侦听器函数(可选)
* @param object 侦听函数绑定的this对象可选
*/
/** @deprecated 请使用 this.event.unwatch() */
unwatch<K extends keyof OopsFramework.TypedEventMap>(event: K, listener?: ListenerFuncTyped<K, OopsFramework.TypedEventMap[K]>, object?: any): void {
this.event.off(event as string, listener as ListenerFunc, object);
this.event.unwatch(event, listener, object);
}
/**
* 触发强类型全局事件
* @param event 事件名(枚举)
* @param data 事件数据
*/
/** @deprecated 请使用 this.event.emit() */
emit<K extends keyof OopsFramework.TypedEventMap>(event: K, data?: OopsFramework.TypedEventMap[K]): void {
this.event.emit(event, data);
}
/**
* 触发强类型异步全局事件(严格类型检查)
* @param event 事件名(枚举)
* @param data 事件数据(必须完全匹配类型定义)
*/
/** @deprecated 请使用 this.event.emitAsync() */
emitAsync<K extends keyof OopsFramework.TypedEventMap>(event: K, data: OopsFramework.TypedEventMap[K]): Promise<void> {
return this.event.emitAsync(event, data);
}
//#endregion
//#region 弱类型事件方法
/**
* 注册全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数
* @param object 侦听函数绑定的this对象
*/
/** @deprecated 请使用 this.event.on() */
on(event: string, listener: ListenerFunc, object: any): void {
this.event.on(event, listener, object);
}
/**
* 监听一次事件,事件响应后,该监听自动移除
* @param event 事件名
* @param listener 事件触发回调方法
* @param object 侦听函数绑定的this对象
*/
/** @deprecated 请使用 this.event.once() */
once(event: string, listener: ListenerFunc, object: any): void {
this.event.once(event, listener, object);
}
/**
* 移除全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数(可选)
* @param object 侦听函数绑定的this对象可选
*/
/** @deprecated 请使用 this.event.off() */
off(event: string, listener?: ListenerFunc, object?: object): void {
this.event.off(event, listener, object);
}
/**
* 触发全局事件
* @param event 事件名
* @param args 事件参数
*/
/** @deprecated 请使用 this.event.dispatchEvent() */
dispatchEvent(event: string, ...args: any[]): void {
this.event.dispatchEvent(event, ...args);
}
/**
* 触发全局事件,支持同步与异步处理
* @param event 事件名
* @param args 事件参数
*/
/** @deprecated 请使用 this.event.dispatchEventAsync() */
dispatchEventAsync(event: string, ...args: any[]): Promise<void> {
return this.event.dispatchEventAsync(event, ...args);
}
//#endregion
//#endregion
//#region 预制节点管理
/** 摊平的节点集合(所有节点不能重名) */
nodes: Map<string, Node> = null!;
/** 通过节点名获取预制上的节点,整个预制不能有重名节点 */
//#region 预制节点管理(兼容旧版本)
/** @deprecated 请使用 this.nodes.getNode() */
getNode(name: string): Node | undefined {
if (this.nodes) {
return this.nodes.get(name);
}
return undefined;
return this.nodes.getNode(name);
}
/** 平摊所有节点存到Map<string, Node>中通过get(name: string)方法获取 */
nodeTreeInfoLite() {
this.nodes = new Map();
ViewUtil.nodeTreeInfoLite(this.node, this.nodes);
/** @deprecated 请使用 this.nodes.nodeTreeInfoLite() */
nodeTreeInfoLite(): void {
this.nodes.nodeTreeInfoLite();
}
/**
* 从资源缓存中找到预制资源名并创建一个显示对象
* @param path 资源路径
* @param bundleName 资源包名
* @returns 预制节点,加载失败返回 null
*/
/** @deprecated 请使用 this.nodes.createPrefabNode() */
async createPrefabNode(path: string, bundleName: string = oops.res.defaultBundleName): Promise<Node | null> {
const prefab = await this.load(bundleName, path, Prefab);
if (!prefab) {
console.warn('[OopsFramework]', `预制体加载失败: ${path}`);
return null;
}
return instantiate(prefab);
return this.nodes.createPrefabNode(path, bundleName);
}
//#endregion
//#region 资源加载管理
/**
* 获取资源
* @param path 资源路径
* @param type 资源类型
* @param bundleName 远程资源包名
*/
//#region 资源加载管理(兼容旧版本)
/** @deprecated 请使用 this.res.getRes() */
getRes<T extends Asset>(path: string, type?: __private.__types_globals__Constructor<T> | null, bundleName?: string): T | null {
return oops.res.get(path, type, bundleName);
return this.res.getRes(path, type, bundleName);
}
/**
* 加载一个资源(自动管理引用计数)
* @param bundleName 远程包名
* @param paths 资源路径
* @param type 资源类型
* @param onProgress 加载进度回调
* @remarks
* - 资源引用会自动注册到全局管理器
* - 组件销毁时会自动减少引用计数
* - 只有引用计数为0时才会真正释放资源
*/
/** @deprecated 请使用 this.res.load() */
async load<T extends Asset>(bundleName: string, paths: Paths | AssetType<T>, type?: AssetType<T>): Promise<T> {
let realBundle: string;
let realPath: string;
if (typeof paths === 'string') {
realBundle = bundleName;
realPath = paths;
}
else {
realBundle = oops.res.defaultBundleName;
realPath = bundleName;
}
resRef.addRef(realBundle, realPath, this);
try {
const result = await oops.res.load(bundleName, paths, type);
if (!result) {
resRef.removeRef(realBundle, realPath, this);
}
return result;
}
catch (error) {
resRef.removeRef(realBundle, realPath, this);
throw error;
}
return this.res.load(bundleName, paths, type);
}
/**
* 加载指定资源包中的多个任意类型资源(回调模式)
* @param bundleName 远程包名或资源路径数组
* @param paths 资源路径数组或进度回调
* @param onProgress 加载进度回调
* @param onComplete 加载完成回调
*/
/** @deprecated 请使用 this.res.loadAny() */
loadAny(bundleName: string | string[], paths: string[] | ProgressCallback, onProgress?: ProgressCallback | CompleteCallback, onComplete?: CompleteCallback): void {
const originalComplete = onComplete as ((err: Error | null, data: Asset[]) => void) | undefined;
const pathsToTrack: { bundle: string; path: string }[] = [];
if (typeof bundleName === 'string' && Array.isArray(paths)) {
paths.forEach(p => {
resRef.addRef(bundleName, p, this);
pathsToTrack.push({ bundle: bundleName, path: p });
});
}
else if (Array.isArray(bundleName)) {
bundleName.forEach(p => {
resRef.addRef(resLoader.defaultBundleName, p, this);
pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: p });
});
}
else if (typeof bundleName === 'string' && typeof paths === 'function') {
resRef.addRef(resLoader.defaultBundleName, bundleName, this);
pathsToTrack.push({ bundle: resLoader.defaultBundleName, path: bundleName });
}
const wrappedComplete = (err: Error | null, data: Asset[]) => {
if (err || !data) {
pathsToTrack.forEach(({ bundle, path }) => {
resRef.removeRef(bundle, path, this);
});
}
originalComplete?.(err, data);
};
oops.res.loadAny(bundleName, paths, onProgress, wrappedComplete);
this.res.loadAny(bundleName, paths, onProgress, onComplete);
}
/**
* 加载文件夹中的资源(回调模式)
* @param bundleName 远程包名
* @param dir 文件夹名
* @param type 资源类型
* @param onProgress 加载进度回调
* @param onComplete 加载完成回调
*/
/** @deprecated 请使用 this.res.loadDir() */
loadDir<T extends Asset>(bundleName: string, dir: string, type: AssetType<T>, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, onProgress: ProgressCallback, onComplete: CompleteCallback): void;
loadDir<T extends Asset>(bundleName: string, dir: string, onComplete?: CompleteCallback): void;
@@ -307,333 +206,101 @@ export class GameComponent extends Component {
onProgress?: ProgressCallback | CompleteCallback,
onComplete?: CompleteCallback,
): void {
let realDir: string;
let realBundle: string;
if (typeof dir === 'string') {
realDir = dir;
realBundle = bundleName;
}
else {
realDir = bundleName;
realBundle = oops.res.defaultBundleName;
}
resRef.addRef(realBundle, realDir, this);
const originalComplete = onComplete as ((err: Error | null, data: T[]) => void) | undefined;
const wrappedComplete = (err: Error | null, data: T[]) => {
if (err || !data) {
resRef.removeRef(realBundle, realDir, this);
}
originalComplete?.(err, data);
};
oops.res.loadDir(bundleName, dir, type, onProgress, wrappedComplete);
this.res.loadDir(bundleName, dir, type, onProgress, onComplete);
}
/**
* 手动释放指定资源引用
* @param path 资源路径
* @param bundleName 资源包名
* @remarks
* - 只减少当前组件对该资源的引用计数
* - 只有引用计数为0时才会真正释放资源
* - 其他组件的引用不受影响
*/
releaseRes(path: string, bundleName: string = resLoader.defaultBundleName) {
resRef.removeRef(bundleName, path, this);
}
/**
* 释放当前组件所有资源引用
* @remarks
* - 自动减少所有资源的引用计数
* - 只有引用计数为0的资源才会被真正释放
* - 共享资源不会被错误释放
*/
release() {
const released = resRef.releaseAllByComponent(this);
if (released.length > 0) {
console.log(`[GameComponent] ${this.node?.name} 释放了 ${released.length} 个资源:`, released);
}
}
/**
* 释放所有文件夹资源引用
* @deprecated 文件夹资源现在也通过全局引用计数管理,直接调用 release() 即可
*/
releaseDir() {
console.warn('[GameComponent] releaseDir() 已废弃,请直接使用 release()');
}
/**
* 获取资源的全局引用计数
* @param path 资源路径
* @param bundleName 资源包名
* @returns 全局引用计数
*/
getResRefCount(path: string, bundleName: string = resLoader.defaultBundleName): number {
return resRef.getRefCount(bundleName, path);
}
/**
* 获取资源的所有引用者
* @param path 资源路径
* @param bundleName 资源包名
* @returns 引用者列表
*/
getResReferrers(path: string, bundleName: string = resLoader.defaultBundleName): string[] {
return resRef.getReferrers(bundleName, path);
}
/**
* 打印当前组件的资源引用情况
*/
printResUsage() {
resRef.printComponentStatus(this);
}
/**
* 打印全局资源引用状态(调试用)
*/
static printGlobalResStatus() {
resRef.printStatus();
}
/**
* 开启/关闭全局资源调试模式
* @param enabled 是否开启
*/
static setResDebugMode(enabled: boolean) {
resRef.enableDebug(enabled);
}
/**
* 设置图片资源
* @param target 目标精灵对象
* @param path 图片资源地址
* @param bundle 资源包名
* @returns 是否设置成功
* @remarks 资源引用计数由 load 方法自动管理,加载失败时会自动回滚
*/
/** @deprecated 请使用 this.res.setSprite() */
async setSprite(target: Sprite, path: string, bundle: string = resLoader.defaultBundleName): Promise<boolean> {
const spriteFrame = await this.load(bundle, path, SpriteFrame);
if (!spriteFrame) {
return false;
}
if (!isValid(target)) {
this.releaseRes(path, bundle);
return false;
}
target.spriteFrame = spriteFrame;
return true;
return this.res.setSprite(target, path, bundle);
}
//#endregion
//#region 音频播放管理
/**
* 播放背景音乐(不受自动释放资源管理)
* @param url 资源地址
* @param params 背景音乐资源播放参数
*/
playMusic(url: string, params?: IAudioParams) {
oops.audio.music.loadAndPlay(url, params);
//#region 音频播放管理(兼容旧版本)
/** @deprecated 请使用 this.audio.playMusic() */
playMusic(url: string, params?: IAudioParams): void {
this.audio.playMusic(url, params);
}
/**
* 播放音效
* @param url 资源地址
* @param params 音效播放参数
* @returns 音效实例,播放失败返回 null
* @remarks 注意:音效资源由 AudioEffectPool 自动管理,不需要在此组件中记录
*/
/** @deprecated 请使用 this.audio.playEffect() */
playEffect(url: string, params?: IAudioParams): Promise<AudioEffect | null> {
return new Promise((resolve) => {
if (params == null) {
params = { bundle: resLoader.defaultBundleName };
}
else if (params.bundle == null) {
params.bundle = resLoader.defaultBundleName;
}
oops.audio.playEffect(url, params).then((ae) => {
resolve(ae ?? null);
});
});
return this.audio.playEffect(url, params);
}
//#endregion
//#region 游戏逻辑事件
/**
* 批量设置当前界面按钮事件
* @param bindRootEvent 是否对预制根节点绑定触摸事件
* @example
* 注按钮节点Label1、Label2必须绑定UIButton等类型的按钮组件才会生效方法名必须与节点名一致
* this.setButton();
*
* Label1(event: EventTouch) { console.log(event.target.name); }
* Label2(event: EventTouch) { console.log(event.target.name); }
*/
protected setButton(bindRootEvent = true) {
this._buttonEnabled = true;
// 自定义按钮批量绑定触摸事件
if (bindRootEvent) {
this.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => {
const self: any = this;
const func = self[event.target.name];
if (func) {
func.call(this, event);
}
// 不触发界面根节点触摸事件、不触发长按钮组件的触摸事件
// else if (event.target != this.node && event.target.getComponent(ButtonTouchLong) == null) {
// console.warn(`名为【${event.target.name}】的按钮事件方法不存在`);
// }
}, this);
}
// Cocos Creator Button组件批量绑定触摸事件使用UIButton支持放连点功能
const regex = /<([^>]+)>/;
const match = this.name.match(regex);
if (!match || !match[1]) {
console.warn('[OopsFramework]', `组件名 "${this.name}" 不符合 "<组件名>" 格式,跳过按钮事件绑定`);
return;
}
const componentName = match[1];
const buttons = this.node.getComponentsInChildren<Button>(Button);
buttons.forEach((b: Button) => {
const node = b.node;
const self: any = this;
const func = self[node.name];
if (func) {
const event = new EventHandler();
event.target = this.node;
event.handler = b.node.name;
event.component = componentName;
b.clickEvents.push(event);
}
// else {
// console.warn(`名为【${node.name}】的按钮事件方法不存在`);
// }
});
//#region 游戏逻辑事件(兼容旧版本)
/** @deprecated 请使用 this.button.setButton() */
protected setButton(bindRootEvent = true): void {
this.button.setButton(bindRootEvent);
}
/**
* 批量设置全局事件
* @example
* this.setEvent("onGlobal");
* this.dispatchEvent("onGlobal", "全局事件");
*
* onGlobal(event: string, args: any) { console.log(args) };
*/
protected setEvent(...args: string[]) {
const self: any = this;
for (const name of args) {
const func = self[name];
if (func)
this.on(name, func, this);
else
console.error(`名为【${name}】的全局事方法不存在`);
}
/** @deprecated 请使用 this.event.setEvent() */
protected setEvent(...args: string[]): void {
this.event.setEvent(...args);
}
/**
* 键盘事件开关
* @param on 打开键盘事件为true
*/
setKeyboard(on: boolean) {
/** @deprecated 请使用 this.keyboard.setKeyboard() */
setKeyboard(on: boolean): void {
if (on) {
this._keyboardEnabled = true;
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
input.on(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this.keyboard.setKeyboard(true, {
onKeyDown: this.onKeyDown.bind(this),
onKeyUp: this.onKeyUp.bind(this),
onKeyPressing: this.onKeyPressing.bind(this)
});
}
else {
this._keyboardEnabled = false;
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
input.off(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this.keyboard.setKeyboard(false);
}
}
/** 键按下 */
protected onKeyDown(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyDown(event: EventKeyboard): void { }
/** 键放开 */
protected onKeyUp(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyUp(event: EventKeyboard): void { }
/** 键长按 */
protected onKeyPressing(event: EventKeyboard) { }
/** @deprecated 请使用 this.keyboard.setKeyboard() 传入 callbacks */
protected onKeyPressing(event: EventKeyboard): void { }
/** 监听游戏从后台进入事件 */
protected setGameShow() {
this.on(EventMessage.GAME_SHOW, this.onGameShow, this);
/** @deprecated 请使用 this.event.setGameShow() */
protected setGameShow(): void {
this.event.setGameShow(this.onGameShow.bind(this));
}
/** 监听游戏切到后台事件 */
protected setGameHide() {
this.on(EventMessage.GAME_HIDE, this.onGameHide, this);
/** @deprecated 请使用 this.event.setGameHide() */
protected setGameHide(): void {
this.event.setGameHide(this.onGameHide.bind(this));
}
/** 监听游戏画笔尺寸变化事件 */
protected setGameResize() {
this.on(EventMessage.GAME_RESIZE, this.onGameResize, this);
/** @deprecated 请使用 this.event.setGameResize() */
protected setGameResize(): void {
this.event.setGameResize(this.onGameResize.bind(this));
}
/** 监听游戏全屏事件 */
protected setGameFullScreen() {
this.on(EventMessage.GAME_FULL_SCREEN, this.onGameFullScreen, this);
/** @deprecated 请使用 this.event.setGameFullScreen() */
protected setGameFullScreen(): void {
this.event.setGameFullScreen(this.onGameFullScreen.bind(this));
}
/** 监听游戏旋转屏幕事件 */
protected setGameOrientation() {
this.on(EventMessage.GAME_ORIENTATION, this.onGameOrientation, this);
/** @deprecated 请使用 this.event.setGameOrientation() */
protected setGameOrientation(): void {
this.event.setGameOrientation(this.onGameOrientation.bind(this));
}
/** 游戏从后台进入事件回调 */
/** @deprecated 请配合 setGameShow() 使用 */
protected onGameShow(): void { }
/** 游戏切到后台事件回调 */
/** @deprecated 请配合 setGameHide() 使用 */
protected onGameHide(): void { }
/** 游戏画笔尺寸变化事件回调 */
/** @deprecated 请配合 setGameResize() 使用 */
protected onGameResize(): void { }
/** 游戏全屏事件回调 */
/** @deprecated 请配合 setGameFullScreen() 使用 */
protected onGameFullScreen(): void { }
/** 游戏旋转屏幕事件回调 */
/** @deprecated 请配合 setGameOrientation() 使用 */
protected onGameOrientation(): void { }
//#endregion
/** 移除自己 */
remove() {
oops.gui.removeByNode(this.node);
}
protected onDestroy() {
if (this._keyboardEnabled) {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
input.off(Input.EventType.KEY_PRESSING, this.onKeyPressing, this);
this._keyboardEnabled = false;
}
if (this._buttonEnabled) {
this.node.off(Node.EventType.TOUCH_END);
this._buttonEnabled = false;
}
if (this._event) {
this._event.clear();
this._event = null;
}
if (this.nodes) {
this.nodes.clear();
this.nodes = null!;
}
this.release();
}
}
//#endregion
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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