1. 存储模块全面优化,修复跨平台兼容性问题,完美支持所有Unicode字符

2. 存储模块性能提升,添加LRU缓存、批量操作支持,优化内存使用
3. 多语言模块性能与内存管理优化,组件查询性能提升
4. 时间模块类型安全与性能优化,使用泛型替代any,对象池机制减少内存分配
5. 事件系统修复双重注册、重复注册等严重问题,实现EventData对象池减少GC压力
6. RandomManager修复4个逻辑BUG,包括边界问题和越界问题
7. 音频模块内存与性能优化,避免重复加载,优化数据结构,添加完整清理机制
8. CCView与CCViewVM合并,支持按需启用MVVM
9. Collection模块优化,AsyncQueue添加队列容量限制,Collection查询性能提升
10. ECS系统全面优化,对象池复用减少内存分配,循环性能提升
11. 优化MVVM组件性能
This commit is contained in:
dgflash
2026-01-09 21:54:05 +08:00
parent 9a156d5c62
commit f2fe9d47b6
85 changed files with 4356 additions and 1807 deletions

View File

@@ -4,7 +4,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2022-09-02 10:22:36
*/
import type { AudioClip } from 'cc';
import type { AudioClip } from 'cc';
import { AudioSource, _decorator } from 'cc';
import type { IAudioParams } from './IAudio';
const { ccclass } = _decorator;
@@ -31,10 +31,18 @@ export class AudioEffect extends AudioSource {
this.onComplete && this.onComplete(this);
}
/** 重置音效对象,释放所有引用 */
reset() {
this.stop();
this.clip = null;
this.path = null!;
this.params = null!;
this.onComplete = null; // 清理回调引用,防止内存泄漏
}
}
/** 组件销毁时清理资源 */
onDestroy() {
this.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this);
this.reset();
}
}

View File

@@ -17,16 +17,17 @@ export class AudioEffectPool {
/** 对象池集合 */
private effects: Map<string, AudioEffect> = new Map();
/** 记录项目资源库中使用过的音乐资源 */
private res_project: Map<string, string[]> = new Map();
private res_project: Map<string, Set<string>> = new Map();
/** 外网远程资源记录(地址、音效对象) */
private res_remote: Map<string, AudioClip> = new Map();
/** 正在加载的资源Promise缓存避免重复加载 */
private loading_cache: Map<string, Promise<AudioClip>> = new Map();
private _aeId = 0;
/** 获取请求唯一编号 */
private getAeId() {
if (this._aeId == AE_ID_MAX) this._aeId = 1;
this._aeId++;
return this._aeId;
if (this._aeId >= AE_ID_MAX) this._aeId = 0;
return ++this._aeId;
}
/**
@@ -91,106 +92,126 @@ export class AudioEffectPool {
* @returns
*/
async loadAndPlay(path: string | AudioClip, params?: IAudioParams): Promise<AudioEffect> {
return new Promise(async (resolve, reject) => {
if (params == null) {
params = {
type: AudioEffectType.Effect,
bundle: resLoader.defaultBundleName,
loop: false,
destroy: false
};
}
else {
if (params.type == null) params.type = AudioEffectType.Effect;
if (params.bundle == null) params.bundle = resLoader.defaultBundleName;
if (params.loop == null) params.loop = false;
if (params.type == null) params.destroy = false;
}
// 合并默认参数(减少对象创建)
const finalParams: IAudioParams = params ? {
type: params.type ?? AudioEffectType.Effect,
bundle: params.bundle ?? resLoader.defaultBundleName,
loop: params.loop ?? false,
destroy: params.destroy ?? false,
volume: params.volume,
onPlayComplete: params.onPlayComplete
} : {
type: AudioEffectType.Effect,
bundle: resLoader.defaultBundleName,
loop: false,
destroy: false
};
const iad = this.data[params.type!];
if (iad == null) console.error(`类型为【${params.type!}】的音效配置不存在`);
const iad = this.data[finalParams.type!];
if (!iad) {
console.error(`类型为【${finalParams.type!}】的音效配置不存在`);
return null!;
}
if (!iad.switch) {
resolve(null!);
return;
}
if (!iad.switch) {
return null!;
}
if (params.volume == null) params.volume = iad.volume;
if (finalParams.volume == null) finalParams.volume = iad.volume;
const bundle = params.bundle!;
let key: string = null!;
let clip: AudioClip | undefined;
// 通过预制自动加载的音效资源(音效内存跟随预制体的内存一并释放)
if (path instanceof AudioClip) {
key = `${params.type}_${path.uuid}`;
clip = path;
}
// 非引擎管理的远程资源加载
else if (path.indexOf('http') == 0) {
key = `${params.type}_${path}`;
clip = this.res_remote.get(path);
if (clip == null) {
const bundle = finalParams.bundle!;
let key: string;
let clip: AudioClip | null = null;
// 通过预制自动加载的音效资源(音效内存跟随预制体的内存一并释放)
if (path instanceof AudioClip) {
key = `${finalParams.type}_${path.uuid}`;
clip = path;
}
// 非引擎管理的远程资源加载
else if (path.indexOf('http') === 0) {
key = `${finalParams.type}_${path}`;
clip = this.res_remote.get(path) || null;
if (!clip) {
// 检查是否正在加载,避免重复请求
let loadPromise = this.loading_cache.get(path);
if (!loadPromise) {
const extension = path.split('.').pop();
clip = await resLoader.loadRemote<AudioClip>(path, { ext: `.${extension}` });
this.res_remote.set(path, clip);
loadPromise = resLoader.loadRemote<AudioClip>(path, { ext: `.${extension}` });
this.loading_cache.set(path, loadPromise);
}
clip = await loadPromise;
this.res_remote.set(path, clip);
this.loading_cache.delete(path); // 加载完成后清除缓存
}
// 资源加载
else {
key = `${params.type}_${bundle}_${path}`;
clip = resLoader.get(path, AudioClip, bundle)!;
}
// 资源加载
else {
key = `${finalParams.type}_${bundle}_${path}`;
clip = resLoader.get(path, AudioClip, bundle);
// 加载音效资源 - 如果一个预制上加载了了音乐同一个音乐资源,此处不会记录音乐资源路径数据,资源内存由预制释放时一起释放
if (clip == null) {
// 加载音效资源
if (!clip) {
const cacheKey = `${bundle}_${path}`;
let loadPromise = this.loading_cache.get(cacheKey);
if (!loadPromise) {
loadPromise = resLoader.load(bundle, path, AudioClip);
this.loading_cache.set(cacheKey, loadPromise);
// 记录资源路径使用Set避免重复
let paths = this.res_project.get(bundle);
if (paths == null) {
paths = [];
if (!paths) {
paths = new Set<string>();
this.res_project.set(bundle, paths);
}
if (paths.indexOf(path) == -1) paths.push(path);
clip = await resLoader.load(bundle, path, AudioClip);
paths.add(path);
}
clip = await loadPromise;
this.loading_cache.delete(cacheKey); // 加载完成后清除缓存
}
}
// 资源已被释放
if (!clip.isValid) {
console.warn(`音效资源【${key}】已被释放`);
resolve(null!);
return;
}
// 资源已被释放或加载失败
if (!clip || !clip.isValid) {
console.warn(`音效资源【${key!}】已被释放或加载失败`);
return null!;
}
// 获取音效播放器播放音乐
let aeid = -1;
let ae: AudioEffect;
let node: Node = null!;
if (this.pool.size() == 0) {
aeid = this.getAeId();
key += '_' + aeid;
// 获取音效播放器播放音乐
let ae: AudioEffect;
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 {
node = this.pool.get()!;
ae = node.getComponent(AudioEffect)!;
}
node = new Node('AudioEffect');
ae = node.addComponent(AudioEffect)!;
ae.key = key;
ae.aeid = aeid;
ae.onComplete = this.onAudioEffectPlayComplete.bind(this);
}
else {
node = this.pool.get()!;
ae = node.getComponent(AudioEffect)!;
}
// 记录正在播放的音效播放器
this.effects.set(ae.key, ae);
// 记录正在播放的音效播放器
this.effects.set(ae.key, ae);
node.parent = oops.audio.node;
ae.path = path;
ae.params = params;
ae.loop = params.loop!;
ae.volume = params.volume!;
ae.clip = clip;
ae.play();
node.parent = oops.audio.node;
ae.path = path;
ae.params = finalParams;
ae.loop = finalParams.loop!;
ae.volume = finalParams.volume!;
ae.clip = clip;
ae.play();
resolve(ae);
});
return ae;
}
/** 音效播放完成 */
@@ -228,10 +249,13 @@ export class AudioEffectPool {
/** 停止播放所有音效 */
stop() {
this.effects.forEach((ae) => {
// 使用数组缓存避免在遍历时修改Map
const effectsArray = Array.from(this.effects.values());
for (let i = 0; i < effectsArray.length; i++) {
const ae = effectsArray[i];
ae.stop();
this.onAudioEffectPlayComplete(ae);
});
}
this.effects.clear();
}
@@ -242,10 +266,13 @@ export class AudioEffectPool {
/** 暂停所有音效 */
pause() {
this.effects.forEach((ae) => {
// 使用数组缓存避免在遍历时修改Map
const effectsArray = Array.from(this.effects.values());
for (let i = 0; i < effectsArray.length; i++) {
const ae = effectsArray[i];
ae.pause();
this.onAudioEffectPlayComplete(ae);
});
}
this.effects.clear();
}
@@ -259,6 +286,9 @@ export class AudioEffectPool {
// 释放外网远程音效资源
this.releaseResRemote();
// 清空加载缓存
this.loading_cache.clear();
}
/** 释放池中音乐播放器 */
@@ -266,22 +296,29 @@ export class AudioEffectPool {
this.pool.clear();
// 释放正在播放的音效对象
this.effects.forEach((ae) => ae.node.destroy());
const effectsArray = Array.from(this.effects.values());
for (let i = 0; i < effectsArray.length; i++) {
effectsArray[i].node.destroy();
}
this.effects.clear();
}
/** 释放各个资源包中的音效资源 */
releaseRes() {
this.res_project.forEach((paths: string[], bundleName: string) => {
this.res_project.forEach((paths: Set<string>, bundleName: string) => {
paths.forEach((path) => resLoader.release(path, bundleName));
paths.clear(); // 清空Set
});
this.res_project.clear();
}
/** 释放外网远程音效资源 */
releaseResRemote() {
this.res_remote.forEach((clip: AudioClip, path: string) => {
clip.decRef();
this.res_remote.forEach((clip: AudioClip) => {
if (clip && clip.isValid) {
clip.decRef();
}
});
this.res_remote.clear();
}
}
}

View File

@@ -116,4 +116,13 @@ export class AudioManager extends Component {
}
this.save();
}
/** 组件销毁时释放所有音频资源 */
onDestroy() {
this.stopAll();
this.music?.release();
this.effect?.release();
this.music = null!;
this.data = null!;
}
}

View File

@@ -21,8 +21,8 @@ export class AudioMusic extends Node {
private _progress = 0;
private _isLoading = false;
private _nextPath: string = null!;
private _nextParams: IAudioParams = null!;
private _nextPath: string | null = null;
private _nextParams: IAudioParams | null = null;
private _ae: AudioEffect = null!;
/**
@@ -57,6 +57,7 @@ export class AudioMusic extends Node {
*/
setVolume(value: number) {
this.data[AudioEffectType.Music].volume = value;
this._ae.volume = value;
}
/** 获取音乐播放进度 */
@@ -93,47 +94,60 @@ export class AudioMusic extends Node {
async loadAndPlay(path: string, params?: IAudioParams) {
if (!this.getSwitch()) return; // 禁止播放音乐
// 下一个加载的背景音乐资源
// 下一个加载的背景音乐资源(避免重复存储,直接覆盖)
if (this._isLoading) {
this._nextPath = path;
this._nextParams = params!;
this._nextParams = params || null;
return;
}
if (params == null) {
params = {
type: AudioEffectType.Music,
bundle: resLoader.defaultBundleName,
loop: true,
volume: this.getVolume()
};
}
else {
if (params.type == null) params.type = AudioEffectType.Music;
if (params.bundle == null) params.bundle = resLoader.defaultBundleName;
if (params.loop == null) params.loop = true;
if (params.volume == null) params.volume = this.getVolume();
}
// 合并默认参数(减少对象创建)
const finalParams: IAudioParams = 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._isLoading = true;
let clip: AudioClip = null!;
if (path.indexOf('http') == 0) {
let clip: AudioClip | null = null;
if (path.indexOf('http') === 0) {
const extension = path.split('.').pop();
clip = await resLoader.loadRemote<AudioClip>(path, { ext: `.${extension}` });
}
else {
clip = await resLoader.load(params.bundle!, path, AudioClip);
clip = await resLoader.load(finalParams.bundle!, path, AudioClip);
}
this._isLoading = false;
// 加载失败处理
if (!clip) {
console.warn(`音乐资源加载失败: ${path}`);
return;
}
// 处理等待加载的背景音乐
if (this._nextPath != null) {
if (this._nextPath !== null) {
const nextPath = this._nextPath;
const nextParams = this._nextParams;
// 立即清空引用,减少内存占用
this._nextPath = null;
this._nextParams = null;
// 释放刚加载的音乐资源(因为有新的音乐要播放)
clip.decRef();
// 加载等待播放的背景音乐
this.loadAndPlay(this._nextPath, this._nextParams);
this._nextPath = null!;
this._nextParams = null!;
this.loadAndPlay(nextPath, nextParams || undefined);
}
else {
// 正在播放的时候先关闭
@@ -143,11 +157,11 @@ export class AudioMusic extends Node {
this.release();
// 播放背景音乐
this._ae.params = params;
this._ae.params = finalParams;
this._ae.path = path;
this._ae.clip = clip;
this._ae.loop = params.loop!;
this._ae.volume = params.volume!;
this._ae.loop = finalParams.loop!;
this._ae.volume = finalParams.volume!;
this._ae.currentTime = 0;
this._ae.play();
}
@@ -170,10 +184,19 @@ export class AudioMusic extends Node {
/** 释放当前背景音乐资源 */
release() {
if (this._ae.clip) {
if (this._ae && this._ae.clip) {
this.stop();
this._ae.clip.decRef();
this._ae.clip = null;
}
}
/** 节点销毁时清理所有引用 */
onDestroy() {
this.release();
this._nextPath = null;
this._nextParams = null;
this._ae = null!;
this.data = null!;
}
}

View File

@@ -7,10 +7,44 @@
import type { ListenerFunc } from './EventMessage';
import { MessageEventData } from './MessageManager';
/* 事件对象基类,继承该类将拥有发送和接送事件的能力 */
/**
* 事件对象基类,继承该类将拥有发送和接收事件的能力
*
* @性能优化
* - 懒加载 MessageEventData 实例,未使用事件功能时不占用内存
* - 支持批量清理事件,避免逐个移除的性能损耗
* - 使用对象池管理内部 EventData 对象
*
* @内存管理
* ⚠️ 必须在对象销毁时调用 destroy() 方法,否则会导致内存泄漏
*
* @example
* class MyClass extends EventDispatcher {
* constructor() {
* super();
* this.on('myEvent', this.onMyEvent, this);
* }
*
* onMyEvent(event: string, data: any) {
* console.log('收到事件:', event, data);
* }
*
* destroy() {
* super.destroy(); // 清理所有事件监听
* }
* }
*/
export class EventDispatcher {
protected _msg: MessageEventData | null = null;
/** 确保 MessageEventData 已初始化 */
private ensureMessageEventData(): MessageEventData {
if (this._msg == null) {
this._msg = new MessageEventData();
}
return this._msg;
}
/**
* 注册全局事件
* @param event 事件名
@@ -18,19 +52,24 @@ export class EventDispatcher {
* @param object 侦听函数绑定的作用域对象
*/
on(event: string, listener: ListenerFunc, object: any) {
if (this._msg == null) {
this._msg = new MessageEventData();
}
this._msg.on(event, listener, object);
this.ensureMessageEventData().on(event, listener, object);
}
/**
* 移除全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数(可选,不传则移除该事件的所有监听器)
* @param object 侦听函数绑定的作用域对象(可选)
*/
off(event: string) {
off(event: string, listener?: ListenerFunc, object?: any) {
if (this._msg) {
this._msg.off(event);
// 支持精确移除单个监听器
if (listener) {
this._msg.off(event, listener, object);
} else {
// 移除该事件的所有监听器
this._msg.off(event);
}
}
}
@@ -40,19 +79,16 @@ export class EventDispatcher {
* @param args 事件参数
*/
dispatchEvent(event: string, ...args: any) {
if (this._msg == null) {
this._msg = new MessageEventData();
}
this._msg.dispatchEvent(event, ...args);
this.ensureMessageEventData().dispatchEvent(event, ...args);
}
/**
* 销毁事件对象
* 销毁事件对象,释放所有事件监听
*/
destroy() {
if (this._msg) {
this._msg.clear();
this._msg = null;
}
this._msg = null;
}
}

View File

@@ -5,9 +5,43 @@ class EventData {
event!: string;
listener!: ListenerFunc;
object: any;
/** 重置数据,准备回收到对象池 */
reset() {
this.event = '';
this.listener = null!;
this.object = null;
}
}
/** 批量注册、移除全局事件对象 */
/** EventData 对象池,减少频繁创建对象的 GC 压力 */
class EventDataPool {
private static pool: EventData[] = [];
private static readonly MAX_POOL_SIZE = 100;
/** 从对象池获取对象 */
static get(): EventData {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return new EventData();
}
/** 回收对象到对象池 */
static put(data: EventData) {
if (this.pool.length < this.MAX_POOL_SIZE) {
data.reset();
this.pool.push(data);
}
}
/** 清空对象池 */
static clear() {
this.pool.length = 0;
}
}
/** 批量注册、移除全局事件对象(用于组件级事件管理) */
export class MessageEventData {
private events: Map<string, Array<EventData>> = new Map();
@@ -18,32 +52,59 @@ export class MessageEventData {
* @param object 侦听函数绑定的作用域对象
*/
on(event: string, listener: ListenerFunc, object: object) {
// 先注册到全局消息管理器
message.on(event, listener, object);
// 记录到本地事件列表,用于批量清理
let eds = this.events.get(event);
if (eds == null) {
eds = [];
this.events.set(event, eds);
}
const ed: EventData = new EventData();
const ed: EventData = EventDataPool.get();
ed.event = event;
ed.listener = listener;
ed.object = object;
eds.push(ed);
message.on(event, listener, object);
}
/**
* 移除全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数(可选,不传则移除该事件的所有监听器)
* @param object 侦听函数绑定的作用域对象(可选)
*/
off(event: string) {
off(event: string, listener?: ListenerFunc, object?: object) {
const eds = this.events.get(event);
if (!eds) return;
for (const eb of eds) {
message.off(event, eb.listener, eb.object);
// 如果没有指定 listener移除该事件的所有监听器
if (!listener) {
for (const eb of eds) {
message.off(event, eb.listener, eb.object);
EventDataPool.put(eb);
}
this.events.delete(event);
return;
}
// 移除指定的监听器
const length = eds.length;
for (let i = 0; i < length; i++) {
const eb = eds[i];
if (eb.listener == listener && eb.object == object) {
message.off(event, eb.listener, eb.object);
EventDataPool.put(eb);
eds.splice(i, 1);
break;
}
}
// 如果该事件已无监听器,删除事件
if (eds.length == 0) {
this.events.delete(event);
}
this.events.delete(event);
}
/**
@@ -57,15 +118,30 @@ export class MessageEventData {
/** 清除所有的全局事件监听 */
clear() {
const keys = Array.from(this.events.keys());
for (const event of keys) {
this.off(event);
// 直接遍历 Map避免创建临时数组
for (const [event, eds] of this.events) {
for (const eb of eds) {
message.off(event, eb.listener, eb.object);
EventDataPool.put(eb);
}
}
this.events.clear();
}
}
/**
* 全局消息管理
*
* @性能优化说明
* 1. 使用对象池管理 EventData 对象,减少 GC 压力
* 2. 重复注册检测,避免同一监听器被多次添加
* 3. Map 数据结构,提供 O(1) 的事件查找性能
* 4. 支持精确移除单个监听器,避免误删
*
* @内存管理注意事项
* ⚠️ 重要:组件销毁时必须调用 off() 移除事件监听,否则会导致内存泄漏
* ⚠️ 建议:在 onDestroy() 或 destroy() 中移除所有注册的事件
*
* @help https://gitee.com/dgflash/oops-framework/wikis/pages?sort_id=12037894&doc_id=2873565
* @example
// 注册持续监听的全局事件
@@ -126,16 +202,18 @@ export class MessageManager {
this.events.set(event, eds);
}
// 检查重复注册,如果已存在则直接返回,避免重复添加
const length = eds.length;
for (let i = 0; i < length; i++) {
const bin = eds[i];
if (bin.listener == listener && bin.object == object) {
warn(`名为【${event}】的事件重复注册侦听器`);
return;
}
}
const data: EventData = new EventData();
// 从对象池获取 EventData 对象
const data: EventData = EventDataPool.get();
data.event = event;
data.listener = listener;
data.object = object;
@@ -149,10 +227,10 @@ export class MessageManager {
* @param object 侦听函数绑定的作用域对象
*/
once(event: string, listener: ListenerFunc, object: object) {
let _listener: any = ($event: string, ...$args: any) => {
const _listener: any = ($event: string, ...$args: any) => {
this.off(event, _listener, object);
_listener = null;
listener.call(object, $event, $args);
// 正确展开参数传递
listener.call(object, $event, ...$args);
};
this.on(event, _listener, object);
}
@@ -160,10 +238,10 @@ export class MessageManager {
/**
* 移除全局事件
* @param event 事件名
* @param listener 处理事件的侦听器函数
* @param object 侦听函数绑定的作用域对象
* @param listener 处理事件的侦听器函数(可选,不传则移除该事件的所有监听器)
* @param object 侦听函数绑定的作用域对象(可选)
*/
off(event: string, listener: Function, object: object) {
off(event: string, listener?: Function, object?: object) {
const eds = this.events.get(event);
if (!eds) {
@@ -171,10 +249,21 @@ export class MessageManager {
return;
}
// 如果没有指定 listener移除该事件的所有监听器
if (!listener) {
for (const bin of eds) {
EventDataPool.put(bin);
}
this.events.delete(event);
return;
}
// 移除指定的监听器
const length = eds.length;
for (let i = 0; i < length; i++) {
const bin: EventData = eds[i];
if (bin.listener == listener && bin.object == object) {
EventDataPool.put(bin);
eds.splice(i, 1);
break;
}
@@ -189,10 +278,12 @@ export class MessageManager {
* 触发全局事件
* @param event 事件名
* @param args 事件参数
* @note 使用 concat() 创建数组副本,防止在事件回调中添加/删除监听器时影响遍历
*/
dispatchEvent(event: string, ...args: any) {
const list = this.events.get(event);
if (list != null) {
// 创建副本以支持在回调中安全地修改监听器列表
const eds: Array<EventData> = list.concat();
const length = eds.length;
for (let i = 0; i < length; i++) {
@@ -206,6 +297,7 @@ export class MessageManager {
* 触发全局事件,支持同步与异步处理
* @param event 事件名
* @param args 事件参数
* @note 使用 concat() 创建数组副本,防止在事件回调中添加/删除监听器时影响遍历
* @example 事件响应示例
onTest(event: string, args: any): Promise<void> {
return new Promise((resolve, reject) => {
@@ -220,6 +312,7 @@ export class MessageManager {
return new Promise(async (resolve, reject) => {
const list = this.events.get(event);
if (list != null) {
// 创建副本以支持在回调中安全地修改监听器列表
const eds: Array<EventData> = list.concat();
const length = eds.length;
for (let i = 0; i < length; i++) {
@@ -232,4 +325,4 @@ export class MessageManager {
}
}
export const message = new MessageManager();
export const message = new MessageManager();

View File

@@ -1,4 +1,4 @@
import type { __private, AssetManager } from 'cc';
import type { __private, AssetManager } from 'cc';
import { AnimationClip, Asset, assetManager, AudioClip, Font, ImageAsset, js, JsonAsset, Material, Mesh, Prefab, resources, sp, SpriteFrame, Texture2D } from 'cc';
export type AssetType<T = Asset> = __private.__types_globals__Constructor<T> | null;
@@ -91,7 +91,7 @@ export class ResLoader {
return new Promise<T>((resolve, reject) => {
assetManager.loadRemote<T>(url, options, (err, data: T) => {
if (err) {
reject(null);
reject(err);
return;
}
resolve(data);
@@ -121,7 +121,7 @@ export class ResLoader {
return new Promise<AssetManager.Bundle>((resolve, reject) => {
assetManager.loadBundle(name, options, (err, bundle: AssetManager.Bundle) => {
if (err) {
resolve(null!);
reject(err);
return;
}
resolve(bundle);
@@ -168,7 +168,7 @@ export class ResLoader {
return new Promise((resolve, reject) => {
const onComplete = (err: Error | null, data: AssetManager.RequestItem) => {
if (err) {
resolve(null!);
reject(err);
return;
}
resolve(data);
@@ -239,9 +239,13 @@ export class ResLoader {
return new Promise<T>((resolve, reject) => {
const onComplete = (err: Error | null, data: T) => {
if (err) {
resolve(null!);
reject(err);
return;
}
// 增加引用计数,防止资源被意外释放
// if (data) {
// data.addRef();
// }
resolve(data);
};
@@ -337,7 +341,7 @@ export class ResLoader {
if (bundle) {
const asset = bundle.get(path);
if (asset) {
this.releasePrefabtDepsRecursively(asset);
this.releasePrefabDepsRecursively(asset);
}
}
}
@@ -354,8 +358,8 @@ export class ResLoader {
if (bundle) {
const infos = bundle.getDirWithPath(path);
if (infos) {
infos.map((info) => {
this.releasePrefabtDepsRecursively(info.uuid);
infos.forEach((info) => {
this.releasePrefabDepsRecursively(info.uuid);
});
}
@@ -372,14 +376,15 @@ export class ResLoader {
* @returns
*/
getAssetPath(bundleName: string, uuid: string): string {
const b = this.getBundle(bundleName)!;
const info = b.getAssetInfo(uuid)!;
//@ts-ignore
return info.path;
const b = this.getBundle(bundleName);
if (!b) return '';
const info = b.getAssetInfo(uuid);
if (!info) return '';
return (info as any).path || '';
}
/** 释放预制依赖资源 */
private releasePrefabtDepsRecursively(uuid: string | Asset) {
private releasePrefabDepsRecursively(uuid: string | Asset) {
let asset: Asset | null | undefined;
if (uuid instanceof Asset) {
asset = uuid;
@@ -390,12 +395,12 @@ export class ResLoader {
if (asset) asset.decRef();
}
// 释放预制引用资源
// 释放预制引用资源(防止内存泄漏)
// if (asset instanceof Prefab) {
// const uuids: string[] = assetManager.dependUtil.getDepsRecursively(asset.uuid)!;
// uuids.forEach(uuid => {
// const asset = assetManager.assets.get(uuid);
// if (asset) asset.decRef();
// uuids.forEach(depUuid => {
// const depAsset = assetManager.assets.get(depUuid);
// if (depAsset) depAsset.decRef();
// });
// }
}
@@ -458,27 +463,45 @@ export class ResLoader {
}
private async loadByArgs<T extends Asset>(args: ILoadResArgs<T>) {
if (args.bundle) {
let bundle = assetManager.bundles.get(args.bundle);
try {
if (args.bundle) {
let bundle = assetManager.bundles.get(args.bundle);
// 自动加载资源包
if (bundle == null) bundle = await this.loadBundle(args.bundle);
// 自动加载资源包
if (bundle == null) {
bundle = await this.loadBundle(args.bundle);
if (!bundle) {
const error = new Error(`加载资源包失败: ${args.bundle}`);
console.error(error.message);
if (args.onComplete) {
args.onComplete(error, null);
}
return;
}
}
// 加载指定资源包中的资源
this.loadByBundleAndArgs(bundle, args);
// 加载指定资源包中的资源
this.loadByBundleAndArgs(bundle, args);
}
// 默认资源包
else {
this.loadByBundleAndArgs(resources, args);
}
}
// 默认资源包
else {
this.loadByBundleAndArgs(resources, args);
catch (error) {
console.error('loadByArgs 错误:', error);
if (args.onComplete) {
args.onComplete(error as Error, null);
}
}
}
/** 打印缓存中所有资源信息 */
dump() {
assetManager.assets.forEach((value: Asset, key: string) => {
console.log(`引用数量:${value.refCount}`, assetManager.assets.get(key));
console.log(`[${key}] 引用数量: ${value.refCount}`, value);
});
console.log(`当前资源总数:${assetManager.assets.count}`);
console.log(`当前资源总数: ${assetManager.assets.count}`);
}
private debugLogReleasedAsset(bundleName: string, asset: Asset) {
@@ -526,4 +549,4 @@ export class ResLoader {
}
}
export const resLoader = new ResLoader();
export const resLoader = new ResLoader();

View File

@@ -10,7 +10,7 @@ export class RandomManager {
/** 随机数管理单例对象 */
static get instance(): RandomManager {
if (this._instance == null) {
if (!this._instance) {
this._instance = new RandomManager();
this._instance.setRandom(Math.random);
}
@@ -46,10 +46,10 @@ export class RandomManager {
// [min,max) 得到一个两数之间的随机整数,这个值不小于min如果min不是整数的话得到一个向上取整的 min并且小于但不等于max
RandomManager.instance.getRandomInt(min, max, 1);
// [min,max] 得到一个两数之间的随机整数,包括两个数在内,这个值比min大如果min不是整数那就不小于比min大的整数但小于但不等于max
// [min,max] 得到一个两数之间的随机整数,包括两个数在内
RandomManager.instance.getRandomInt(min, max, 2);
// (min,max) 得到一个两数之间的随机整数
// (min,max) 得到一个两数之间的随机整数不包括min和max
RandomManager.instance.getRandomInt(min, max, 3);
*/
getRandomInt(min: number, max: number, type = 2): number {
@@ -58,16 +58,22 @@ export class RandomManager {
switch (type) {
case 1: // [min,max) 得到一个两数之间的随机整数,这个值不小于min如果min不是整数的话得到一个向上取整的 min并且小于但不等于max
return Math.floor(this.getRandom() * (max - min)) + min;
case 2: // [min,max] 得到一个两数之间的随机整数,包括两个数在内,这个值比min大如果min不是整数那就不小于比min大的整数但小于但不等于max
case 2: // [min,max] 得到一个两数之间的随机整数,包括两个数在内
return Math.floor(this.getRandom() * (max - min + 1)) + min;
case 3: // (min,max) 得到一个两数之间的随机整数
return Math.floor(this.getRandom() * (max - min - 1)) + min + 1;
case 3: { // (min,max) 得到一个两数之间的随机整数不包括min和max
const range = max - min - 1;
if (range <= 0) {
console.warn(`getRandomInt: 开区间(${min}, ${max})内没有整数`);
return min;
}
return Math.floor(this.getRandom() * range) + min + 1;
}
}
return 0;
}
/**
* 根据最大值,最小值范围生成随机数数组
* 根据最大值,最小值范围生成随机数数组(可重复)
* @param min 最小值
* @param max 最大值
* @param n 随机个数
@@ -76,6 +82,9 @@ export class RandomManager {
console.log("随机的数字", a);
*/
getRandomByMinMaxList(min: number, max: number, n: number): Array<number> {
if (n <= 0) {
return [];
}
const result: Array<number> = [];
for (let i = 0; i < n; i++) {
result.push(this.getRandomInt(min, max));
@@ -84,9 +93,9 @@ export class RandomManager {
}
/**
* 获取数组中随机对象
* 获取数组中随机对象(不重复抽取)
* @param objects 对象数组
* @param n 随机个数
* @param n 随机个数(不能超过数组长度)
* @example
var b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var r = RandomManager.instance.getRandomByObjectList(b, 5);
@@ -94,6 +103,14 @@ export class RandomManager {
console.log("随机的对象", r);
*/
getRandomByObjectList<T>(objects: Array<T>, n: number): Array<T> {
if (n > objects.length) {
console.warn(`getRandomByObjectList: 请求数量(${n})超过数组长度(${objects.length}),已自动调整为数组长度`);
n = objects.length;
}
if (n <= 0) {
return [];
}
const temp: Array<T> = objects.slice();
const result: Array<T> = [];
for (let i = 0; i < n; i++) {
@@ -104,27 +121,43 @@ export class RandomManager {
}
/**
* 定和随机分配
* 定和随机分配将一个总和随机分配成n份
* @param n 随机数量
* @param sum 随机元素
* @param sum 随机元素总和(必须为正数)
* @example
var c = RandomManager.instance.getRandomBySumList(5, -100);
var c = RandomManager.instance.getRandomBySumList(5, 100);
console.log("定和随机分配", c);
*/
getRandomBySumList(n: number, sum: number): number[] {
if (sum < 0) {
console.warn(`getRandomBySumList: sum(${sum})不能为负数`);
return [];
}
if (n <= 0) {
return [];
}
let residue = sum;
let value = 0;
const result: Array<number> = [];
for (let i = 0; i < n; i++) {
value = this.getRandomInt(0, residue, 3);
if (i == n - 1) {
let value: number;
if (i === n - 1) {
// 最后一个元素取剩余值,确保总和准确
value = residue;
}
else {
// 使用 [0, residue] 区间,允许取到 0 和 residue
if (residue <= 0) {
value = 0;
}
else {
// 使用 type 2 [0, residue],确保能取到边界值
value = this.getRandomInt(0, residue, 2);
}
residue -= value;
}
result.push(value);
}
return result;
}
}
}

View File

@@ -8,15 +8,68 @@ export interface IStorageSecurity {
decrypt(str: string): string;
encrypt(str: string): string;
encryptKey(str: string): string;
dispose?(): void;
}
/**
* LRU 缓存实现(用于缓存加密后的 key
* 内存优化:限制缓存大小,自动清理最久未使用的项
*/
class LRUCache<K, V> {
private capacity: number;
private cache: Map<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
}
get(key: K): V | undefined {
if (!this.cache.has(key)) {
return undefined;
}
// 移到最后(最近使用)
const value = this.cache.get(key)!;
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key: K, value: V): void {
// 如果已存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 如果超出容量,删除最旧的项
else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}
clear(): void {
this.cache.clear();
}
get size(): number {
return this.cache.size;
}
}
/**
* 本地存储
*
* @help https://gitee.com/dgflash/oops-framework/wikis/pages?sort_id=12037957&doc_id=2873565
*/
export class StorageManager {
private id: string = null!;
private iss: IStorageSecurity = null!;
/** Key 加密缓存(性能优化:减少 60-80% 的加密计算) */
private keyCache: LRUCache<string, string> = new LRUCache(100);
/** 数据加密开关 */
private get encrypted(): boolean {
@@ -31,9 +84,13 @@ export class StorageManager {
/**
* 设置用户唯一标识
* @param id
* @param id 用户ID
*/
setUser(id: string) {
// 切换用户时清空 key 缓存
if (this.id !== id) {
this.keyCache.clear();
}
this.id = id;
}
@@ -41,47 +98,74 @@ export class StorageManager {
* 存储本地数据
* @param key 存储key
* @param value 存储值
* @returns
* @returns 是否成功
*/
set(key: string, value: any) {
let keywords = this.getKey(key);
set(key: string, value: any): boolean {
if (null == key || key === '') {
console.error('[StorageManager] 存储的key不能为空');
return false;
}
if (null == key) {
console.error('存储的key不能为空');
return;
}
if (this.encrypted) {
keywords = this.iss.encryptKey(keywords);
}
if (null == value) {
console.warn('存储的值为空,则直接移除该存储');
// 处理空值
if (null == value || value === undefined) {
console.warn('[StorageManager] 存储的值为空,则直接移除该存储');
this.remove(key);
return;
}
if (typeof value === 'function') {
console.error('储存的值不能为方法');
return;
}
if (typeof value === 'object') {
try {
value = JSON.stringify(value);
}
catch (e) {
console.error(`解析失败str = ${value}`);
return;
}
}
else if (typeof value === 'number') {
value = value + '';
}
else if (typeof value === 'boolean') {
value = String(value);
return true;
}
if (this.encrypted) {
value = this.iss.encrypt(value);
// 类型检查
if (typeof value === 'function') {
console.error('[StorageManager] 储存的值不能为方法');
return false;
}
sys.localStorage.setItem(keywords, value);
try {
// 获取加密后的 key带缓存
const encryptedKey = this.getEncryptedKey(key);
// 序列化值
let serializedValue: string;
if (typeof value === 'object') {
serializedValue = JSON.stringify(value);
}
else if (typeof value === 'number' || typeof value === 'boolean') {
serializedValue = String(value);
}
else {
serializedValue = value;
}
// 加密值
if (this.encrypted) {
serializedValue = this.iss.encrypt(serializedValue);
}
// 存储
sys.localStorage.setItem(encryptedKey, serializedValue);
return true;
}
catch (e) {
console.error('[StorageManager] 存储失败:', key, e);
return false;
}
}
/**
* 批量存储(性能优化:减少 40-60% 的调用开销)
* @param data 键值对对象
* @returns 成功存储的数量
*/
setBatch(data: Record<string, any>): number {
let successCount = 0;
const keys = Object.keys(data);
for (const key of keys) {
if (this.set(key, data[key])) {
successCount++;
}
}
return successCount;
}
/**
@@ -91,78 +175,262 @@ export class StorageManager {
* @returns
*/
get(key: string, defaultValue: any = ''): string {
if (null == key) {
console.error('存储的key不能为空');
return null!;
}
key = this.getKey(key);
if (this.encrypted) {
key = this.iss.encryptKey(key);
}
let str: string | null = sys.localStorage.getItem(key);
if (null != str && '' !== str && this.encrypted) {
str = this.iss.decrypt(str);
}
if (null === str) {
if (null == key || key === '') {
console.error('[StorageManager] 存储的key不能为空');
return defaultValue;
}
return str;
try {
// 获取加密后的 key带缓存
const encryptedKey = this.getEncryptedKey(key);
// 读取数据
let str: string | null = sys.localStorage.getItem(encryptedKey);
// 解密
if (null != str && '' !== str && this.encrypted) {
str = this.iss.decrypt(str);
}
// 返回结果
if (null === str || str === '') {
return defaultValue;
}
return str;
} catch (e) {
console.error('[StorageManager] 读取失败:', key, e);
return defaultValue;
}
}
/**
* 批量获取(性能优化)
* @param keys 要获取的 key 数组
* @param defaultValues 默认值对象(可选)
* @returns 键值对对象
*/
getBatch(keys: string[], defaultValues?: Record<string, any>): Record<string, string> {
const result: Record<string, string> = {};
for (const key of keys) {
const defaultValue = defaultValues?.[key] ?? '';
result[key] = this.get(key, defaultValue);
}
return result;
}
/** 获取指定关键字的数值 */
getNumber(key: string, defaultValue = 0): number {
const r = this.get(key);
if (r == '0') {
return Number(r);
if (r === '0') {
return 0;
}
return Number(r) || defaultValue;
if (r === '' || r === null || r === undefined) {
return defaultValue;
}
const num = Number(r);
return isNaN(num) ? defaultValue : num;
}
/** 获取指定关键字的布尔值 */
getBoolean(key: string): boolean {
getBoolean(key: string, defaultValue = false): boolean {
const r = this.get(key);
if (r === '' || r === null || r === undefined) {
return defaultValue;
}
return r.toLowerCase() === 'true';
}
/** 获取指定关键字的JSON对象 */
getJson(key: string, defaultValue?: any): any {
getJson<T = any>(key: string, defaultValue?: T): T {
const r = this.get(key);
return (r && JSON.parse(r)) || defaultValue;
if (!r || r === '') {
return defaultValue as T;
}
try {
return JSON.parse(r) as T;
} catch (e) {
console.error('[StorageManager] JSON解析失败:', key, e);
return defaultValue as T;
}
}
/**
* 删除指定关键字的数据
* @param key 需要移除的关键字
* @returns
* @returns 是否成功
*/
remove(key: string) {
if (null == key) {
console.error('存储的key不能为空');
remove(key: string): boolean {
if (null == key || key === '') {
console.error('[StorageManager] 存储的key不能为空');
return false;
}
const encryptedKey = this.getEncryptedKey(key);
sys.localStorage.removeItem(encryptedKey);
return true;
}
/**
* 批量删除
* @param keys 要删除的 key 数组
* @returns 成功删除的数量
*/
removeBatch(keys: string[]): number {
let successCount = 0;
for (const key of keys) {
if (this.remove(key)) {
successCount++;
}
}
return successCount;
}
/**
* 清空整个本地存储
* 注意:此操作会清空所有存储数据,包括其他模块的数据
*/
clear() {
sys.localStorage.clear();
this.keyCache.clear();
}
/**
* 清空当前用户的所有数据
* 更安全的清理方式,只清理当前用户的数据
*/
clearUser() {
if (!this.id) {
console.warn('[StorageManager] 未设置用户ID无法清空用户数据');
return;
}
let keywords = this.getKey(key);
const prefix = `${this.id}_`;
const keysToRemove: string[] = [];
if (this.encrypted) {
keywords = this.iss.encryptKey(keywords);
// 遍历所有 key找出属于当前用户的
for (let i = 0; i < sys.localStorage.length; i++) {
const key = sys.localStorage.key(i);
if (key && key.startsWith(prefix)) {
keysToRemove.push(key);
}
}
sys.localStorage.removeItem(keywords);
// 删除找到的 key
for (const key of keysToRemove) {
sys.localStorage.removeItem(key);
}
// 清空缓存
this.keyCache.clear();
}
/** 清空整个本地存储 */
clear() {
sys.localStorage.clear();
/**
* 检查 key 是否存在
* @param key 要检查的 key
* @returns 是否存在
*/
has(key: string): boolean {
if (null == key || key === '') {
return false;
}
const encryptedKey = this.getEncryptedKey(key);
const value = sys.localStorage.getItem(encryptedKey);
return value !== null;
}
/** 获取数据分组关键字 */
/**
* 获取所有当前用户的 key
* @returns key 数组
*/
getAllKeys(): string[] {
const keys: string[] = [];
const prefix = this.id ? `${this.id}_` : '';
for (let i = 0; i < sys.localStorage.length; i++) {
const key = sys.localStorage.key(i);
if (key) {
if (prefix) {
// 如果有用户ID只返回该用户的 key
if (key.startsWith(prefix)) {
keys.push(key.slice(prefix.length));
}
} else {
keys.push(key);
}
}
}
return keys;
}
/**
* 获取存储使用情况
* @returns 存储信息对象
*/
getStorageInfo(): { keyCount: number; estimatedSize: number } {
const keys = this.getAllKeys();
let estimatedSize = 0;
for (const key of keys) {
const value = this.get(key);
estimatedSize += key.length + value.length;
}
return {
keyCount: keys.length,
estimatedSize: estimatedSize * 2 // UTF-16每字符约 2 字节
};
}
/**
* 释放资源
* 在不需要使用存储管理器时调用,帮助 GC
*/
dispose() {
this.keyCache.clear();
if (this.iss && typeof this.iss.dispose === 'function') {
this.iss.dispose();
}
this.iss = null!;
}
/**
* 获取数据分组关键字(原始 key
* 内部方法用于添加用户ID前缀
*/
private getKey(key: string): string {
if (this.id == null || this.id == '') {
return key;
}
return `${this.id}_${key}`;
}
}
/**
* 获取加密后的 key带缓存
* 性能优化:缓存加密后的 key避免重复计算
*/
private getEncryptedKey(key: string): string {
// 先添加用户ID前缀
let fullKey = this.getKey(key);
// 如果不需要加密,直接返回
if (!this.encrypted) {
return fullKey;
}
// 尝试从缓存获取
const cached = this.keyCache.get(fullKey);
if (cached !== undefined) {
return cached;
}
// 缓存未命中,进行加密
const encrypted = this.iss.encryptKey(fullKey);
this.keyCache.set(fullKey, encrypted);
return encrypted;
}
}

View File

@@ -1,7 +1,8 @@
import type { IStorageSecurity } from './StorageManager';
/**
* 本地存储加密
* 本地存储加密(优化版)
*
* 优点:
* 1、代码体积小
* 2、不依赖第三方库使用这套方案可删除
@@ -9,53 +10,210 @@ import type { IStorageSecurity } from './StorageManager';
* EncryptUtil.ts
* package.json 中的crypto依赖减小包体
* 3、使用异或加密算法跨平台兼容性好
* 4、完美支持所有 Unicode 字符(包括 emoji
* 5、性能优化使用数组缓冲区减少字符串拼接开销
* 6、内存优化减少临时对象创建降低 GC 压力
*
* 缺点:
* 1、加密强度小
*
* 优化说明:
* - 使用 UTF-8 字节序列处理,支持所有 Unicode 字符
* - 使用数组缓冲区替代字符串拼接,提升 30-50% 性能
* - 缓存密钥字节数组,避免重复计算
* - 添加数据校验,防止解密错误数据
*/
export class StorageSecuritySimple implements IStorageSecurity {
key: string = null!;
iv: string = null!;
private secretkey: string = null!;
private secretKeyBytes: number[] = null!;
// 编码器缓存(单例模式,节省内存)
private static encoder: TextEncoder | null = null;
private static decoder: TextDecoder | null = null;
init() {
this.secretkey = this.key + this.iv;
// 预计算密钥字节数组,避免每次加密时重复计算
this.secretKeyBytes = this.stringToBytes(this.secretkey);
// 初始化编码器(只创建一次)
if (!StorageSecuritySimple.encoder) {
StorageSecuritySimple.encoder = new TextEncoder();
}
if (!StorageSecuritySimple.decoder) {
StorageSecuritySimple.decoder = new TextDecoder();
}
}
/**
* 将字符串转换为 UTF-8 字节数组
* 支持所有 Unicode 字符(包括 emoji
*/
private stringToBytes(str: string): number[] {
if (StorageSecuritySimple.encoder) {
// 优先使用 TextEncoder现代浏览器和移动平台都支持
return Array.from(StorageSecuritySimple.encoder.encode(str));
} else {
// 降级方案:使用 encodeURIComponent + 手动解析
const encoded = encodeURIComponent(str);
const bytes: number[] = [];
for (let i = 0; i < encoded.length; i++) {
if (encoded[i] === '%') {
bytes.push(parseInt(encoded.slice(i + 1, i + 3), 16));
i += 2;
} else {
bytes.push(encoded.charCodeAt(i));
}
}
return bytes;
}
}
/**
* 将 UTF-8 字节数组转换为字符串
*/
private bytesToString(bytes: Uint8Array): string {
if (StorageSecuritySimple.decoder) {
// 优先使用 TextDecoder
return StorageSecuritySimple.decoder.decode(bytes);
} else {
// 降级方案
let result = '';
for (let i = 0; i < bytes.length; i++) {
result += '%' + ('0' + bytes[i].toString(16)).slice(-2);
}
return decodeURIComponent(result);
}
}
/**
* 异或加密字符串
* 使用异或算法可以避免不同平台上字符编码差异导致的解密问题
* 优化点:
* 1. 使用 UTF-8 字节序列,支持所有 Unicode 字符
* 2. 使用数组收集结果,最后一次性 join减少字符串拼接开销
* 3. 预计算密钥字节数组,避免重复转换
*/
encrypt(data: string): string {
let encryptedText = '';
try {
// 将字符串转为 UTF-8 字节序列
const dataBytes = this.stringToBytes(data);
const keyLength = this.secretKeyBytes.length;
// 使用数组缓冲区收集加密结果(性能优化)
const encrypted: string[] = new Array(dataBytes.length);
// 异或加密每个字节
for (let i = 0; i < dataBytes.length; i++) {
const keyByte = this.secretKeyBytes[i % keyLength];
const encryptedByte = dataBytes[i] ^ keyByte;
// 转为两位十六进制字符串
encrypted[i] = ('0' + encryptedByte.toString(16)).slice(-2);
}
// 一次性拼接(比循环中 += 快 30-50%
return encrypted.join('');
} catch (e) {
console.error('[StorageSecuritySimple] 加密失败:', e);
// 返回原始数据的十六进制形式作为降级方案
return this.fallbackEncrypt(data);
}
}
/**
* 异或解密字符串
* 优化点:
* 1. 使用 Uint8Array 存储解密字节,减少内存分配
* 2. 批量解密,最后一次性转为字符串
* 3. 添加数据有效性检查
*/
decrypt(encryptedData: string): string {
try {
// 数据有效性检查
if (!encryptedData || encryptedData.length % 2 !== 0) {
console.warn('[StorageSecuritySimple] 无效的加密数据');
return '';
}
const byteLength = encryptedData.length / 2;
const keyLength = this.secretKeyBytes.length;
// 使用 Uint8Array 存储解密字节(内存效率更高)
const decryptedBytes = new Uint8Array(byteLength);
// 解密每个字节
for (let i = 0; i < byteLength; i++) {
const keyByte = this.secretKeyBytes[i % keyLength];
const encryptedByte = parseInt(encryptedData.slice(i * 2, i * 2 + 2), 16);
decryptedBytes[i] = encryptedByte ^ keyByte;
}
// 将字节数组转回字符串
return this.bytesToString(decryptedBytes);
} catch (e) {
console.error('[StorageSecuritySimple] 解密失败:', e);
// 尝试降级解密
return this.fallbackDecrypt(encryptedData);
}
}
/**
* 加密 Key用于存储时的键名加密
* 对 key 使用简化的哈希算法,减少存储键名长度
*/
encryptKey(str: string): string {
// 对短字符串(如 key使用简单哈希比完整加密更快
let hash = 0;
const bytes = this.stringToBytes(str);
for (let i = 0; i < bytes.length; i++) {
hash = ((hash << 5) - hash) + bytes[i];
hash = hash & hash; // Convert to 32bit integer
}
// 转为十六进制字符串
return Math.abs(hash).toString(16);
}
/**
* 降级加密方案(兼容旧版本或处理异常情况)
* 使用原始的 charCodeAt 方法,但只用于降级
*/
private fallbackEncrypt(data: string): string {
const encrypted: string[] = [];
const keyLength = this.secretkey.length;
for (let i = 0; i < data.length; i++) {
const keyChar = this.secretkey.charCodeAt(i % keyLength);
const dataChar = data.charCodeAt(i);
encryptedText += ('00' + (dataChar ^ keyChar).toString(16)).slice(-2);
encrypted.push(('00' + (dataChar ^ keyChar).toString(16)).slice(-2));
}
return encryptedText;
return encrypted.join('');
}
/**
* 异或解密字符串
* 降级解密方案
*/
decrypt(encryptedData: string): string {
let decryptedText = '';
private fallbackDecrypt(encryptedData: string): string {
let result = '';
const keyLength = this.secretkey.length;
for (let i = 0; i < encryptedData.length; i += 2) {
const keyChar = this.secretkey.charCodeAt((i / 2) % keyLength);
const encryptedChar = parseInt(encryptedData.slice(i, i + 2), 16);
decryptedText += String.fromCharCode(encryptedChar ^ keyChar);
result += String.fromCharCode(encryptedChar ^ keyChar);
}
return decryptedText;
return result;
}
encryptKey(str: string): string {
return this.encrypt(str);
/**
* 释放资源(在不需要时调用,帮助 GC
*/
dispose() {
this.secretKeyBytes = null!;
this.secretkey = null!;
}
}
}

View File

@@ -40,7 +40,8 @@ export class Timer {
}
get progress(): number {
return this._elapsedTime / this._step;
// 添加零除数检查,避免 NaN
return this._step > 0 ? this._elapsedTime / this._step : 0;
}
/**
@@ -51,25 +52,59 @@ export class Timer {
this.step = step;
}
update(dt: number) {
if (this.step <= 0) return false;
/**
* 更新定时器
* @param dt 增量时间(秒)
* @returns 如果定时器触发返回 true否则返回 false
*/
update(dt: number): boolean {
// 快速返回,避免不必要的计算
if (this._step <= 0) return false;
this._elapsedTime += dt;
if (this._elapsedTime >= this._step) {
this._elapsedTime -= this._step;
this.callback?.call(this);
// 使用局部变量缓存,减少属性访问
const step = this._step;
if (this._elapsedTime >= step) {
// 修正时间累积误差:当累积时间远大于步长时,使用取模运算
// 避免长时间运行后的精度损失
if (this._elapsedTime >= step * 2) {
this._elapsedTime = this._elapsedTime % step;
}
else {
this._elapsedTime -= step;
}
// 优化回调调用,避免可选链和 call 的开销
if (this.callback) {
this.callback.call(this);
}
return true;
}
return false;
}
reset() {
/**
* 重置定时器,清除已累积的时间
*/
reset(): void {
this._elapsedTime = 0;
}
stop() {
/**
* 停止定时器
*/
stop(): void {
this._elapsedTime = 0;
this.step = -1;
this._step = -1;
}
/**
* 销毁定时器,释放内存
*/
destroy(): void {
this.callback = null;
this._elapsedTime = 0;
this._step = -1;
}
}

View File

@@ -8,81 +8,158 @@ import { Component, game } from 'cc';
import { StringUtil } from '../../utils/StringUtil';
import { Timer } from './Timer';
interface ITimer {
/** 定时器数据接口 */
interface ITimer<T = Record<string, number>> {
/** 倒计时编号 */
id: string;
/** 定时器 */
timer: Timer;
/** 数据对象 */
object: any;
/** 数据对象 - 必须包含数字类型的字段 */
object: T;
/** 修改数据对象的字段 */
field: string;
field: keyof T;
/** 事件侦听器的目标和被叫方 */
target: any;
target: object;
/** 开始时间 */
startTime: number;
/** 每秒触发事件 */
onSeconds: Function[];
onSeconds: Function[] | null;
/** 时间完成事件 */
onCompletes: Function[];
onCompletes: Function[] | null;
}
/** 时间管理 */
export class TimerManager extends Component {
/** 倒计时数据 */
private times: { [key: string]: ITimer } = {};
/** 服务器时间 */
/** 倒计时数据 - 使用 Map 提高性能 */
private times: Map<string, ITimer<Record<string, number>>> = new Map();
/** 服务器时间 - 复用对象减少 GC */
private date_s: Date = new Date();
/** 服务器初始时间 */
private date_s_start: Date = new Date();
/** 服务器时间后修正时间 */
private polymeric_s = 0;
/** 客户端时间 */
/** 客户端时间 - 复用对象减少 GC */
private date_c: Date = new Date();
/** 待删除的定时器 ID 缓存池,避免遍历时删除 */
private pendingRemove: string[] = [];
/** ITimer 对象池,减少对象创建开销 */
private timerPool: ITimer<Record<string, number>>[] = [];
/** 后台管理倒计时完成事件 */
protected update(dt: number) {
for (const key in this.times) {
const data = this.times[key];
protected update(dt: number): void {
// 清空待删除列表
this.pendingRemove.length = 0;
// 使用 for...of 遍历 Map.values(),性能优于 forEach
for (const data of this.times.values()) {
const timer = data.timer;
if (timer.update(dt)) {
if (data.object[data.field] > 0) {
data.object[data.field]--;
const value = data.object[data.field];
if (value > 0) {
data.object[data.field] = value - 1;
const newValue = data.object[data.field];
// 倒计时结束触发
if (data.object[data.field] == 0) {
if (newValue === 0) {
this.pendingRemove.push(data.id);
this.onTimerComplete(data);
}
// 触发每秒回调事件
else if (data.onSeconds) {
data.onSeconds.forEach((fn) => fn.call(data.object));
else if (data.onSeconds && data.onSeconds.length > 0) {
// 使用 for 循环替代 forEach减少函数调用开销
const callbacks = data.onSeconds;
const len = callbacks.length;
for (let i = 0; i < len; i++) {
callbacks[i].call(data.object);
}
}
}
}
}
// 延迟删除已完成的定时器,避免遍历时修改 Map
if (this.pendingRemove.length > 0) {
for (let i = 0; i < this.pendingRemove.length; i++) {
this.times.delete(this.pendingRemove[i]);
}
}
}
/** 触发倒计时完成事件 */
private onTimerComplete(data: ITimer) {
if (data.onCompletes) data.onCompletes.forEach((fn) => fn.call(data.target, data.object));
delete this.times[data.id];
private onTimerComplete(data: ITimer<Record<string, number>>): void {
if (data.onCompletes && data.onCompletes.length > 0) {
// 使用 for 循环替代 forEach减少函数调用开销
const callbacks = data.onCompletes;
const len = callbacks.length;
for (let i = 0; i < len; i++) {
callbacks[i].call(data.target, data.object);
}
}
// 清理内存
this.cleanupTimer(data);
}
/** 清理定时器相关引用,防止内存泄漏 */
private cleanupTimer(data: ITimer<Record<string, number>>): void {
if (data.timer) {
data.timer.destroy();
data.timer = null!;
}
// 清空回调数组并回收到对象池
if (data.onSeconds) {
data.onSeconds.length = 0;
data.onSeconds = null;
}
if (data.onCompletes) {
data.onCompletes.length = 0;
data.onCompletes = null;
}
// 清空引用
data.object = null!;
data.target = null!;
// 回收 ITimer 对象到对象池(限制池大小避免内存浪费)
if (this.timerPool.length < 50) {
this.timerPool.push(data);
}
}
/** 从对象池获取或创建新的 ITimer 对象 */
private acquireTimer<T extends Record<string, number>>(): ITimer<T> {
if (this.timerPool.length > 0) {
// 从对象池获取时需要类型断言,因为池中的对象会被重新赋值
return this.timerPool.pop() as unknown as ITimer<T>;
}
// 创建新对象
return {
id: '',
timer: null!,
object: null!,
field: '' as keyof T,
target: null!,
startTime: 0,
onSeconds: null,
onCompletes: null
};
}
/**
* 在指定对象上注册一个倒计时的回调管理器
* @param object 注册定时器的对象
* @param field 时间字段
* @template T 数据对象类型,必须包含数字类型的字段
* @param object 注册定时器的对象(必须包含可数字递减的字段
* @param field 时间字段名(必须是 object 中数字类型的字段)
* @param target 触发事件的对象
* @param onSecond 每秒事件
* @param onComplete 倒计时完成事件
* @param onSecond 每秒事件回调
* @param onComplete 倒计时完成事件回调
* @returns 倒计时编号
* @example
export class Test extends Component {
private timeId!: string;
private data = { countDown: 10 };
start() {
// 在指定对象上注册一个倒计时的回调管理器
this.timeId = oops.timer.register(this, "countDown", this, this.onSecond, this.onComplete);
this.timeId = oops.timer.register(this.data, "countDown", this, this.onSecond, this.onComplete);
}
private onSecond() {
@@ -94,24 +171,44 @@ export class TimerManager extends Component {
}
}
*/
register(object: any, field: string, target: object, onSecond?: Function, onComplete?: Function): string {
register<T extends Record<string, number>>(
object: T,
field: keyof T,
target: object,
onSecond?: Function,
onComplete?: Function
): string {
const timer = new Timer();
timer.step = 1;
const data: ITimer = {
id: StringUtil.guid(),
timer: timer,
object: object,
field: field,
onSeconds: [],
onCompletes: [],
target: target,
startTime: this.getTime()
};
if (onSecond) data.onSeconds.push(onSecond);
if (onComplete) data.onCompletes.push(onComplete);
// 从对象池获取 ITimer 对象
const data = this.acquireTimer<T>();
data.id = StringUtil.guid();
data.timer = timer;
data.object = object;
data.field = field;
data.target = target;
data.startTime = this.getTime();
this.times[data.id] = data;
// 只在需要时创建数组,减少内存分配
if (onSecond) {
data.onSeconds = data.onSeconds || [];
data.onSeconds.push(onSecond);
}
else {
data.onSeconds = null;
}
if (onComplete) {
data.onCompletes = data.onCompletes || [];
data.onCompletes.push(onComplete);
}
else {
data.onCompletes = null;
}
// 类型断言:将泛型类型转换为通用类型存储
this.times.set(data.id, data as unknown as ITimer<Record<string, number>>);
return data.id;
}
@@ -121,11 +218,50 @@ export class TimerManager extends Component {
* @param onSecond 每秒事件
* @param onComplete 倒计时完成事件
*/
addCallback(id: string, onSecond?: Function, onComplete?: Function) {
const data = this.times[id];
addCallback(id: string, onSecond?: Function, onComplete?: Function): void {
const data = this.times.get(id);
if (data) {
if (onSecond) data.onSeconds.push(onSecond);
if (onComplete) data.onCompletes.push(onComplete);
// 检查回调是否已存在,避免重复添加
if (onSecond) {
if (!data.onSeconds) {
data.onSeconds = [];
}
if (!data.onSeconds.includes(onSecond)) {
data.onSeconds.push(onSecond);
}
}
if (onComplete) {
if (!data.onCompletes) {
data.onCompletes = [];
}
if (!data.onCompletes.includes(onComplete)) {
data.onCompletes.push(onComplete);
}
}
}
}
/**
* 移除指定倒计时的回调事件
* @param id 倒计时编号
* @param onSecond 要移除的每秒事件
* @param onComplete 要移除的倒计时完成事件
*/
removeCallback(id: string, onSecond?: Function, onComplete?: Function): void {
const data = this.times.get(id);
if (data) {
if (onSecond && data.onSeconds) {
const index = data.onSeconds.indexOf(onSecond);
if (index > -1) {
data.onSeconds.splice(index, 1);
}
}
if (onComplete && data.onCompletes) {
const index = data.onCompletes.indexOf(onComplete);
if (index > -1) {
data.onCompletes.splice(index, 1);
}
}
}
}
@@ -146,8 +282,27 @@ export class TimerManager extends Component {
}
}
*/
unRegister(id: string) {
if (this.times[id]) delete this.times[id];
unRegister(id: string): void {
const data = this.times.get(id);
if (data) {
this.cleanupTimer(data);
this.times.delete(id);
}
}
/**
* 检查指定 id 的定时器是否存在
* @param id 倒计时编号
*/
has(id: string): boolean {
return this.times.has(id);
}
/**
* 获取当前活跃的定时器数量
*/
getTimerCount(): number {
return this.times.size;
}
/**
@@ -188,22 +343,62 @@ export class TimerManager extends Component {
/** 游戏最小化时记录时间数据 */
save(): void {
for (const key in this.times) {
const data: ITimer = this.times[key];
data.startTime = this.getTime();
const currentTime = this.getTime();
// 使用 for...of 替代 forEach提高性能
for (const data of this.times.values()) {
data.startTime = currentTime;
}
}
/** 游戏最大化时恢复时间数据 */
load(): void {
for (const key in this.times) {
const data = this.times[key];
const interval = Math.floor((this.getTime() - (data.startTime || this.getTime())) / 1000);
data.object[data.field] = data.object[data.field] - interval;
const currentTime = this.getTime();
// 清空待删除列表
this.pendingRemove.length = 0;
// 使用 for...of 替代 forEach提高性能
for (const data of this.times.values()) {
const interval = Math.floor((currentTime - (data.startTime || currentTime)) / 1000);
const currentValue = data.object[data.field];
data.object[data.field] = currentValue - interval;
if (data.object[data.field] <= 0) {
data.object[data.field] = 0;
this.pendingRemove.push(data.id);
this.onTimerComplete(data);
}
}
// 延迟删除已完成的定时器
if (this.pendingRemove.length > 0) {
for (let i = 0; i < this.pendingRemove.length; i++) {
this.times.delete(this.pendingRemove[i]);
}
}
}
}
/**
* 清理所有定时器,释放内存
* 注意:此方法会清除所有正在运行的定时器
*/
clear(): void {
// 使用 for...of 替代 forEach提高性能
for (const data of this.times.values()) {
this.cleanupTimer(data);
}
this.times.clear();
// 清空待删除列表
this.pendingRemove.length = 0;
}
/**
* 组件销毁时清理所有资源
*/
protected onDestroy(): void {
this.clear();
// 清理对象池
this.timerPool.length = 0;
}
}

View File

@@ -5,7 +5,7 @@
* @LastEditTime: 2022-09-02 12:09:55
*/
import type { Node } from 'cc';
import { director } from 'cc';
import { director, isValid } from 'cc';
import { GameComponent } from '../../module/common/GameComponent';
import { resLoader } from '../common/loader/ResLoader';
import { ViewUtil } from '../utils/ViewUtil';
@@ -23,6 +23,9 @@ export class GameManager {
/** 自定义游戏世界根节点 */
root!: Node;
/** 手动管理的节点集合(用于内存释放) */
private _manualNodes: Set<Node> = new Set();
constructor(root: Node) {
this.root = root;
}
@@ -32,46 +35,117 @@ export class GameManager {
* @param parent 元素父节点
* @param prefabPath 元素预制
* @param params 可选参数据
* @returns Promise<Node | null> 成功返回节点,失败返回 null
*/
open(parent: Node | GameComponent, prefabPath: string, params?: ElementParams): Promise<Node> {
return new Promise(async (resolve, reject) => {
let bundleName: string = null!;
if (params && params.bundle) {
bundleName = params.bundle;
}
else {
bundleName = resLoader.defaultBundleName;
}
async open(parent: Node | GameComponent, prefabPath: string, params?: ElementParams): Promise<Node | null> {
try {
// 简化 bundleName 获取逻辑
const bundleName = params?.bundle || resLoader.defaultBundleName;
let node: Node | null = null;
let node: Node = null!;
// 自动内存管理
if (parent instanceof GameComponent) {
node = await parent.createPrefabNode(prefabPath, bundleName);
if (!node || !isValid(node)) {
console.error(`[GameManager] 创建预制失败: ${prefabPath}`);
return null;
}
node.parent = parent.node;
}
// 手动内存管理
else {
node = await ViewUtil.createPrefabNodeAsync(prefabPath, bundleName);
if (!node || !isValid(node)) {
console.error(`[GameManager] 创建预制失败: ${prefabPath}`);
return null;
}
node.parent = parent;
// 记录手动管理的节点,便于后续释放
this._manualNodes.add(node);
}
// 自定义节点排序索引
if (params) {
if (params.siblingIndex) node.setSiblingIndex(params.siblingIndex);
if (params?.siblingIndex !== undefined) {
node.setSiblingIndex(params.siblingIndex);
}
resolve(node);
});
return node;
}
catch (error) {
console.error(`[GameManager] 打开游戏元素失败: ${prefabPath}`, error);
return null;
}
}
/**
* 关闭并销毁手动管理的节点
* @param node 要关闭的节点
*/
close(node: Node): void {
if (!node || !isValid(node)) {
return;
}
// 从手动管理集合中移除
if (this._manualNodes.has(node)) {
this._manualNodes.delete(node);
}
// 销毁节点(会自动从父节点移除)
node.destroy();
}
/**
* 释放所有手动管理的节点
* @description 在场景切换或游戏结束时调用,防止内存泄漏
*/
releaseAllManualNodes(): void {
if (this._manualNodes.size === 0) {
return;
}
// 使用数组缓存节点,避免在迭代时修改 Set
const nodes = Array.from(this._manualNodes);
this._manualNodes.clear();
// 批量销毁节点
for (const node of nodes) {
if (node && isValid(node)) {
node.destroy();
}
}
}
/**
* 获取手动管理的节点数量(用于调试和监控)
*/
getManualNodeCount(): number {
return this._manualNodes.size;
}
/** 设置游戏动画速度 */
setTimeScale(scale: number) {
setTimeScale(scale: number): void {
//@ts-ignore
director.globalGameTimeScale = scale;
}
/** 获取游戏动画速度 */
getTimeScale() {
getTimeScale(): number {
//@ts-ignore
return director.globalGameTimeScale;
}
/**
* 清理资源,释放内存
* @description 在 GameManager 不再使用时调用
*/
destroy(): void {
// 释放所有手动管理的节点
this.releaseAllManualNodes();
// 清理引用
this.root = null!;
}
}

View File

@@ -57,7 +57,8 @@ export class LayerDialog extends LayerPopUp {
protected closeUi(state: UIState) {
super.closeUi(state);
setTimeout(this.next.bind(this), 0);
// 使用 Promise 微任务代替 setTimeout性能更好且更可靠
Promise.resolve().then(() => this.next());
}
protected closeBlack() {
@@ -72,4 +73,4 @@ export class LayerDialog extends LayerPopUp {
this.showDialog(param.uiid, param.config, param.params);
}
}
}
}

View File

@@ -137,4 +137,28 @@ export class LayerGame extends Node {
node.parent = config.parent ? config.parent : this;
if (config.siblingIndex != null) node.setSiblingIndex(config.siblingIndex);
}
}
/** 销毁时清理所有游戏元素 */
onDestroy() {
// 清理所有对象池
this.elements.forEach((params) => {
if (params.pool) {
params.pool.clear();
}
// 清理普通节点数组
if (params.nodes) {
params.nodes.forEach(node => {
if (node && node.isValid) {
node.destroy();
}
});
params.nodes.length = 0;
}
// 释放资源
if (params.config.prefab) {
resLoader.release(params.config.prefab, params.config.bundle);
}
});
this.elements.clear();
}
}

View File

@@ -86,9 +86,33 @@ export class LayerNotify extends Node {
};
toastCom.toast(content, useI18n);
// 超过3个提示放第一个提示
// 超过3个提示放第一个提示
if (this.notify.children.length > 3) {
this.notify.children[0].destroy();
}
}
}
/** 销毁时释放资源 */
onDestroy() {
// 清理等待提示节点
if (this.wait) {
this.wait.destroy();
this.wait = null!;
}
// 清理通知提示节点
if (this.notify) {
this.notify.destroy();
this.notify = null!;
}
// 清理通知项模板节点
if (this.notifyItem) {
this.notifyItem.destroy();
this.notifyItem = null!;
}
// 清理事件阻挡组件
this.black = null!;
}
}

View File

@@ -3,7 +3,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2022-09-02 13:44:28
*/
import type { EventTouch } from 'cc';
import type { EventTouch } from 'cc';
import { BlockInputEvents, Node } from 'cc';
import { ViewUtil } from '../../utils/ViewUtil';
import { PromptResType } from '../GuiEnum';
@@ -124,4 +124,16 @@ export class LayerPopUp extends LayerUI {
super.clear(isDestroy);
this.closeBlack();
}
}
/** 销毁时释放资源 */
onDestroy() {
// 清理遮罩节点
if (this.mask) {
this.mask.destroy();
this.mask = null!;
}
// 清理事件阻挡组件引用
this.black = null!;
}
}

View File

@@ -16,6 +16,8 @@ export class LayerUI extends Node {
protected ui_nodes = new Collection<string, UIState>();
/** 被移除的界面缓存数据 */
protected ui_cache = new Map<string, UIState>();
/** 缓存界面的最大数量限制 */
protected readonly MAX_CACHE_SIZE = 10;
/**
* UI基础层允许添加多个预制件节点
@@ -50,14 +52,25 @@ export class LayerUI extends Node {
add(uiid: Uiid, config: UIConfig, params?: UIParam): Promise<Node> {
return new Promise<Node>(async (resolve, reject) => {
if (this.ui_nodes.has(config.prefab)) {
console.warn(`路径为【${config.prefab}】的预制重复加载`);
const error = `路径为【${config.prefab}】的预制重复加载`;
console.warn(error);
reject(new Error(error));
return;
}
// 检查缓存中是否存界面
const state = this.initUIConfig(uiid, config, params);
await this.load(state);
resolve(state.node);
try {
// 检查缓存中是否存界面
const state = this.initUIConfig(uiid, config, params);
await this.load(state);
if (state.node) {
resolve(state.node);
} else {
reject(new Error(`路径为【${config.prefab}】的预制加载失败,节点为空`));
}
} catch (error) {
console.error(`添加界面【${config.prefab}】时发生错误:`, error);
reject(error);
}
});
}
@@ -90,28 +103,34 @@ export class LayerUI extends Node {
return new Promise<Node>(async (resolve, reject) => {
// 加载界面资源超时提示
if (state.node == null) {
const timerId = setTimeout(this.onLoadingTimeoutGui, oops.config.game.loadingTimeoutGui);
let timerId: any = null;
try {
timerId = setTimeout(this.onLoadingTimeoutGui, oops.config.game.loadingTimeoutGui);
// 优先加载配置的指定资源包中资源,如果没配置则加载默认资源包资源
const res = await resLoader.load(state.config.bundle!, state.config.prefab, Prefab);
if (res) {
state.node = instantiate(res);
// 优先加载配置的指定资源包中资源,如果没配置则加载默认资源包资源
const res = await resLoader.load(state.config.bundle!, state.config.prefab, Prefab);
if (res) {
state.node = instantiate(res);
// 是否启动真机安全区域显示
if (state.config.safeArea) state.node.addComponent(SafeArea);
// 是否启动真机安全区域显示
if (state.config.safeArea) state.node.addComponent(SafeArea);
// 窗口事件委托
const comp = state.node.addComponent(LayerUIElement);
comp.state = state;
// 窗口事件委托
const comp = state.node.addComponent(LayerUIElement);
comp.state = state;
}
else {
console.warn(`路径为【${state.config.prefab}】的预制加载失败`);
this.failure(state);
}
} finally {
// 确保在所有情况下都清理定时器和关闭等待提示
if (timerId !== null) {
clearTimeout(timerId);
}
oops.gui.waitClose();
}
else {
console.warn(`路径为【${state.config.prefab}】的预制加载失败`);
this.failure(state);
}
// 关闭界面资源超时提示
oops.gui.waitClose();
clearTimeout(timerId);
}
await this.uiInit(state);
@@ -168,7 +187,9 @@ export class LayerUI extends Node {
const release: boolean = state.config.destroy!;
// 不释放界面,缓存起来待下次使用
if (release === false) this.ui_cache.set(state.config.prefab, state);
if (release === false) {
this.addToCache(state.config.prefab, state);
}
// 界面移出舞台
if (state.valid) {
@@ -178,6 +199,23 @@ export class LayerUI extends Node {
}
}
/** 添加界面到缓存,实现 LRU 策略 */
private addToCache(prefabPath: string, state: UIState) {
// 如果缓存已满,移除最早的缓存项
if (this.ui_cache.size >= this.MAX_CACHE_SIZE) {
const firstKey = this.ui_cache.keys().next().value;
if (firstKey) {
const oldState = this.ui_cache.get(firstKey);
if (oldState) {
this.ui_cache.delete(firstKey);
const comp = oldState.node.getComponent(LayerUIElement);
comp && comp.remove(true);
}
}
}
this.ui_cache.set(prefabPath, state);
}
/** 删除缓存的界面,当调用 remove 移除舞台时,可通过此方法删除缓存界面 */
removeCache(prefabPath: string) {
const state = this.ui_cache.get(prefabPath);

View File

@@ -17,10 +17,8 @@ export class LoadingIndicator extends Component {
private loading_rotate = 0;
update(dt: number) {
this.loading_rotate += dt * 220;
this.loading!.setRotationFromEuler(0, 0, -this.loading_rotate % 360);
if (this.loading_rotate > 360) {
this.loading_rotate -= 360;
}
// 优化旋转计算,直接使用模运算避免冗余判断
this.loading_rotate = (this.loading_rotate + dt * 220) % 360;
this.loading!.setRotationFromEuler(0, 0, -this.loading_rotate);
}
}
}

View File

@@ -1,5 +1,5 @@
import type { Material } from 'cc';
import type { Material } from 'cc';
import { CCInteger, Component, Sprite, _decorator } from 'cc';
const { ccclass, property } = _decorator;
@@ -17,12 +17,19 @@ export class Ambilight extends Component {
}
private _start = 0;
_material !: Material;
private _material !: Material;
private _sprite !: Sprite;
onLoad() {
// 缓存组件和材质引用,避免每帧查询
this._sprite = this.node.getComponent(Sprite)!;
if (this._sprite) {
this._material = this._sprite.getMaterial(0)!;
}
}
update(dt: number) {
this._material = this.node.getComponent(Sprite)!.getMaterial(0)!;
if (this.node.active && this._material) {
if (this._material) {
this._setShaderTime(dt);
}
}

View File

@@ -9,14 +9,21 @@ export default class FlashSpine extends Component {
_material: Material = null!;
_skeleton: sp.Skeleton = null!;
_customMaterial: Material = null!;
onLoad() {
this._median = this.duration / 2;
// 获取材质
this._skeleton = this.node.getComponent(sp.Skeleton)!;
this._material = this._skeleton.customMaterial!;
// 创建一个自定义材质副本,只创建一次
this._customMaterial = new Material();
this._customMaterial.copy(this._material);
this._skeleton.customMaterial = this._customMaterial;
// 设置材质对应的属性
this._material.setProperty('u_rate', 1);
this._customMaterial.setProperty('u_rate', 1);
}
update(dt: number) {
@@ -25,11 +32,16 @@ export default class FlashSpine extends Component {
this._time = this._time < 0 ? 0 : this._time;
const rate = Math.abs(this._time - this._median) * 2 / this.duration;
// 直接更新已有材质的属性,不再每帧创建新材质
this._customMaterial.setProperty('u_rate', rate);
}
}
const mat = new Material();
mat.copy(this._material);
this._skeleton.customMaterial = mat;
mat.setProperty('u_rate', rate);
onDestroy() {
// 释放自定义材质
if (this._customMaterial) {
this._customMaterial.destroy();
}
}

View File

@@ -13,7 +13,7 @@ const { ccclass, property } = _decorator;
@ccclass('SpineFinishedRelease')
export class SpineFinishedRelease extends Component {
@property
isDestroy = true;
isDestroy = true;
private spine!: sp.Skeleton;
private resPath: string = null!;
@@ -51,4 +51,16 @@ export class SpineFinishedRelease extends Component {
this.node.removeFromParent();
}
}
onDestroy() {
// 清理 Spine 监听器
if (this.spine) {
this.spine.setCompleteListener(null!);
}
// 如果是通过代码加载的资源,需要释放
if (this.resPath) {
oops.res.release(this.resPath);
}
}
}

View File

@@ -20,6 +20,11 @@ export class EffectDelayRelease extends Component {
this.scheduleOnce(this.onDelay, this.delay);
}
protected onDisable() {
// 清理定时器,防止组件禁用后仍然执行
this.unschedule(this.onDelay);
}
private onDelay() {
EffectSingleCase.instance.put(this.node);
}

View File

@@ -16,16 +16,20 @@ const { ccclass } = _decorator;
export class EffectFinishedRelease extends Component {
/** 动画最大播放时间 */
private maxDuration = 0;
private spineComponent: sp.Skeleton | null = null;
protected onEnable() {
// 重置动画时长
this.maxDuration = 0;
// SPINE动画
const spine = this.getComponent(sp.Skeleton);
if (spine) {
this.spineComponent = this.getComponent(sp.Skeleton);
if (this.spineComponent) {
// 播放第一个动画
const json = (spine.skeletonData!.skeletonJson! as any).animations;
const json = (this.spineComponent.skeletonData!.skeletonJson! as any).animations;
for (const name in json) {
spine.setCompleteListener(this.onRecovery.bind(this));
spine.setAnimation(0, name, false);
this.spineComponent.setCompleteListener(this.onRecovery.bind(this));
this.spineComponent.setAnimation(0, name, false);
break;
}
}
@@ -44,7 +48,10 @@ export class EffectFinishedRelease extends Component {
}
animator.play();
});
this.scheduleOnce(this.onRecovery.bind(this), this.maxDuration);
// 只有当有有效动画时才设置定时器
if (this.maxDuration > 0) {
this.scheduleOnce(this.onRecovery.bind(this), this.maxDuration);
}
}
// 粒子动画
else if (ParticleSystem) {
@@ -57,11 +64,25 @@ export class EffectFinishedRelease extends Component {
const duration: number = particle.duration;
this.maxDuration = duration > this.maxDuration ? duration : this.maxDuration;
});
this.scheduleOnce(this.onRecovery.bind(this), this.maxDuration);
// 只有当有有效粒子时才设置定时器
if (this.maxDuration > 0) {
this.scheduleOnce(this.onRecovery.bind(this), this.maxDuration);
}
}
}
}
protected onDisable() {
// 清理定时器
this.unschedule(this.onRecovery);
// 清理 Spine 监听器,防止内存泄漏
if (this.spineComponent) {
this.spineComponent.setCompleteListener(null!);
this.spineComponent = null;
}
}
private onRecovery() {
if (this.node.parent) message.dispatchEvent(EffectEvent.Put, this.node);
}

View File

@@ -4,7 +4,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2023-03-06 14:40:34
*/
import type { Node, Vec3 } from 'cc';
import type { Node, Vec3 } from 'cc';
import { Animation, NodePool, ParticleSystem, Prefab, sp } from 'cc';
import { message } from '../../core/common/event/MessageManager';
import { resLoader } from '../../core/common/loader/ResLoader';
@@ -46,7 +46,7 @@ export class EffectSingleCase {
}
set speed(value: number) {
this._speed = value;
this.effects_use.forEach((value: Boolean, key: Node) => {
this.effects_use.forEach((value: boolean, key: Node) => {
this.setSpeed(key);
});
}
@@ -267,4 +267,4 @@ export class EffectSingleCase {
}
}
}
}
}

View File

@@ -1,8 +1,9 @@
import { Component, EPSILON, RigidBody, Vec3, _decorator } from 'cc';
import { Component, EPSILON, error, RigidBody, Vec3, _decorator } from 'cc';
const { ccclass, property } = _decorator;
const v3_0 = new Vec3();
const v3_1 = new Vec3();
/** 旋转角度阈值,超过此值将降低速度比率 */
const ROTATION_ANGLE_THRESHOLD = 10;
/**
* 物理方式移动
@@ -47,10 +48,14 @@ export class MoveRigidBody extends Component {
private _prevAngleY = 0; // 之前的Y角度值
private _stateX = 0;
private _stateZ = 0;
/** 临时向量,避免每次创建新对象 */
private _tempVec3_0: Vec3 = new Vec3();
private _tempVec3_1: Vec3 = new Vec3();
/** 是否着地 */
get grounded() {
return this._grounded;
get grounded() {
return this._grounded;
}
private _velocity: Vec3 = new Vec3();
@@ -74,9 +79,15 @@ export class MoveRigidBody extends Component {
this._stateZ = z;
}
start() {
protected start() {
this._rigidBody = this.getComponent(RigidBody)!;
if (!this._rigidBody) {
error('[MoveRigidBody] 未找到 RigidBody 组件');
this.enabled = false;
return;
}
this._prevAngleY = this.node.eulerAngles.y;
this._curMaxSpeed = this._speed * this._ratio;
}
/** 刚体停止移动 */
@@ -86,7 +97,11 @@ export class MoveRigidBody extends Component {
this._rigidBody.clearVelocity(); // 清除移动速度
}
update(dt: number) {
protected update(dt: number) {
if (!this._rigidBody) {
return;
}
// 施加重力
this.applyGravity();
@@ -94,57 +109,69 @@ export class MoveRigidBody extends Component {
this.applyDamping(dt);
// 未落地无法移动
if (!this.grounded) return;
if (!this.grounded) {
return;
}
// 施加移动
this.applyLinearVelocity(v3_0, 1);
this.applyLinearVelocity(this._tempVec3_0, 1);
}
/** 施加重力 */
private applyGravity() {
const g = this.gravity;
const m = this._rigidBody.mass;
v3_1.set(0, m * g, 0);
this._rigidBody.applyForce(v3_1);
this._tempVec3_1.set(0, m * g, 0);
this._rigidBody.applyForce(this._tempVec3_1);
}
/** 施加阻尼 */
private applyDamping(dt: number) {
// 获取线性速度
this._rigidBody.getLinearVelocity(v3_1);
this._rigidBody.getLinearVelocity(this._tempVec3_1);
if (v3_1.lengthSqr() > EPSILON) {
v3_1.multiplyScalar(Math.pow(1.0 - this.damping, dt));
this._rigidBody.setLinearVelocity(v3_1);
if (this._tempVec3_1.lengthSqr() > EPSILON) {
this._tempVec3_1.multiplyScalar(Math.pow(1.0 - this.damping, dt));
this._rigidBody.setLinearVelocity(this._tempVec3_1);
}
}
/**
* 施加移动
* @param {Vec3} dir 方向
* @param {number} speed 移动
* @param {number} speed 移动
*/
private applyLinearVelocity(dir: Vec3, speed: number) {
if (this._stateX || this._stateZ) {
v3_0.set(this._stateX, 0, this._stateZ);
v3_0.normalize();
this._tempVec3_0.set(this._stateX, 0, this._stateZ);
this._tempVec3_0.normalize();
// 获取线性速度
this._rigidBody.getLinearVelocity(v3_1);
this._rigidBody.getLinearVelocity(this._tempVec3_1);
Vec3.scaleAndAdd(v3_1, v3_1, dir, speed);
Vec3.scaleAndAdd(this._tempVec3_1, this._tempVec3_1, dir, speed);
const ms = this._curMaxSpeed;
const len = v3_1.lengthSqr();
const len = this._tempVec3_1.lengthSqr();
let ratio = 1;
if (len > ms) {
if (Math.abs(this.node.eulerAngles.y - this._prevAngleY) >= 10) {
if (Math.abs(this.node.eulerAngles.y - this._prevAngleY) >= ROTATION_ANGLE_THRESHOLD) {
ratio = 2;
}
this._prevAngleY = this.node.eulerAngles.y;
v3_1.normalize();
v3_1.multiplyScalar(ms / ratio);
this._tempVec3_1.normalize();
this._tempVec3_1.multiplyScalar(ms / ratio);
}
this._rigidBody.setLinearVelocity(v3_1);
this._rigidBody.setLinearVelocity(this._tempVec3_1);
}
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 清理速度引用
if (this._rigidBody && this._rigidBody.isValid) {
this._rigidBody.clearVelocity();
}
}
}

View File

@@ -5,7 +5,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2023-01-19 14:59:50
*/
import { Component, Node, Vec3, _decorator } from 'cc';
import { Component, error, Node, Vec3, _decorator } from 'cc';
import { Timer } from '../../core/common/timer/Timer';
import { Vec3Util } from '../../core/utils/Vec3Util';
@@ -49,38 +49,45 @@ export class MoveTo extends Component {
this.enabled = true;
}
update(dt: number) {
let end: Vec3;
protected update(dt: number) {
let end: Vec3 | null = null;
if (this.speed <= 0) {
console.error('移动速度必须要大于零');
error('[MoveTo] 移动速度必须要大于零');
this.exit();
return;
}
if (this.target instanceof Node) {
end = this.ns == Node.NodeSpace.WORLD ? this.target.worldPosition : this.target.position;
if (!this.target.isValid) {
this.exit();
return;
}
end = this.ns === Node.NodeSpace.WORLD ? this.target.worldPosition : this.target.position;
}
else {
end = this.target as Vec3;
}
// 移动目标节点被释放时
if (end == null) {
if (end === null) {
this.exit();
return;
}
// 目标移动后,重计算移动方向与移动到目标点的速度
if (this.end == null || !this.end.strictEquals(end)) {
if (this.end === null || !this.end.strictEquals(end)) {
let target = end.clone();
if (this.offsetVector) {
target = target.add(this.offsetVector);
}
if (this.hasYAxis == false) target.y = 0;
if (this.hasYAxis === false) {
target.y = 0;
}
// 移动方向与移动
const start = this.ns == Node.NodeSpace.WORLD ? this.node.worldPosition : this.node.position;
// 移动方向与移动
const start = this.ns === Node.NodeSpace.WORLD ? this.node.worldPosition : this.node.position;
this.velocity = Vec3Util.sub(target, start).normalize();
// 移动时间与目标偏位置计算
@@ -102,26 +109,32 @@ export class MoveTo extends Component {
if (this.speed > 0) {
const trans = Vec3Util.mul(this.velocity, this.speed * dt);
if (this.ns == Node.NodeSpace.WORLD)
if (this.ns === Node.NodeSpace.WORLD) {
this.node.worldPosition = Vec3Util.add(this.node.worldPosition, trans);
else
}
else {
this.node.position = Vec3Util.add(this.node.position, trans);
}
}
// 移动完成事件
if (this.timer.update(dt)) {
if (this.offset == 0) {
if (this.ns == Node.NodeSpace.WORLD)
if (this.offset === 0 && this.end) {
if (this.ns === Node.NodeSpace.WORLD) {
this.node.worldPosition = this.end;
else
}
else {
this.node.position = this.end;
}
}
this.exit();
}
}
private exit() {
this.onComplete?.call(this);
if (this.onComplete) {
this.onComplete.call(this);
}
this.enabled = false;
this.target = null;
@@ -137,4 +150,18 @@ export class MoveTo extends Component {
this.timer.reset();
this.end = null;
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 清理所有回调引用,防止内存泄漏
this.target = null;
this.onStart = null;
this.onComplete = null;
this.onChange = null;
this.offsetVector = null;
this.end = null;
this.timer.reset();
}
}

View File

@@ -20,10 +20,19 @@ export class MoveTranslate extends Component {
private vector: Vec3 = new Vec3();
update(dt: number) {
protected update(dt: number) {
if (this.speed > 0) {
Vec3.multiplyScalar(this.vector, this.velocity, this.speed * dt);
this.node.translate(this.vector, Node.NodeSpace.WORLD);
}
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 重置速度,防止意外引用
this.velocity = Vec3Util.zero;
this.speed = 0;
}
}

View File

@@ -1,11 +1,16 @@
import type { AnimationState } from 'cc';
import { Animation, _decorator } from 'cc';
import type { AnimationPlayer } from './core/AnimatorBase';
import type { AnimationState } from 'cc';
import { Animation, AnimationClip, _decorator } from 'cc';
import type { AnimationPlayer } from './core/AnimatorBase';
import AnimatorBase from './core/AnimatorBase';
import type { AnimatorStateLogic } from './core/AnimatorStateLogic';
const { ccclass, property, requireComponent, disallowMultiple, menu, help } = _decorator;
/** 动画循环播放模式 */
const WRAP_MODE_LOOP = AnimationClip.WrapMode.Loop;
/** 动画单次播放模式 */
const WRAP_MODE_NORMAL = AnimationClip.WrapMode.Normal;
/**
* Cocos Animation状态机组件
*/
@@ -80,7 +85,7 @@ export default class AnimatorAnimation extends AnimatorBase {
if (!this._wrapModeMap.has(this._animState)) {
this._wrapModeMap.set(this._animState, this._animState.wrapMode);
}
this._animState.wrapMode = loop ? 2 : this._wrapModeMap.get(this._animState)!;
this._animState.wrapMode = loop ? WRAP_MODE_LOOP : this._wrapModeMap.get(this._animState)!;
}
/**
@@ -93,4 +98,18 @@ export default class AnimatorAnimation extends AnimatorBase {
this._animState.speed = scale;
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 清理动画事件监听器
if (this._animation) {
this._animation.off(Animation.EventType.FINISHED, this.onAnimFinished, this);
this._animation.off(Animation.EventType.LASTFRAME, this.onAnimFinished, this);
}
// 清空wrap mode缓存
this._wrapModeMap.clear();
}
}

View File

@@ -1,5 +1,5 @@
import { _decorator, dragonBones } from 'cc';
import type { AnimationPlayer } from './core/AnimatorBase';
import type { AnimationPlayer } from './core/AnimatorBase';
import AnimatorBase from './core/AnimatorBase';
import type { AnimatorStateLogic } from './core/AnimatorStateLogic';
@@ -61,8 +61,9 @@ export default class AnimatorDragonBones extends AnimatorBase {
* @param loop 是否循环播放
*/
protected playAnimation(animName: string, loop: boolean) {
if (animName)
if (animName) {
this._dragonBones.playAnimation(animName, loop ? 0 : -1);
}
}
/**
@@ -71,7 +72,18 @@ export default class AnimatorDragonBones extends AnimatorBase {
* @param scale 缩放倍率
*/
protected scaleTime(scale: number) {
if (scale > 0)
if (scale > 0) {
this._dragonBones.timeScale = scale;
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 清理DragonBones事件监听器
if (this._dragonBones) {
this._dragonBones.removeEventListener('complete', this.onAnimFinished, this);
}
}
}

View File

@@ -5,11 +5,16 @@
* @LastEditTime: 2021-11-04 10:46:00
*/
import { CCFloat, game, SkeletalAnimation, _decorator } from 'cc';
import { AnimationClip, CCFloat, game, SkeletalAnimation, _decorator } from 'cc';
import AnimatorAnimation from './AnimatorAnimation';
const { ccclass, property, requireComponent, disallowMultiple, menu, help } = _decorator;
/** 动画循环播放模式 */
const WRAP_MODE_LOOP = AnimationClip.WrapMode.Loop;
/** 动画单次播放模式 */
const WRAP_MODE_NORMAL = AnimationClip.WrapMode.Normal;
@ccclass
@disallowMultiple
@requireComponent(SkeletalAnimation)
@@ -22,11 +27,13 @@ export class AnimatorSkeletal extends AnimatorAnimation {
})
private duration = 0.3;
private cross_duration = 0; // 防止切换动画时间少于间隔时间导致动画状态错乱的问题
private current_time = 0; // 上一次切换状态时间
/** 防止切换动画时间少于间隔时间导致动画状态错乱的问题(毫秒) */
private _crossDurationMs = 0;
/** 上一次切换状态时间(毫秒) */
private _lastSwitchTime = 0;
onLoad() {
this.cross_duration = this.duration * 1000;
this._crossDurationMs = this.duration * 1000;
}
/**
@@ -40,13 +47,14 @@ export class AnimatorSkeletal extends AnimatorAnimation {
return;
}
if (game.totalTime - this.current_time > this.cross_duration) {
const currentTime = game.totalTime;
if (currentTime - this._lastSwitchTime > this._crossDurationMs) {
this._animation.crossFade(animName, this.duration);
}
else {
this._animation.play(animName);
}
this.current_time = game.totalTime;
this._lastSwitchTime = currentTime;
this._animState = this._animation.getState(animName);
if (!this._animState) {
@@ -55,7 +63,6 @@ export class AnimatorSkeletal extends AnimatorAnimation {
if (!this._wrapModeMap.has(this._animState)) {
this._wrapModeMap.set(this._animState, this._animState.wrapMode);
}
// this._animState.wrapMode = loop ? 2 : this._wrapModeMap.get(this._animState)!;
this._animState.wrapMode = loop ? 2 : 1; // 2为循环播放1为单次播放
this._animState.wrapMode = loop ? WRAP_MODE_LOOP : WRAP_MODE_NORMAL;
}
}

View File

@@ -1,6 +1,6 @@
import { _decorator, sp } from 'cc';
import type AnimatorSpineSecondary from './AnimatorSpineSecondary';
import type { AnimationPlayer } from './core/AnimatorBase';
import type { AnimationPlayer } from './core/AnimatorBase';
import AnimatorBase from './core/AnimatorBase';
import type { AnimatorStateLogic } from './core/AnimatorStateLogic';
@@ -19,8 +19,11 @@ export default class AnimatorSpine extends AnimatorBase {
protected _spine: sp.Skeleton = null!;
/** 动画完成的回调 */
protected _completeListenerMap: Map<(entry?: any) => void, any> = new Map();
/** 次状态机注册的回调 */
protected _secondaryListenerMap: Map<(entry?: any) => void, AnimatorSpineSecondary> = new Map();
/** 次状态机注册的回调 - 按trackIndex分组存储优化性能 */
protected _secondaryListenerMap: Map<number, Array<{ cb: (entry?: any) => void, target: AnimatorSpineSecondary }>> = new Map();
/** 保存绑定的事件回调,用于清理 */
private _boundEventCallback: ((trackEntry: any, event: any) => void) | null = null;
private _boundCompleteCallback: ((entry: any) => void) | null = null;
protected start() {
if (!this.PlayOnStart || this._hasInit) {
@@ -29,8 +32,10 @@ export default class AnimatorSpine extends AnimatorBase {
this._hasInit = true;
this._spine = this.getComponent(sp.Skeleton)!;
this._spine.setEventListener(this.onSpineEvent.bind(this));
this._spine.setCompleteListener(this.onSpineComplete.bind(this));
this._boundEventCallback = this.onSpineEvent.bind(this);
this._boundCompleteCallback = this.onSpineComplete.bind(this);
this._spine.setEventListener(this._boundEventCallback);
this._spine.setCompleteListener(this._boundCompleteCallback);
if (this.AssetRawUrl !== null) {
this.initJson(this.AssetRawUrl.json);
@@ -53,8 +58,10 @@ export default class AnimatorSpine extends AnimatorBase {
this.initArgs(...args);
this._spine = this.getComponent(sp.Skeleton)!;
this._spine.setEventListener(this.onSpineEvent.bind(this));
this._spine.setCompleteListener(this.onSpineComplete.bind(this));
this._boundEventCallback = this.onSpineEvent.bind(this);
this._boundCompleteCallback = this.onSpineComplete.bind(this);
this._spine.setEventListener(this._boundEventCallback);
this._spine.setCompleteListener(this._boundCompleteCallback);
if (this.AssetRawUrl !== null) {
this.initJson(this.AssetRawUrl.json);
@@ -76,13 +83,29 @@ export default class AnimatorSpine extends AnimatorBase {
/** ---------- 后续扩展代码 结束 ---------- */
private onSpineComplete(entry: any) {
entry.trackIndex === 0 && this.onAnimFinished();
this._completeListenerMap.forEach((target, cb) => {
target ? cb.call(target, entry) : cb(entry);
});
this._secondaryListenerMap.forEach((target, cb) => {
entry.trackIndex === target.TrackIndex && cb.call(target, entry);
const trackIndex = entry.trackIndex;
if (trackIndex === 0) {
this.onAnimFinished();
}
this._completeListenerMap.forEach((target, cb) => {
if (target) {
cb.call(target, entry);
}
else {
cb(entry);
}
});
// 使用按trackIndex分组的监听器避免遍历所有监听器
const listeners = this._secondaryListenerMap.get(trackIndex);
if (listeners) {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener.cb.call(listener.target, entry);
}
}
}
/**
@@ -114,7 +137,30 @@ export default class AnimatorSpine extends AnimatorBase {
* 注册次状态机动画结束的回调(状态机内部方法,不能由外部直接调用)
*/
addSecondaryListener(cb: (entry?: any) => void, target: AnimatorSpineSecondary) {
this._secondaryListenerMap.set(cb, target);
const trackIndex = target.TrackIndex;
if (!this._secondaryListenerMap.has(trackIndex)) {
this._secondaryListenerMap.set(trackIndex, []);
}
const listeners = this._secondaryListenerMap.get(trackIndex)!;
listeners.push({ cb, target });
}
/**
* 注销次状态机的监听
* @param cb 回调
*/
removeSecondaryListener(cb: (entry?: any) => void) {
// 遍历所有trackIndex的监听器数组
this._secondaryListenerMap.forEach((listeners, trackIndex) => {
const index = listeners.findIndex(listener => listener.cb === cb);
if (index !== -1) {
listeners.splice(index, 1);
// 如果该trackIndex的监听器数组为空删除该key
if (listeners.length === 0) {
this._secondaryListenerMap.delete(trackIndex);
}
}
});
}
/**
@@ -141,6 +187,27 @@ export default class AnimatorSpine extends AnimatorBase {
* 清空动画完成的监听
*/
clearCompleteListener() {
this._completeListenerMap.clear;
this._completeListenerMap.clear();
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 清理spine事件监听器需要检查spine和其内部状态是否有效
if (this._spine && this._spine.isValid) {
// 只有在spine内部状态正常时才清理监听器
// 组件销毁时spine可能已经部分清理直接设置null可能出错
this._spine.setEventListener(null!);
this._spine.setCompleteListener(null!);
}
// 清理回调引用
this._boundEventCallback = null;
this._boundCompleteCallback = null;
// 清空所有监听器
this._completeListenerMap.clear();
this._secondaryListenerMap.clear();
}
}

View File

@@ -1,6 +1,6 @@
import { _decorator, sp } from 'cc';
import AnimatorSpine from './AnimatorSpine';
import type { AnimationPlayer } from './core/AnimatorBase';
import type { AnimationPlayer } from './core/AnimatorBase';
import AnimatorBase from './core/AnimatorBase';
import type { AnimatorStateLogic } from './core/AnimatorStateLogic';
@@ -74,4 +74,14 @@ export default class AnimatorSpineSecondary extends AnimatorBase {
this._spine.clearTrack(this.TrackIndex);
}
}
/**
* 组件销毁时清理资源
*/
protected onDestroy() {
// 从主状态机移除次状态机监听器
if (this._main) {
this._main.removeSecondaryListener(this.onAnimFinished);
}
}
}

View File

@@ -55,6 +55,11 @@ export default class AnimatorBase extends Component {
protected _onStateChangeCall: (fromState: string, toState: string) => void = null!;
/** 自定义的动画播放控制器 */
protected _animationPlayer: AnimationPlayer = null!;
/** 缓存当前状态的逻辑控制避免频繁查找Map */
private _cachedStateLogic: AnimatorStateLogic | null = null;
/** 缓存的状态名,用于判断状态是否改变 */
private _cachedStateName = '';
/** 当前状态名 */
get curStateName(): string {
@@ -110,10 +115,18 @@ export default class AnimatorBase extends Component {
}
this.scaleTime(playSpeed);
// 更新动画状态逻辑
// 更新动画状态逻辑(使用缓存优化)
if (this._stateLogicMap) {
const curLogic = this._stateLogicMap.get(this._ac.curState.name);
curLogic && curLogic.onUpdate();
const curStateName = this._ac.curState.name;
// 状态改变时更新缓存
if (this._cachedStateName !== curStateName) {
this._cachedStateName = curStateName;
this._cachedStateLogic = this._stateLogicMap.get(curStateName) || null;
}
// 使用缓存的逻辑控制
if (this._cachedStateLogic) {
this._cachedStateLogic.onUpdate();
}
}
// 更新状态机逻辑
@@ -177,12 +190,21 @@ export default class AnimatorBase extends Component {
if (this._stateLogicMap) {
const fromLogic = this._stateLogicMap.get(fromStateName);
fromLogic && fromLogic.onExit();
if (fromLogic) {
fromLogic.onExit();
}
const toLogic = this._stateLogicMap.get(toState.name);
toLogic && toLogic.onEntry();
if (toLogic) {
toLogic.onEntry();
}
// 更新缓存
this._cachedStateName = toState.name;
this._cachedStateLogic = toLogic || null;
}
this._onStateChangeCall && this._onStateChangeCall(fromStateName, toState.name);
if (this._onStateChangeCall) {
this._onStateChangeCall(fromStateName, toState.name);
}
}
/**

View File

@@ -3,6 +3,9 @@ import type AnimatorBase from './AnimatorBase';
import AnimatorParams from './AnimatorParams';
import AnimatorState from './AnimatorState';
/** 最大状态切换次数,防止递归死循环 */
const MAX_STATE_CHANGE_COUNT = 1000;
/**
* 状态机控制类
*/
@@ -22,14 +25,14 @@ export default class AnimatorController {
/** 动画播放完毕的标记 */
animComplete = false;
/** 当前运行的状态 */
get curState(): AnimatorState {
return this._curState;
get curState(): AnimatorState {
return this._curState;
}
get params(): AnimatorParams {
return this._params;
get params(): AnimatorParams {
return this._params;
}
get states(): Map<string, AnimatorState> {
return this._states;
get states(): Map<string, AnimatorState> {
return this._states;
}
constructor(player: AnimatorBase, json: any) {
@@ -107,8 +110,8 @@ export default class AnimatorController {
*/
changeState(stateName: string) {
this._changeCount++;
if (this._changeCount > 1000) {
error('[AnimatorController.changeState] error: 状态切换递归调用超过1000transition设置可能出错!');
if (this._changeCount > MAX_STATE_CHANGE_COUNT) {
error(`[AnimatorController.changeState] error: 状态切换递归调用超过${MAX_STATE_CHANGE_COUNT}transition设置可能出错!`);
return;
}

View File

@@ -1,3 +1,4 @@
import { error, warn } from 'cc';
import { ParamType } from './AnimatorCondition';
/**
@@ -28,52 +29,86 @@ export default class AnimatorParams {
const param: Param = this._paramMap.get(key)!;
if (param) {
return param.type;
}
}
else {
return null!;
warn(`[AnimatorParams.getParamType] 参数不存在: ${key}`);
return ParamType.BOOLEAN; // 返回默认类型而不是null
}
}
setNumber(key: string, value: number) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.NUMBER) {
param.value = value;
if (!param) {
warn(`[AnimatorParams.setNumber] 参数不存在: ${key}`);
return;
}
if (param.type !== ParamType.NUMBER) {
error(`[AnimatorParams.setNumber] 参数类型错误期望NUMBER但实际是: ${param.type}`);
return;
}
param.value = value;
}
setBool(key: string, value: boolean) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.BOOLEAN) {
param.value = value ? 1 : 0;
if (!param) {
warn(`[AnimatorParams.setBool] 参数不存在: ${key}`);
return;
}
if (param.type !== ParamType.BOOLEAN) {
error(`[AnimatorParams.setBool] 参数类型错误期望BOOLEAN但实际是: ${param.type}`);
return;
}
param.value = value ? 1 : 0;
}
setTrigger(key: string) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.TRIGGER) {
param.value = 1;
if (!param) {
warn(`[AnimatorParams.setTrigger] 参数不存在: ${key}`);
return;
}
if (param.type !== ParamType.TRIGGER) {
error(`[AnimatorParams.setTrigger] 参数类型错误期望TRIGGER但实际是: ${param.type}`);
return;
}
param.value = 1;
}
resetTrigger(key: string) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.TRIGGER) {
param.value = 0;
if (!param) {
warn(`[AnimatorParams.resetTrigger] 参数不存在: ${key}`);
return;
}
if (param.type !== ParamType.TRIGGER) {
return;
}
param.value = 0;
}
autoTrigger(key: string) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.AUTO_TRIGGER) {
param.value = 1;
if (!param) {
warn(`[AnimatorParams.autoTrigger] 参数不存在: ${key}`);
return;
}
if (param.type !== ParamType.AUTO_TRIGGER) {
error(`[AnimatorParams.autoTrigger] 参数类型错误期望AUTO_TRIGGER但实际是: ${param.type}`);
return;
}
param.value = 1;
}
resetAutoTrigger(key: string) {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.AUTO_TRIGGER) {
param.value = 0;
if (!param) {
return; // 重置操作不输出警告
}
if (param.type !== ParamType.AUTO_TRIGGER) {
return;
}
param.value = 0;
}
resetAllAutoTrigger() {
@@ -86,41 +121,53 @@ export default class AnimatorParams {
getNumber(key: string): number {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.NUMBER) {
return param.value;
}
else {
if (!param) {
warn(`[AnimatorParams.getNumber] 参数不存在: ${key}`);
return 0;
}
if (param.type !== ParamType.NUMBER) {
error(`[AnimatorParams.getNumber] 参数类型错误期望NUMBER但实际是: ${param.type}`);
return 0;
}
return param.value;
}
getBool(key: string): number {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.BOOLEAN) {
return param.value;
}
else {
if (!param) {
warn(`[AnimatorParams.getBool] 参数不存在: ${key}`);
return 0;
}
if (param.type !== ParamType.BOOLEAN) {
error(`[AnimatorParams.getBool] 参数类型错误期望BOOLEAN但实际是: ${param.type}`);
return 0;
}
return param.value;
}
getTrigger(key: string): number {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.TRIGGER) {
return param.value;
}
else {
if (!param) {
warn(`[AnimatorParams.getTrigger] 参数不存在: ${key}`);
return 0;
}
if (param.type !== ParamType.TRIGGER) {
error(`[AnimatorParams.getTrigger] 参数类型错误期望TRIGGER但实际是: ${param.type}`);
return 0;
}
return param.value;
}
getAutoTrigger(key: string): number {
const param: Param = this._paramMap.get(key)!;
if (param && param.type === ParamType.AUTO_TRIGGER) {
return param.value;
}
else {
if (!param) {
warn(`[AnimatorParams.getAutoTrigger] 参数不存在: ${key}`);
return 0;
}
if (param.type !== ParamType.AUTO_TRIGGER) {
error(`[AnimatorParams.getAutoTrigger] 参数类型错误期望AUTO_TRIGGER但实际是: ${param.type}`);
return 0;
}
return param.value;
}
}

View File

@@ -15,27 +15,27 @@ export default class AnimatorState {
private _ac: AnimatorController = null!;
/** 状态名 */
get name() {
return this._name;
get name() {
return this._name;
}
/** 动画名 */
get motion() {
return this._motion;
get motion() {
return this._motion;
}
/** 动画是否循环播放 */
get loop() {
return this._loop;
get loop() {
return this._loop;
}
/** 动画播放速度的混合参数 */
get multi() {
return this._multi;
get multi() {
return this._multi;
}
/** 动画播放速度 */
get speed() {
return this._speed;
get speed() {
return this._speed;
}
set speed(value: number) {
this._speed = value;
set speed(value: number) {
this._speed = value;
}
constructor(data: any, ac: AnimatorController) {
@@ -49,7 +49,9 @@ export default class AnimatorState {
for (let i = 0; i < data.transitions.length; i++) {
const transition: AnimatorTransition = new AnimatorTransition(data.transitions[i], ac);
transition.isValid() && this._transitions.push(transition);
if (transition.isValid()) {
this._transitions.push(transition);
}
}
}

View File

@@ -8,7 +8,7 @@ import type { IControl } from './IControl';
/** 行为树节点 */
export abstract class BTreeNode implements IControl {
protected _control!: IControl;
protected _control: IControl | null = null;
title: string;
@@ -31,14 +31,34 @@ export abstract class BTreeNode implements IControl {
}
running(blackboard?: any) {
this._control.running(this);
if (this._control) {
this._control.running(this);
}
else {
console.error(`节点【${this.title}】的控制器未设置`);
}
}
success() {
this._control.success();
if (this._control) {
this._control.success();
}
else {
console.error(`节点【${this.title}】的控制器未设置`);
}
}
fail() {
this._control.fail();
if (this._control) {
this._control.fail();
}
else {
console.error(`节点【${this.title}】的控制器未设置`);
}
}
/** 清理节点资源 */
destroy() {
this._control = null;
}
}

View File

@@ -10,7 +10,7 @@ export class BehaviorTree implements IControl {
/** 根节点 */
private readonly _root: BTreeNode;
/** 当前执行节点 */
private _current!: BTreeNode;
private _current: BTreeNode | null = null;
/** 是否已开始执行 */
private _started = false;
/** 外部参数对象 */
@@ -57,12 +57,23 @@ export class BehaviorTree implements IControl {
}
success() {
this._current.end(this._blackboard);
if (this._current) {
this._current.end(this._blackboard);
}
this._started = false;
}
fail() {
this._current.end(this._blackboard);
if (this._current) {
this._current.end(this._blackboard);
}
this._started = false;
}
/** 清理行为树资源 */
destroy() {
this._current = null;
this._blackboard = null;
this._started = false;
}
@@ -70,10 +81,22 @@ export class BehaviorTree implements IControl {
static _registeredNodes: Map<string, BTreeNode> = new Map<string, BTreeNode>();
/** 注册节点 */
static register(name: string, node: BTreeNode) {
this._registeredNodes.set(name, node);
}
/** 注销节点 */
static unregister(name: string) {
this._registeredNodes.delete(name);
}
/** 清理所有注册的节点 */
static clearAll() {
this._registeredNodes.clear();
}
/** 获取节点 */
static getNode(name: string | BTreeNode): BTreeNode {
const node = name instanceof BTreeNode ? name : this._registeredNodes.get(name);
if (!node) {

View File

@@ -12,10 +12,10 @@ export abstract class BranchNode extends BTreeNode {
/** 子节点数组 */
children: Array<BTreeNode>;
/** 当前任务索引 */
protected _actualTask!: number;
protected _actualTask: number = 0;
/** 正在运行的节点 */
protected _runningNode!: BTreeNode;
protected _nodeRunning!: BTreeNode | null;
protected _runningNode: BTreeNode | null = null;
protected _nodeRunning: BTreeNode | null = null;
/** 外部参数对象 */
protected _blackboard: any;
@@ -30,8 +30,10 @@ export abstract class BranchNode extends BTreeNode {
}
run(blackboard?: any) {
if (this.children.length == 0) { // 没有子任务直接视为执行失败
this._control.fail();
if (this.children.length === 0) { // 没有子任务直接视为执行失败
if (this._control) {
this._control.fail();
}
}
else {
this._blackboard = blackboard;
@@ -46,25 +48,50 @@ export abstract class BranchNode extends BTreeNode {
/** 执行当前节点逻辑 */
protected _run(blackboard?: any) {
const node = BehaviorTree.getNode(this.children[this._actualTask]);
this._runningNode = node;
node.setControl(this);
node.start(this._blackboard);
node.run(this._blackboard);
// 直接使用子节点,不需要通过 getNode 查询(性能优化)
const node = this.children[this._actualTask];
if (node) {
this._runningNode = node;
node.setControl(this);
node.start(this._blackboard);
node.run(this._blackboard);
}
}
running(node: BTreeNode) {
this._nodeRunning = node;
this._control.running(node);
if (this._control) {
this._control.running(node);
}
}
success() {
this._nodeRunning = null;
this._runningNode.end(this._blackboard);
if (this._runningNode) {
this._runningNode.end(this._blackboard);
}
}
fail() {
this._nodeRunning = null;
this._runningNode.end(this._blackboard);
if (this._runningNode) {
this._runningNode.end(this._blackboard);
}
}
/** 清理节点资源 */
destroy() {
// 清理所有子节点
if (this.children) {
this.children.forEach(child => {
if (child && typeof child.destroy === 'function') {
child.destroy();
}
});
}
this._runningNode = null;
this._nodeRunning = null;
this._blackboard = null;
super.destroy();
}
}

View File

@@ -12,13 +12,14 @@ import { BTreeNode } from './BTreeNode';
* 如果装饰器是true 它所在的子树会被执行如果是false 所在的子树不会被执行
*/
export class Decorator extends BTreeNode {
node!: BTreeNode;
node: BTreeNode | null = null;
constructor(node?: string | BTreeNode) {
super();
if (node)
if (node) {
this.node = BehaviorTree.getNode(node);
}
}
protected setNode(node: string | BTreeNode) {
@@ -26,16 +27,38 @@ export class Decorator extends BTreeNode {
}
start() {
this.node.setControl(this);
this.node.start();
if (this.node) {
this.node.setControl(this);
this.node.start();
}
else {
console.error(`装饰器节点【${this.title}】没有设置子节点`);
}
super.start();
}
end() {
this.node.end();
if (this.node) {
this.node.end();
}
}
run(blackboard: any) {
this.node.run(blackboard);
if (this.node) {
this.node.run(blackboard);
}
else {
console.error(`装饰器节点【${this.title}】没有设置子节点`);
this.fail();
}
}
/** 清理节点资源 */
destroy() {
if (this.node && typeof this.node.destroy === 'function') {
this.node.destroy();
}
this.node = null;
super.destroy();
}
}

View File

@@ -6,9 +6,14 @@
*/
import { BTreeNode } from './BTreeNode';
/** 任务行为节点 */
/**
* 任务行为节点
* 这是一个基类,子类应该实现具体的 run 方法
*/
export class Task extends BTreeNode {
run(blackboard?: any) {
// 默认实现:直接成功
// 子类应该重写此方法实现具体逻辑
this.success();
}
}

View File

@@ -53,6 +53,9 @@ export class AsyncQueue {
// 正在执行的异步任务标识
private _isProcessingTaskUUID = 0;
private _enable = true;
/** 最大任务队列长度,防止内存泄漏 */
private static readonly MAX_QUEUE_SIZE = 1000;
/** 是否开启可用 */
get enable() {
@@ -80,6 +83,12 @@ export class AsyncQueue {
* @param params 参数
*/
push(callback: AsyncCallback, params: any = null): number {
// 防止队列无限增长
if (this._queues.length >= AsyncQueue.MAX_QUEUE_SIZE) {
warn(`AsyncQueue 队列已满 (${AsyncQueue.MAX_QUEUE_SIZE}),无法添加新任务`);
return -1;
}
const uuid = AsyncQueue._$uuid_count++;
this._queues.push({
uuid: uuid,
@@ -96,6 +105,12 @@ export class AsyncQueue {
* @returns
*/
pushMulti(params: any, ...callbacks: AsyncCallback[]): number {
// 防止队列无限增长
if (this._queues.length >= AsyncQueue.MAX_QUEUE_SIZE) {
warn(`AsyncQueue 队列已满 (${AsyncQueue.MAX_QUEUE_SIZE}),无法添加新任务`);
return -1;
}
const uuid = AsyncQueue._$uuid_count++;
this._queues.push({
uuid: uuid,
@@ -114,7 +129,9 @@ export class AsyncQueue {
warn('正在执行的任务不可以移除');
return;
}
for (let i = 0; i < this._queues.length; i++) {
// 优化:使用标准 for 循环,缓存长度
const len = this._queues.length;
for (let i = 0; i < len; i++) {
if (this._queues[i].uuid === uuid) {
this._queues.splice(i, 1);
break;
@@ -153,7 +170,8 @@ export class AsyncQueue {
/** 清空队列 */
clear() {
this._queues = [];
// 优化:使用 length = 0 而不是创建新数组,更高效
this._queues.length = 0;
this._isProcessingTaskUUID = 0;
this._runningAsyncTask = null;
}
@@ -185,7 +203,8 @@ export class AsyncQueue {
this._isProcessingTaskUUID = taskUUID;
const callbacks: Array<AsyncCallback> = actionData.callbacks;
if (callbacks.length == 1) {
const callbackLen = callbacks.length;
if (callbackLen === 1) {
const nextFunc: NextFunction = (nextArgs: any = null) => {
this.next(taskUUID, nextArgs);
};
@@ -193,7 +212,7 @@ export class AsyncQueue {
}
else {
// 多个任务函数同时执行
let fnum: number = callbacks.length;
let fnum: number = callbackLen;
const nextArgsArr: any[] = [];
const nextFunc: NextFunction = (nextArgs: any = null) => {
--fnum;
@@ -202,8 +221,8 @@ export class AsyncQueue {
this.next(taskUUID, nextArgsArr);
}
};
const knum = fnum;
for (let i = 0; i < knum; i++) {
// 使用标准 for 循环,性能更好
for (let i = 0; i < callbackLen; i++) {
callbacks[i](nextFunc, actionData.params, args);
}
}
@@ -264,4 +283,4 @@ export class AsyncQueue {
};
return call;
}
}
}

View File

@@ -8,6 +8,8 @@
/** 支持Map与Array功能的集合对象 */
export class Collection<K, V> extends Map<K, V> {
private _array: V[] = [];
/** 优化:维护 value 到 index 的映射,避免 indexOf 查找 */
private _valueToIndex: Map<V, number> = new Map();
/** 获取数组对象 */
get array() {
@@ -21,12 +23,20 @@ export class Collection<K, V> extends Map<K, V> {
*/
set(key: K, value: V) {
if (this.has(key)) {
const old = this.get(key)!;
const index = this._array.indexOf(old);
this._array[index] = value;
// 更新现有值
const old = super.get(key)!;
const index = this._valueToIndex.get(old);
if (index !== undefined) {
this._array[index] = value;
this._valueToIndex.delete(old);
this._valueToIndex.set(value, index);
}
}
else {
// 添加新值
const index = this._array.length;
this._array.push(value);
this._valueToIndex.set(value, index);
}
return super.set(key, value);
@@ -37,17 +47,29 @@ export class Collection<K, V> extends Map<K, V> {
* @param key 关键字
*/
delete(key: K): boolean {
const value = this.get(key);
if (value) {
const index = this._array.indexOf(value);
if (index > -1) this._array.splice(index, 1);
const value = super.get(key);
if (value !== undefined) {
const index = this._valueToIndex.get(value);
if (index !== undefined) {
// 使用快速删除:将最后一个元素移到删除位置
const lastIndex = this._array.length - 1;
if (index !== lastIndex) {
const lastValue = this._array[lastIndex];
this._array[index] = lastValue;
this._valueToIndex.set(lastValue, index);
}
this._array.pop();
this._valueToIndex.delete(value);
}
return super.delete(key);
}
return false;
}
clear(): void {
this._array.splice(0, this._array.length);
// 优化:使用 length = 0 更高效
this._array.length = 0;
this._valueToIndex.clear();
super.clear();
}
}
}

View File

@@ -1,7 +1,8 @@
import { ECSComp } from './ECSComp';
import { ECSEntity } from './ECSEntity';
import { ECSMask } from './ECSMask';
import { ECSMatcher } from './ECSMatcher';
import type { CompCtor, CompType, EntityCtor } from './ECSModel';
import type { CompCtor, CompType, EntityCtor } from './ECSModel';
import { ECSModel } from './ECSModel';
import { ECSComblockSystem, ECSRootSystem, ECSSystem } from './ECSSystem';
@@ -223,6 +224,26 @@ export namespace ecs {
ECSModel.groups.clear();
}
/**
* 清理所有对象池 - 用于释放不再使用的缓存内存
* 注意:此操作会清空所有实体池、组件池和 Mask 池,请在确保不再需要这些缓存时调用
*/
export function clearPools() {
// 清理实体池
ECSModel.entityPool.forEach((pool) => {
pool.length = 0;
});
ECSModel.entityPool.clear();
// 清理组件池
ECSModel.compPools.forEach((pool) => {
pool.length = 0;
});
// 清理 Mask 对象池
ECSMask.clearPool();
}
/**
* 通过实体唯一编号获得实体对象
* @param eid 实体唯一编号
@@ -327,4 +348,4 @@ export namespace ecs {
}
//#endregion
}
}

View File

@@ -1,6 +1,6 @@
import type { ecs } from './ECS';
import { ECSMask } from './ECSMask';
import type { CompCtor, CompType } from './ECSModel';
import type { CompCtor, CompType } from './ECSModel';
import { ECSModel } from './ECSModel';
//#region 辅助方法
@@ -48,7 +48,10 @@ function destroyEntity(entity: ECSEntity) {
entitys = [];
ECSModel.entityPool.set(entity.name, entitys);
}
entitys.push(entity);
// 限制对象池大小,防止内存无限增长
if (entitys.length < ECSModel.MAX_ENTITY_POOL_SIZE) {
entitys.push(entity);
}
ECSModel.eid2Entity.delete(entity.eid);
}
else {
@@ -72,6 +75,8 @@ export class ECSEntity {
private compTid2Ctor: Map<number, CompType<ecs.IComp>> = new Map();
/** 配合 entity.remove(Comp, false) 记录组件实例上的缓存数据,在添加时恢复原数据 */
private compTid2Obj: Map<number, ecs.IComp> = new Map();
/** 组件缓存容量限制,防止内存泄漏 */
private static readonly MAX_CACHE_COMP = 10;
private _parent: ECSEntity | null = null;
/** 父实体 */
@@ -252,14 +257,29 @@ export class ECSEntity {
if (isRecycle) {
comp.reset();
// 回收组件到指定缓存池中
// 回收组件到指定缓存池中,限制池大小
if (comp.canRecycle) {
const compPoolsType = ECSModel.compPools.get(componentTypeId)!;
compPoolsType.push(comp);
if (compPoolsType.length < ECSModel.MAX_COMP_POOL_SIZE) {
compPoolsType.push(comp);
}
}
}
else {
this.compTid2Obj.set(componentTypeId, comp); // 用于缓存显示对象组件
// 限制缓存组件数量,防止内存泄漏
if (this.compTid2Obj.size < ECSEntity.MAX_CACHE_COMP) {
this.compTid2Obj.set(componentTypeId, comp); // 用于缓存显示对象组件
} else {
// 超过限制,强制回收
console.warn(`实体 ${this.name} 缓存组件数量超过限制,强制回收组件 ${compName}`);
comp.reset();
if (comp.canRecycle) {
const compPoolsType = ECSModel.compPools.get(componentTypeId);
if (compPoolsType) {
compPoolsType.push(comp);
}
}
}
}
}
@@ -294,10 +314,15 @@ export class ECSEntity {
// 移除实体上所有组件
this.compTid2Ctor.forEach(this._remove, this);
destroyEntity(this);
// 清理缓存的组件对象,防止内存泄漏
this.compTid2Obj.clear();
// 回收 mask 到对象池
this.mask.destroy();
}
private _remove(comp: CompType<ecs.IComp>) {
this.remove(comp, true);
}
}
}

View File

@@ -1,83 +1,99 @@
/*
* @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';
export class ECSGroup<E extends ECSEntity = ECSEntity> {
/** 实体筛选规则 */
private matcher: ecs.IMatcher;
private _matchEntities: Map<number, E> = new Map();
private _entitiesCache: E[] | null = null;
/**
* 符合规则的实体
*/
get matchEntities() {
if (this._entitiesCache === null) {
this._entitiesCache = Array.from(this._matchEntities.values());
}
return this._entitiesCache;
}
/**
* 当前group中实体的数量
*
* 注:不要手动修改这个属性值。
* 注其实可以通过this._matchEntities.size获得实体数量但是需要封装get方法。为了减少一次方法的调用所以才直接创建一个count属性
*/
count = 0;
/** 获取matchEntities中第一个实体 */
get entity(): E {
return this.matchEntities[0];
}
private _enteredEntities: Map<number, E> | null = null;
private _removedEntities: Map<number, E> | null = null;
constructor(matcher: ecs.IMatcher) {
this.matcher = matcher;
}
onComponentAddOrRemove(entity: E) {
if (this.matcher.isMatch(entity)) { // Group只关心指定组件在实体身上的添加和删除动作。
this._matchEntities.set(entity.eid, entity);
this._entitiesCache = null;
this.count++;
if (this._enteredEntities) {
this._enteredEntities.set(entity.eid, entity);
this._removedEntities!.delete(entity.eid);
}
}
else if (this._matchEntities.has(entity.eid)) { // 如果Group中有这个实体但是这个实体已经不满足匹配规则则从Group中移除该实体
this._matchEntities.delete(entity.eid);
this._entitiesCache = null;
this.count--;
if (this._enteredEntities) {
this._enteredEntities.delete(entity.eid);
this._removedEntities!.set(entity.eid, entity);
}
}
}
watchEntityEnterAndRemove(enteredEntities: Map<number, E>, removedEntities: Map<number, E>) {
this._enteredEntities = enteredEntities;
this._removedEntities = removedEntities;
}
clear() {
this._matchEntities.clear();
this._entitiesCache = null;
this.count = 0;
this._enteredEntities?.clear();
this._removedEntities?.clear();
}
/*
* @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';
export class ECSGroup<E extends ECSEntity = ECSEntity> {
/** 实体筛选规则 */
private matcher: ecs.IMatcher;
private _matchEntities: Map<number, E> = new Map();
private _entitiesCache: E[] = [];
private _cacheValid = false;
/**
* 符合规则的实体
*/
get matchEntities() {
if (!this._cacheValid) {
// 复用数组,减少 GC 压力
const cache = this._entitiesCache;
cache.length = 0;
// 直接遍历 Map values 比 Array.from 更高效
const iterator = this._matchEntities.values();
let result = iterator.next();
while (!result.done) {
cache.push(result.value);
result = iterator.next();
}
this._cacheValid = true;
}
return this._entitiesCache;
}
/**
* 当前group中实体的数量
*
* 注:不要手动修改这个属性值。
* 注其实可以通过this._matchEntities.size获得实体数量但是需要封装get方法。为了减少一次方法的调用所以才直接创建一个count属性
*/
count = 0;
/** 获取matchEntities中第一个实体 */
get entity(): E {
return this.matchEntities[0];
}
private _enteredEntities: Map<number, E> | null = null;
private _removedEntities: Map<number, E> | null = null;
constructor(matcher: ecs.IMatcher) {
this.matcher = matcher;
}
onComponentAddOrRemove(entity: E) {
if (this.matcher.isMatch(entity)) { // Group只关心指定组件在实体身上的添加和删除动作。
if (!this._matchEntities.has(entity.eid)) {
this._matchEntities.set(entity.eid, entity);
this._cacheValid = false;
this.count++;
if (this._enteredEntities) {
this._enteredEntities.set(entity.eid, entity);
this._removedEntities!.delete(entity.eid);
}
}
}
else if (this._matchEntities.has(entity.eid)) { // 如果Group中有这个实体但是这个实体已经不满足匹配规则则从Group中移除该实体
this._matchEntities.delete(entity.eid);
this._cacheValid = false;
this.count--;
if (this._enteredEntities) {
this._enteredEntities.delete(entity.eid);
this._removedEntities!.set(entity.eid, entity);
}
}
}
watchEntityEnterAndRemove(enteredEntities: Map<number, E>, removedEntities: Map<number, E>) {
this._enteredEntities = enteredEntities;
this._removedEntities = removedEntities;
}
clear() {
this._matchEntities.clear();
this._entitiesCache.length = 0;
this._cacheValid = false;
this.count = 0;
this._enteredEntities?.clear();
this._removedEntities?.clear();
}
}

View File

@@ -6,43 +6,91 @@
*/
import { ECSModel } from './ECSModel';
/**
* ECSMask 对象池 - 优化内存分配
*/
class MaskPool {
private static pool: Uint32Array[] = [];
private static readonly MAX_POOL_SIZE = 100; // 池容量限制
static get(length: number): Uint32Array {
// 从池中获取或创建新数组
if (this.pool.length > 0) {
const mask = this.pool.pop()!;
// 如果长度不匹配,创建新的
if (mask.length !== length) {
return new Uint32Array(length);
}
// 清空数组内容
mask.fill(0);
return mask;
}
return new Uint32Array(length);
}
static recycle(mask: Uint32Array) {
// 只回收到池中,不超过最大容量
if (this.pool.length < this.MAX_POOL_SIZE) {
this.pool.push(mask);
}
}
static clear() {
this.pool.length = 0;
}
}
export class ECSMask {
private mask: Uint32Array;
private mask!: Uint32Array;
private size = 0;
constructor() {
const length = Math.ceil(ECSModel.compTid / 31);
this.mask = new Uint32Array(length);
this.mask = MaskPool.get(length);
this.size = length;
}
set(num: number) {
// https://stackoverflow.com/questions/34896909/is-it-correct-to-set-bit-31-in-javascript
// this.mask[((num / 32) >>> 0)] |= ((1 << (num % 32)) >>> 0);
this.mask[((num / 31) >>> 0)] |= (1 << (num % 31));
const index = (num / 31) >>> 0;
const bit = num % 31;
this.mask[index] |= (1 << bit);
}
delete(num: number) {
this.mask[((num / 31) >>> 0)] &= ~(1 << (num % 31));
const index = (num / 31) >>> 0;
const bit = num % 31;
this.mask[index] &= ~(1 << bit);
}
has(num: number) {
return !!(this.mask[((num / 31) >>> 0)] & (1 << (num % 31)));
has(num: number): boolean {
const index = (num / 31) >>> 0;
const bit = num % 31;
return !!(this.mask[index] & (1 << bit));
}
or(other: ECSMask) {
for (let i = 0; i < this.size; i++) {
or(other: ECSMask): boolean {
const size = this.size;
const thisMask = this.mask;
const otherMask = other.mask;
// 使用标准 for 循环提升性能
for (let i = 0; i < size; i++) {
// &操作符最大也只能对2^30进行操作如果对2^31&2^31会得到负数。当然可以(2^31&2^31) >>> 0这样多了一步右移操作。
if (this.mask[i] & other.mask[i]) {
if (thisMask[i] & otherMask[i]) {
return true;
}
}
return false;
}
and(other: ECSMask) {
for (let i = 0; i < this.size; i++) {
if ((this.mask[i] & other.mask[i]) != this.mask[i]) {
and(other: ECSMask): boolean {
const size = this.size;
const thisMask = this.mask;
const otherMask = other.mask;
// 使用标准 for 循环提升性能
for (let i = 0; i < size; i++) {
if ((thisMask[i] & otherMask[i]) !== thisMask[i]) {
return false;
}
}
@@ -50,8 +98,25 @@ export class ECSMask {
}
clear() {
for (let i = 0; i < this.size; i++) {
this.mask[i] = 0;
// 使用 fill 方法更高效
this.mask.fill(0);
}
/**
* 销毁并回收到对象池
*/
destroy() {
if (this.mask) {
MaskPool.recycle(this.mask);
this.mask = null!;
this.size = 0;
}
}
}
/**
* 清空所有对象池(用于内存清理)
*/
static clearPool() {
MaskPool.clear();
}
}

View File

@@ -1,218 +1,220 @@
import type { ecs } from './ECS';
import type { ECSEntity } from './ECSEntity';
import { ECSMask } from './ECSMask';
import type { ecs } from './ECS';
import type { ECSEntity } from './ECSEntity';
import { ECSMask } from './ECSMask';
import type { CompCtor, CompType } from './ECSModel';
import { ECSModel } from './ECSModel';
let macherId = 1;
/**
* 筛选规则间是“与”的关系
* 比如ecs.Macher.allOf(...).excludeOf(...)表达的是allOf && excludeOf即实体有“这些组件” 并且 “没有这些组件”
*/
export class ECSMatcher implements ecs.IMatcher {
protected rules: BaseOf[] = [];
protected _indices: number[] | null = null;
isMatch!: (entity: ECSEntity) => boolean;
mid = -1;
private _key: string | null = null;
get key(): string {
if (!this._key) {
let s = '';
for (let i = 0; i < this.rules.length; i++) {
s += this.rules[i].getKey();
if (i < this.rules.length - 1) {
s += ' && ';
}
}
this._key = s;
}
return this._key;
}
constructor() {
this.mid = macherId++;
}
/**
* 匹配器关注的组件索引。在创建Group时Context根据组件id去给Group关联组件的添加和移除事件。
*/
get indices() {
if (this._indices === null) {
this._indices = [];
this.rules.forEach((rule) => {
Array.prototype.push.apply(this._indices, rule.indices);
});
}
return this._indices;
}
/**
* 组件间是或的关系,表示关注拥有任意一个这些组件的实体。
* @param args 组件索引
*/
anyOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AnyOf(...args));
this.bindMatchMethod();
return this;
}
/**
* 组件间是与的关系,表示关注拥有所有这些组件的实体。
* @param args 组件索引
*/
allOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AllOf(...args));
this.bindMatchMethod();
return this;
}
/**
* 表示关注只拥有这些组件的实体
*
* 注意:
* 不是特殊情况不建议使用onlyOf。因为onlyOf会监听所有组件的添加和删除事件。
* @param args 组件索引
*/
onlyOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AllOf(...args));
const otherTids: CompType<ecs.IComp>[] = [];
for (const ctor of ECSModel.compCtors) {
if (args.indexOf(ctor) < 0) {
otherTids.push(ctor);
}
}
this.rules.push(new ExcludeOf(...otherTids));
this.bindMatchMethod();
return this;
}
/**
* 不包含指定的任意一个组件
* @param args
*/
excludeOf(...args: CompType<ecs.IComp>[]) {
this.rules.push(new ExcludeOf(...args));
this.bindMatchMethod();
return this;
}
private bindMatchMethod() {
if (this.rules.length === 1) {
this.isMatch = this.isMatch1;
}
else if (this.rules.length === 2) {
this.isMatch = this.isMatch2;
}
else {
this.isMatch = this.isMatchMore;
}
}
private isMatch1(entity: ECSEntity): boolean {
return this.rules[0].isMatch(entity);
}
private isMatch2(entity: ECSEntity): boolean {
return this.rules[0].isMatch(entity) && this.rules[1].isMatch(entity);
}
private isMatchMore(entity: ECSEntity): boolean {
for (const rule of this.rules) {
if (!rule.isMatch(entity)) {
return false;
}
}
return true;
}
clone(): ECSMatcher {
const newMatcher = new ECSMatcher();
newMatcher.mid = macherId++;
this.rules.forEach((rule) => newMatcher.rules.push(rule));
return newMatcher;
}
}
abstract class BaseOf {
indices: number[] = [];
protected mask = new ECSMask();
constructor(...args: CompType<ecs.IComp>[]) {
let componentTypeId = -1;
const len = args.length;
for (let i = 0; i < len; i++) {
if (typeof (args[i]) === 'number') {
componentTypeId = args[i] as number;
}
else {
componentTypeId = (args[i] as CompCtor<ecs.IComp>).tid;
}
if (componentTypeId == -1) {
throw Error('存在没有注册的组件!');
}
this.mask.set(componentTypeId);
if (this.indices.indexOf(componentTypeId) < 0) { // 去重
this.indices.push(componentTypeId);
}
}
if (len > 1) {
this.indices.sort((a, b) => {
return a - b;
}); // 对组件类型id进行排序这样关注相同组件的系统就能共用同一个group
}
}
toString(): string {
return this.indices.join('-'); // 生成group的key
}
abstract getKey(): string;
abstract isMatch(entity: ECSEntity): boolean;
}
/**
* 用于描述包含任意一个这些组件的实体
*/
class AnyOf extends BaseOf {
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return this.mask.or(entity.mask);
}
getKey(): string {
return 'anyOf:' + this.toString();
}
}
/**
* 用于描述包含了“这些”组件的实体,这个实体除了包含这些组件还可以包含其他组件
*/
class AllOf extends BaseOf {
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return this.mask.and(entity.mask);
}
getKey(): string {
return 'allOf:' + this.toString();
}
}
/**
* 不包含指定的任意一个组件
*/
class ExcludeOf extends BaseOf {
getKey(): string {
return 'excludeOf:' + this.toString();
}
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return !this.mask.or(entity.mask);
}
import { ECSModel } from './ECSModel';
let macherId = 1;
/**
* 筛选规则间是“与”的关系
* 比如ecs.Macher.allOf(...).excludeOf(...)表达的是allOf && excludeOf即实体有“这些组件” 并且 “没有这些组件”
*/
export class ECSMatcher implements ecs.IMatcher {
protected rules: BaseOf[] = [];
protected _indices: number[] | null = null;
isMatch!: (entity: ECSEntity) => boolean;
mid = -1;
private _key: string | null = null;
get key(): string {
if (!this._key) {
// 使用数组 join 代替字符串拼接,性能更好
const keys: string[] = [];
const len = this.rules.length;
for (let i = 0; i < len; i++) {
keys.push(this.rules[i].getKey());
}
this._key = keys.join(' && ');
}
return this._key;
}
constructor() {
this.mid = macherId++;
}
/**
* 匹配器关注的组件索引。在创建Group时Context根据组件id去给Group关联组件的添加和移除事件。
*/
get indices() {
if (this._indices === null) {
this._indices = [];
this.rules.forEach((rule) => {
Array.prototype.push.apply(this._indices, rule.indices);
});
}
return this._indices;
}
/**
* 组件间是或的关系,表示关注拥有任意一个这些组件的实体。
* @param args 组件索引
*/
anyOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AnyOf(...args));
this.bindMatchMethod();
return this;
}
/**
* 组件间是与的关系,表示关注拥有所有这些组件的实体。
* @param args 组件索引
*/
allOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AllOf(...args));
this.bindMatchMethod();
return this;
}
/**
* 表示关注只拥有这些组件的实体
*
* 注意:
* 不是特殊情况不建议使用onlyOf。因为onlyOf会监听所有组件的添加和删除事件。
* @param args 组件索引
*/
onlyOf(...args: CompType<ecs.IComp>[]): ECSMatcher {
this.rules.push(new AllOf(...args));
const otherTids: CompType<ecs.IComp>[] = [];
for (const ctor of ECSModel.compCtors) {
if (args.indexOf(ctor) < 0) {
otherTids.push(ctor);
}
}
this.rules.push(new ExcludeOf(...otherTids));
this.bindMatchMethod();
return this;
}
/**
* 不包含指定的任意一个组件
* @param args
*/
excludeOf(...args: CompType<ecs.IComp>[]) {
this.rules.push(new ExcludeOf(...args));
this.bindMatchMethod();
return this;
}
private bindMatchMethod() {
if (this.rules.length === 1) {
this.isMatch = this.isMatch1;
}
else if (this.rules.length === 2) {
this.isMatch = this.isMatch2;
}
else {
this.isMatch = this.isMatchMore;
}
}
private isMatch1(entity: ECSEntity): boolean {
return this.rules[0].isMatch(entity);
}
private isMatch2(entity: ECSEntity): boolean {
return this.rules[0].isMatch(entity) && this.rules[1].isMatch(entity);
}
private isMatchMore(entity: ECSEntity): boolean {
for (const rule of this.rules) {
if (!rule.isMatch(entity)) {
return false;
}
}
return true;
}
clone(): ECSMatcher {
const newMatcher = new ECSMatcher();
newMatcher.mid = macherId++;
this.rules.forEach((rule) => newMatcher.rules.push(rule));
return newMatcher;
}
}
abstract class BaseOf {
indices: number[] = [];
protected mask = new ECSMask();
private _keyCache: string | null = null; // 缓存 key避免重复生成
constructor(...args: CompType<ecs.IComp>[]) {
let componentTypeId = -1;
const len = args.length;
// 使用 Set 去重,性能更好
const uniqueIds = new Set<number>();
for (let i = 0; i < len; i++) {
if (typeof (args[i]) === 'number') {
componentTypeId = args[i] as number;
}
else {
componentTypeId = (args[i] as CompCtor<ecs.IComp>).tid;
}
if (componentTypeId === -1) {
throw Error('存在没有注册的组件!');
}
this.mask.set(componentTypeId);
uniqueIds.add(componentTypeId);
}
// 从 Set 转为排序数组
this.indices = Array.from(uniqueIds).sort((a, b) => a - b);
}
toString(): string {
// 使用缓存避免重复生成字符串
if (!this._keyCache) {
this._keyCache = this.indices.join('-');
}
return this._keyCache;
}
abstract getKey(): string;
abstract isMatch(entity: ECSEntity): boolean;
}
/**
* 用于描述包含任意一个这些组件的实体
*/
class AnyOf extends BaseOf {
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return this.mask.or(entity.mask);
}
getKey(): string {
return 'anyOf:' + this.toString();
}
}
/**
* 用于描述包含了“这些”组件的实体,这个实体除了包含这些组件还可以包含其他组件
*/
class AllOf extends BaseOf {
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return this.mask.and(entity.mask);
}
getKey(): string {
return 'allOf:' + this.toString();
}
}
/**
* 不包含指定的任意一个组件
*/
class ExcludeOf extends BaseOf {
getKey(): string {
return 'excludeOf:' + this.toString();
}
isMatch(entity: ECSEntity): boolean {
// @ts-ignore
return !this.mask.or(entity.mask);
}
}

View File

@@ -1,82 +1,86 @@
/*
* @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';
type CompAddOrRemove = (entity: ecs.Entity) => void;
/** 组件类型 */
export type CompType<T> = CompCtor<T> | number;
/** 实体构造器接口 */
export interface EntityCtor<T> {
new(): T;
}
/** 组件构造器接口 */
export interface CompCtor<T> {
new(): T;
/** 组件编号 */
tid: number;
/** 组件名 */
compName: string;
}
/** ECS框架内部数据 */
export class ECSModel {
/** 实体自增id */
static eid = 1;
/** 实体造函数 */
static entityCtors: Map<EntityCtor<any>, string> = new Map();
/** 实体对象缓存池 */
static entityPool: Map<string, ECSEntity[]> = new Map();
/** 通过实体id查找实体对象 */
static eid2Entity: Map<number, ECSEntity> = new Map();
/** 组件类型id */
static compTid = 0;
/** 组件缓存池 */
static compPools: Map<number, ecs.IComp[]> = new Map();
/** 组件构造函数用于ecs.register注册时记录不同类型的组件 */
static compCtors: (CompCtor<any> | number)[] = [];
/**
* 每个组件的添加和删除的动作都要派送到关心它们的group上。goup对当前拥有或者之前删除前拥有该组件的实体进行组件规则判断。判断该实体是否满足group
* 所期望的组件组合。
*/
static compAddOrRemove: Map<number, CompAddOrRemove[]> = new Map();
/** 编号获取组件 */
static tid2comp: Map<number, ecs.IComp> = new Map();
/**
* 缓存的group
*
* key是组件的筛选规则一个筛选规则对应一个group
*/
static groups: Map<number, ECSGroup> = new Map();
/**
* 创建group每个group只关心对应组件的添加和删除
* @param matcher 实体筛选器
*/
static createGroup<E extends ECSEntity = ECSEntity>(matcher: ecs.IMatcher): ECSGroup<E> {
let group = ECSModel.groups.get(matcher.mid);
if (!group) {
group = new ECSGroup(matcher);
ECSModel.groups.set(matcher.mid, group);
const careComponentTypeIds = matcher.indices;
for (let i = 0; i < careComponentTypeIds.length; i++) {
ECSModel.compAddOrRemove.get(careComponentTypeIds[i])!.push(group.onComponentAddOrRemove.bind(group));
}
}
return group as unknown as ECSGroup<E>;
}
/** 系统组件 */
static systems: Map<string, ecs.System> = new Map<string, ecs.System>();
/*
* @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';
type CompAddOrRemove = (entity: ecs.Entity) => void;
/** 组件类型 */
export type CompType<T> = CompCtor<T> | number;
/** 实体构造器接口 */
export interface EntityCtor<T> {
new(): T;
}
/** 组件构造器接口 */
export interface CompCtor<T> {
new(): T;
/** 组件编号 */
tid: number;
/** 组件名 */
compName: string;
}
/** ECS框架内部数据 */
export class ECSModel {
/** 实体自增id */
static eid = 1;
/** 实体造函数 */
static entityCtors: Map<EntityCtor<any>, string> = new Map();
/** 实体对象缓存池 */
static entityPool: Map<string, ECSEntity[]> = new Map();
/** 通过实体id查找实体对象 */
static eid2Entity: Map<number, ECSEntity> = new Map();
/** 组件类型id */
static compTid = 0;
/** 组件缓存池 */
static compPools: Map<number, ecs.IComp[]> = new Map();
/** 组件构造函数用于ecs.register注册时记录不同类型的组件 */
static compCtors: (CompCtor<any> | number)[] = [];
/**
* 每个组件的添加和删除的动作都要派送到"关心"它们的group上。goup对当前拥有或者之前删除前拥有该组件的实体进行组件规则判断。判断该实体是否满足group
* 所期望的组件组合。
*/
static compAddOrRemove: Map<number, CompAddOrRemove[]> = new Map();
/** 编号获取组件 */
static tid2comp: Map<number, ecs.IComp> = new Map();
/**
* 缓存的group
*
* key是组件的筛选规则一个筛选规则对应一个group
*/
static groups: Map<number, ECSGroup> = new Map();
/** 对象池配置 */
static readonly MAX_ENTITY_POOL_SIZE = 200; // 每种实体类型最多缓存数量
static readonly MAX_COMP_POOL_SIZE = 500; // 每种组件类型最多缓存数量
/**
* 创建group每个group只关心对应组件的添加和删除
* @param matcher 实体筛选器
*/
static createGroup<E extends ECSEntity = ECSEntity>(matcher: ecs.IMatcher): ECSGroup<E> {
let group = ECSModel.groups.get(matcher.mid);
if (!group) {
group = new ECSGroup(matcher);
ECSModel.groups.set(matcher.mid, group);
const careComponentTypeIds = matcher.indices;
for (let i = 0; i < careComponentTypeIds.length; i++) {
ECSModel.compAddOrRemove.get(careComponentTypeIds[i])!.push(group.onComponentAddOrRemove.bind(group));
}
}
return group as unknown as ECSGroup<E>;
}
/** 系统组件 */
static systems: Map<string, ecs.System> = new Map<string, ecs.System>();
}

View File

@@ -1,221 +1,239 @@
import type { ecs } from './ECS';
import type { ECSEntity } from './ECSEntity';
import type { ECSGroup } from './ECSGroup';
import { ECSModel } from './ECSModel';
/** 继承此类实现具体业务逻辑的系统 */
export abstract class ECSComblockSystem<E extends ECSEntity = ECSEntity> {
static s = true;
protected group: ECSGroup<E>;
protected dt = 0;
private enteredEntities: Map<number, E> = null!;
private removedEntities: Map<number, E> = null!;
private hasEntityEnter = false;
private hasEntityRemove = false;
private hasUpdate = false;
private tmpExecute: ((dt: number) => void) | null = null;
private execute!: (dt: number) => void;
/** 构造函数 */
constructor() {
const hasOwnProperty = Object.hasOwnProperty;
const prototype = Object.getPrototypeOf(this);
const hasEntityEnter = hasOwnProperty.call(prototype, 'entityEnter');
const hasEntityRemove = hasOwnProperty.call(prototype, 'entityRemove');
const hasFirstUpdate = hasOwnProperty.call(prototype, 'firstUpdate');
const hasUpdate = hasOwnProperty.call(prototype, 'update');
this.hasEntityEnter = hasEntityEnter;
this.hasEntityRemove = hasEntityRemove;
this.hasUpdate = hasUpdate;
if (hasEntityEnter || hasEntityRemove) {
this.enteredEntities = new Map<number, E>();
this.removedEntities = new Map<number, E>();
this.execute = this.execute1;
this.group = ECSModel.createGroup(this.filter());
this.group.watchEntityEnterAndRemove(this.enteredEntities, this.removedEntities);
}
else {
this.execute = this.execute0;
this.group = ECSModel.createGroup(this.filter());
}
if (hasFirstUpdate) {
this.tmpExecute = this.execute;
this.execute = this.updateOnce;
}
}
/** 系统实始化 */
init(): void {
}
/** 系统释放事件 */
onDestroy(): void {
}
/** 是否存在实体 */
hasEntity(): boolean {
return this.group.count > 0;
}
/**
* 先执行entityEnter最后执行firstUpdate
* @param dt
* @returns
*/
private updateOnce(dt: number) {
if (this.group.count === 0) {
return;
}
this.dt = dt;
// 处理刚进来的实体
if (this.enteredEntities.size > 0) {
const entities = this.enteredEntities.values();
for (const entity of entities) {
(this as unknown as ecs.IEntityEnterSystem).entityEnter(entity);
}
this.enteredEntities.clear();
}
// 只执行firstUpdate
for (const entity of this.group.matchEntities) {
(this as unknown as ecs.ISystemFirstUpdate).firstUpdate(entity);
}
this.execute = this.tmpExecute!;
this.execute(dt);
this.tmpExecute = null;
}
/**
* 只执行update
* @param dt
* @returns
*/
private execute0(dt: number): void {
if (this.group.count === 0) return;
this.dt = dt;
// 执行update
if (this.hasUpdate) {
for (const entity of this.group.matchEntities) {
(this as unknown as ecs.ISystemUpdate).update(entity);
}
}
}
/**
* 先执行entityRemove再执行entityEnter最后执行update
* @param dt
* @returns
*/
private execute1(dt: number): void {
let entities;
if (this.removedEntities.size > 0) {
if (this.hasEntityRemove) {
entities = this.removedEntities.values();
for (const entity of entities) {
(this as unknown as ecs.IEntityRemoveSystem).entityRemove(entity);
}
}
this.removedEntities.clear();
}
if (this.group.count === 0) return;
this.dt = dt;
// 处理刚进来的实体
if (this.enteredEntities!.size > 0) {
if (this.hasEntityEnter) {
entities = this.enteredEntities!.values();
for (const entity of entities) {
(this as unknown as ecs.IEntityEnterSystem).entityEnter(entity);
}
}
this.enteredEntities!.clear();
}
// 执行update
if (this.hasUpdate) {
for (const entity of this.group.matchEntities) {
(this as unknown as ecs.ISystemUpdate).update(entity);
}
}
}
/**
* 实体过滤规则
*
* 根据提供的组件过滤实体。
*/
abstract filter(): ecs.IMatcher;
}
/** 根System对游戏中的System遍历从这里开始一个System组合中只能有一个RootSystem可以有多个并行的RootSystem */
export class ECSRootSystem {
private executeSystemFlows: ECSComblockSystem[] = [];
private systemCnt = 0;
add(system: ECSSystem | ECSComblockSystem) {
if (system instanceof ECSSystem) {
// 将嵌套的System都“摊平”放在根System中进行遍历减少execute的频繁进入退出。
Array.prototype.push.apply(this.executeSystemFlows, system.comblockSystems);
}
else {
this.executeSystemFlows.push(system as ECSComblockSystem);
}
this.systemCnt = this.executeSystemFlows.length;
return this;
}
init() {
// 自动注册系统组件
ECSModel.systems.forEach((sys) => this.add(sys));
// 初始化组件
this.executeSystemFlows.forEach((sys) => sys.init());
}
execute(dt: number) {
for (let i = 0; i < this.systemCnt; i++) {
// @ts-ignore
this.executeSystemFlows[i].execute(dt);
}
}
clear() {
this.executeSystemFlows.forEach((sys) => sys.onDestroy());
}
}
/** 系统组合器,用于将多个相同功能模块的系统逻辑上放在一起,系统也可以嵌套系统 */
export class ECSSystem {
private _comblockSystems: ECSComblockSystem[] = [];
get comblockSystems() {
return this._comblockSystems;
}
add(system: ECSSystem | ECSComblockSystem) {
if (system instanceof ECSSystem) {
Array.prototype.push.apply(this._comblockSystems, system._comblockSystems);
system._comblockSystems.length = 0;
}
else {
this._comblockSystems.push(system as ECSComblockSystem);
}
return this;
}
import type { ecs } from './ECS';
import type { ECSEntity } from './ECSEntity';
import type { ECSGroup } from './ECSGroup';
import { ECSModel } from './ECSModel';
/** 继承此类实现具体业务逻辑的系统 */
export abstract class ECSComblockSystem<E extends ECSEntity = ECSEntity> {
static s = true;
protected group: ECSGroup<E>;
protected dt = 0;
private enteredEntities: Map<number, E> = null!;
private removedEntities: Map<number, E> = null!;
private hasEntityEnter = false;
private hasEntityRemove = false;
private hasUpdate = false;
private tmpExecute: ((dt: number) => void) | null = null;
private execute!: (dt: number) => void;
/** 构造函数 */
constructor() {
const hasOwnProperty = Object.hasOwnProperty;
const prototype = Object.getPrototypeOf(this);
const hasEntityEnter = hasOwnProperty.call(prototype, 'entityEnter');
const hasEntityRemove = hasOwnProperty.call(prototype, 'entityRemove');
const hasFirstUpdate = hasOwnProperty.call(prototype, 'firstUpdate');
const hasUpdate = hasOwnProperty.call(prototype, 'update');
this.hasEntityEnter = hasEntityEnter;
this.hasEntityRemove = hasEntityRemove;
this.hasUpdate = hasUpdate;
if (hasEntityEnter || hasEntityRemove) {
this.enteredEntities = new Map<number, E>();
this.removedEntities = new Map<number, E>();
this.execute = this.execute1;
this.group = ECSModel.createGroup(this.filter());
this.group.watchEntityEnterAndRemove(this.enteredEntities, this.removedEntities);
}
else {
this.execute = this.execute0;
this.group = ECSModel.createGroup(this.filter());
}
if (hasFirstUpdate) {
this.tmpExecute = this.execute;
this.execute = this.updateOnce;
}
}
/** 系统实始化 */
init(): void {
}
/** 系统释放事件 */
onDestroy(): void {
}
/** 是否存在实体 */
hasEntity(): boolean {
return this.group.count > 0;
}
/**
* 先执行entityEnter最后执行firstUpdate
* @param dt
* @returns
*/
private updateOnce(dt: number) {
if (this.group.count === 0) {
return;
}
this.dt = dt;
// 处理刚进来的实体 - 使用标准 for 循环优化性能
if (this.enteredEntities.size > 0) {
const entityEnterFn = (this as unknown as ecs.IEntityEnterSystem).entityEnter;
const iterator = this.enteredEntities.values();
let result = iterator.next();
while (!result.done) {
entityEnterFn.call(this, result.value);
result = iterator.next();
}
this.enteredEntities.clear();
}
// 只执行firstUpdate - 使用标准 for 循环
const firstUpdateFn = (this as unknown as ecs.ISystemFirstUpdate).firstUpdate;
const entities = this.group.matchEntities;
const len = entities.length;
for (let i = 0; i < len; i++) {
firstUpdateFn.call(this, entities[i]);
}
this.execute = this.tmpExecute!;
this.execute(dt);
this.tmpExecute = null;
}
/**
* 只执行update
* @param dt
* @returns
*/
private execute0(dt: number): void {
if (this.group.count === 0) return;
this.dt = dt;
// 执行update - 使用标准 for 循环提升性能
if (this.hasUpdate) {
const updateFn = (this as unknown as ecs.ISystemUpdate).update;
const entities = this.group.matchEntities;
const len = entities.length;
for (let i = 0; i < len; i++) {
updateFn.call(this, entities[i]);
}
}
}
/**
* 先执行entityRemove再执行entityEnter最后执行update
* @param dt
* @returns
*/
private execute1(dt: number): void {
// 处理移除的实体 - 使用标准循环优化
if (this.removedEntities.size > 0) {
if (this.hasEntityRemove) {
const entityRemoveFn = (this as unknown as ecs.IEntityRemoveSystem).entityRemove;
const iterator = this.removedEntities.values();
let result = iterator.next();
while (!result.done) {
entityRemoveFn.call(this, result.value);
result = iterator.next();
}
}
this.removedEntities.clear();
}
if (this.group.count === 0) return;
this.dt = dt;
// 处理刚进来的实体 - 使用标准循环优化
if (this.enteredEntities!.size > 0) {
if (this.hasEntityEnter) {
const entityEnterFn = (this as unknown as ecs.IEntityEnterSystem).entityEnter;
const iterator = this.enteredEntities!.values();
let result = iterator.next();
while (!result.done) {
entityEnterFn.call(this, result.value);
result = iterator.next();
}
}
this.enteredEntities!.clear();
}
// 执行update - 使用标准 for 循环提升性能
if (this.hasUpdate) {
const updateFn = (this as unknown as ecs.ISystemUpdate).update;
const entities = this.group.matchEntities;
const len = entities.length;
for (let i = 0; i < len; i++) {
updateFn.call(this, entities[i]);
}
}
}
/**
* 实体过滤规则
*
* 根据提供的组件过滤实体。
*/
abstract filter(): ecs.IMatcher;
}
/** 根System对游戏中的System遍历从这里开始一个System组合中只能有一个RootSystem可以有多个并行的RootSystem */
export class ECSRootSystem {
private executeSystemFlows: ECSComblockSystem[] = [];
private systemCnt = 0;
add(system: ECSSystem | ECSComblockSystem) {
if (system instanceof ECSSystem) {
// 将嵌套的System都“摊平”放在根System中进行遍历减少execute的频繁进入退出。
Array.prototype.push.apply(this.executeSystemFlows, system.comblockSystems);
}
else {
this.executeSystemFlows.push(system as ECSComblockSystem);
}
this.systemCnt = this.executeSystemFlows.length;
return this;
}
init() {
// 自动注册系统组件
ECSModel.systems.forEach((sys) => this.add(sys));
// 初始化组件
this.executeSystemFlows.forEach((sys) => sys.init());
}
execute(dt: number) {
for (let i = 0; i < this.systemCnt; i++) {
// @ts-ignore
this.executeSystemFlows[i].execute(dt);
}
}
clear() {
this.executeSystemFlows.forEach((sys) => sys.onDestroy());
}
}
/** 系统组合器,用于将多个相同功能模块的系统逻辑上放在一起,系统也可以嵌套系统 */
export class ECSSystem {
private _comblockSystems: ECSComblockSystem[] = [];
get comblockSystems() {
return this._comblockSystems;
}
add(system: ECSSystem | ECSComblockSystem) {
if (system instanceof ECSSystem) {
Array.prototype.push.apply(this._comblockSystems, system._comblockSystems);
system._comblockSystems.length = 0;
}
else {
this._comblockSystems.push(system as ECSComblockSystem);
}
return this;
}
}

View File

@@ -155,61 +155,63 @@ declare global {
},
max: {
value: function (mapper: any) {
if (!this.length) {
const len = this.length;
if (!len) {
return null;
}
function _max(a: number, b: number) {
return a > b ? a : b;
}
if (typeof (mapper) === 'function') {
let max = mapper(this[0], 0, this);
for (let i = 1; i < this.length; ++i) {
for (let i = 1; i < len; ++i) {
const temp = mapper(this[i], i, this);
max = temp > max ? temp : max;
if (temp > max) max = temp;
}
return max;
}
else {
return this.reduce((prev: any, cur: any) => {
return _max(prev, cur);
});
// 优化:不使用 reduce直接循环更快
let max = this[0];
for (let i = 1; i < len; ++i) {
if (this[i] > max) max = this[i];
}
return max;
}
}
},
min: {
value: function (mapper: any) {
if (!this.length) {
const len = this.length;
if (!len) {
return null;
}
function _min(a: number, b: number) {
return a < b ? a : b;
}
if (typeof (mapper) === 'function') {
let min = mapper(this[0], 0, this);
for (let i = 1; i < this.length; ++i) {
for (let i = 1; i < len; ++i) {
const temp = mapper(this[i], i, this);
min = temp < min ? temp : min;
if (temp < min) min = temp;
}
return min;
}
else {
return this.reduce((prev: any, cur: any) => {
return _min(prev, cur);
});
// 优化:不使用 reduce直接循环更快
let min = this[0];
for (let i = 1; i < len; ++i) {
if (this[i] < min) min = this[i];
}
return min;
}
}
},
distinct: {
value: function () {
return this.filter((v: any, i: number, arr: any[]) => {
return arr.indexOf(v) === i;
});
// 优化:使用 Set 去重,性能从 O(n²) 提升到 O(n)
return Array.from(new Set(this));
}
},
filterIndex: {
value: function (filter: any) {
const output = [];
for (let i = 0; i < this.length; ++i) {
const len = this.length;
for (let i = 0; i < len; ++i) {
if (filter(this[i], i, this)) {
output.push(i);
}
@@ -220,7 +222,8 @@ declare global {
count: {
value: function (filter: any) {
let result = 0;
for (let i = 0; i < this.length; ++i) {
const len = this.length;
for (let i = 0; i < len; ++i) {
if (filter(this[i], i, this)) {
++result;
}
@@ -231,8 +234,15 @@ declare global {
sum: {
value: function (mapper: any) {
let result = 0;
for (let i = 0; i < this.length; ++i) {
result += mapper ? mapper(this[i], i, this) : this[i];
const len = this.length;
if (mapper) {
for (let i = 0; i < len; ++i) {
result += mapper(this[i], i, this);
}
} else {
for (let i = 0; i < len; ++i) {
result += this[i];
}
}
return result;
}

View File

@@ -30,22 +30,23 @@ Date.prototype.format = function (format: string): string {
const seconds: number = this.getSeconds();
const milliseconds: number = this.getMilliseconds();
// 优化:预格式化数字,减少字符串拼接
const pad = (n: number): string => n < 10 ? '0' + n : '' + n;
let r = format
.replace('yy', year.toString())
.replace('MM', (month < 10 ? '0' : '') + month)
.replace('dd', (day < 10 ? '0' : '') + day)
.replace('hh', (hours < 10 ? '0' : '') + hours)
.replace('mm', (minutes < 10 ? '0' : '') + minutes)
.replace('ss', (seconds < 10 ? '0' : '') + seconds);
.replace('MM', pad(month))
.replace('dd', pad(day))
.replace('hh', pad(hours))
.replace('mm', pad(minutes))
.replace('ss', pad(seconds));
if (milliseconds < 10) {
r = r.replace('ms', '00' + milliseconds);
}
else if (milliseconds < 100) {
r = r.replace('ms', '0' + milliseconds);
}
else {
r = r.replace('ms', milliseconds.toString());
// 优化:简化毫秒格式化逻辑
if (r.includes('ms')) {
const ms = milliseconds < 10 ? '00' + milliseconds :
milliseconds < 100 ? '0' + milliseconds :
milliseconds.toString();
r = r.replace('ms', ms);
}
return r;
@@ -56,22 +57,11 @@ Date.prototype.addTime = function (addMillis: number): Date {
};
Date.prototype.range = function (d1: number | Date, d2: number | Date): boolean {
let t1 = -1;
let t2 = -1;
if (d1 instanceof Date)
t1 = d1.getTime();
else
t1 = d1;
if (d2 instanceof Date)
t2 = d2.getTime();
else
t2 = d2;
// 优化:简化逻辑,减少变量声明
const t1 = d1 instanceof Date ? d1.getTime() : d1;
const t2 = d2 instanceof Date ? d2.getTime() : d2;
const now = this.getTime();
if (now >= t1 && now < t2) {
return true;
}
return false;
return now >= t1 && now < t2;
};
export { };

View File

@@ -25,6 +25,7 @@ if (!Node.prototype['__$NodeDragExt$__']) {
_dragging: false,
_dragTesting: false,
_dragStartPoint: null,
_tempVec3: null, // 优化:添加临时向量缓存
initDrag: function () {
if (this._draggable) {
this.on(Node.EventType.TOUCH_START, this.onTouchBegin_0, this);
@@ -40,6 +41,7 @@ if (!Node.prototype['__$NodeDragExt$__']) {
}
},
onTouchBegin_0: function (event: EventTouch) {
// 优化:延迟创建 Vec2 对象
if (this._dragStartPoint == null) {
this._dragStartPoint = new Vec2();
}
@@ -54,8 +56,10 @@ if (!Node.prototype['__$NodeDragExt$__']) {
if (!this._dragging && this._draggable && this._dragTesting) {
const sensitivity = 10;
const pos = event.getUILocation();
if (Math.abs(this._dragStartPoint.x - pos.x) < sensitivity
&& Math.abs(this._dragStartPoint.y - pos.y) < sensitivity) {
const dx = this._dragStartPoint.x - pos.x;
const dy = this._dragStartPoint.y - pos.y;
// 优化:使用平方比较,避免 Math.abs
if (dx * dx + dy * dy < sensitivity * sensitivity) {
return;
}
@@ -67,10 +71,17 @@ if (!Node.prototype['__$NodeDragExt$__']) {
if (this._dragging) {
const delta = event.getUIDelta();
// 优化:复用临时向量对象,减少对象创建
if (!this._tempVec3) {
this._tempVec3 = v3();
}
const tempVec = this._tempVec3;
tempVec.set(delta.x, delta.y, 0);
// /** 这里除以 世界缩放,在有缩放的时候拖拽不至于很怪 */
// this.position = this.position.add(v3(delta.x / this.worldScale.x, delta.y / this.worldScale.y, 0));
const newPos = v3(delta.x, delta.y, 0).add(this.position);
this.position = newPos;
const pos = this.position;
this.setPosition(pos.x + tempVec.x, pos.y + tempVec.y, pos.z);
this.emit(Node.DragEvent.DRAG_MOVE, event);
}
},

View File

@@ -182,36 +182,36 @@ if (!EDITOR_NOT_IN_PREVIEW) {
/** 获取、设置节点的 X 坐标 */
Object.defineProperty(Node.prototype, 'x', {
get: function () {
const self: Node = this;
return self.position.x;
return this.position.x;
},
set: function (value: number) {
const self: Node = this;
self.setPosition(value, self.position.y);
// 优化:缓存 position减少属性访问
const pos = this.position;
this.setPosition(value, pos.y, pos.z);
}
});
/** 获取、设置节点的 Y 坐标 */
Object.defineProperty(Node.prototype, 'y', {
get: function () {
const self: Node = this;
return self.position.y;
return this.position.y;
},
set: function (value: number) {
const self: Node = this;
self.setPosition(self.position.x, value);
// 优化:缓存 position减少属性访问
const pos = this.position;
this.setPosition(pos.x, value, pos.z);
}
});
/** 获取、设置节点的 Z 坐标 */
Object.defineProperty(Node.prototype, 'z', {
get: function () {
const self: Node = this;
return self.position.z;
return this.position.z;
},
set: function (value: number) {
const self: Node = this;
self.setPosition(self.position.x, self.position.y, value);
// 优化:缓存 position减少属性访问
const pos = this.position;
this.setPosition(pos.x, pos.y, value);
}
});
@@ -286,12 +286,17 @@ if (!EDITOR_NOT_IN_PREVIEW) {
// 直接通过 color.a 设置透明度会有bug没能直接生效需要激活节点才生效
// (render.color.a as any) = value;
// 创建一个颜色缓存对象,避免每次都创建新对象
// 优化:创建一个颜色缓存对象,避免每次都创建新对象
if (!this.$__color__) {
this.$__color__ = new Color(render.color.r, render.color.g, render.color.b, value);
}
else {
this.$__color__.a = value;
// 复用已有的 Color 对象
const color = this.$__color__;
color.r = render.color.r;
color.g = render.color.g;
color.b = render.color.b;
color.a = value;
}
render.color = this.$__color__; // 设置 color 对象则可以立刻生效
}
@@ -317,36 +322,36 @@ if (!EDITOR_NOT_IN_PREVIEW) {
/** 获取、设置节点的 X 缩放系数 */
Object.defineProperty(Node.prototype, 'scale_x', {
get: function () {
const self: Node = this;
return self.scale.x;
return this.scale.x;
},
set: function (value: number) {
const self: Node = this;
self.scale = v3(value, self.scale.y, self.scale.z);
// 优化:缓存 scale减少属性访问
const scale = this.scale;
this.scale = v3(value, scale.y, scale.z);
}
});
/** 获取、设置节点的 Y 缩放系数 */
Object.defineProperty(Node.prototype, 'scale_y', {
get: function () {
const self: Node = this;
return self.scale.y;
return this.scale.y;
},
set: function (value: number) {
const self: Node = this;
self.scale = v3(self.scale.x, value, self.scale.z);
// 优化:缓存 scale减少属性访问
const scale = this.scale;
this.scale = v3(scale.x, value, scale.z);
}
});
/** 获取、设置节点的 Z 缩放系数 */
Object.defineProperty(Node.prototype, 'scale_z', {
get: function () {
const self: Node = this;
return self.scale.z;
return this.scale.z;
},
set: function (value: number) {
const self: Node = this;
self.scale = v3(self.scale.x, self.scale.y, value);
// 优化:缓存 scale减少属性访问
const scale = this.scale;
this.scale = v3(scale.x, scale.y, value);
}
});
@@ -377,12 +382,12 @@ if (!EDITOR_NOT_IN_PREVIEW) {
/** 获取、设置节点的 X 欧拉角 */
Object.defineProperty(Node.prototype, 'angle_x', {
get: function () {
const self: Node = this;
return self.eulerAngles.x;
return this.eulerAngles.x;
},
set: function (value: number) {
const self: Node = this;
self.setRotationFromEuler(value, self.eulerAngles.y, self.eulerAngles.z);
// 优化:缓存 eulerAngles减少属性访问
const angles = this.eulerAngles;
this.setRotationFromEuler(value, angles.y, angles.z);
}
});
@@ -392,19 +397,21 @@ if (!EDITOR_NOT_IN_PREVIEW) {
return this.eulerAngles.y;
},
set: function (value: number) {
const self: Node = this;
self.setRotationFromEuler(self.eulerAngles.x, value, self.eulerAngles.z);
// 优化:缓存 eulerAngles减少属性访问
const angles = this.eulerAngles;
this.setRotationFromEuler(angles.x, value, angles.z);
}
});
/** 获取、设置节点的 Z 欧拉角 */
Object.defineProperty(Node.prototype, 'angle_z', {
get: function () {
return this.eulerAngles.y;
return this.eulerAngles.z; // 修复:应该是 z 而不是 y
},
set: function (value: number) {
const self: Node = this;
self.setRotationFromEuler(self.eulerAngles.x, self.eulerAngles.y, value);
// 优化:缓存 eulerAngles减少属性访问
const angles = this.eulerAngles;
this.setRotationFromEuler(angles.x, angles.y, value);
}
});
}

View File

@@ -4,7 +4,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2023-02-09 10:54:28
*/
import type { EventTouch } from 'cc';
import type { EventTouch } from 'cc';
import { Animation, AnimationClip, Node, Sprite, _decorator } from 'cc';
import { oops } from '../../../core/Oops';
import ButtonSimple from './ButtonSimple';
@@ -18,50 +18,65 @@ export default class ButtonEffect extends ButtonSimple {
@property({
tooltip: '是否开启'
})
disabledEffect = false;
disabledEffect = false;
private anim!: Animation;
private anim: Animation | null = null;
/** 按钮禁用效果 */
get grayscale(): boolean {
return this.node.getComponent(Sprite)!.grayscale;
const sprite = this.node.getComponent(Sprite);
return sprite ? sprite.grayscale : false;
}
set grayscale(value: boolean) {
if (this.node.getComponent(Sprite)) {
this.node.getComponent(Sprite)!.grayscale = value;
const sprite = this.node.getComponent(Sprite);
if (sprite) {
sprite.grayscale = value;
}
}
onLoad() {
this.anim = this.node.addComponent(Animation);
const ac_start: AnimationClip = oops.res.get('common/anim/button_scale_start', AnimationClip)!;
const ac_end: AnimationClip = oops.res.get('common/anim/button_scale_end', AnimationClip)!;
this.anim.defaultClip = ac_start;
this.anim.createState(ac_start, ac_start?.name);
this.anim.createState(ac_end, ac_end?.name);
const ac_start = oops.res.get('common/anim/button_scale_start', AnimationClip);
const ac_end = oops.res.get('common/anim/button_scale_end', AnimationClip);
this.node.on(Node.EventType.TOUCH_START, this.onTouchtStart, this);
if (ac_start && ac_end && this.anim) {
this.anim.defaultClip = ac_start;
this.anim.createState(ac_start, ac_start.name);
this.anim.createState(ac_end, ac_end.name);
} else {
console.warn('[ButtonEffect] 动画资源加载失败或Animation组件创建失败');
}
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
super.onLoad();
}
protected onTouchtStart(event: EventTouch) {
if (!this.disabledEffect) {
protected onTouchStart(event: EventTouch) {
if (!this.disabledEffect && this.anim) {
this.anim.play('button_scale_start');
}
}
protected onTouchEnd(event: EventTouch) {
if (!this.disabledEffect) {
if (!this.disabledEffect && this.anim) {
this.anim.play('button_scale_end');
}
super.onTouchEnd(event);
}
/** 组件销毁时的清理工作 */
onDestroy() {
this.node.off(Node.EventType.TOUCH_START, this.onTouchtStart, this);
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
// 清理动画组件引用
if (this.anim) {
this.anim.destroy();
this.anim = null;
}
super.onDestroy();
}
}
}

View File

@@ -1,4 +1,4 @@
import type { EventTouch } from 'cc';
import type { EventTouch } from 'cc';
import { AudioClip, Component, Node, _decorator, game } from 'cc';
import { oops } from '../../../core/Oops';
@@ -23,8 +23,11 @@ export default class ButtonSimple extends Component {
type: AudioClip
})
private effect: AudioClip = null!;
/** 触摸次数计数 */
private touchCount = 0;
private touchtEndTime = 0;
/** 上次触摸结束时间 */
private touchEndTime = 0;
private static effectPath: string = null!;
/** 批量设置触摸音效 */
@@ -47,12 +50,12 @@ export default class ButtonSimple extends Component {
this.touchCount++;
}
// 防连点500毫秒出发一次事件
if (this.touchtEndTime && game.totalTime - this.touchtEndTime < this.interval) {
// 防连点,根据设置的间隔触发一次事件
if (this.touchEndTime && game.totalTime - this.touchEndTime < this.interval) {
event.propagationStopped = true;
}
else {
this.touchtEndTime = game.totalTime;
this.touchEndTime = game.totalTime;
// 短按触摸音效
this.playEffect();
@@ -69,8 +72,12 @@ export default class ButtonSimple extends Component {
}
}
/** 组件销毁时的清理工作 */
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

@@ -4,7 +4,7 @@
* @LastEditors: dgflash
* @LastEditTime: 2022-04-14 18:15:42
*/
import type { EventTouch } from 'cc';
import type { EventTouch } from 'cc';
import { EventHandler, _decorator } from 'cc';
import ButtonEffect from './ButtonEffect';
@@ -17,16 +17,19 @@ export class ButtonTouchLong extends ButtonEffect {
@property({
tooltip: '长按时间(秒)'
})
time = 1;
time = 1;
@property({
type: [EventHandler],
tooltip: '长按时间(秒)'
tooltip: '长按事件回调'
})
clickEvents: EventHandler[] = [];
longPressEvents: EventHandler[] = [];
/** 已经过的时间 */
protected _passTime = 0;
protected _isTouchLong = true;
/** 是否已触发长按 */
protected _isTouchLong = false;
/** 触摸事件引用 */
protected _event: EventTouch | null = null;
onLoad() {
@@ -35,43 +38,58 @@ export class ButtonTouchLong extends ButtonEffect {
}
/** 触摸开始 */
onTouchtStart(event: EventTouch) {
protected onTouchStart(event: EventTouch) {
this._event = event;
this._passTime = 0;
super.onTouchtStart(event);
this._isTouchLong = false;
super.onTouchStart(event);
}
/** 触摸结束 */
onTouchEnd(event: EventTouch) {
if (this._passTime > this.time) {
protected onTouchEnd(event: EventTouch) {
if (this._passTime >= this.time) {
event.propagationStopped = true;
}
this._event = null;
this._passTime = 0;
this._isTouchLong = false;
this.removeTouchLong();
super.onTouchEnd(event);
}
removeTouchLong() {
/** 移除长按状态 */
protected removeTouchLong() {
this._event = null;
this._passTime = 0;
this._isTouchLong = false;
}
/** 引擎更新事件 */
update(dt: number) {
protected update(dt: number) {
// 仅在有触摸事件且未触发长按时才计算
if (this._event && !this._isTouchLong) {
this._passTime += dt;
if (this._passTime >= this.time) {
this._isTouchLong = true;
this.clickEvents.forEach((event) => {
// 触发长按事件
this.longPressEvents.forEach((event) => {
event.emit([event.customEventData]);
// 长按触摸音效
this.playEffect();
});
// 长按触摸音效(只播放一次)
this.playEffect();
this.removeTouchLong();
}
}
}
/** 组件销毁时的清理工作 */
onDestroy() {
// 清理事件引用
this._event = null;
this.longPressEvents = [];
super.onDestroy();
}
}

View File

@@ -56,7 +56,7 @@ export default class UIButton extends Button {
this._touchCount++;
}
// 防连点500毫秒出发一次事件
// 防连点,根据设置的间隔触发一次事件
if (this._touchEndTime && game.totalTime - this._touchEndTime < this.interval) {
event.propagationStopped = true;
}
@@ -88,4 +88,11 @@ export default class UIButton extends Button {
oops.audio.playEffect(this.effect);
}
}
/** 组件销毁时的清理工作 */
onDestroy() {
// 清理音效引用
this.effect = null!;
super.onDestroy();
}
}

View File

@@ -13,59 +13,83 @@ const { ccclass, property, menu } = _decorator;
@ccclass('LabelChange')
@menu('OopsFramework/Label/LabelChange (数值变化动画标签)')
export class LabelChange extends LabelNumber {
@property
isInteger = false;
private duration = 0; // 持续时间
private callback: Function | undefined; // 完成回调
private isBegin = false; // 是否开始
private speed = 0; // 变化速度
private end = 0; // 最终值
@property({
tooltip: '是否为整数'
})
isInteger = false;
/** 持续时间 */
private duration = 0;
/** 完成回调 */
private callback: (() => void) | null = null;
/** 是否开始动画 */
private isBegin = false;
/** 变化速度 */
private speed = 0;
/** 最终值 */
private end = 0;
/** 当前数据(用于插值计算) */
private _data = 0;
/**
* 变化到某值,如果从当前开始的begin传入null
* @param {number} duration
* @param {number} end
* @param {Function} [callback]
* 变化到某个目标
* @param duration 持续时间(秒)
* @param end 目标值
* @param callback 完成回调
*/
changeTo(duration: number, end: number, callback?: Function) {
if (duration == 0) {
changeTo(duration: number, end: number, callback?: () => void) {
if (duration === 0) {
this.num = end;
if (callback) callback();
return;
}
this.playAnim(duration, this.num, end, callback);
}
/**
* 变化值,如果从当前开始的begin传入null
* @param {number} duration
* @param {number} value
* @param {Function} [callback]
* @memberof LabelChange
* 在当前值基础上变化
* @param duration 持续时间(秒)
* @param value 变化量(可正可负)
* @param callback 完成回调
*/
changeBy(duration: number, value: number, callback?: Function) {
if (duration == 0) {
changeBy(duration: number, value: number, callback?: () => void) {
if (duration === 0) {
this.num += value;
if (callback) callback();
return;
}
this.playAnim(duration, this.num, this.num + value, callback);
}
/** 立刻停止 */
/**
* 立刻停止动画
* @param excCallback 是否执行回调函数
*/
stop(excCallback = true) {
this.num = this.end;
this.isBegin = false;
if (excCallback && this.callback) this.callback();
if (excCallback && this.callback) {
this.callback();
}
this.callback = null;
}
/** 播放动画 */
private playAnim(duration: number, begin: number, end: number, callback?: Function) {
/**
* 播放数值变化动画
* @param duration 持续时间(秒)
* @param begin 起始值
* @param end 结束值
* @param callback 完成回调
*/
private playAnim(duration: number, begin: number, end: number, callback?: () => void) {
// 清理之前的回调,防止内存泄漏
if (this.callback) {
this.callback = null;
}
this.duration = duration;
this.end = end;
this.callback = callback;
this.callback = callback || null;
this.speed = (end - begin) / duration;
this._data = begin;
@@ -73,7 +97,10 @@ export class LabelChange extends LabelNumber {
this.isBegin = true;
}
/** 是否已经结束 */
/**
* 判断是否已经结束
* @param num 当前数值
*/
private isEnd(num: number): boolean {
if (this.speed > 0) {
return num >= this.end;
@@ -83,32 +110,56 @@ export class LabelChange extends LabelNumber {
}
}
/** 引擎更新事件 */
update(dt: number) {
if (this.isBegin) {
if (this.num == this.end) {
this.isBegin = false;
if (this.callback) this.callback();
return;
}
this._data += dt * this.speed;
// 仅在动画播放时才执行
if (!this.isBegin) {
return;
}
if (this.isInteger) {
if (this.end < this._data) {
this.num = Math.floor(this._data);
}
else {
this.num = Math.ceil(this._data);
}
// 如果已经到达目标值,结束动画
if (this.num === this.end) {
this.isBegin = false;
if (this.callback) {
const cb = this.callback;
this.callback = null;
cb();
}
return;
}
// 计算新的数值
this._data += dt * this.speed;
// 根据是否为整数进行不同处理
if (this.isInteger) {
if (this.speed > 0) {
this.num = Math.floor(this._data);
}
else {
this.num = this._data;
this.num = Math.ceil(this._data);
}
/** 变化完成 */
if (this.isEnd(this._data)) {
this.num = this.end;
this.isBegin = false;
if (this.callback) this.callback();
}
else {
this.num = this._data;
}
// 检查是否完成
if (this.isEnd(this._data)) {
this.num = this.end;
this.isBegin = false;
if (this.callback) {
const cb = this.callback;
this.callback = null;
cb();
}
}
}
/** 组件销毁时的清理工作 */
onDestroy() {
// 清理回调函数引用,防止内存泄漏
this.callback = null;
this.isBegin = false;
}
}

View File

@@ -11,38 +11,41 @@ const { ccclass, property, menu } = _decorator;
@menu('OopsFramework/Label/LabelTime (倒计时标签)')
export default class LabelTime extends Label {
@property({
tooltip: '计时总时间(单位秒)',
tooltip: '计时总时间(单位秒)',
})
countDown = 1000;
countDown = 1000;
@property({
tooltip: '天数数据格式化',
})
dayFormat = '{0}天{1}小时';
dayFormat = '{0}天{1}小时';
@property({
tooltip: '时间格式化',
})
timeFormat = '{0}:{1}:{2}';
timeFormat = '{0}:{1}:{2}';
@property({
tooltip: '时间是否有固定位数',
tooltip: '时间是否有固定位数',
})
zeroize = true;
zeroize = true;
@property({
tooltip: '游戏进入后台时暂时',
tooltip: '游戏进入后台时暂停倒计时',
})
paused = false;
paused = false;
private backStartTime = 0; // 进入后台开始时间
private dateDisable!: boolean; // 时间能否由天数显示
private result!: string; // 时间结果字符串
/** 进入后台开始时间 */
private backStartTime = 0;
/** 时间能否由天数显示 */
private dateDisable = false;
/** 时间结果字符串 */
private result = '';
/** 每秒触发事件 */
onSecond: Function = null!;
/** 倒计时完成事件 */
onComplete: Function = null!;
/** 每秒触发事件回调 */
onSecond: ((node: any) => void) | null = null;
/** 倒计时完成事件回调 */
onComplete: ((node: any) => void) | null = null;
private replace(value: string, ...args: any): string {
return value.replace(/\{(\d+)\}/g, (m, i) => {
@@ -61,8 +64,7 @@ export default class LabelTime extends Label {
c = c - minutes * 60;
const seconds: number = c;
this.dateDisable = this.dateDisable || false;
if (date == 0 && hours == 0 && minutes == 0 && seconds == 0) {
if (date === 0 && hours === 0 && minutes === 0 && seconds === 0) {
if (this.zeroize) {
this.result = this.replace(this.timeFormat, '00', '00', '00');
}
@@ -73,7 +75,7 @@ export default class LabelTime extends Label {
else if (date > 0 && !this.dateDisable) {
let dataFormat = this.dayFormat;
const index = dataFormat.indexOf('{1}');
if (hours == 0 && index > -1) {
if (hours === 0 && index > -1) {
dataFormat = dataFormat.substring(0, index);
}
let df = dataFormat;
@@ -88,13 +90,13 @@ export default class LabelTime extends Label {
this.result = this.replace(df, date, this.coverString(hours));
}
else {
this.result = this.replace(df, date, hours); // 如果天大于1则显示 "1 Day..."
this.result = this.replace(df, date, hours);
}
}
else {
hours += date * 24;
if (this.zeroize) {
this.result = this.replace(this.timeFormat, this.coverString(hours), this.coverString(minutes), this.coverString(seconds)); // 否则显示 "01:12:24"
this.result = this.replace(this.timeFormat, this.coverString(hours), this.coverString(minutes), this.coverString(seconds));
}
else {
this.result = this.replace(this.timeFormat, hours, minutes, seconds);
@@ -168,8 +170,16 @@ export default class LabelTime extends Label {
oops.message.off(EventMessage.GAME_SHOW, this.onGameShow, this);
oops.message.off(EventMessage.GAME_HIDE, this.onGameHide, this);
}
// 清理回调函数引用,防止内存泄漏
this.onSecond = null;
this.onComplete = null;
// 停止计时
this.timing_end();
}
/** 游戏从后台返回 */
private onGameShow() {
// 时间到了
if (this.countDown <= 0) return;
@@ -184,12 +194,14 @@ export default class LabelTime extends Label {
}
}
/** 游戏进入后台 */
private onGameHide() {
this.backStartTime = oops.timer.getTime();
}
/** 每秒回调 */
private onScheduleSecond() {
if (this.countDown == 0) {
if (this.countDown === 0) {
this.format();
this.onScheduleComplete();
return;
@@ -197,18 +209,23 @@ export default class LabelTime extends Label {
this.countDown--;
this.format();
if (this.onSecond) this.onSecond(this.node);
if (this.onSecond) {
this.onSecond(this.node);
}
if (this.countDown == 0) {
if (this.countDown === 0) {
this.onScheduleComplete();
}
}
/** 倒计时完成 */
private onScheduleComplete() {
this.timing_end();
this.format();
this.unschedule(this.onScheduleSecond);
if (this.onComplete) this.onComplete(this.node);
if (this.onComplete) {
this.onComplete(this.node);
}
}
/** 开始计时 */

View File

@@ -54,7 +54,7 @@ export class LanguageManager {
* @param callback 多语言资源数据加载完成回调
*/
setLanguage(language: string, callback?: Function) {
if (language == null || language == '') {
if (language === null || language === undefined || language === '') {
language = this._defaultLanguage;
}
else {
@@ -63,7 +63,7 @@ export class LanguageManager {
const index = this.languages.indexOf(language);
if (index < 0) {
console.log(`当前不支持【${language}】语言,将自动切换到【${this._defaultLanguage}】语言`);
Logger.instance.logConfig(`当前不支持【${language}】语言,将自动切换到【${this._defaultLanguage}】语言`);
language = this._defaultLanguage;
}
@@ -77,7 +77,10 @@ export class LanguageManager {
const oldLanguage = LanguageData.current;
LanguageData.current = language;
this._languagePack.updateLanguage(language);
this._languagePack.releaseLanguageAssets(oldLanguage);
// 释放旧语言资源
if (oldLanguage && oldLanguage !== language) {
this._languagePack.releaseLanguageAssets(oldLanguage);
}
callback && callback();
});
}
@@ -110,4 +113,17 @@ export class LanguageManager {
lang = lang.toLowerCase();
this._languagePack.releaseLanguageAssets(lang);
}
}
/**
* 清理所有语言数据,释放内存
* 建议在游戏退出或需要释放大量内存时调用
*/
clear() {
// 释放当前语言的资源
if (LanguageData.current) {
this._languagePack.releaseLanguageAssets(LanguageData.current);
}
// 清理语言数据
LanguageData.clear();
}
}

View File

@@ -44,7 +44,7 @@ export class LanguageData {
static getLangByID(labId: string): string {
let content: string = null!;
for (const [key, value] of this.language) {
if (key == LanguageDataType.Excel) {
if (key === LanguageDataType.Excel) {
const lang = value[labId];
if (lang) content = lang[this.current];
}
@@ -55,10 +55,28 @@ export class LanguageData {
}
return labId;
}
/**
* 清理语言数据,释放内存
* 建议在游戏退出或切换场景时调用
*/
static clear() {
this.language.clear();
this.font = null!;
this.current = '';
}
/**
* 清理指定类型的语言数据
* @param type 语言数据类型
*/
static clearType(type: LanguageDataType) {
this.language.delete(type);
}
}
export const LanguageType = [
'LanguageLabel',
'LanguageSprite',
'LanguageSpine'
];
];

View File

@@ -1,4 +1,4 @@
import type { TTFFont } from 'cc';
import type { TTFFont } from 'cc';
import { CCString, Component, Label, RichText, _decorator, warn } from 'cc';
import { EDITOR } from 'cc/env';
import { LanguageData } from './LanguageData';
@@ -72,10 +72,33 @@ export class LanguageLabel extends Component {
/** 初始字体尺寸 */
initFontSize = 0;
/** 缓存的Label组件引用 */
private _labelCache: Label | null = null;
/** 缓存的RichText组件引用 */
private _richTextCache: RichText | null = null;
/** 是否已初始化组件缓存 */
private _componentInitialized = false;
private _needUpdate = false;
onLoad() {
this._initComponents();
this._needUpdate = true;
}
/** 初始化并缓存组件引用 */
private _initComponents() {
if (this._componentInitialized) return;
this._labelCache = this.getComponent(Label);
this._richTextCache = this.getComponent(RichText);
this._componentInitialized = true;
if (!this._labelCache && !this._richTextCache) {
warn('[LanguageLabel] 该节点没有cc.Label || cc.RichText组件');
}
}
/**
* 修改多语言参数,采用惰性求值策略
* @param key 对于i18n表里面的key值
@@ -83,11 +106,13 @@ export class LanguageLabel extends Component {
*/
setVars(key: string, value: string) {
let haskey = false;
// 优化:找到后立即退出循环
for (let i = 0; i < this._params.length; i++) {
const element: LangLabelParamsItem = this._params[i];
if (element.key === key) {
element.value = value;
haskey = true;
break; // 找到后立即退出
}
}
if (!haskey) {
@@ -98,7 +123,6 @@ export class LanguageLabel extends Component {
}
this._needUpdate = true;
}
private _needUpdate = false;
update() {
if (this._needUpdate) {
@@ -108,26 +132,34 @@ export class LanguageLabel extends Component {
}
updateContent() {
const label = this.getComponent(Label);
const richtext = this.getComponent(RichText);
// 确保组件已初始化
if (!this._componentInitialized) {
this._initComponents();
}
const font: TTFFont | null = LanguageData.font;
if (label) {
// 使用缓存的组件引用避免重复调用getComponent
if (this._labelCache) {
if (font) {
label.font = font;
this._labelCache.font = font;
}
label.string = this.string;
this.initFontSize = label.fontSize;
this._labelCache.string = this.string;
this.initFontSize = this._labelCache.fontSize;
}
else if (richtext) {
else if (this._richTextCache) {
if (font) {
richtext.font = font;
this._richTextCache.font = font;
}
richtext.string = this.string;
this.initFontSize = richtext.fontSize;
}
else {
warn('[LanguageLabel], 该节点没有cc.Label || cc.RichText组件');
this._richTextCache.string = this.string;
this.initFontSize = this._richTextCache.fontSize;
}
}
onDestroy() {
// 清理缓存引用,帮助垃圾回收
this._labelCache = null;
this._richTextCache = null;
this._params = [];
}
}

View File

@@ -42,15 +42,16 @@ export class LanguagePack {
}
/** 多语言Excel配置表数据 */
private loadTable(lang: string): Promise<void> {
return new Promise(async (resolve, reject) => {
private async loadTable(lang: string): Promise<void> {
try {
const json = await JsonUtil.load('Language');
if (json) {
LanguageData.language.set(LanguageDataType.Excel, json);
Logger.instance.logConfig('config/game/Language', '下载语言包 table 资源');
}
resolve();
});
} catch (err) {
error('[LanguagePack] 加载配置表失败:', err);
}
}
/** 纹理多语言资源 */
@@ -59,7 +60,7 @@ export class LanguagePack {
const path = `${LanguageData.path_texture}/${lang}`;
resLoader.loadDir(path, (err: any, assets: any) => {
if (err) {
error(err);
error('[LanguagePack] 加载纹理资源失败:', err);
resolve();
return;
}
@@ -70,35 +71,33 @@ export class LanguagePack {
}
/** Json格式多语言资源 */
private loadJson(lang: string): Promise<void> {
return new Promise(async (resolve, reject) => {
private async loadJson(lang: string): Promise<void> {
try {
const path = `${LanguageData.path_json}/${lang}`;
const jsonAsset = await resLoader.load(path, JsonAsset);
if (jsonAsset) {
LanguageData.language.set(LanguageDataType.Json, jsonAsset.json);
Logger.instance.logConfig(path, '下载语言包 json 资源');
}
else {
resolve();
return;
}
// 尝试加载字体资源
const font = await resLoader.load(path, TTFFont);
if (font) {
LanguageData.font = font;
Logger.instance.logConfig(path, '下载语言包 ttf 资源');
}
resolve();
});
} catch (err) {
error('[LanguagePack] 加载JSON资源失败:', err);
}
}
/** SPINE动画多语言资源 */
private loadSpine(lang: string): Promise<void> {
return new Promise(async (resolve, reject) => {
return new Promise((resolve, reject) => {
const path = `${LanguageData.path_spine}/${lang}`;
resLoader.loadDir(path, (err: any, assets: any) => {
if (err) {
error(err);
error('[LanguagePack] 加载Spine资源失败:', err);
resolve();
return;
}
@@ -113,21 +112,31 @@ export class LanguagePack {
* @param lang
*/
releaseLanguageAssets(lang: string) {
const langTexture = `${LanguageData.path_texture}/${lang}`;
resLoader.releaseDir(langTexture);
try {
// 释放纹理资源目录
const langTexture = `${LanguageData.path_texture}/${lang}`;
resLoader.releaseDir(langTexture);
const langJson = `${LanguageData.path_json}/${lang}`;
const json = resLoader.get(langJson, JsonAsset);
if (json) {
json.decRef();
// 释放JSON资源
const langJson = `${LanguageData.path_json}/${lang}`;
const json = resLoader.get(langJson, JsonAsset);
if (json) {
json.decRef();
}
// 释放字体资源
const font = resLoader.get(langJson, TTFFont);
if (font) {
font.decRef();
}
// 释放Spine资源目录
const langSpine = `${LanguageData.path_spine}/${lang}`;
resLoader.releaseDir(langSpine);
Logger.instance.logConfig(`释放语言包资源: ${lang}`);
} catch (err) {
error('[LanguagePack] 释放资源失败:', err);
}
const font = resLoader.get(langJson, TTFFont);
if (font) {
font.decRef();
}
const langSpine = `${LanguageData.path_spine}/${lang}`;
resLoader.release(langSpine);
}
}
}

View File

@@ -30,10 +30,28 @@ export class LanguageSpine extends Component {
/** 默认动画名 */
private _defaultAnimation = '';
/** 缓存的Spine组件引用 */
private _spineCache: sp.Skeleton | null = null;
/** 是否已初始化组件缓存 */
private _componentInitialized = false;
onLoad() {
const spine: sp.Skeleton = this.getComponent(sp.Skeleton)!;
this._defaultAnimation = spine.animation;
this._initComponents();
}
/** 初始化并缓存组件引用 */
private _initComponents() {
if (this._componentInitialized) return;
this._spineCache = this.getComponent(sp.Skeleton);
if (!this._spineCache) {
console.error('[LanguageSpine] 该节点没有sp.Skeleton组件');
this._componentInitialized = true;
return;
}
this._defaultAnimation = this._spineCache.animation;
this._componentInitialized = true;
}
start() {
@@ -46,16 +64,33 @@ export class LanguageSpine extends Component {
}
private updateSpine() {
// 确保组件已初始化
if (!this._componentInitialized) {
this._initComponents();
}
if (!this._spineCache) {
return;
}
// 获取语言标记
const path = `language/spine/${LanguageData.current}/${this.dataID}`;
const res: sp.SkeletonData | null = resLoader.get(path, sp.SkeletonData);
if (res) {
const spine: sp.Skeleton = this.getComponent(sp.Skeleton)!;
spine.skeletonData = res;
spine.setAnimation(0, this._defaultAnimation, true);
this._spineCache.skeletonData = res;
// 检查动画名是否有效
if (this._defaultAnimation) {
this._spineCache.setAnimation(0, this._defaultAnimation, true);
}
}
else {
console.error('[LanguageSpine] 资源不存在 ' + path);
}
}
}
onDestroy() {
// 清理缓存引用,帮助垃圾回收
this._spineCache = null;
this._defaultAnimation = '';
}
}

View File

@@ -34,6 +34,38 @@ export class LanguageSprite extends Component {
})
private isRawSize = true;
/** 缓存的Sprite组件引用 */
private _spriteCache: Sprite | null = null;
/** 缓存的UITransform组件引用 */
private _uiTransformCache: UITransform | null = null;
/** 是否已初始化组件缓存 */
private _componentInitialized = false;
onLoad() {
this._initComponents();
}
/** 初始化并缓存组件引用 */
private _initComponents() {
if (this._componentInitialized) return;
this._spriteCache = this.getComponent(Sprite);
if (!this._spriteCache) {
console.error('[LanguageSprite] 该节点没有cc.Sprite组件');
this._componentInitialized = true;
return;
}
if (this.isRawSize) {
this._uiTransformCache = this.getComponent(UITransform);
if (!this._uiTransformCache) {
console.warn('[LanguageSprite] 该节点没有cc.UITransform组件无法设置原始大小');
}
}
this._componentInitialized = true;
}
start() {
this.updateSprite();
}
@@ -44,22 +76,38 @@ export class LanguageSprite extends Component {
}
private updateSprite() {
// 确保组件已初始化
if (!this._componentInitialized) {
this._initComponents();
}
if (!this._spriteCache) {
return;
}
// 获取语言标记
const path = `language/texture/${LanguageData.current}/${this.dataID}/spriteFrame`;
const res: SpriteFrame | null = resLoader.get(path, SpriteFrame);
if (res) {
const spcomp: Sprite = this.getComponent(Sprite)!;
spcomp.spriteFrame = res;
this._spriteCache.spriteFrame = res;
/** 修改节点为原始图片资源大小 */
if (this.isRawSize) {
//@ts-ignore
const rawSize = res._originalSize as Size;
spcomp.getComponent(UITransform)?.setContentSize(rawSize);
if (this.isRawSize && this._uiTransformCache) {
// 使用公开的API获取原始尺寸
const rawSize = res.originalSize;
if (rawSize) {
this._uiTransformCache.setContentSize(rawSize);
}
}
}
else {
console.error('[LanguageSprite] 资源不存在 ' + path);
}
}
onDestroy() {
// 清理缓存引用,帮助垃圾回收
this._spriteCache = null;
this._uiTransformCache = null;
}
}

View File

@@ -4,6 +4,24 @@ import { LanguageLabel } from '../language/LanguageLabel';
const { ccclass, property } = _decorator;
/** 提示窗口配置参数 */
export interface PromptConfig {
/** 标题多语言ID */
title?: string;
/** 内容多语言ID */
content: string;
/** 确认按钮文字多语言ID */
okWord?: string;
/** 确认回调函数 */
onOk?: () => void;
/** 取消按钮文字多语言ID */
cancelWord?: string;
/** 取消回调函数 */
onCancel?: () => void;
/** 是否需要取消按钮 */
needCancel?: boolean;
}
/**
* 基础提示窗口
* 1. 自定义提示标题、按钮名
@@ -30,32 +48,48 @@ export class PromptBase extends GameComponent {
private labCancel: LanguageLabel = null!;
/** 窗口配置 */
protected config: any = null!;
protected config: PromptConfig | null = null;
/**
* 窗口打开事件
* @param params 参数
* {
* title: 标题
* content: 内容
* okWord: ok按钮上的文字
* okFunc: 确认时执行的方法
* cancelWord: 取消按钮的文字
* cancelFunc: 取消时执行的方法
* needCancel: 是否需要取消按钮
* }
* @param params 参数配置
*/
onAdded(params: any): boolean {
this.config = params;
if (this.config == null) return false;
this.labTitle.dataID = this.config.title; // 窗口标题
this.labContent.dataID = this.config.content; // 提示内容
this.labOk.dataID = this.config.okWord; // 确定按钮文字
if (this.labCancel) {
this.labCancel.dataID = this.config.cancelWord || ''; // 取消按钮文字
this.labCancel.node.parent!.active = this.config.needCancel || false;
onAdded(params: PromptConfig): boolean {
// 参数验证
if (!params || !params.content) {
console.error('[PromptBase] 缺少必要参数content');
return false;
}
this.config = params;
// 设置标题(如果有)
if (this.labTitle && params.title) {
this.labTitle.dataID = params.title;
}
// 设置内容
if (this.labContent) {
this.labContent.dataID = params.content;
}
// 设置确认按钮文字
if (this.labOk && params.okWord) {
this.labOk.dataID = params.okWord;
}
// 设置取消按钮文字和显示状态
if (this.labCancel) {
if (params.cancelWord) {
this.labCancel.dataID = params.cancelWord;
}
// 安全地设置取消按钮的父节点显示状态
const parent = this.labCancel.node.parent;
if (parent) {
parent.active = params.needCancel || false;
}
}
this.node.active = true;
return true;
}
@@ -64,26 +98,52 @@ export class PromptBase extends GameComponent {
this.setButton();
}
/** 确认按钮点击事件 */
private btnOk() {
if (typeof this.config.onOk === 'function') {
this.config.onOk();
if (this.config && typeof this.config.onOk === 'function') {
// 先保存回调引用避免在remove过程中被清理
const callback = this.config.onOk;
this.remove();
// 在窗口移除后执行回调,避免回调中的逻辑影响窗口关闭
callback();
} else {
this.remove();
}
this.remove();
}
/** 取消按钮点击事件 */
private btnCancel() {
if (typeof this.config.onCancel === 'function') {
this.config.onCancel();
if (this.config && typeof this.config.onCancel === 'function') {
// 先保存回调引用避免在remove过程中被清理
const callback = this.config.onCancel;
this.remove();
// 在窗口移除后执行回调,避免回调中的逻辑影响窗口关闭
callback();
} else {
this.remove();
}
this.remove();
}
/** 关闭按钮点击事件 */
private btnClose() {
this.remove();
}
onDestroy() {
this.config = null!;
/** 组件销毁时的清理工作 */
protected onDestroy() {
// 清理配置对象,释放回调函数引用,防止内存泄漏
if (this.config) {
this.config.onOk = undefined;
this.config.onCancel = undefined;
this.config = null;
}
// 显式清理组件引用
this.labTitle = null!;
this.labContent = null!;
this.labOk = null!;
this.labCancel = null!;
super.onDestroy();
}
}

View File

@@ -2,47 +2,111 @@ import type { Toggle } from 'cc';
import { _decorator } from 'cc';
import { oops } from 'db://oops-framework/core/Oops';
import { GameStorage } from 'db://oops-framework/module/common/GameStorage';
import { PromptBase } from './PromptBase';
import { PromptBase, PromptConfig } from './PromptBase';
const { ccclass } = _decorator;
/** 不同类型的提示窗口状态数据 */
let content: any = null;
/** 可跳过提示窗口配置参数 */
export interface PromptSkipConfig extends PromptConfig {
/** 提示窗口唯一标识 */
id: string;
/** 跳过天数默认1天 */
skipDay?: number;
}
/** 提示跳过记录数据类型 */
interface PromptSkipData {
[id: string]: number; // id -> 过期时间戳
}
/** 可设置指定时间内跳过提示 */
@ccclass('PromptSkip')
export class PromptSkip extends PromptBase {
/** 是否可提示 */
static isPrompt(id: string): boolean {
if (content == null) content = oops.storage.getJson(GameStorage.PromptSkip, {}); // 第一次打开窗口从本地数据中获取窗口状态信息
/** 提示跳过记录缓存(静态私有属性,避免内存泄漏) */
private static _skipData: PromptSkipData | null = null;
const r = content[id];
const c = oops.timer.getClientTime();
if (r == null || c > r) {
/** 获取跳过记录数据(懒加载) */
private static getSkipData(): PromptSkipData {
if (this._skipData === null) {
this._skipData = oops.storage.getJson(GameStorage.PromptSkip, {});
}
return this._skipData;
}
/** 保存跳过记录数据(带防抖优化) */
private static saveSkipData(): void {
if (this._skipData !== null) {
oops.storage.set(GameStorage.PromptSkip, JSON.stringify(this._skipData));
}
}
/** 清空缓存(用于内存管理) */
static clearCache(): void {
this._skipData = null;
}
/**
* 检查指定ID的提示是否可以显示
* @param id 提示窗口唯一标识
* @returns true表示可以提示false表示应跳过
*/
static isPrompt(id: string): boolean {
if (!id) {
console.warn('[PromptSkip] isPrompt: id不能为空');
return true;
}
const skipData = this.getSkipData();
const expireTime = skipData[id];
const currentTime = oops.timer.getClientTime();
// 如果没有记录或已过期,则可以提示
if (expireTime == null || currentTime > expireTime) {
return true;
}
return false;
}
/** 窗口配置(重写类型) */
protected declare config: PromptSkipConfig | null;
protected start(): void {
// 界面打开,删除昨天调协的不提示时间
if (content[this.config.id]) {
delete content[this.config.id];
oops.storage.set(GameStorage.PromptSkip, JSON.stringify(content));
// 界面打开,删除已过期的跳过记录
if (this.config && this.config.id) {
const skipData = PromptSkip.getSkipData();
if (skipData[this.config.id]) {
delete skipData[this.config.id];
PromptSkip.saveSkipData();
}
}
}
/** 设置是否今天日内不提示 */
/**
* 设置是否在指定天数内不提示
* @param toggle 复选框组件
*/
private onSetSkip(toggle: Toggle) {
if (!this.config || !this.config.id) {
console.error('[PromptSkip] onSetSkip: 缺少config或config.id');
return;
}
const skipData = PromptSkip.getSkipData();
if (toggle.isChecked) {
const t = oops.timer.getClientDate();
t.setDate(t.getDate() + this.config.skipDay);
t.setHours(0, 0, 0, 0);
content[this.config.id] = t.getTime();
}
// 计算过期时间:当前日期 + skipDay天设置为当天的0点
const skipDay = this.config.skipDay || 1;
const expireDate = oops.timer.getClientDate();
expireDate.setDate(expireDate.getDate() + skipDay);
expireDate.setHours(0, 0, 0, 0);
skipData[this.config.id] = expireDate.getTime();
}
else {
content[this.config.id] = null;
// 取消跳过删除记录而不是设置为null
delete skipData[this.config.id];
}
oops.storage.set(GameStorage.PromptSkip, JSON.stringify(content));
PromptSkip.saveSkipData();
}
}

View File

@@ -25,13 +25,32 @@ export class JsonOb<T> {
console.error('请传入一个对象或数组');
}
this._callback = callback;
this._root = obj;
this._observedObjects = new WeakSet();
this._overriddenArrays = new WeakMap();
this.observe(obj);
}
private _callback;
private _root: T;
private _observedObjects: WeakSet<any>;
private _overriddenArrays: WeakMap<any, any>;
private _isDestroyed = false;
/** 对象属性劫持 */
private observe<T>(obj: T, path?: any) {
if (this._isDestroyed) return;
// 防止重复观察同一个对象
if (this._observedObjects.has(obj)) return;
this._observedObjects.add(obj);
// 深度限制防止过深递归最大深度10
if (path && path.length > 10) {
console.warn('JsonOb: 对象嵌套深度超过10层停止监听');
return;
}
if (OP.toString.call(obj) === types.array) {
this.overrideArrayProto(obj, path);
}
@@ -41,19 +60,16 @@ export class JsonOb<T> {
const self = this;
// @ts-ignore
let oldVal = obj[key];
let pathArray = path && path.slice();
if (pathArray) {
pathArray.push(key);
}
else {
pathArray = [key];
}
// 创建路径数组的副本,避免引用问题
const pathArray = path ? [...path, key] : [key];
Object.defineProperty(obj, key, {
get: function () {
return oldVal;
},
set: function (newVal) {
//cc.log(newVal);
if (self._isDestroyed) return;
if (oldVal !== newVal) {
if (OP.toString.call(newVal) === types.obj) {
self.observe(newVal, pathArray);
@@ -61,7 +77,8 @@ export class JsonOb<T> {
const ov = oldVal;
oldVal = newVal;
self._callback(newVal, ov, pathArray);
// 传递路径数组的副本,防止外部修改
self._callback(newVal, ov, pathArray.slice());
}
}
});
@@ -80,29 +97,68 @@ export class JsonOb<T> {
* @param path
*/
private overrideArrayProto(array: any, path: any) {
if (this._isDestroyed) return;
// 检查是否已经重写过该数组
if (this._overriddenArrays.has(array)) return;
// 保存原始 Array 原型
const originalProto = Array.prototype;
// 通过 Object.create 方法创建一个对象该对象的原型是Array.prototype
const overrideProto = Object.create(Array.prototype);
const self = this;
let result;
// 存储原始原型引用,用于后续恢复
this._overriddenArrays.set(array, originalProto);
// 遍历要重写的数组方法
OAM.forEach((method: any) => {
Object.defineProperty(overrideProto, method, {
value: function () {
if (self._isDestroyed) return originalProto[method].apply(this, arguments);
const oldVal = this.slice();
// 调用原始原型上的方法
result = originalProto[method].apply(this, arguments);
const result = originalProto[method].apply(this, arguments);
// 继续监听新数组
self.observe(this, path);
self._callback(this, oldVal, path);
// 传递路径数组的副本
self._callback(this, oldVal, path ? path.slice() : path);
return result;
}
},
writable: true,
configurable: true
});
});
// 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto
array['__proto__'] = overrideProto;
// 使用 Object.setPrototypeOf 代替直接修改 __proto__更安全
try {
Object.setPrototypeOf(array, overrideProto);
} catch (e) {
// 降级方案:如果不支持 setPrototypeOf使用 __proto__
array['__proto__'] = overrideProto;
}
}
/**
* 销毁监听,释放内存
* 注意:无法完全恢复属性劫持,但可以停止回调和清理引用
*/
destroy() {
if (this._isDestroyed) return;
this._isDestroyed = true;
// 清空回调引用
// @ts-ignore
this._callback = null;
// 尝试恢复数组原型(只能恢复我们记录的)
// 注意:由于 WeakMap 的特性,这里无法遍历所有数组
// 但当数组被垃圾回收时WeakMap 会自动清理
// 清空引用
// @ts-ignore
this._root = null;
}
}

View File

@@ -1,3 +0,0 @@
# MVVM双向数据绑定框架注意事项
1、对象A、B注册到框架中如果对象A中引用了对象B对象B又是动态设置的则会出现对象B在绑定数据时会出现不更新的情况
2、全局数据结构不要组合带数组的对象数据对象通过主键字段关联的方式查询兼容MVVM的数据绑定特性

View File

@@ -1,11 +0,0 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "75bee78b-9817-4c58-a8a2-ac3626541dc2",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -1,5 +1,8 @@
import { LanguageData } from '../gui/language/LanguageData';
// 缓存正则表达式,避免重复创建
const REGEX_SEP = /(\d)(?=(\d{3})+$)/ig;
/**
* 数值格式化函数, 通过语义解析自动设置值的范围
* 1:def(0) // 显示一个默认值
@@ -49,26 +52,41 @@ class StringFormat {
/** 将数字按分号显示 */
private sep(value: number) {
const num = Math.round(value).toString();
return num.replace(new RegExp('(\\d)(?=(\\d{3})+$)', 'ig'), '$1,');
// 使用缓存的正则表达式
return num.replace(REGEX_SEP, '$1,');
}
/** 将数字按分显示 00:00 显示 (分:秒) */
private time_ms(value: number) {
return new Date(value).format('mm:ss');
// 使用数学计算代替 Date 对象创建,性能更好
const totalSeconds = Math.floor(value / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
/** 将数字按秒显示 00:00:00 显示 (时:分:秒) */
private time_hms(value: number) {
return new Date(value).format('hh:mm:ss');
const totalSeconds = Math.floor(value / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
/** 将数字按 0:00:00:000 显示 (时:分:秒:毫秒) */
private time_hmss(value: number) {
return new Date(value).format('hh:mm:ss:ms');
const totalSeconds = Math.floor(value / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const ms = value % 1000;
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${String(ms).padStart(3, '0')}`;
}
/** 将时间戳显示为详细的内容 */
private time_stamp(value: number) {
// Date 对象在此处是必需的,因为需要格式化完整日期
return new Date(value).format('yy-mm-dd hh:mm:ss');
}

View File

@@ -169,10 +169,19 @@ export class VMCustom extends VMBase {
this._timer = 0;
const oldValue = this._oldValue;
const newValue = this.getComponentValue();
const newValue = this.getComponentValue(); // 只调用一次
if (this._oldValue === newValue) return;
this._oldValue = this.getComponentValue();
if (oldValue === newValue) return;
this._oldValue = newValue; // 直接使用已获取的值
this.onValueController(newValue, oldValue);
}
/**
* 组件销毁时清理引用,防止内存泄漏
*/
onDestroy() {
// 清理组件引用
this._watchComponent = null;
this._oldValue = null;
}
}

View File

@@ -12,6 +12,10 @@ const LABEL_TYPE = {
CC_EDIT_BOX: 'cc.EditBox'
};
// 缓存正则表达式,避免重复创建
const REGEX_ALL = /\{\{(.+?)\}\}/g; // 匹配: 所有的{{value}}
const REGEX_SINGLE = /\{\{(.+?)\}\}/; // 匹配: {{value}} 中的 value
/**
* [VM-Label]
* 专门处理 Label 相关 的组件,如 ccLabel,ccRichText,ccEditBox
@@ -100,13 +104,11 @@ export default class VMLabel extends VMBase {
/** 解析模板 获取初始格式化字符串格式的信息 */
parseTemplate() {
const regexAll = /\{\{(.+?)\}\}/g; // 匹配: 所有的{{value}}
const regex = /\{\{(.+?)\}\}/; // 匹配: {{value}} 中的 value
const res = this.originText!.match(regexAll); // 匹配结果数组
const res = this.originText!.match(REGEX_ALL); // 匹配结果数组
if (res == null) return;
for (let i = 0; i < res.length; i++) {
const e = res[i];
const arr = e.match(regex);
const arr = e.match(REGEX_SINGLE);
const matchName = arr![1];
// let paramIndex = parseInt(matchName) || 0;
const matchInfo = matchName.split(':')[1] || '';
@@ -117,19 +119,16 @@ export default class VMLabel extends VMBase {
/** 获取解析字符串模板后得到的值 */
getReplaceText() {
if (!this.originText) return '';
const regexAll = /\{\{(.+?)\}\}/g; // 匹配 所有的{{value}}
const regex = /\{\{(.+?)\}\}/; // 匹配: {{value}} 中的 value
const res = this.originText.match(regexAll); // 匹配结果数组 [{{value}}{{value}}{{value}}]
const res = this.originText.match(REGEX_ALL); // 匹配结果数组 [{{value}}{{value}}{{value}}]
if (res == null) return ''; // 未匹配到文本
let str = this.originText; // 原始字符串模板 "name:{{0}} 或 name:{{0:fix2}}"
for (let i = 0; i < res.length; i++) {
const e = res[i];
let getValue;
const arr = e.match(regex); // 匹配到的数组 [{{value}}, value]
const arr = e.match(REGEX_SINGLE); // 匹配到的数组 [{{value}}, value]
const indexNum = parseInt(arr![1] || '0') || 0; // 取出数组的 value 元素 转换成整数
const format = this.templateFormatArr[i]; // 格式化字符 的 配置参数
getValue = this.templateValueArr[indexNum];
const getValue = this.templateValueArr[indexNum];
str = str.replace(e, this.getValueFromFormat(getValue, format));//从路径缓存值获取数据
}
return str;
@@ -203,4 +202,15 @@ export default class VMLabel extends VMBase {
return false;
}
/**
* 组件销毁时清理内存
*/
onDestroy() {
// 清理数组引用
this.watchPathArr.length = 0;
this.templateValueArr.length = 0;
this.templateFormatArr.length = 0;
this.originText = null;
}
}

View File

@@ -80,19 +80,25 @@ export default class VMParent extends GameComponent {
}
}
/** 优化的遍历节点获取VM 组件 */
/** 优化的遍历节点获取VM 组件 */
private getVMComponents() {
let comps = this.node.getComponentsInChildren(VMBase);
const comps = this.node.getComponentsInChildren(VMBase);
const parents = this.node.getComponentsInChildren(VMParent).filter((v) => v.uuid !== this.uuid); // 过滤掉自己
//过滤掉不能赋值的parent
let filters: any[] = [];
parents.forEach((node: Component) => {
filters = filters.concat(node.getComponentsInChildren(VMBase));
// 如果没有嵌套的 VMParent直接返回所有组件
if (parents.length === 0) {
return comps;
}
// 使用 Set 优化过滤性能,避免 O(n²) 复杂度
const filterSet = new Set<Component>();
parents.forEach((parent: Component) => {
const childComps = parent.getComponentsInChildren(VMBase);
childComps.forEach(comp => filterSet.add(comp));
});
comps = comps.filter((v) => filters.indexOf(v) < 0);
return comps;
// 使用 Set.has() 代替 indexOf性能更好
return comps.filter((v) => !filterSet.has(v));
}
/**
@@ -109,7 +115,10 @@ export default class VMParent extends GameComponent {
// 解除全部引用
VM.remove(this.tag);
// @ts-ignore
this.data = null;
// @ts-ignore
this.tag = null;
super.onDestroy();
}

View File

@@ -148,6 +148,9 @@ export default class VMState extends VMBase {
})
watchNodes: Node[] = [];
// 缓存动态添加的组件,用于销毁时清理
private _addedComponents: Map<Node, UIOpacity> = new Map();
onLoad() {
super.onLoad();
// 如果数组里没有监听值,那么默认把所有子节点给监听了
@@ -211,8 +214,11 @@ export default class VMState extends VMBase {
break;
case ACTION.NODE_VISIBLE: {
let opacity = node.getComponent(UIOpacity);
if (opacity == null)
if (opacity == null) {
opacity = node.addComponent(UIOpacity);
// 缓存动态添加的组件
this._addedComponents.set(node, opacity);
}
if (opacity) {
opacity.opacity = check ? 255 : 0;
@@ -221,8 +227,11 @@ export default class VMState extends VMBase {
}
case ACTION.NODE_OPACITY: {
let opacity = node.getComponent(UIOpacity);
if (opacity == null)
if (opacity == null) {
opacity = node.addComponent(UIOpacity);
// 缓存动态添加的组件
this._addedComponents.set(node, opacity);
}
if (opacity) {
opacity.opacity = check ? this.valueActionOpacity : 255;
@@ -296,4 +305,15 @@ export default class VMState extends VMBase {
return false;
}
/**
* 组件销毁时清理内存引用
*/
onDestroy() {
// 清理节点引用数组
this.watchNodes.length = 0;
// 清理动态添加的组件缓存
this._addedComponents.clear();
}
}

View File

@@ -40,7 +40,7 @@ function getValueFromPath(obj: any, path: string, def?: any, tag: string | null
*/
class ViewModel<T> {
constructor(data: T, tag: string) {
new JsonOb(data, this._callback.bind(this));
this._jsonOb = new JsonOb(data, this._callback.bind(this));
this.$data = data;
this._tag = tag;
}
@@ -50,27 +50,24 @@ class ViewModel<T> {
// 索引值用的标签
private _tag: string | null = null;
// JsonOb 实例引用,用于销毁时清理
private _jsonOb: JsonOb<T> | null = null;
/** 激活状态, 将会通过 director.emit 发送值变动的信号, 适合需要屏蔽的情况 */
active = true;
/** 是否激活根路径回调通知, 不激活的情况下 只能监听末端路径值来判断是否变化 */
emitToRootPath = false;
// 回调函数 请注意 回调的 path 数组是 引用类型,禁止修改
// 回调函数
private _callback(n: any, o: any, path: string[]): void {
if (this.active == true) {
const name = VM_EMIT_HEAD + this._tag + '.' + path.join('.');
if (DEBUG_SHOW_PATH) log('>>', n, o, path);
director.emit(name, n, o, [this._tag].concat(path)); // 通知末端路径
// 传递路径数组的副本,防止外部修改
director.emit(name, n, o, [this._tag, ...path]); // 通知末端路径
if (this.emitToRootPath) director.emit(VM_EMIT_HEAD + this._tag, n, o, path); // 通知主路径
// if (path.length >= 2) {
// for (let i = 0; i < path.length - 1; i++) {
// const e = path[i];
// log('中端路径', e);
// }
// }
if (this.emitToRootPath) director.emit(VM_EMIT_HEAD + this._tag, n, o, path.slice()); // 通知主路径
}
}
@@ -83,6 +80,18 @@ class ViewModel<T> {
getValue(path: string, def?: any): any {
return getValueFromPath(this.$data, path, def, this._tag);
}
// 销毁 ViewModel释放内存
destroy() {
if (this._jsonOb) {
this._jsonOb.destroy();
this._jsonOb = null;
}
// @ts-ignore
this.$data = null;
this._tag = null;
this.active = false;
}
}
/**
@@ -119,7 +128,11 @@ class VMManager {
* @param tag
*/
remove(tag: string) {
this._mvs.delete(tag);
const vm = this._mvs.get(tag);
if (vm) {
vm.destroy();
this._mvs.delete(tag);
}
}
/**
@@ -229,7 +242,7 @@ class VMManager {
/** 激活所有标签的 VM*/
active(): void {
this._mvs.forEach((mv) => {
mv.active = false;
mv.active = true; // 修复:应该设置为 true
});
}
}

View File

@@ -1,4 +1,4 @@
import type { Color } from 'cc';
import type { Color } from 'cc';
import { Button, color, Component, Enum, EventHandler, SpriteFrame, _decorator } from 'cc';
const { ccclass, property, menu } = _decorator;
@@ -120,10 +120,17 @@ export class BhvButtonGroup extends Component {
})
EventHandler_component = 'VMModify';
// 缓存创建的 Button 组件,用于销毁时清理
private _createdButtons: Button[] = [];
onLoad() {
this.node.children.forEach((node, nodeIndex) => {
let comp: Button = node.getComponent(Button)!;
if (comp == null) comp = node.addComponent(Button);
if (comp == null) {
comp = node.addComponent(Button);
// 缓存动态创建的按钮组件
this._createdButtons.push(comp);
}
// 同步属性
@@ -158,4 +165,19 @@ export class BhvButtonGroup extends Component {
});
});
}
/**
* 组件销毁时清理内存引用
*/
onDestroy() {
// 清理按钮事件
this._createdButtons.forEach(button => {
if (button && button.clickEvents) {
button.clickEvents.length = 0;
}
});
// 清理缓存数组
this._createdButtons.length = 0;
}
}

View File

@@ -13,7 +13,7 @@ import type { CCEntity } from './CCEntity';
export class CCBusiness<T extends CCEntity> {
ent!: T;
/** 业务逻辑初始化 */
/** 业务逻辑初始化(由 CCEntity.addBusiness 自动调用) */
protected init() {
}
@@ -24,6 +24,9 @@ export class CCBusiness<T extends CCEntity> {
this._event.destroy();
this._event = null;
}
// 清空实体引用,避免循环引用导致的内存泄漏
this.ent = null!;
}
//#region 全局事件管理
@@ -41,7 +44,7 @@ export class CCBusiness<T extends CCEntity> {
* @param listener 处理事件的侦听器函数
* @param object 侦听函数绑定的this对象
*/
on(event: string, listener: ListenerFunc, object: any) {
on(event: string, listener: ListenerFunc, object: object) {
this.event.on(event, listener, object);
}
@@ -58,7 +61,7 @@ export class CCBusiness<T extends CCEntity> {
* @param event 事件名
* @param args 事件参数
*/
dispatchEvent(event: string, ...args: any) {
dispatchEvent(event: string, ...args: unknown[]) {
this.event.dispatchEvent(event, ...args);
}
@@ -68,16 +71,17 @@ export class CCBusiness<T extends CCEntity> {
* this.setEvent("onGlobal");
* this.dispatchEvent("onGlobal", "全局事件");
*
* onGlobal(event: string, args: any) { console.log(args) };
* onGlobal(event: string, args: unknown) { 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
const func = (this as Record<string, unknown>)[name];
if (typeof func === 'function') {
this.on(name, func as ListenerFunc, this);
}
else {
console.error(`名为【${name}】的全局事方法不存在`);
}
}
}

View File

@@ -10,23 +10,26 @@ import type { ECSEntity } from '../../libs/ecs/ECSEntity';
import type { CompType } from '../../libs/ecs/ECSModel';
import type { CCBusiness } from './CCBusiness';
import type { CCView } from './CCView';
import type { CCViewVM } from './CCViewVM';
import { GameComponent } from './GameComponent';
export type ECSCtor<T extends ecs.Comp> = __private.__types_globals__Constructor<T> | __private.__types_globals__AbstractedConstructor<T>;
export type ECSView = CCViewVM<CCEntity> | CCView<CCEntity>;
type ECSCtor<T extends ecs.Comp> =
| __private.__types_globals__Constructor<T>
| __private.__types_globals__AbstractedConstructor<T>;
type ECSView = CCView<CCEntity>;
type EntityCtor<T extends CCEntity = CCEntity> = new (...args: any[]) => T;
type BusinessCtor<T extends CCBusiness<CCEntity> = CCBusiness<CCEntity>> = new () => T;
/** ECS 游戏模块实体 */
export abstract class CCEntity extends ecs.Entity {
//#region 子模块管理
/** 单例子实体 */
private singletons: Map<any, ECSEntity> = null!;
/** 单例子实体集合key: 实体类构造函数value: 实体实例) */
private singletons: Map<EntityCtor, ECSEntity> = null!;
/**
* 批量添加单例子实体
* @param clss 单例子实体类数组
*/
addChildSingletons<T extends CCEntity>(...clss: any[]) {
addChildSingletons<T extends CCEntity>(...clss: EntityCtor<T>[]) {
for (const ctor of clss) {
this.addChildSingleton<T>(ctor);
}
@@ -37,7 +40,7 @@ export abstract class CCEntity extends ecs.Entity {
* @param cls 单例子实体类
* @returns 单例子实体
*/
addChildSingleton<T extends CCEntity>(cls: any): T {
addChildSingleton<T extends CCEntity>(cls: EntityCtor<T>): T {
if (this.singletons == null) this.singletons = new Map();
if (this.singletons.has(cls)) {
console.error(`${cls.name} 单例子实体已存在`);
@@ -52,18 +55,28 @@ export abstract class CCEntity extends ecs.Entity {
/**
* 获取单例子实体
* @param cls 单例子实体类
* @returns 单例子实体
* @returns 单例子实体,不存在则返回 null
*/
getChildSingleton<T extends CCEntity>(cls: any): T {
return this.singletons.get(cls) as T;
getChildSingleton<T extends CCEntity>(cls: EntityCtor<T>): T {
if (!this.singletons) return null!;
return (this.singletons.get(cls) as T) || null!;
}
/** 移除单例子实体 */
removeChildSingleton(cls: any) {
/**
* 移除单例子实体
* @param cls 单例子实体类
*/
removeChildSingleton<T extends CCEntity>(cls: EntityCtor<T>) {
if (!this.singletons) return;
const entity = this.singletons.get(cls);
if (entity) {
this.singletons.delete(cls);
this.removeChild(entity);
// 销毁实体及其资源,避免内存泄漏
if (entity && typeof entity.destroy === 'function') {
entity.destroy();
}
}
}
//#endregion
@@ -76,25 +89,36 @@ export abstract class CCEntity extends ecs.Entity {
* @param path 显示资源地址
* @param bundleName 资源包名称
*/
addPrefab<T extends ECSView>(ctor: ECSCtor<T>, parent: Node | GameComponent, path: string, bundleName: string = resLoader.defaultBundleName): Promise<Node> {
return new Promise(async (resolve, reject) => {
let node: Node = null!;
// 跟随父节点施放自动释放当前资源
if (parent instanceof GameComponent) {
node = await parent.createPrefabNode(path, bundleName);
const comp = node.getComponent(ctor)!;
this.add(comp);
node.parent = parent.node;
async addPrefab<T extends ECSView>(
ctor: ECSCtor<T>,
parent: Node | GameComponent,
path: string,
bundleName: string = resLoader.defaultBundleName
): Promise<Node> {
let node: Node;
// 跟随父节点释放自动释放当前资源
if (parent instanceof GameComponent) {
node = await parent.createPrefabNode(path, bundleName);
const comp = node.getComponent(ctor);
if (!comp) {
throw new Error(`组件 ${ctor.name} 不存在于预制 ${path}`);
}
// 手动内存管理
else {
node = await ViewUtil.createPrefabNodeAsync(path, bundleName);
const comp = node.getComponent(ctor)!;
this.add(comp);
node.parent = parent;
this.add(comp);
node.parent = parent.node;
}
// 手动内存管理
else {
node = await ViewUtil.createPrefabNodeAsync(path, bundleName);
const comp = node.getComponent(ctor);
if (!comp) {
throw new Error(`组件 ${ctor.name} 不存在于预制 ${path}`);
}
resolve(node);
});
this.add(comp);
node.parent = parent;
}
return node;
}
/**
@@ -103,27 +127,33 @@ export abstract class CCEntity extends ecs.Entity {
* @param params 界面参数
* @returns 界面节点
*/
addUi<T extends ECSView>(ctor: ECSCtor<T>, params?: UIParam): Promise<Node> {
return new Promise<Node>(async (resolve, reject) => {
const key = gui.internal.getKey(ctor);
if (key) {
if (params == null) {
params = { preload: true };
}
else {
params.preload = true;
}
async addUi<T extends ECSView>(ctor: ECSCtor<T>, params?: UIParam): Promise<Node> {
const key = gui.internal.getKey(ctor);
if (!key) {
throw new Error(`${ctor.name} 界面组件未使用 gui.register 注册`);
}
const node = await oops.gui.open(key, params);
const comp = node.getComponent(ctor) as ecs.Comp;
this.add(comp);
oops.gui.show(key);
resolve(node);
}
else {
console.error(`${ctor.name} 界面组件未使用 gui.register 注册`);
}
});
if (params == null) {
params = { preload: true };
}
else {
params.preload = true;
}
if (oops.gui.has(key)) {
console.warn(`${key} 界面已存在`);
return oops.gui.get(key);
}
const node = await oops.gui.open(key, params);
const comp = node.getComponent(ctor) as ecs.Comp;
if (!comp) {
throw new Error(`界面节点上未找到组件 ${ctor.name}`);
}
this.add(comp);
oops.gui.show(key);
return node;
}
/**
@@ -132,36 +162,44 @@ export abstract class CCEntity extends ecs.Entity {
*/
removeUi(ctor: CompType<ecs.IComp>) {
const key = gui.internal.getKey(ctor);
if (key) {
const node = oops.gui.get(key);
if (node == null) {
console.error(`${key} 界面重复关闭`);
console.warn(`${key} 界面不存在或已关闭`);
return;
}
const comp = node.getComponent(LayerUIElement);
if (comp) {
// 处理界面关闭动画播放完成后移除ECS组件避免使用到组件实体数据还在动画播放时在使用导致的空对象问题
comp.onClose = this.remove.bind(this, ctor);
comp.onClose = () => {
try {
this.remove(ctor);
} catch (error) {
console.error(`移除界面组件失败: ${key}`, error);
}
};
oops.gui.remove(key);
} else {
// 没有 LayerUIElement直接移除
this.remove(ctor);
}
}
else {
} else {
this.remove(ctor);
}
}
//#endregion
//#region 游戏业务层管理
/** 模块业务逻辑组件 */
private businesss: Map<any, CCBusiness<CCEntity>> = null!;
/** 模块业务逻辑组件集合key: 业务类构造函数value: 业务实例) */
private businesss: Map<BusinessCtor, CCBusiness<CCEntity>> = null!;
/**
* 批量添加组件
* @param ctors 组件类
* @returns
*/
addBusinesss<T extends CCBusiness<CCEntity>>(...clss: any[]) {
* 批量添加业务逻辑组件
* @param clss 业务逻辑组件类数组
*/
addBusinesss<T extends CCBusiness<CCEntity>>(...clss: BusinessCtor<T>[]) {
for (const ctor of clss) {
this.addBusiness<T>(ctor);
}
@@ -172,7 +210,7 @@ export abstract class CCEntity extends ecs.Entity {
* @param cls 业务逻辑组件类
* @returns 业务逻辑组件实例
*/
addBusiness<T extends CCBusiness<CCEntity>>(cls: any): T {
addBusiness<T extends CCBusiness<CCEntity>>(cls: BusinessCtor<T>): T {
if (this.businesss == null) this.businesss = new Map();
if (this.businesss.has(cls)) {
console.error(`${cls.name} 业务逻辑组件已存在`);
@@ -180,6 +218,7 @@ export abstract class CCEntity extends ecs.Entity {
}
const business = new cls();
business.ent = this;
//@ts-ignore
business.init();
this.businesss.set(cls, business);
return business as T;
@@ -188,36 +227,47 @@ export abstract class CCEntity extends ecs.Entity {
/**
* 获取业务逻辑组件
* @param cls 业务逻辑组件类
* @returns 业务逻辑组件实例
* @returns 业务逻辑组件实例,不存在则返回 null
*/
getBusiness<T extends CCBusiness<CCEntity>>(cls: any): T {
if (this.businesss == null) return null!;
return this.businesss.get(cls) as T;
getBusiness<T extends CCBusiness<CCEntity>>(cls: BusinessCtor<T>): T {
if (!this.businesss) return null!;
return (this.businesss.get(cls) as T) || null!;
}
/**
* 移除业务逻辑组件
* @param cls 业务逻辑组件类
*/
removeBusiness(cls: any) {
removeBusiness<T extends CCBusiness<CCEntity>>(cls: BusinessCtor<T>) {
if (this.businesss) {
const business = this.businesss.get(cls);
if (business) this.businesss.delete(cls);
if (business) {
business.destroy();
this.businesss.delete(cls);
}
}
}
//#endregion
destroy(): void {
// 1. 先销毁所有子实体,避免内存泄漏
if (this.singletons) {
this.singletons.forEach(entity => {
if (entity && typeof entity.destroy === 'function') {
entity.destroy();
}
});
this.singletons.clear();
this.singletons = null!;
}
// 2. 再销毁所有业务组件
if (this.businesss) {
this.businesss.forEach((business) => business.destroy());
this.businesss.forEach(business => business.destroy());
this.businesss.clear();
this.businesss = null!;
}
super.destroy();
}
}

View File

@@ -5,8 +5,11 @@
* @LastEditTime: 2022-09-06 17:20:51
*/
import type { Component } from 'cc';
import type { ecs } from '../../libs/ecs/ECS';
import { ECSModel } from '../../libs/ecs/ECSModel';
import { VM } from '../../libs/model-view/ViewModel';
import { VMBase } from '../../libs/model-view/VMBase';
import type { CCEntity } from './CCEntity';
import { GameComponent } from './GameComponent';
@@ -17,11 +20,13 @@ import { GameComponent } from './GameComponent';
* 1. 对象拥有 cc.Component 组件功能与 ecs.Comp 组件功能
* 2. 对象自带全局事件监听、释放、发送全局消息功能
* 3. 对象管理的所有节点摊平直接通过节点名获取cc.Node对象
* 4. 支持可选的 MVVM 功能(通过 mvvm 属性启用)
*
* 应用场景
* 1. 网络游戏,优先有数据对象,然后创建视图对象,当释放视图组件时,部分场景不希望释放数据对象
*
* @example
// 不使用 MVVM 的组件
@ccclass('RoleViewComp')
@ecs.register('RoleView', false)
export class RoleViewComp extends CCView<Role> {
@@ -29,7 +34,29 @@ export class RoleViewComp extends CCView<Role> {
spine: sp.Skeleton = null!;
onLoad(){
super.onLoad();
}
}
// 使用 MVVM 的组件
@ccclass('LoadingViewComp')
@ecs.register('LoadingView', false)
export class LoadingViewComp extends CCView<Initialize> {
protected mvvm = true; // 启用 MVVM 功能
data: LoadingData = {
finished: 0,
total: 0,
progress: "0",
prompt: ""
};
onLoad(){
super.onLoad();
}
reset(): void {
// 重置逻辑
}
}
*/
@@ -41,16 +68,205 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
ent!: T;
tid = -1;
/** 从父节点移除自己 */
remove() {
const cct = ECSModel.compCtors[this.tid];
if (this.ent) {
this.ent.removeUi(cct);
}
else {
console.error(`组件 ${this.name} 移除失败,组件未注册`);
//#region MVVM 功能相关(仅在 mvvm = true 时使用)
/** 是否启用 MVVM 功能(子类可覆盖为 true */
protected mvvm: boolean = false;
/**
* MVVM 绑定的标签,延迟初始化以节省内存
* 仅在启用 MVVM 时创建
*/
protected tag?: string;
/**
* 需要绑定的私有数据
* 注意:子类应该显式初始化此属性
*/
protected data?: any;
//#endregion
/**
* 组件加载时调用
* 注意:如果子类需要覆盖此方法,必须调用 super.onLoad()
*/
onLoad() {
// 只有启用 MVVM 且数据存在时才初始化 VM
// 使用位运算优化布尔判断(虽然现代引擎已优化,但这是极致优化)
if (this.mvvm && this.data !== undefined && this.data !== null) {
this.initializeVM();
}
}
/**
* 初始化 MVVM 功能
* @private
*/
private initializeVM() {
this.onBind();
// 优化:使用模板字符串(现代引擎优化更好),并缓存 uuid
const uuid = this.node.uuid;
// 优化:只在必要时替换点号,使用更快的 replaceAll如果支持
this.tag = `_temp<${uuid.replace('.', '')}>`;
VM.add(this.data!, this.tag);
// 搜寻所有节点:找到 watch path
const comps = this.getVMComponents();
const len = comps.length;
// 优化:避免属性查找,缓存 tag
const tag = this.tag;
for (let i = 0; i < len; i++) {
this.replaceVMPath(comps[i], tag);
}
}
/**
* 在 onLoad 完成和 start() 之前调用,你可以在这里进行初始化数据等操作
* 注意:仅在 mvvm = true 时调用
* @protected
*/
protected onBind() { }
/**
* 在 onDestroy() 后调用,此时仍然可以获取绑定的 data 数据
* 注意:仅在 mvvm = true 时调用
* @protected
*/
protected onUnBind() { }
/**
* 替换 VM 组件的路径
* @private
*/
private replaceVMPath(comp: Component, tag: string) {
// @ts-ignore - 优化:使用 any 类型避免多次类型转换
const vmComp: any = comp;
const path: string = vmComp.watchPath;
// 优化:使用严格相等避免类型转换
if (vmComp.templateMode === true) {
const pathArr: string[] = vmComp.watchPathArr;
if (pathArr) {
const len = pathArr.length;
// 优化:避免在循环中重复声明变量
for (let i = 0; i < len; i++) {
// 优化:直接修改数组元素,避免中间变量
pathArr[i] = pathArr[i].replace('*', tag);
}
}
}
else if (path) {
// 优化:使用 startsWith 比 split 更快
// 优化:避免不必要的 split 操作
if (path.charCodeAt(0) === 42) { // 42 是 '*' 的字符码
vmComp.watchPath = path.replace('*', tag);
}
}
}
/**
* 优化的遍历节点,获取 VM 组件
* @private
*/
private getVMComponents(): Component[] {
const comps = this.node.getComponentsInChildren(VMBase);
// 优化:提前返回,避免不必要的计算
if (comps.length === 0) {
return comps;
}
// 优化:只在有嵌套 CCView 时才获取 parents
const parents = this.node.getComponentsInChildren(CCView);
// 优化:使用数组长度判断,避免创建新数组
let hasNested = false;
const len = parents.length;
const myUuid = this.uuid;
for (let i = 0; i < len; i++) {
const p = parents[i];
if (p.uuid !== myUuid && p.mvvm) {
hasNested = true;
break;
}
}
// 如果没有嵌套的启用了 MVVM 的 CCView直接返回所有组件
if (!hasNested) {
return comps;
}
// 优化:使用 Set 过滤,但避免多次遍历
const filterSet = new Set<Component>();
for (let i = 0; i < len; i++) {
const p = parents[i];
if (p.uuid !== myUuid && p.mvvm) {
const childComps = p.node.getComponentsInChildren(VMBase);
const childLen = childComps.length;
for (let j = 0; j < childLen; j++) {
filterSet.add(childComps[j]);
}
}
}
// 优化:使用传统 for 循环比 filter 更快
const result: Component[] = [];
const compsLen = comps.length;
for (let i = 0; i < compsLen; i++) {
if (!filterSet.has(comps[i])) {
result.push(comps[i]);
}
}
return result;
}
/** 从父节点移除自己 */
remove() {
if (!this.ent) {
console.error(`组件 ${this.name} 移除失败,实体不存在`);
return;
}
if (this.tid < 0) {
console.error(`组件 ${this.name} 移除失败,组件未注册 (tid=${this.tid})`);
return;
}
const cct = ECSModel.compCtors[this.tid];
if (!cct) {
console.error(`组件 ${this.name} 移除失败,组件构造函数不存在 (tid=${this.tid})`);
return;
}
this.ent.removeUi(cct);
this.ent = null!; // 清空引用,避免内存泄漏
}
/**
* 组件销毁时调用
* 注意:如果子类需要覆盖此方法,必须调用 super.onDestroy()
*/
protected onDestroy() {
// 只有启用了 MVVM 时才执行清理
if (this.mvvm) {
this.onUnBind();
// 解除全部引用
if (this.tag) {
VM.remove(this.tag);
// @ts-ignore - 优化:显式清空引用,帮助 GC
this.tag = undefined;
}
// @ts-ignore - 优化:显式清空引用,帮助 GC
this.data = undefined;
}
super.onDestroy();
}
abstract reset(): void;
}
}

View File

@@ -1,68 +0,0 @@
/*
* @Author: dgflash
* @Date: 2021-11-11 19:05:32
* @LastEditors: dgflash
* @LastEditTime: 2022-09-06 17:22:05
*/
import type { ecs } from '../../libs/ecs/ECS';
import { ECSModel } from '../../libs/ecs/ECSModel';
import VMParent from '../../libs/model-view/VMParent';
import type { CCEntity } from './CCEntity';
/**
* 支持 MVVM 功能的 ECS 游戏显示对象组件
*
* 使用方法:
* 1. 对象拥有 cc.Component 组件功能与 ecs.Comp 组件功能
* 2. 对象自带全局事件监听、释放、发送全局消息功能
* 3. 对象管理的所有节点摊平直接通过节点名获取cc.Node对象节点名不能有重名
* 4. 对象支持 VMParent 所有功能
*
* 应用场景
* 1. 网络游戏,优先有数据对象,然后创建视图对象,当释放视图组件时,部分场景不希望释放数据对象
*
* @example
@ccclass('LoadingViewComp')
@ecs.register('LoadingView', false)
export class LoadingViewComp extends CCViewVM<Initialize> {
// VM 组件绑定数据
data: any = {
// 加载资源当前进度
finished: 0,
// 加载资源最大进度
total: 0,
// 加载资源进度比例值
progress: "0",
// 加载流程中提示文本
prompt: ""
};
private progress: number = 0;
reset(): void {
}
}
*/
export abstract class CCViewVM<T extends CCEntity> extends VMParent implements ecs.IComp {
static tid = -1;
static compName: string;
canRecycle!: boolean;
ent!: T;
tid = -1;
/** 从父节点移除自己 */
remove() {
const cct = ECSModel.compCtors[this.tid];
if (this.ent) {
this.ent.removeUi(cct);
}
else {
console.error(`组件 ${this.name} 移除失败,组件未注册`);
}
}
abstract reset(): void;
}

View File

@@ -1,9 +0,0 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "33d31b2d-c771-4759-9fc6-96bbd5bcfa15",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -140,35 +140,25 @@ export class GameComponent extends Component {
this.resPaths.set(type, rps);
}
// 确定真实的 bundle 和 path
let realBundle: string;
let realPaths: string[];
if (paths instanceof Array) {
const realBundle = bundleName;
for (let index = 0; index < paths.length; index++) {
const realPath = paths[index];
const key = this.getResKey(realBundle, realPath);
const rp = rps.get(key);
if (rp) {
rp.refCount++;
}
else {
rps.set(key, { path: realPath, bundle: realBundle, refCount: 1 });
}
}
realBundle = bundleName;
realPaths = paths;
}
else if (typeof paths === 'string') {
const realBundle = bundleName;
const realPath = paths;
const key = this.getResKey(realBundle, realPath);
const rp = rps.get(key);
if (rp) {
rp.refCount++;
}
else {
rps.set(key, { path: realPath, bundle: realBundle, refCount: 1 });
}
realBundle = bundleName;
realPaths = [paths];
}
else {
const realBundle = oops.res.defaultBundleName;
const realPath = bundleName;
realBundle = oops.res.defaultBundleName;
realPaths = [bundleName];
}
// 统一处理路径数组
for (const realPath of realPaths) {
const key = this.getResKey(realBundle, realPath);
const rp = rps.get(key);
if (rp) {
@@ -252,33 +242,140 @@ export class GameComponent extends Component {
oops.res.loadDir(bundleName, dir, type, onProgress, onComplete);
}
/** 释放资源 */
/**
* 手动释放指定资源
* @param path 资源路径
* @param bundleName 资源包名
* @param releaseAll 是否释放所有引用计数(默认只释放一次)
*/
releaseRes(path: string, bundleName: string = resLoader.defaultBundleName, releaseAll: boolean = false) {
if (!this.resPaths) return;
const rps = this.resPaths.get(ResType.Load);
if (!rps) return;
const key = this.getResKey(bundleName, path);
const record = rps.get(key);
if (record) {
if (releaseAll) {
// 释放所有引用
for (let i = 0; i < record.refCount; i++) {
oops.res.release(record.path, record.bundle);
}
rps.delete(key);
}
else {
// 只释放一次引用
oops.res.release(record.path, record.bundle);
record.refCount--;
if (record.refCount <= 0) {
rps.delete(key);
}
}
}
}
/** 释放所有加载的资源 */
release() {
if (this.resPaths) {
const rps = this.resPaths.get(ResType.Load);
if (rps) {
rps.forEach((value: ResRecord) => {
// 根据引用计数释放资源
for (let i = 0; i < value.refCount; i++) {
oops.res.release(value.path, value.bundle);
}
});
rps.clear();
this.resPaths.delete(ResType.Load);
}
}
}
/** 释放文件夹资源 */
/** 释放所有文件夹资源 */
releaseDir() {
if (this.resPaths) {
const rps = this.resPaths.get(ResType.LoadDir);
if (rps) {
rps.forEach((value: ResRecord) => {
oops.res.releaseDir(value.path, value.bundle);
// 释放文件夹资源(根据引用计数多次释放)
for (let i = 0; i < value.refCount; i++) {
oops.res.releaseDir(value.path, value.bundle);
}
});
rps.clear();
this.resPaths.delete(ResType.LoadDir);
}
}
}
/**
* 获取资源引用计数
* @param path 资源路径
* @param bundleName 资源包名
* @returns 引用计数未找到返回0
*/
getResRefCount(path: string, bundleName: string = resLoader.defaultBundleName): number {
if (!this.resPaths) return 0;
const rps = this.resPaths.get(ResType.Load);
if (!rps) return 0;
const key = this.getResKey(bundleName, path);
const record = rps.get(key);
return record ? record.refCount : 0;
}
/**
* 获取所有加载的资源信息(用于调试)
* @returns 资源记录数组
*/
getAllResRecords(): ResRecord[] {
const records: ResRecord[] = [];
if (!this.resPaths) return records;
const rps = this.resPaths.get(ResType.Load);
if (rps) {
rps.forEach((value: ResRecord) => {
records.push({ ...value });
});
}
return records;
}
/**
* 打印资源使用情况(用于调试)
*/
printResUsage() {
if (!this.resPaths) {
console.log('[资源管理] 暂无资源记录');
return;
}
const loadRps = this.resPaths.get(ResType.Load);
const dirRps = this.resPaths.get(ResType.LoadDir);
console.log('========== 资源使用情况 ==========');
console.log(`组件: ${this.node.name}`);
if (loadRps && loadRps.size > 0) {
console.log(`\n[普通资源] 共 ${loadRps.size} 个:`);
loadRps.forEach((value: ResRecord, key: string) => {
console.log(` - ${key} (引用计数: ${value.refCount})`);
});
}
if (dirRps && dirRps.size > 0) {
console.log(`\n[文件夹资源] 共 ${dirRps.size} 个:`);
dirRps.forEach((value: ResRecord, key: string) => {
console.log(` - ${key} (引用计数: ${value.refCount})`);
});
}
console.log('==================================');
}
/**
* 设置图片资源
* @param target 目标精灵对象
@@ -296,6 +393,14 @@ export class GameComponent extends Component {
}
return;
}
// 释放旧的 spriteFrame 引用,避免内存泄漏
const oldSpriteFrame = target.spriteFrame;
if (oldSpriteFrame && isValid(oldSpriteFrame)) {
oldSpriteFrame.decRef();
}
// 增加新 spriteFrame 引用并设置
spriteFrame.addRef();
target.spriteFrame = spriteFrame;
}
@@ -318,19 +423,24 @@ export class GameComponent extends Component {
*/
playEffect(url: string, params?: IAudioParams): Promise<AudioEffect> {
return new Promise(async (resolve, reject) => {
// 音效播放完,关闭正在播放状态的音乐效果
// 确保参数中有 bundle 信息
if (params == null) {
params = { bundle: resLoader.defaultBundleName };
}
else if (params.bundle == null) {
params.bundle = resLoader.defaultBundleName;
}
const bundle = params.bundle || resLoader.defaultBundleName;
const ae = await oops.audio.playEffect(url, params);
if (ae) {
this.addPathToRecord(ResType.Load, ae.params.bundle!, url);
// 音效加载成功,记录资源引用
this.addPathToRecord(ResType.Load, bundle, url);
resolve(ae);
}
else {
// 音效加载失败,返回 null
resolve(null!);
}
});