fix: 更新提交

This commit is contained in:
han_han9
2025-11-26 22:57:07 +08:00
parent 8a6620cf8f
commit 4c16bec13f
640 changed files with 70914 additions and 13327 deletions

View File

@@ -127,7 +127,7 @@ export function advancedThrottle(delay: number, options: {
let lastCallTime = 0;
let lastInvokeTime = 0;
let timeoutId: any = null;
let maxTimeoutId: any = null;
const maxTimeoutId: any = null;
let lastArgs: any[] = [];
let lastThis: any = null;
let result: any;

View File

@@ -59,9 +59,7 @@ export class EventManager extends Singleton {
}
// 使用 some 提前终止循环,一旦发现重复就停止查找
const isDuplicate = eds.some(
(bin) => bin.listener === listener && bin.obj === obj,
);
const isDuplicate = eds.some((bin) => bin.listener === listener && bin.obj === obj);
if (isDuplicate) {
LogUtils.warn(`名为【${event}】的事件重复注册侦听器`);
return; // 避免重复添加
@@ -102,9 +100,7 @@ export class EventManager extends Singleton {
return;
}
const index = eds.findIndex(
(bin) => bin.listener === listener && bin.obj === obj,
);
const index = eds.findIndex((bin) => bin.listener === listener && bin.obj === obj);
if (index !== -1) {
eds.splice(index, 1);
}
@@ -129,9 +125,7 @@ export class EventManager extends Singleton {
if (isValid(eventBin.obj)) {
eventBin.listener.call(eventBin.obj, ...args);
} else {
LogUtils.warn(
`事件【${event}】的侦听器对象已被销毁,无法触发`,
);
LogUtils.warn(`事件【${event}】的侦听器对象已被销毁,无法触发`);
// 注意:这里可能会导致数组长度变化,但因为是副本所以不影响循环
this.off(event, eventBin.listener, eventBin.obj);
}

View File

@@ -1,8 +1,8 @@
import { IGuideConfig } from "./GuideData";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
import { Singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
import ResManager from "../res/ResManager";
const TAG = "GuideConfigManager";
@@ -23,10 +23,10 @@ export class GuideConfigManager extends Singleton {
private async loadDefaultConfigs() {
// 这里可以加载默认的引导配置
const asset = await ResManager.getInstance().loadAsset<JsonAsset>(
"guide-config",
JsonAsset,
);
const asset = await ResManager.getInstance().loadAsset<JsonAsset>({
path: "guide-config",
type: JsonAsset,
});
if (asset?.json) {
for (const key in asset.json) {
if (Object.prototype.hasOwnProperty.call(asset.json, key)) {
@@ -133,9 +133,7 @@ export class GuideConfigManager extends Singleton {
* @returns 启用的引导配置列表
*/
public getEnabledGuideConfigs(): IGuideConfig[] {
return Array.from(this._configs.values()).filter(
(config) => config.enabled !== false,
);
return Array.from(this._configs.values()).filter((config) => config.enabled !== false);
}
/**
@@ -201,20 +199,14 @@ export class GuideConfigManager extends Singleton {
// 检查步骤引用
for (const step of config.steps) {
if (step.nextStepId && !stepIds.has(step.nextStepId)) {
LogUtils.error(
TAG,
`步骤 ${step.id} 的下一步骤ID不存在: ${step.nextStepId}`,
);
LogUtils.error(TAG, `步骤 ${step.id} 的下一步骤ID不存在: ${step.nextStepId}`);
return false;
}
if (step.branches) {
for (const branch of step.branches) {
if (!stepIds.has(branch.nextStepId)) {
LogUtils.error(
TAG,
`步骤 ${step.id} 的分枝下一步骤ID不存在: ${branch.nextStepId}`,
);
LogUtils.error(TAG, `步骤 ${step.id} 的分枝下一步骤ID不存在: ${branch.nextStepId}`);
return false;
}
}
@@ -269,10 +261,7 @@ export class GuideConfigManager extends Singleton {
} else if (data.configs) {
// 多个配置
const successCount = this.addGuideConfigs(data.configs);
LogUtils.info(
TAG,
`导入配置成功: ${successCount}/${data.configs.length}`,
);
LogUtils.info(TAG, `导入配置成功: ${successCount}/${data.configs.length}`);
return successCount > 0;
} else {
LogUtils.error(TAG, "JSON格式不正确");

View File

@@ -16,7 +16,7 @@ import UIManager from "../ui/UIManager";
import { UIType } from "../ui/UIDecorator";
import { GuideTargetComponent } from "./GuideTargetComponent";
import StorageManager, { GUIDE_HISTORY_KEY } from "../storage/StorageManager";
import { Singleton, singleton } from "../Singleton";
import { Singleton } from "../Singleton";
import { EventManager } from "../event/EventManager";
import LogUtils from "../utils/LogUtils";

View File

@@ -1,5 +1,19 @@
declare module "cc" {
interface SpriteFrame {
remoteUrl?: string;
lastAccessTime?: number;
}
interface Asset {
cacheKey?: string;
lastAccessTime?: number;
}
namespace sp {
namespace spine {
interface Slot {
texture?: Texture2D;
}
}
}
}

View File

@@ -1,6 +1,6 @@
import { HttpError, HttpResponse, IResponseData } from "./HttpRequest";
import LogUtils from "../utils/LogUtils";
import { HttpMethod, NetworkConfig } from "./Types";
import { HttpMethod, IHttpConfig } from "./Types";
import { StringUtils } from "../utils/StringUtils";
import { Toast } from "../ui/default/DefaultToast";
@@ -20,12 +20,12 @@ export default class HttpClient {
private readonly maxRetries: number = 3;
private token: string = "";
constructor(cfg: NetworkConfig) {
constructor(cfg: IHttpConfig) {
// 设置默认请求头
this.headers.set("Content-Type", "application/json");
this.rootUrl = cfg.httpRootUrl;
this.timeout = cfg.defaultTimeout;
this.maxRetries = cfg.defaultRetryCount;
this.rootUrl = cfg.url;
this.timeout = cfg.timeout || 30;
this.maxRetries = cfg.retryCount || 3;
}
public setHeader(key: string, value: string) {
@@ -37,15 +37,10 @@ export default class HttpClient {
method: HttpMethod,
data: unknown,
callback: (response: HttpResponse<T>) => void = null,
headers?: Map<string, string>
headers?: Map<string, string>,
): Promise<HttpResponse<T>> {
// 生成请求签名,包含 URL、方法、数据和头部信息
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
const requestSignature = this.generateRequestSignature(url, method, data, headers);
// 检查是否已有相同的请求在进行中
const reqInfo = this.requestQueues.get(requestSignature);
@@ -63,13 +58,7 @@ export default class HttpClient {
const abortController = new AbortController();
const callbacks = [callback];
const responsePromise = this.executeRequest<T>(
url,
method,
data,
headers,
abortController
);
const responsePromise = this.executeRequest<T>(url, method, data, headers, abortController);
// 将请求添加到队列
this.requestQueues.set(requestSignature, {
@@ -88,13 +77,8 @@ export default class HttpClient {
if (tempCallback) {
tempCallback(response);
}
} catch (callError) {
LogUtils.error(
TAG,
"回调函数执行失败",
callError.message,
callError.stack
);
} catch (err) {
LogUtils.error(TAG, "回调函数执行失败", err.message, err.stack);
}
}
this.requestQueues.delete(requestSignature);
@@ -113,13 +97,8 @@ export default class HttpClient {
if (tempCallback) {
tempCallback(errorResponse);
}
} catch (callError) {
LogUtils.error(
TAG,
"回调函数执行失败",
callError.message,
callError.stack
);
} catch (err_) {
LogUtils.error(TAG, "回调函数执行失败", err_.message, err_.stack);
}
}
this.requestQueues.delete(requestSignature);
@@ -135,7 +114,7 @@ export default class HttpClient {
data: unknown,
headers?: Map<string, string>,
abortController?: AbortController,
retryCount: number = 0
retryCount: number = 0,
): Promise<HttpResponse<T>> {
try {
// 合并请求头
@@ -184,17 +163,11 @@ export default class HttpClient {
});
// 执行请求
const response = await Promise.race([
fetch(url, requestConfig),
timeoutPromise,
]);
const response = await Promise.race([fetch(url, requestConfig), timeoutPromise]);
// 检查响应状态
if (!response.ok) {
const error =
response.status >= 500
? new Error("Server error")
: new Error(`HTTP ${response.status}`);
const error = response.status >= 500 ? new Error("Server error") : new Error(`HTTP ${response.status}`);
throw error;
}
@@ -211,19 +184,12 @@ export default class HttpClient {
message: err.message,
};
}
if (
!responseData.success &&
!StringUtils.isEmpty(responseData.message)
) {
if (!responseData.success && !StringUtils.isEmpty(responseData.message)) {
Toast.show(responseData.message);
}
if (response.headers.get("Authorization")?.startsWith("Bearer ")) {
LogUtils.log(
"HttpClient",
"获取到新的token",
response.headers.get("Authorization")
);
LogUtils.log("HttpClient", "获取到新的token", response.headers.get("Authorization"));
this.token = response.headers.get("Authorization");
}
@@ -239,19 +205,8 @@ export default class HttpClient {
}
if (retryCount < this.maxRetries) {
LogUtils.info(
"HttpClient",
`重试请求 ${retryCount + 1}/${this.maxRetries}`,
url
);
return this.executeRequest(
url,
method,
data,
headers,
abortController,
retryCount + 1
);
LogUtils.info("HttpClient", `重试请求 ${retryCount + 1}/${this.maxRetries}`, url);
return this.executeRequest(url, method, data, headers, abortController, retryCount + 1);
}
let errMessage = HttpError.UNKNOWN_ERROR;
@@ -273,18 +228,8 @@ export default class HttpClient {
/**
* 取消指定签名的请求
*/
public cancelRequest(
url: string,
method: "GET" | "POST",
data: any,
headers?: Map<string, string>
): boolean {
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
public cancelRequest(url: string, method: "GET" | "POST", data: any, headers?: Map<string, string>): boolean {
const requestSignature = this.generateRequestSignature(url, method, data, headers);
const queue = this.requestQueues.get(requestSignature);
if (queue) {
@@ -341,30 +286,15 @@ export default class HttpClient {
/**
* 检查指定请求是否正在进行中
*/
public isRequestPending(
url: string,
method: "GET" | "POST",
data: any,
headers?: Map<string, string>
): boolean {
const requestSignature = this.generateRequestSignature(
url,
method,
data,
headers
);
public isRequestPending(url: string, method: "GET" | "POST", data: any, headers?: Map<string, string>): boolean {
const requestSignature = this.generateRequestSignature(url, method, data, headers);
return this.requestQueues.has(requestSignature);
}
/**
* 生成请求签名,用于识别相同的请求
*/
private generateRequestSignature(
url: string,
method: string,
data: any,
headers?: Map<string, string>
): string {
private generateRequestSignature(url: string, method: string, data: any, headers?: Map<string, string>): string {
// 确保 URL 是完整的
if (!url.toLowerCase().startsWith("http")) {
url = this.rootUrl + url;

View File

@@ -1,37 +1,39 @@
import LogUtils from "../utils/LogUtils";
import { WebSocketClient } from "./WebSocketClient";
import { SerializerManager } from "./Serializer";
import { WebSocketConfig } from "./Types";
import { INetworkConfig } from "./Types";
import { singleton, Singleton } from "../Singleton";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
import { StringUtils } from "../utils/StringUtils";
import HttpClient from "./HttpClient";
import ResManager from "../res/ResManager";
const TAG = "Network";
/**
* 网络管理器
*/
@singleton({auto: true})
@singleton({ auto: true })
export class NetworkManager extends Singleton {
private httpClient: HttpClient;
private webSocketClients = new Map<string, WebSocketClient>();
private serializerManager: SerializerManager;
private wsClient: WebSocketClient;
private config: INetworkConfig;
protected async onInit() {
LogUtils.info(TAG, "初始化网络管理器");
this.serializerManager = new SerializerManager();
const asset = await ResManager.getInstance().loadAsset<JsonAsset>(
"net-config",
JsonAsset,
);
const { err, asset } = await ResManager.getInstance().loadAsset<JsonAsset>({
path: "net-config",
type: JsonAsset,
});
if (StringUtils.isEmpty(asset?.json)) {
LogUtils.error(TAG, "加载网络配置失败");
return;
}
if (err) {
LogUtils.error(TAG, "加载网络配置失败", err);
return;
}
this.httpClient = new HttpClient(asset.json);
this.config = asset.json;
LogUtils.info(TAG, "网络管理器初始化完成");
}
@@ -40,50 +42,28 @@ export class NetworkManager extends Singleton {
* 获取HTTP客户端
*/
public getHttpClient(): HttpClient {
return this.httpClient;
}
/**
* 创建WebSocket客户端
*/
public createWebSocketClient(
name: string,
config: WebSocketConfig,
): WebSocketClient {
if (this.webSocketClients.has(name)) {
LogUtils.warn(TAG, `WebSocket客户端已存在: ${name}`);
return this.webSocketClients.get(name)!;
if (!this.httpClient) {
this.httpClient = new HttpClient(this.config.httpConfig);
}
const client = new WebSocketClient(config);
this.webSocketClients.set(name, client);
LogUtils.info(TAG, `创建WebSocket客户端: ${name}`);
return client;
return this.httpClient;
}
/**
* 获取WebSocket客户端
*/
public getWebSocketClient(name: string): WebSocketClient | null {
return this.webSocketClients.get(name) || null;
public getWebSocketClient() {
if (!this.wsClient) {
this.wsClient = new WebSocketClient(this.config.wsConfig);
}
return this.wsClient;
}
/**
* 获取序列化管理器
*/
public getSerializerManager(): SerializerManager {
return this.serializerManager;
}
/**
* 销毁网络管理器
*/
public destroy(): void {
protected onRelease(): void {
LogUtils.info(TAG, "销毁网络管理器");
for (const [, client] of this.webSocketClients)
void client.disconnect();
this.webSocketClients.clear();
if (this.wsClient) {
this.wsClient.disconnect();
}
this.wsClient = null;
this.httpClient = null;
}
}

View File

@@ -28,9 +28,8 @@ export enum NetworkStatus {
}
/** HTTP请求配置 */
export interface HttpRequestConfig {
export interface IHttpConfig {
url: string;
method: HttpMethod;
headers?: Record<string, string>;
data?: any;
timeout?: number;
@@ -46,11 +45,11 @@ export interface HttpResponse<T = any> {
status: number;
statusText: string;
headers: Record<string, string>;
config: HttpRequestConfig;
config: IHttpConfig;
}
/** WebSocket配置 */
export interface WebSocketConfig {
export interface IWebSocketConfig {
url: string;
protocols?: string[];
reconnectInterval?: number;
@@ -59,6 +58,11 @@ export interface WebSocketConfig {
heartbeatTimeout?: number;
binaryType?: "blob" | "arraybuffer";
autoReconnect?: boolean;
// 前后台管理配置
disconnectOnBackground?: boolean; // 进入后台时是否自动断开连接默认true
reconnectOnForeground?: boolean; // 回到前台时是否自动重连默认true
foregroundReconnectDelay?: number; // 回到前台后重连延迟时间(ms)默认1000
}
/** WebSocket消息 */
@@ -91,11 +95,7 @@ export enum WorkerMessageType {
}
/** 网络配置 */
export interface NetworkConfig {
httpRootUrl?: string;
wsRootUrl?: string;
workerPath?: string;
defaultTimeout?: number;
defaultRetryCount?: number;
defaultRetryDelay?: number;
export interface INetworkConfig {
httpConfig?: IHttpConfig;
wsConfig?: IWebSocketConfig;
}

View File

@@ -1,26 +1,46 @@
import { Game, game } from "cc";
import LogUtils from "../utils/LogUtils";
import {
WebSocketConfig,
WebSocketMessage,
NetworkStatus,
SerializationType,
} from "./Types";
import { IWebSocketConfig, WebSocketMessage, NetworkStatus, SerializationType } from "./Types";
import { UIManager } from "../ui";
import DefaultNetworkMask from "../ui/default/DefaultNetworkMask";
const TAG = "Network";
/**
* 重连状态枚举
*/
enum ReconnectState {
NONE = "none",
RECONNECTING = "reconnecting",
FAILED = "failed",
}
/**
* WebSocket客户端
*/
export class WebSocketClient {
private config: WebSocketConfig;
private config: IWebSocketConfig;
private status: NetworkStatus = NetworkStatus.IDLE;
private messageHandlers = new Map<
string,
(message: WebSocketMessage) => void
>();
private messageHandlers = new Map<string, (message: WebSocketMessage) => void>();
private statusHandlers: Array<(status: NetworkStatus) => void> = [];
private socket: WebSocket = null;
constructor(config: WebSocketConfig) {
// 重连相关属性
private reconnectAttempts: number = 0;
private reconnectState: ReconnectState = ReconnectState.NONE;
private isShowingMask: boolean = false;
private reconnectTimer: NodeJS.Timeout = null;
// 心跳相关属性
private heartbeatTimer: NodeJS.Timeout = null;
private heartbeatTimeoutTimer: NodeJS.Timeout = null;
private lastHeartbeatTime: number = 0;
// 前后台状态管理
private isInBackground: boolean = false;
private wasConnectedBeforeBackground: boolean = false;
constructor(config: IWebSocketConfig) {
this.config = {
reconnectInterval: 5000,
maxReconnectAttempts: 5,
@@ -28,46 +48,172 @@ export class WebSocketClient {
heartbeatTimeout: 10000,
binaryType: "arraybuffer",
autoReconnect: true,
// 设置前后台管理的默认值
disconnectOnBackground: true,
reconnectOnForeground: true,
foregroundReconnectDelay: 1000,
...config,
};
this.setupWorkerHandlers();
// 初始化前后台状态监听
this.initBackgroundStateListener();
}
/**
* 连接WebSocket
*/
public async connect(): Promise<void> {
// TODO: 实现WebSocket连接逻辑
if (this.status === NetworkStatus.CONNECTED || this.status === NetworkStatus.CONNECTING) {
LogUtils.warn(TAG, "WebSocket已连接或正在连接中");
return;
}
LogUtils.info(TAG, `连接WebSocket: ${this.config.url}`);
this.setStatus(NetworkStatus.CONNECTING);
try {
// 清理之前的连接
this.cleanup();
// 创建WebSocket连接
this.socket = new WebSocket(this.config.url);
this.socket.binaryType = this.config.binaryType;
// 设置事件监听器
this.setupSocketEventHandlers();
// 等待连接建立
await this.waitForConnection();
LogUtils.info(TAG, "WebSocket连接成功");
this.setStatus(NetworkStatus.CONNECTED);
// 重置重连状态
this.reconnectAttempts = 0;
this.reconnectState = ReconnectState.NONE;
// 开始心跳检测
this.startHeartbeat();
} catch (err) {
LogUtils.error(TAG, "WebSocket连接失败:", err);
this.setStatus(NetworkStatus.DISCONNECTED);
this.hideLoadingMask();
// 如果启用自动重连,则尝试重连
if (this.config.autoReconnect) {
this.attemptReconnect();
}
}
}
/**
* 初始化前后台状态监听
*/
private initBackgroundStateListener(): void {
// 监听游戏进入后台事件
game.on(Game.EVENT_HIDE, this.onGameHide, this);
// 监听游戏回到前台事件
game.on(Game.EVENT_SHOW, this.onGameShow, this);
}
/**
* 游戏进入后台时的处理
*/
private onGameHide(): void {
console.log("[WebSocketClient] 游戏进入后台");
this.isInBackground = true;
// 记录进入后台前的连接状态
this.wasConnectedBeforeBackground = this.status === NetworkStatus.CONNECTED;
// 如果配置允许且当前已连接,则主动断开
if (this.config.disconnectOnBackground && this.status === NetworkStatus.CONNECTED) {
console.log("[WebSocketClient] 主动断开网络连接(进入后台)");
this.disconnect();
}
}
/**
* 游戏回到前台时的处理
*/
private onGameShow(): void {
console.log("[WebSocketClient] 游戏回到前台");
this.isInBackground = false;
// 如果配置允许且进入后台前是连接状态,则自动重连
if (this.config.reconnectOnForeground && this.wasConnectedBeforeBackground) {
console.log("[WebSocketClient] 自动重连网络(回到前台)");
const delay = this.config.foregroundReconnectDelay || 1000;
setTimeout(() => {
this.connect();
}, delay);
}
this.wasConnectedBeforeBackground = false;
}
/**
* 销毁时清理前后台状态监听
*/
public destroy(): void {
game.off(Game.EVENT_HIDE, this.onGameHide, this);
game.off(Game.EVENT_SHOW, this.onGameShow, this);
// 清理连接
this.disconnect();
// 清理所有处理器
this.messageHandlers.clear();
this.statusHandlers.length = 0;
}
/**
* 断开连接
*/
public async disconnect(): Promise<void> {
// TODO: 实现WebSocket断开逻辑
LogUtils.info(TAG, "断开WebSocket连接");
// 停止自动重连
this.config.autoReconnect = false;
// 清理连接
this.cleanup();
// 设置状态
this.setStatus(NetworkStatus.DISCONNECTED);
this.hideLoadingMask();
}
/**
* 发送消息
*/
public async send(
data: any,
type: SerializationType = "json",
): Promise<void> {
// TODO: 实现消息发送逻辑
LogUtils.debug(TAG, `发送WebSocket消息: ${type}`);
public async send(data: any, type: SerializationType = "json"): Promise<void> {
if (this.status !== NetworkStatus.CONNECTED || !this.socket) {
throw new Error("WebSocket未连接");
}
try {
let message: string | ArrayBuffer;
if (type === "json") {
message = JSON.stringify(data);
} else if (type === "binary") {
message = data as ArrayBuffer;
} else {
throw new Error(`不支持的序列化类型: ${type}`);
}
this.socket.send(message);
LogUtils.debug(TAG, `发送WebSocket消息: ${type}`, data);
} catch (err) {
LogUtils.error(TAG, "发送WebSocket消息失败:", err);
throw err;
}
}
/**
* 注册消息处理器
*/
public onMessage(
type: string,
handler: (message: WebSocketMessage) => void,
): void {
public onMessage(type: string, handler: (message: WebSocketMessage) => void): void {
this.messageHandlers.set(type, handler);
}
@@ -85,6 +231,27 @@ export class WebSocketClient {
return this.status;
}
/**
* 显示重连失败对话框
*/
private showReconnectFailedDialog(): void {
const opt = {
title: "网络连接失败",
content: `网络连接失败,已重试${this.config.maxReconnectAttempts}次。请检查网络设置后重试。`,
onConfirm: () => {
this.reconnectAttempts = 0;
this.reconnectState = ReconnectState.NONE;
this.connect();
},
onCancel: () => {
LogUtils.info(TAG, "用户取消重连");
// 可以在这里处理用户取消的逻辑,比如返回登录页面等
},
};
UIManager.getInstance().openUI("CommonDialogBox", opt);
LogUtils.error(TAG, "显示重连失败对话框");
}
private setupWorkerHandlers(): void {
// TODO: 设置Worker消息处理器
}
@@ -95,4 +262,269 @@ export class WebSocketClient {
for (const handler of this.statusHandlers) handler(status);
}
}
/**
* 显示加载遮罩
*/
private showLoadingMask(): void {
if (this.isShowingMask) {
return;
}
this.isShowingMask = true;
let message = "网络连接中...";
if (this.reconnectState === ReconnectState.RECONNECTING) {
message = `网络重连中... (${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`;
}
UIManager.getInstance().openUI(DefaultNetworkMask, message);
LogUtils.info(TAG, "显示网络连接遮罩:", message);
}
/**
* 隐藏加载遮罩
*/
private hideLoadingMask(): void {
if (!this.isShowingMask) {
return;
}
this.isShowingMask = false;
UIManager.getInstance().closeUI(DefaultNetworkMask);
LogUtils.info(TAG, "隐藏网络连接遮罩");
}
/**
* 清理WebSocket连接和定时器
*/
private cleanup(): void {
// 关闭WebSocket连接
if (this.socket) {
this.socket.onopen = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.onmessage = null;
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.close();
}
this.socket = null;
}
// 清理定时器
this.stopHeartbeat();
this.stopReconnectTimer();
}
/**
* 设置WebSocket事件处理器
*/
private setupSocketEventHandlers(): void {
if (!this.socket) return;
this.socket.onopen = (event) => {
LogUtils.info(TAG, "WebSocket连接已打开");
};
this.socket.onclose = (event) => {
LogUtils.info(TAG, "WebSocket连接已关闭", event.code, event.reason);
this.setStatus(NetworkStatus.DISCONNECTED);
this.stopHeartbeat();
// 如果不是主动断开且启用自动重连,则尝试重连
if (this.config.autoReconnect && event.code !== 1000) {
this.attemptReconnect();
}
};
this.socket.onerror = (event) => {
LogUtils.error(TAG, "WebSocket连接错误", event);
this.setStatus(NetworkStatus.ERROR);
};
this.socket.onmessage = (event) => {
this.handleMessage(event.data);
};
}
/**
* 等待WebSocket连接建立
*/
private waitForConnection(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error("WebSocket未初始化"));
return;
}
const timeout = setTimeout(() => {
reject(new Error("WebSocket连接超时"));
}, 10000); // 10秒超时
// 保存原有的事件处理器
const originalOnOpen = this.socket.onopen;
const originalOnError = this.socket.onerror;
// 临时设置连接等待的事件处理器
this.socket.onopen = (event) => {
clearTimeout(timeout);
// 恢复原有的事件处理器
this.socket.onopen = originalOnOpen;
this.socket.onerror = originalOnError;
// 调用原有的onopen处理器
if (originalOnOpen) {
originalOnOpen.call(this.socket, event);
}
resolve();
};
this.socket.onerror = (_) => {
clearTimeout(timeout);
// 恢复原有的事件处理器
this.socket.onopen = originalOnOpen;
this.socket.onerror = originalOnError;
reject(new Error("WebSocket连接失败"));
};
});
}
/**
* 开始心跳检测
*/
private startHeartbeat(): void {
if (!this.config.heartbeatInterval || this.config.heartbeatInterval <= 0) {
return;
}
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.config.heartbeatInterval);
}
/**
* 停止心跳检测
*/
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
/**
* 发送心跳包
*/
private sendHeartbeat(): void {
if (this.status !== NetworkStatus.CONNECTED) {
return;
}
try {
const heartbeatData = {
type: "heartbeat",
timestamp: Date.now(),
};
this.send(heartbeatData, "json");
this.lastHeartbeatTime = Date.now();
// 设置心跳超时检测
this.heartbeatTimeoutTimer = setTimeout(() => {
LogUtils.warn(TAG, "心跳超时,可能网络异常");
if (this.config.autoReconnect) {
this.attemptReconnect();
}
}, this.config.heartbeatTimeout);
} catch (err) {
LogUtils.error(TAG, "发送心跳失败:", err);
}
}
/**
* 尝试重连
*/
private attemptReconnect(): void {
if (this.reconnectState === ReconnectState.RECONNECTING) {
return;
}
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
this.reconnectState = ReconnectState.FAILED;
this.showReconnectFailedDialog();
return;
}
this.reconnectState = ReconnectState.RECONNECTING;
this.reconnectAttempts++;
LogUtils.info(TAG, `开始第${this.reconnectAttempts}次重连尝试`);
this.showLoadingMask();
this.reconnectTimer = setTimeout(() => {
this.connect();
}, this.config.reconnectInterval);
}
/**
* 停止重连定时器
*/
private stopReconnectTimer(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* 处理接收到的消息
*/
private handleMessage(data: string | ArrayBuffer): void {
try {
let parsedData: any;
if (typeof data === "string") {
parsedData = JSON.parse(data);
} else {
// 处理二进制数据
// 这里可以根据实际需求实现二进制数据的反序列化
LogUtils.warn(TAG, "收到二进制消息,暂未实现处理逻辑");
return;
}
// 处理心跳响应直接检查原始数据的type字段
if (parsedData.type === "heartbeat_response" || parsedData.type === "heartbeat") {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
LogUtils.debug(TAG, "收到心跳响应");
return;
}
// 构造WebSocketMessage对象
const message: WebSocketMessage = {
id: parsedData.id || Date.now().toString(),
type: parsedData.type || "json",
data: parsedData.data || parsedData,
timestamp: parsedData.timestamp || Date.now(),
};
// 分发消息给对应的处理器
const handler = this.messageHandlers.get(parsedData.type);
if (handler) {
handler(message);
} else {
LogUtils.warn(TAG, `未找到消息类型 ${parsedData.type} 的处理器`);
}
} catch (err) {
LogUtils.error(TAG, "处理WebSocket消息失败:", err);
}
}
}

View File

@@ -0,0 +1,7 @@
export { WebSocketClient } from "./WebSocketClient";
export { default as HttpClient } from "./HttpClient";
export { default as HttpRequest } from "./HttpRequest";
export { NetworkManager } from "./NetworkManager";
export type { ISerializer } from "./Serializer";
export { JsonSerializer, ProtobufSerializer, SerializerManager } from "./Serializer";
export * from "./Types";

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "72082034-0f4c-43b7-b108-0019caeec10a",
"uuid": "41cbd31e-dddf-4cca-af5e-30fa952aaea2",
"files": [],
"subMetas": {},
"userData": {}

View File

@@ -1,193 +0,0 @@
import { nsStringUtils } from "@taqu/Util/StringUtils";
import { isValid } from "cc";
import { assetManager, ImageAsset, Texture2D } from "cc";
import { SpriteFrame } from "cc";
import { UITransform } from "cc";
import { Sprite } from "cc";
import { _decorator } from "cc";
import { EDITOR } from "cc/env";
const { ccclass, property } = _decorator;
@ccclass("CCRemoteSprite")
export class CCRemoteSprite extends Sprite {
private static _remoteSpriteCache: Map<string, SpriteFrame> = new Map();
private static _loadingSpriteCache: Map<string, Promise<SpriteFrame>> = new Map();
private static _checkTimer: NodeJS.Timeout;
/**
* 远程图片URL
*/
@property({
displayName: "远程图片URL",
tooltip: "远程图片的URL地址",
visible: true
})
private _remoteUrl: string = "";
public get remoteUrl(): string {
return this._remoteUrl;
}
public set remoteUrl(value: string) {
const newValue = value || "";
if (this._remoteUrl !== newValue) {
this._remoteUrl = newValue;
if (nsStringUtils.isEmpty(this._remoteUrl)) {
this.release();
return;
}
if (this.spriteFrame == null || this._remoteUrl !== this.spriteFrame.remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch(err => {
app.log.error("RemoteSprite", `加载远程图片失败: ${this._remoteUrl}`, err);
// 可以添加默认图片或错误状态处理
if (this.isValid) {
this.spriteFrame = null;
}
});
}
}
}
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
if (!CCRemoteSprite._checkTimer) {
CCRemoteSprite._checkTimer = setInterval(() => {
CCRemoteSprite.checkCache();
}, 5000);
}
if (!nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.addRef();
}
if (!nsStringUtils.isEmpty(this._remoteUrl) && this.spriteFrame?.remoteUrl !== this._remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch(err => {
app.log.error("RemoteSprite", `onLoad加载远程图片失败: ${this._remoteUrl}`, err);
});
} else if (nsStringUtils.isEmpty(this._remoteUrl) && !nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.release();
}
}
private async loadRemoteSprite(url: string): Promise<void> {
if (nsStringUtils.isEmpty(url)) {
return;
}
// URL 相同,且已经加载完成,直接返回
if (this.spriteFrame && this.spriteFrame.remoteUrl == url) {
return;
}
this.release();
if (CCRemoteSprite._remoteSpriteCache.has(url)) {
const sp = CCRemoteSprite._remoteSpriteCache.get(url);
sp.addRef();
this.spriteFrame = sp;
return;
}
let loadingPromise: Promise<SpriteFrame> = null;
if (CCRemoteSprite._loadingSpriteCache.has(url)) {
loadingPromise = CCRemoteSprite._loadingSpriteCache.get(url);
} else {
loadingPromise = new Promise<SpriteFrame>((resolve, reject) => {
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
if (err) {
app.log.error(`loadRemote error: ${url}`, err);
CCRemoteSprite._loadingSpriteCache.delete(url);
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.remoteUrl = url;
CCRemoteSprite._remoteSpriteCache.set(url, spriteFrame);
CCRemoteSprite._loadingSpriteCache.delete(url);
resolve(spriteFrame);
} catch (createErr) {
CCRemoteSprite._loadingSpriteCache.delete(url);
reject(createErr);
}
}
});
});
CCRemoteSprite._loadingSpriteCache.set(url, loadingPromise);
}
const sp = await loadingPromise;
sp.addRef();
// // 检查是否在加载过程中URL发生了变化
if (sp && (!isValid(this.node, true) || this._remoteUrl !== url || this.spriteFrame?.remoteUrl === url)) {
sp.decRef();
return;
}
console.log("loadSpriteFrame:", url);
this.spriteFrame = sp;
}
public setSize(widthOrHeight: number, height?: number): void {
height ??= widthOrHeight;
this.sizeMode = Sprite.SizeMode.CUSTOM;
this.getComponent(UITransform).setContentSize(widthOrHeight, height);
}
public release(): void {
if (!nsStringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.decRef();
}
this.spriteFrame = null;
}
public onDestroy(): void {
this.release();
super.onDestroy();
}
private static checkCache() {
// 检查缓存中是否有已加载的SpriteFrame
let clearUrls = [];
for (const [url, spriteFrame] of CCRemoteSprite._remoteSpriteCache) {
if (spriteFrame.refCount <= 0) {
let texture = spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (spriteFrame.packable && spriteFrame.original) {
texture = spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (spriteFrame.isValid) {
spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
clearUrls.push(url);
}
}
app.log.log("清理缓存:", clearUrls.length);
clearUrls.forEach(url => {
CCRemoteSprite._remoteSpriteCache.delete(url);
});
}
}

View File

@@ -1,156 +0,0 @@
import { _decorator, Sprite, SpriteFrame } from "cc";
import LogUtils from "../utils/LogUtils";
import { ResManager } from "./ResManager";
import { EDITOR } from "cc/env";
const { ccclass, property } = _decorator;
const TAG = "LocalSprite";
@ccclass("LocalSprite")
export default class LocalSprite extends Sprite {
@property({
displayName: "Bundle名称",
tooltip: "资源包名称默认为resources",
visible: true,
})
private _bundleName: string = "resources";
@property({
displayName: "资源路径",
tooltip: "SpriteFrame资源路径",
visible: true,
})
private _assetPath: string = "";
private _isLoading: boolean = false;
private _loadingKey: string = "";
/**
* 获取Bundle名称
*/
get bundleName(): string {
return this._bundleName;
}
/**
* 设置Bundle名称
*/
set bundleName(value: string) {
const newValue = value || "resources";
if (this._bundleName !== newValue) {
this._bundleName = newValue;
void this.loadSpriteFrame();
}
}
/**
* 获取资源路径
*/
get assetPath(): string {
return this._assetPath;
}
/**
* 设置资源路径
*/
set assetPath(value: string) {
if (this._assetPath !== value) {
this._assetPath = value;
void this.loadSpriteFrame();
}
}
/**
* 获取是否正在加载
*/
get isLoading(): boolean {
return this._isLoading;
}
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
void this.loadSpriteFrame();
}
/**
* 设置Bundle和资源路径
*/
public setAsset(assetPath: string, bundleName: string = "resources"): void {
// 先释放旧资源
if (this._bundleName && this._assetPath) {
ResManager.getInstance().releaseAsset(
this._bundleName,
this._assetPath,
);
}
this._bundleName = bundleName || "resources";
this._assetPath = assetPath;
void this.loadSpriteFrame();
}
/**
* 加载SpriteFrame
*/
private async loadSpriteFrame(): Promise<void> {
// 检查路径是否有效
if (!this._assetPath) {
LogUtils.warn(TAG, "资源路径为空");
this.spriteFrame = null;
return;
}
const loadKey = `${this._bundleName}/${this._assetPath}`;
// 防止重复加载
if (this._isLoading && loadKey == this._loadingKey) {
LogUtils.warn(
TAG,
`正在加载中,跳过重复请求: ${this._bundleName}/${this._assetPath}`,
);
return;
}
this._isLoading = true;
this._loadingKey = loadKey;
try {
LogUtils.log(
TAG,
`开始加载SpriteFrame: ${this._bundleName}/${this._assetPath}`,
);
const spriteFrame =
await ResManager.getInstance().loadAsset<SpriteFrame>(
this._assetPath,
SpriteFrame,
this._bundleName,
);
if (spriteFrame && this.isValid && loadKey == this._loadingKey) {
this.spriteFrame = spriteFrame;
LogUtils.log(
TAG,
`加载SpriteFrame成功: ${this._bundleName}/${this._assetPath}`,
);
} else {
LogUtils.log(
TAG,
`加载SpriteFrame失败: ${this._bundleName}/${this._assetPath}`,
);
}
} catch (err) {
LogUtils.error(
TAG,
`加载SpriteFrame失败: ${this._bundleName}/${this._assetPath}`,
err,
);
if (this.isValid) {
this.spriteFrame = null;
}
} finally {
this._isLoading = false;
}
}
}

View File

@@ -1,22 +1,29 @@
import { isValid } from "cc";
import { assetManager, ImageAsset, Texture2D } from "cc";
import { SpriteFrame } from "cc";
import { isValid, SpriteFrame } from "cc";
import { UITransform } from "cc";
import { Sprite } from "cc";
import { _decorator } from "cc";
import { EDITOR } from "cc/env";
import { StringUtils } from "../utils/StringUtils";
import LogUtils from "../utils/LogUtils";
import ResManager from "./ResManager";
const { ccclass, property } = _decorator;
const CACHE_EXPIRE_TIME = 30000; // 30s 缓存30s
const CACHE_CHECK_INTERVAL = 5000; // 5秒检查一次
@ccclass("RemoteSprite")
export class RemoteSprite extends Sprite {
private static _remoteSpriteCache: Map<string, SpriteFrame> = new Map();
private static _loadingSpriteCache: Map<string, Promise<SpriteFrame>> = new Map();
private static _checkTimer: NodeJS.Timeout;
@property({
displayName: "bundle 资源",
tooltip: "bundle 资源",
visible: true,
})
private bundle: string = "";
@property({
displayName: "资源路径",
tooltip: "资源路径",
visible: true,
})
private path: string = "";
/**
* 远程图片URL
*/
@@ -51,24 +58,21 @@ export class RemoteSprite extends Sprite {
}
}
// override set spriteFrame(value: SpriteFrame) {
// super.spriteFrame = value;
// }
public setSpriteFrame(sf: SpriteFrame) {
this.remoteUrl = null;
this.spriteFrame = sf;
}
public setBundleSpriteFrame(bundle: string, path: string) {}
public onLoad(): void {
super.onLoad();
if (EDITOR) {
return;
}
if (!RemoteSprite._checkTimer) {
RemoteSprite._checkTimer = setInterval(() => {
RemoteSprite.checkCache();
}, CACHE_CHECK_INTERVAL);
}
if (!StringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.addRef();
this.spriteFrame.lastAccessTime = Date.now();
}
if (!StringUtils.isEmpty(this._remoteUrl) && this.spriteFrame?.remoteUrl !== this._remoteUrl) {
this.loadRemoteSprite(this._remoteUrl).catch((err) => {
@@ -89,52 +93,10 @@ export class RemoteSprite extends Sprite {
return;
}
if (RemoteSprite._remoteSpriteCache.has(url)) {
const sp = RemoteSprite._remoteSpriteCache.get(url);
sp.lastAccessTime = Date.now();
sp.addRef();
this.release();
this.spriteFrame = sp;
return;
}
let loadingPromise: Promise<SpriteFrame> = null;
if (RemoteSprite._loadingSpriteCache.has(url)) {
loadingPromise = RemoteSprite._loadingSpriteCache.get(url);
} else {
loadingPromise = new Promise<SpriteFrame>((resolve, reject) => {
LogUtils.log("RemoteSprite", `loadRemoteSprite: ${url}`);
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
if (err) {
LogUtils.error(`loadRemote error: ${url}`, err);
RemoteSprite._loadingSpriteCache.delete(url);
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.remoteUrl = url;
RemoteSprite._remoteSpriteCache.set(url, spriteFrame);
RemoteSprite._loadingSpriteCache.delete(url);
resolve(spriteFrame);
} catch (createErr) {
RemoteSprite._loadingSpriteCache.delete(url);
reject(createErr);
}
}
});
});
RemoteSprite._loadingSpriteCache.set(url, loadingPromise);
}
const sp = await loadingPromise;
sp.addRef();
sp.lastAccessTime = Date.now();
const { err, spriteFrame: sp } = await ResManager.getInstance().loadRemoteSprite({ url });
// // 检查是否在加载过程中URL发生了变化
if (sp && (!isValid(this.node, true) || this._remoteUrl !== url || this.spriteFrame?.remoteUrl === url)) {
sp.decRef(false);
ResManager.getInstance().releaseAsset(sp);
return;
}
console.log("loadSpriteFrame:", url);
@@ -150,7 +112,7 @@ export class RemoteSprite extends Sprite {
public release(): void {
if (!StringUtils.isEmpty(this.spriteFrame?.remoteUrl)) {
this.spriteFrame.decRef(false);
ResManager.getInstance().releaseAsset(this.spriteFrame);
}
this.spriteFrame = null;
}
@@ -159,48 +121,4 @@ export class RemoteSprite extends Sprite {
this.release();
super.onDestroy();
}
private static checkCache() {
// 检查缓存中是否有已加载的SpriteFrame
let clearUrls = [];
let now = Date.now();
for (const [url, spriteFrame] of RemoteSprite._remoteSpriteCache) {
if (spriteFrame.refCount <= 0 && spriteFrame.lastAccessTime < now - CACHE_EXPIRE_TIME) {
let texture = spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (spriteFrame.packable && spriteFrame.original) {
texture = spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (spriteFrame.isValid) {
spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
clearUrls.push(url);
}
}
clearUrls.forEach((url) => {
RemoteSprite._remoteSpriteCache.delete(url);
});
LogUtils.log("清理缓存:", clearUrls.length, RemoteSprite._remoteSpriteCache.size);
}
}

View File

@@ -1,387 +0,0 @@
import { _decorator, SpriteFrame, ImageAsset, Texture2D, assetManager } from "cc";
import NodeSingleton from "../NodeSingleton";
import LogUtils from "../utils/LogUtils";
import { StringUtils } from "../utils/StringUtils";
const { ccclass } = _decorator;
enum LoadState {
NONE,
LOADING,
LOADED,
FAILED,
}
// 添加加载超时和重试机制
const LOAD_TIMEOUT = 30000; // 30秒超时
const MAX_RETRY_COUNT = 3;
// 资源缓存项接口
class CacheItem {
spriteFrame: SpriteFrame | null;
loadState: LoadState;
loadPromise: Promise<SpriteFrame> | null;
private _refCount: number;
private _retryCount: number = 0;
private _loadStartTime: number = 0;
get refCount(): number {
return this._refCount;
}
set refCount(value: number) {
this._lastAccessTime = Date.now();
this._refCount = value;
}
private _lastAccessTime: number;
get lastAccessTime(): number {
return this._lastAccessTime;
}
constructor() {
this.loadState = LoadState.NONE;
this.refCount = 0;
}
get retryCount(): number {
return this._retryCount;
}
incrementRetry(): void {
this._retryCount++;
}
resetRetry(): void {
this._retryCount = 0;
}
setLoadStartTime(): void {
this._loadStartTime = Date.now();
}
isLoadTimeout(): boolean {
return Date.now() - this._loadStartTime > LOAD_TIMEOUT;
}
}
const TAG = "RemoteSpriteCache";
const CLEANUP_CHECK_INTERVAL = 10; // 10秒检查一次
const CACHE_EXPIRE_TIME = 1 * 30 * 1000; // 1分钟
// 添加最大缓存数量限制
const MAX_CACHE_SIZE = 100; // 最大缓存数量
@ccclass("RemoteSpriteCache")
export class RemoteSpriteCache extends NodeSingleton {
private readonly _cache = new Map<string, CacheItem>();
private _lastCleanupTime = 0;
private _maxCacheSize = MAX_CACHE_SIZE;
/**
* 设置最大缓存数量
*/
public setMaxCacheSize(size: number): void {
this._maxCacheSize = Math.max(10, size);
LogUtils.info(TAG, `设置最大缓存数量: ${this._maxCacheSize}`);
}
protected init(): void {
this._lastCleanupTime = Date.now();
LogUtils.log(TAG, "RemoteSpriteCache initialized");
}
/**
* 检查并注册精灵
*/
public checkAndRegisterSprite(spriteFrame: SpriteFrame): boolean {
if (spriteFrame && !StringUtils.isEmpty(spriteFrame.remoteUrl)) {
const cacheItem = this._cache.get(spriteFrame.remoteUrl);
if (cacheItem) {
cacheItem.refCount++;
return true;
}
}
return false;
}
protected update(dt: number): void {
const currentTime = Date.now();
// 每隔指定时间检查一次过期资源
if (currentTime - this._lastCleanupTime >= CLEANUP_CHECK_INTERVAL * 1000) {
this.cleanupUnusedResources();
this._lastCleanupTime = currentTime;
}
}
/**
* 加载SpriteFrame带重试机制
*/
public async loadSpriteFrame(url: string): Promise<SpriteFrame> {
let cacheItem = this._cache.get(url);
if (cacheItem) {
if (cacheItem.loadState === LoadState.LOADED && cacheItem.spriteFrame) {
cacheItem.refCount++;
return cacheItem.spriteFrame;
} else if (cacheItem.loadState === LoadState.LOADING && cacheItem.loadPromise) {
try {
const spriteFrame = await cacheItem.loadPromise;
if (spriteFrame) {
cacheItem.refCount++;
}
return spriteFrame;
} catch (err) {
LogUtils.error(TAG, `等待加载失败: ${url}`, err);
// 如果等待失败,重新尝试加载
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
}
}
// 如果加载失败且重试次数未达到上限,重新尝试
if (cacheItem.loadState === LoadState.FAILED && cacheItem.retryCount < MAX_RETRY_COUNT) {
LogUtils.warn(TAG, `重试加载: ${url}, 第${cacheItem.retryCount + 1}`);
cacheItem.incrementRetry();
return this.doLoadSpriteFrame(url, cacheItem);
}
if (cacheItem.loadState === LoadState.FAILED) {
throw new Error(`加载失败且已达到最大重试次数: ${url}`);
}
}
cacheItem = new CacheItem();
this._cache.set(url, cacheItem);
return this.doLoadSpriteFrame(url, cacheItem);
}
private async doLoadSpriteFrame(url: string, cacheItem: CacheItem): Promise<SpriteFrame> {
cacheItem.loadState = LoadState.LOADING;
cacheItem.setLoadStartTime();
cacheItem.loadPromise = new Promise<SpriteFrame>((resolve, reject) => {
const timeoutId = setTimeout(() => {
LogUtils.error(TAG, `加载超时: ${url}`);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(new Error(`加载超时: ${url}`));
}, LOAD_TIMEOUT);
console.log(`开始加载资源: ${url}`);
assetManager.loadRemote(url, (err, asset: ImageAsset) => {
clearTimeout(timeoutId);
if (err) {
LogUtils.error(TAG, `loadRemote error: ${url}`, err);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(err);
} else {
try {
asset.addRef();
const texture = new Texture2D();
texture.image = asset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
spriteFrame.addRef();
spriteFrame.remoteUrl = url;
cacheItem.refCount++;
cacheItem.spriteFrame = spriteFrame;
cacheItem.loadState = LoadState.LOADED;
cacheItem.loadPromise = null;
cacheItem.resetRetry();
resolve(spriteFrame);
} catch (createErr) {
LogUtils.error(TAG, `创建SpriteFrame失败: ${url}`, createErr);
cacheItem.loadState = LoadState.FAILED;
cacheItem.loadPromise = null;
reject(createErr);
}
}
});
});
return await cacheItem.loadPromise;
}
/**
* 释放资源引用
*/
public releaseResource(url: string): void {
if (url) {
const cacheItem = this._cache.get(url);
if (cacheItem) {
cacheItem.refCount--;
console.log(`释放资源引用: ${url}, 引用计数: ${cacheItem.refCount}`);
}
}
}
/**
* 清理未使用的资源
*/
public cleanupUnusedResources(): void {
const currentCacheSize = this._cache.size;
const targetSize = Math.floor(this._maxCacheSize * 0.8); // 目标大小为阈值的80%
// 如果未达到阈值仅清理引用计数为0且缓存超时的资源
if (currentCacheSize < this._maxCacheSize) {
this.cleanupExpiredResources();
return;
}
// 达到阈值时按使用时间排序清理引用计数为0的资源
this.cleanupByUsageTime(targetSize);
}
/**
* 清理过期的资源引用计数为0且超时
*/
private cleanupExpiredResources(): void {
const urlsToRemove: string[] = [];
let cleanedCount = 0;
const currentTime = Date.now();
for (const [url, cacheItem] of this._cache.entries()) {
if (
cacheItem.spriteFrame &&
cacheItem.loadState === LoadState.LOADED &&
cacheItem.refCount <= 0 &&
cacheItem.lastAccessTime < currentTime - CACHE_EXPIRE_TIME
) {
this.destroyCacheItem(cacheItem);
urlsToRemove.push(url);
cleanedCount++;
}
}
// 从缓存中移除已清理的项
for (const url of urlsToRemove) {
this._cache.delete(url);
}
if (cleanedCount > 0) {
LogUtils.log(TAG, `清理过期资源: ${cleanedCount}`);
}
}
/**
* 按使用时间排序清理资源只清理引用计数为0的资源
*/
private cleanupByUsageTime(targetSize: number): void {
// 直接筛选并排序需要清理的资源
const entriesToClean = Array.from(this._cache.entries())
.filter(([_, cacheItem]) => cacheItem.refCount <= 0 && cacheItem.loadState === LoadState.LOADED)
.sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime);
let cleanedCount = 0;
// 清理资源直到达到目标大小
for (const [url, cacheItem] of entriesToClean) {
if (this._cache.size <= targetSize) {
break;
}
this.destroyCacheItem(cacheItem);
this._cache.delete(url);
cleanedCount++;
}
if (cleanedCount > 0) {
const finalSize = this._cache.size;
LogUtils.log(
TAG,
`缓存清理完成: 清理了 ${cleanedCount} 个资源,当前缓存大小: ${finalSize}/${this._maxCacheSize}`,
);
}
}
/**
* 销毁缓存项中的资源
*/
private destroyCacheItem(cacheItem: CacheItem): void {
if (!cacheItem.spriteFrame) {
return;
}
try {
// 先获取texture引用
let texture = cacheItem.spriteFrame.texture as Texture2D;
let imageAsset: ImageAsset = null;
// 如果已加入动态合图必须取原始的Texture2D
if (cacheItem.spriteFrame.packable && cacheItem.spriteFrame.original) {
texture = cacheItem.spriteFrame.original._texture as Texture2D;
}
// 获取ImageAsset引用
if (texture?.image) {
imageAsset = texture.image;
}
// 先销毁spriteFrame这会自动处理对texture的引用
if (cacheItem.spriteFrame.isValid) {
cacheItem.spriteFrame.destroy();
}
// 再销毁texture
if (texture?.isValid) {
texture.destroy();
}
// 最后减少ImageAsset的引用计数
if (imageAsset?.isValid) {
imageAsset.decRef();
}
} catch (err) {
LogUtils.error(TAG, `销毁缓存项时发生错误`, err);
}
}
/**
* 获取缓存统计信息
*/
public getCacheStats(): {
total: number;
loaded: number;
loading: number;
failed: number;
} {
const stats = { total: 0, loaded: 0, loading: 0, failed: 0 };
for (const [, cacheItem] of this._cache.entries()) {
stats.total++;
switch (cacheItem.loadState) {
case LoadState.LOADED:
stats.loaded++;
break;
case LoadState.LOADING:
stats.loading++;
break;
case LoadState.FAILED:
stats.failed++;
break;
}
}
return stats;
}
/**
* 清空所有缓存
*/
public clearAllCache(): void {
for (const [, cacheItem] of this._cache.entries()) {
if (cacheItem.spriteFrame?.isValid) {
cacheItem.spriteFrame.destroy();
}
}
this._cache.clear();
LogUtils.log(TAG, "All cache cleared");
}
protected onDestroy(): void {
this.clearAllCache();
super.onDestroy();
}
}

View File

@@ -1,18 +1,11 @@
import LogUtils from "../utils/LogUtils";
import { singleton, Singleton } from "../Singleton";
import {
ITaskConfig,
ITaskRuntimeData,
ITaskReward,
TaskStatus,
TaskType,
TaskEventType,
} from "./TaskData";
import { ITaskConfig, ITaskRuntimeData, ITaskReward, TaskStatus, TaskType, TaskEventType } from "./TaskData";
import { TaskTypeManager } from "./TaskTypeManager";
import { EventManager } from "../event/EventManager";
import TimeManager from "../timer/TimerManager";
import { ResManager } from "../res/ResManager";
import { JsonAsset } from "cc";
import ResManager from "../res/ResManager";
const TAG = "TaskManager";
@@ -70,10 +63,11 @@ export class TaskManager extends Singleton {
private async loadTaskConfigs(): Promise<void> {
// TODO: 从配置文件或服务器加载任务配置
LogUtils.info(TAG, "加载任务配置");
const jsonAsset = await ResManager.getInstance().loadAsset<JsonAsset>(
"task-configs",
JsonAsset,
);
const jsonAsset = await ResManager.getInstance().loadAsset<JsonAsset>({
path: "task-configs",
type: JsonAsset,
bundle: "configs",
});
const taskConfigs = jsonAsset.json as Record<string, ITaskConfig[]>;
for (const taskType of Object.keys(taskConfigs)) {
@@ -103,29 +97,20 @@ export class TaskManager extends Singleton {
if (
runtimeData.status === TaskStatus.ACTIVE &&
runtimeData.activatedTime &&
this.typeManager.isTaskExpired(
config,
runtimeData.activatedTime,
)
this.typeManager.isTaskExpired(config, runtimeData.activatedTime)
) {
this.expireTask(taskId);
continue;
}
// 检查任务是否需要重置
if (
runtimeData.resetTime &&
this.typeManager.shouldResetTask(config, runtimeData.resetTime)
) {
if (runtimeData.resetTime && this.typeManager.shouldResetTask(config, runtimeData.resetTime)) {
this.resetTask(taskId);
continue;
}
// 检查任务是否可以激活
if (
runtimeData.status === TaskStatus.INACTIVE &&
this.canActivateTask(config)
) {
if (runtimeData.status === TaskStatus.INACTIVE && this.canActivateTask(config)) {
this.activateTask(taskId);
}
}
@@ -234,10 +219,7 @@ export class TaskManager extends Singleton {
}
if (runtimeData.status !== TaskStatus.INACTIVE) {
LogUtils.warn(
TAG,
`任务状态不正确,无法激活: ${taskId}, 当前状态: ${runtimeData.status}`,
);
LogUtils.warn(TAG, `任务状态不正确,无法激活: ${taskId}, 当前状态: ${runtimeData.status}`);
return false;
}
@@ -266,11 +248,7 @@ export class TaskManager extends Singleton {
/**
* 更新任务进度
*/
public updateTaskProgress(
taskId: string,
targetId: string,
progress: number,
): boolean {
public updateTaskProgress(taskId: string, targetId: string, progress: number): boolean {
try {
const runtimeData = this.taskRuntimeData.get(taskId);
if (!runtimeData) {
@@ -279,10 +257,7 @@ export class TaskManager extends Singleton {
}
if (runtimeData.status !== TaskStatus.ACTIVE) {
LogUtils.debug(
TAG,
`任务状态不正确,无法更新进度: ${taskId}, 当前状态: ${runtimeData.status}`,
);
LogUtils.debug(TAG, `任务状态不正确,无法更新进度: ${taskId}, 当前状态: ${runtimeData.status}`);
return false;
}
@@ -293,10 +268,7 @@ export class TaskManager extends Singleton {
}
const oldProgress = target.currentCount;
target.currentCount = Math.min(
target.currentCount + progress,
target.targetCount,
);
target.currentCount = Math.min(target.currentCount + progress, target.targetCount);
EventManager.getInstance().emit(
TaskEventType.TASK_PROGRESS_UPDATED,
taskId,
@@ -314,10 +286,7 @@ export class TaskManager extends Singleton {
this.completeTask(taskId);
}
LogUtils.debug(
TAG,
`任务进度已更新: ${taskId}, ${targetId}, ${target.currentCount}/${target.targetCount}`,
);
LogUtils.debug(TAG, `任务进度已更新: ${taskId}, ${targetId}, ${target.currentCount}/${target.targetCount}`);
return true;
} catch (err) {
LogUtils.error(TAG, `更新任务进度失败: ${taskId}`, err);
@@ -337,10 +306,7 @@ export class TaskManager extends Singleton {
}
if (runtimeData.status !== TaskStatus.ACTIVE) {
LogUtils.warn(
TAG,
`任务状态不正确,无法完成: ${taskId}, 当前状态: ${runtimeData.status}`,
);
LogUtils.warn(TAG, `任务状态不正确,无法完成: ${taskId}, 当前状态: ${runtimeData.status}`);
return false;
}
@@ -377,10 +343,7 @@ export class TaskManager extends Singleton {
}
if (runtimeData.status !== TaskStatus.COMPLETED) {
LogUtils.warn(
TAG,
`任务状态不正确,无法领取奖励: ${taskId}, 当前状态: ${runtimeData.status}`,
);
LogUtils.warn(TAG, `任务状态不正确,无法领取奖励: ${taskId}, 当前状态: ${runtimeData.status}`);
return null;
}
@@ -398,10 +361,7 @@ export class TaskManager extends Singleton {
},
);
LogUtils.info(
TAG,
`任务奖励已领取: ${taskId}, 奖励数量: ${rewards.length}`,
);
LogUtils.info(TAG, `任务奖励已领取: ${taskId}, 奖励数量: ${rewards.length}`);
return rewards;
} catch (err) {
LogUtils.error(TAG, `领取任务奖励失败: ${taskId}`, err);
@@ -495,12 +455,8 @@ export class TaskManager extends Singleton {
// 检查前置任务
if (config.prerequisites) {
for (const prerequisiteId of config.prerequisites) {
const prerequisiteData =
this.taskRuntimeData.get(prerequisiteId);
if (
!prerequisiteData ||
prerequisiteData.status !== TaskStatus.CLAIMED
) {
const prerequisiteData = this.taskRuntimeData.get(prerequisiteId);
if (!prerequisiteData || prerequisiteData.status !== TaskStatus.CLAIMED) {
return false;
}
}

View File

@@ -0,0 +1,166 @@
import { Component, Node, Label, Button, _decorator, tween, Vec3, UIOpacity } from "cc";
const { ccclass, property } = _decorator;
/**
* 网络错误对话框选项
*/
export interface NetworkErrorDialogOptions {
title?: string;
message?: string;
onRetry?: () => void;
onCancel?: () => void;
}
/**
* 网络错误对话框组件
*/
@ccclass("NetworkErrorDialog")
export class NetworkErrorDialog extends Component {
@property(Node)
private dialogPanel: Node = null;
@property(Label)
private titleLabel: Label = null;
@property(Label)
private messageLabel: Label = null;
@property(Button)
private retryButton: Button = null;
@property(Button)
private cancelButton: Button = null;
@property(Node)
private maskBackground: Node = null;
private options: NetworkErrorDialogOptions = null;
private maskOpacity: UIOpacity = null;
onLoad() {
// 初始化时隐藏
this.node.active = false;
// 获取遮罩的UIOpacity组件
if (this.maskBackground) {
this.maskOpacity = this.maskBackground.getComponent(UIOpacity);
if (!this.maskOpacity) {
this.maskOpacity = this.maskBackground.addComponent(UIOpacity);
}
}
// 绑定按钮事件
if (this.retryButton) {
this.retryButton.node.on(Button.EventType.CLICK, this.onRetryClick, this);
}
if (this.cancelButton) {
this.cancelButton.node.on(Button.EventType.CLICK, this.onCancelClick, this);
}
// 点击遮罩背景关闭对话框
if (this.maskBackground) {
this.maskBackground.on(Node.EventType.TOUCH_END, this.onMaskClick, this);
}
}
/**
* 显示对话框
*/
public show(options: NetworkErrorDialogOptions): void {
this.options = options;
this.node.active = true;
// 设置文本内容
if (this.titleLabel) {
this.titleLabel.string = options.title || "网络连接失败";
}
if (this.messageLabel) {
this.messageLabel.string = options.message || "网络连接失败,请检查网络设置后重试";
}
// 播放显示动画
if (this.dialogPanel) {
this.dialogPanel.setScale(0.5, 0.5, 1);
tween(this.dialogPanel)
.to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: "backOut" })
.start();
}
// 遮罩淡入
if (this.maskOpacity) {
this.maskOpacity.opacity = 0;
tween(this.maskOpacity).to(0.2, { opacity: 180 }).start();
}
}
/**
* 隐藏对话框
*/
public hide(): void {
// 播放隐藏动画
if (this.dialogPanel) {
tween(this.dialogPanel)
.to(0.2, { scale: new Vec3(0.5, 0.5, 1) }, { easing: "backIn" })
.start();
}
// 遮罩淡出
if (this.maskOpacity) {
tween(this.maskOpacity)
.to(0.2, { opacity: 0 })
.call(() => {
this.node.active = false;
this.options = null;
})
.start();
} else {
this.node.active = false;
this.options = null;
}
}
/**
* 重试按钮点击
*/
private onRetryClick(): void {
if (this.options?.onRetry) {
this.options.onRetry();
}
this.hide();
}
/**
* 取消按钮点击
*/
private onCancelClick(): void {
if (this.options?.onCancel) {
this.options.onCancel();
}
this.hide();
}
/**
* 遮罩点击
*/
private onMaskClick(): void {
// 可以选择是否允许点击遮罩关闭对话框
// this.onCancelClick();
}
onDestroy() {
if (this.retryButton) {
this.retryButton.node.off(Button.EventType.CLICK, this.onRetryClick, this);
}
if (this.cancelButton) {
this.cancelButton.node.off(Button.EventType.CLICK, this.onCancelClick, this);
}
if (this.maskBackground) {
this.maskBackground.off(Node.EventType.TOUCH_END, this.onMaskClick, this);
}
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "21febc68-719c-4eab-90d3-193f9f15abba",
"uuid": "54075100-2d10-46a4-9f67-469788f8af2e",
"files": [],
"subMetas": {},
"userData": {}

View File

@@ -0,0 +1,120 @@
import { Component, Node, Label, _decorator, tween, Vec3 } from "cc";
const { ccclass, property } = _decorator;
/**
* 网络连接遮罩组件
*/
@ccclass('NetworkMask')
export class NetworkMask extends Component {
@property(Node)
private loadingIcon: Node = null;
@property(Label)
private messageLabel: Label = null;
@property(Node)
private maskBackground: Node = null;
private isShowing: boolean = false;
private rotationTween: any = null;
onLoad() {
// 初始化时隐藏
this.node.active = false;
}
/**
* 显示网络遮罩
*/
public show(message: string = "网络连接中..."): void {
if (this.isShowing) {
return;
}
this.isShowing = true;
this.node.active = true;
// 设置消息文本
if (this.messageLabel) {
this.messageLabel.string = message;
}
// 开始loading动画
this.startLoadingAnimation();
// 淡入动画
if (this.maskBackground) {
this.maskBackground.setScale(0.8, 0.8, 1);
tween(this.maskBackground)
.to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
.start();
}
}
/**
* 隐藏网络遮罩
*/
public hide(): void {
if (!this.isShowing) {
return;
}
this.isShowing = false;
// 停止loading动画
this.stopLoadingAnimation();
// 淡出动画
if (this.maskBackground) {
tween(this.maskBackground)
.to(0.2, { scale: new Vec3(0.8, 0.8, 1) }, { easing: 'backIn' })
.call(() => {
this.node.active = false;
})
.start();
} else {
this.node.active = false;
}
}
/**
* 更新消息
*/
public updateMessage(message: string): void {
if (this.messageLabel) {
this.messageLabel.string = message;
}
}
/**
* 开始loading动画
*/
private startLoadingAnimation(): void {
if (!this.loadingIcon) {
return;
}
this.stopLoadingAnimation();
// 旋转动画
this.rotationTween = tween(this.loadingIcon)
.by(1, { eulerAngles: new Vec3(0, 0, -360) })
.repeatForever()
.start();
}
/**
* 停止loading动画
*/
private stopLoadingAnimation(): void {
if (this.rotationTween) {
this.rotationTween.stop();
this.rotationTween = null;
}
}
onDestroy() {
this.stopLoadingAnimation();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7969055e-3f6c-4f72-81cb-b30ac9a544a6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { _decorator, Button, Component, Constructor } from "cc";
import { EDITOR } from "cc/env";
import { StringUtils } from "../utils/StringUtils";
@@ -305,6 +306,7 @@ export enum UIType {
TOAST = 2,
GUIDE = 3,
LOADING = 4,
SYSTEM = 5,
}
export interface UIConfigData {
prefab: string;

View File

@@ -1,9 +1,9 @@
import { Camera, Constructor, error, find, game, Game, instantiate, js, Node, NodePool, Prefab, Widget } from "cc";
import { Camera, Constructor, find, game, Game, instantiate, js, Node, NodePool, Prefab, Widget } from "cc";
import BaseUI from "./BaseUI";
import { ResManager } from "../res/ResManager";
import { getUIConfigByClass, UIConfigData, UIType } from "./UIDecorator";
import { singleton, Singleton } from "../Singleton";
import LogUtils from "../utils/LogUtils";
import ResManager from "../res/ResManager";
@singleton({
auto: true,
@@ -12,8 +12,7 @@ export default class UIManager extends Singleton {
private uiPrefabMap: Map<string, Prefab> = new Map();
private uiCacheMap: Map<string, NodePool> = new Map();
private uiShowMap: Map<string, Set<BaseUI>> = new Map();
// 新增跟踪正在加载中的UI
private uiLoadingMap: Map<string, Promise<BaseUI>> = new Map();
private uiLoadingMap: Map<string, Promise<{ err: Error; asset?: Prefab }>> = new Map();
private root: Node;
private camera: Camera;
private uiTypeMap: Map<UIType, Node> = new Map();
@@ -87,6 +86,11 @@ export default class UIManager extends Singleton {
throw new Error(`${className} 没有配置 UIConfig 装饰器`);
}
const uiPrefab: Prefab = await this.loadUIPrefab(config);
if (!uiPrefab) {
return null;
}
// 检查是否已经打开或正在加载中
if (!config.isMulti) {
// 检查是否已经显示
@@ -95,35 +99,8 @@ export default class UIManager extends Singleton {
return null;
}
}
// 检查是否正在加载中
if (this.uiLoadingMap.has(className)) {
LogUtils.warn("UIManager", `UI ${className} 正在加载中,不允许重复打开 请等待加载完成`);
await this.uiLoadingMap.get(className);
return this.openUI(classOrName, ...data);
}
// 创建加载Promise并添加到加载映射中
const loadingPromise = this.loadUIInternal<T>(uiClass, className, config, ...data);
this.uiLoadingMap.set(className, loadingPromise);
try {
const result = await loadingPromise;
return result;
} finally {
this.uiLoadingMap.delete(className);
}
}
private async loadUIInternal<T extends BaseUI>(
uiClass: Constructor<T>,
className: string,
config: UIConfigData,
...data: unknown[]
): Promise<T> {
let uiNode: Node = null;
// 检查缓存
if (this.uiCacheMap.has(className)) {
const uiPool = this.uiCacheMap.get(className);
if (uiPool.size() > 0) {
@@ -132,26 +109,13 @@ export default class UIManager extends Singleton {
}
if (uiNode == null) {
// 加载预制体
if (this.uiPrefabMap.has(config.prefab)) {
uiNode = instantiate(this.uiPrefabMap.get(config.prefab));
} else {
const prefab = await ResManager.getInstance().loadAsset<Prefab>(config.prefab, Prefab, config.bundle);
if (prefab == null) {
error(`未找到名为 ${config.prefab} 的预制体`);
return null;
}
this.uiPrefabMap.set(config.prefab, prefab);
uiNode = instantiate(prefab);
}
uiNode = instantiate(uiPrefab);
}
// 设置父节点
const typeNode = this.uiTypeMap.get(config.type);
if (!typeNode) {
throw new Error(`未找到类型为 ${config.type} 的容器节点`);
}
uiNode.parent = typeNode;
let baseUI = uiNode.getComponent(BaseUI);
if (!baseUI) {
@@ -159,7 +123,7 @@ export default class UIManager extends Singleton {
}
uiNode.active = true;
baseUI.onShow(...data);
void baseUI.onShow(...data);
if (this.uiShowMap.has(className)) {
this.uiShowMap.get(className).add(baseUI);
@@ -171,6 +135,36 @@ export default class UIManager extends Singleton {
return baseUI as T;
}
private async loadUIPrefab(config: UIConfigData): Promise<Prefab> {
if (this.uiPrefabMap.has(config.prefab)) {
return this.uiPrefabMap.get(config.prefab);
}
if (this.uiLoadingMap.has(config.prefab)) {
const loadingPromise = await this.uiLoadingMap.get(config.prefab);
if (loadingPromise.err) {
console.error(`加载 UI 预制体失败 ${config.prefab}`, loadingPromise.err);
}
return loadingPromise.asset;
}
const loadingPromise = ResManager.getInstance().loadAsset<Prefab>({
bundle: config.bundle,
path: config.prefab,
type: Prefab,
});
this.uiLoadingMap.set(config.prefab, loadingPromise);
const { err, asset } = await loadingPromise;
if (err) {
console.error(`加载 UI 预制体失败 ${config.prefab}`, err);
return null;
}
return asset;
}
/**
* ⚠️ 关闭UI, 若需要关闭的UI 正在打开过程中,暂不支持关闭
* @param classOrName 类名或实例
*/
public async closeUI<T extends BaseUI>(classOrName: Constructor<T> | string | T) {
let uiName: string;
let uiClass: Constructor<T>;
@@ -206,6 +200,13 @@ export default class UIManager extends Singleton {
this.uiCacheMap.get(uiName).put(ui.node);
} else {
ui.node.destroy();
if (this.uiCacheMap.size <= 0) {
const prefab = this.uiPrefabMap.get(config.prefab);
if (prefab) {
ResManager.getInstance().releaseAsset(prefab);
this.uiPrefabMap.delete(config.prefab);
}
}
}
if (this.uiShowMap.has(uiName)) {
this.uiShowMap.get(uiName).delete(ui);
@@ -290,4 +291,8 @@ export default class UIManager extends Singleton {
}
}
}
public showLoading() {}
public hideLoading() {}
}

View File

@@ -0,0 +1,12 @@
import BaseUI from "../BaseUI";
import { uiConfig, UIType } from "../UIDecorator";
import { _decorator } from "cc";
const { ccclass } = _decorator;
@ccclass()
@uiConfig({
prefab: "prefabs/uis/DefaultNetworkMask",
bundle: "framework-res",
type: UIType.LOADING,
})
export default class DefaultNetworkMask extends BaseUI {}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "23692c6a-0fd9-4f54-8507-835c598c48a2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,8 @@
export { NetworkMask } from "./NetworkMask";
// export { NetworkErrorDialog, NetworkErrorDialogOptions } from "./NetworkErrorDialog";
export { default as BaseUI } from "./BaseUI";
export { default as BasePopup } from "./BasePopup";
export { default as BaseLayer } from "./BaseLayer";
export { default as BaseToast } from "./BaseToast";
export { default as BaseLoading } from "./BaseLoading";
export { default as UIManager } from "./UIManager";

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5e9b4e13-31ec-4d07-8d53-d5e882ddede8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import { DEV } from "cc/env";
/**
@@ -30,8 +29,6 @@ export default class LogUtils {
private static _prefix: string = "Max:";
private static _level: LogLevel = DEV ? LogLevel.DEBUG : LogLevel.ERROR;
private static _enableTimestamp: boolean = true;
private static _enableStackTrace: boolean = true;
private static _enableColors: boolean = true;
/**
* 日志颜色配置
@@ -82,27 +79,6 @@ export default class LogUtils {
this._level = level;
}
/**
* 启用/禁用时间戳
*/
public static enableTimestamp(enable: boolean): void {
this._enableTimestamp = enable;
}
/**
* 启用/禁用堆栈跟踪
*/
public static enableStackTrace(enable: boolean): void {
this._enableStackTrace = enable;
}
/**
* 启用/禁用颜色
*/
public static enableColors(enable: boolean): void {
this._enableColors = enable;
}
/**
* 设置自定义颜色配置
*/
@@ -122,28 +98,12 @@ export default class LogUtils {
*/
private static _getPrefix(): string {
let prefix = this._prefix;
if (this._enableTimestamp) {
const now = new Date();
const timestamp = `[${now.toLocaleTimeString()}]`;
prefix = `${timestamp} ${prefix}`;
}
const now = new Date();
const timestamp = `[${now.toLocaleTimeString()}]`;
prefix = `${timestamp} ${prefix}`;
return prefix;
}
/**
* 获取堆栈信息
*/
private static _getStackTrace(): string {
if (!this._enableStackTrace) return "";
const stack = new Error().stack;
if (stack) {
const lines = stack.split("\n");
// 跳过前几行Error构造和当前方法
return lines.slice(3, 5).join("\n");
}
return "";
}
/**
* 生成 TAG 颜色
*/
@@ -201,11 +161,9 @@ export default class LogUtils {
const prefix = this._getPrefix();
// 判断是否应该使用 TAG 颜色(仅在警告等级以下)
const shouldUseTagColor =
tag &&
(colorKey === "debug" || colorKey === "info" || colorKey === "log");
const shouldUseTagColor = tag && (colorKey === "debug" || colorKey === "info" || colorKey === "log");
if (this._enableColors && typeof window !== "undefined") {
if (typeof window !== "undefined") {
// 浏览器环境,使用 CSS 样式
if (shouldUseTagColor) {
// 有 TAG 且为警告等级以下,使用 TAG 颜色
@@ -217,18 +175,10 @@ export default class LogUtils {
if (tag) {
// 有 TAG 但是警告等级及以上,显示 TAG 但使用 LogLevel 颜色
const tagPrefix = `${prefix}[${tag}]`;
console[method](
`%c${tagPrefix}`,
this._colors[colorKey],
...remainingData,
);
console[method](`%c${tagPrefix}`, this._colors[colorKey], ...remainingData);
} else {
// 无 TAG使用 LogLevel 颜色
console[method](
`%c${prefix}`,
this._colors[colorKey],
...remainingData,
);
console[method](`%c${prefix}`, this._colors[colorKey], ...remainingData);
}
}
} else {
@@ -283,67 +233,7 @@ export default class LogUtils {
*/
public static error(...data: unknown[]): void {
if (this._level <= LogLevel.ERROR) {
const stackTrace = this._getStackTrace();
this._logWithColor("error", "error", ...data);
if (stackTrace) {
console.error(stackTrace);
}
}
}
/**
* 分组日志开始
*/
public static group(label: string): void {
if (this._level <= LogLevel.DEBUG) {
const prefix = this._getPrefix();
if (this._enableColors && typeof window !== "undefined") {
console.group(`%c${prefix}`, this._colors.info, label);
} else {
console.group(prefix, label);
}
}
}
/**
* 分组日志结束
*/
public static groupEnd(): void {
if (this._level <= LogLevel.DEBUG) {
console.groupEnd();
}
}
/**
* 表格形式输出
*/
public static table(data: any): void {
if (this._level <= LogLevel.DEBUG) {
const prefix = this._getPrefix();
if (this._enableColors && typeof window !== "undefined") {
console.log(`%c${prefix}`, this._colors.info, "Table data:");
} else {
console.log(prefix, "Table data:");
}
console.table(data);
}
}
/**
* 性能计时开始
*/
public static time(label: string): void {
if (this._level <= LogLevel.DEBUG) {
console.time(`${this._prefix} ${label}`);
}
}
/**
* 性能计时结束
*/
public static timeEnd(label: string): void {
if (this._level <= LogLevel.DEBUG) {
console.timeEnd(`${this._prefix} ${label}`);
}
}

View File

@@ -3,6 +3,7 @@ import LogUtils from "./LogUtils";
import { EventTouch } from "cc";
import { Button } from "cc";
import { EDITOR } from "cc/env";
import { StringUtils } from "./StringUtils";
const TAG = "NodeUtils";
@@ -28,21 +29,21 @@ export default class NodeUtils {
try {
// 扩展 Node 原型,添加 getOrAddComponent 方法
if (!Node.prototype.getOrAddComponent) {
Node.prototype.getOrAddComponent = function <
T extends Component,
>(classConstructor: new () => T): T {
let component = this.getComponent(classConstructor);
Node.prototype.getOrAddComponent = function <T extends Component>(
classConstructor: new () => T,
path?: string,
): T {
const node = StringUtils.isEmpty(path) ? this : this.findChildRecursive(path);
if (node == null) {
LogUtils.error(TAG, `未找到路径为 ${path} 的子节点`);
return null;
}
let component = node.getComponent(classConstructor);
if (!component) {
component = this.addComponent(classConstructor);
LogUtils.debug(
TAG,
`添加组件: ${classConstructor.name} 到节点: ${this.name}`
);
component = node.addComponent(classConstructor);
LogUtils.debug(TAG, `添加组件: ${classConstructor.name} 到节点: ${node.name}`);
} else {
LogUtils.debug(
TAG,
`获取现有组件: ${classConstructor.name} 从节点: ${this.name}`
);
LogUtils.debug(TAG, `获取现有组件: ${classConstructor.name} 从节点: ${this.name}`);
}
return component;
};
@@ -50,22 +51,14 @@ export default class NodeUtils {
// 扩展 Node 原型,添加 getOrCreateChild 方法
if (!Node.prototype.getOrCreateChild) {
Node.prototype.getOrCreateChild = function (
name: string
): Node {
Node.prototype.getOrCreateChild = function (name: string): Node {
let child = this.getChildByName(name);
if (!child) {
child = new Node(name);
child.setParent(this);
LogUtils.debug(
TAG,
`创建子节点: ${name} 在节点: ${this.name}`
);
LogUtils.debug(TAG, `创建子节点: ${name} 在节点: ${this.name}`);
} else {
LogUtils.debug(
TAG,
`获取现有子节点: ${name} 从节点: ${this.name}`
);
LogUtils.debug(TAG, `获取现有子节点: ${name} 从节点: ${this.name}`);
}
return child;
};
@@ -73,22 +66,13 @@ export default class NodeUtils {
// 扩展 Node 原型,添加 removeAllComponents 方法
if (!Node.prototype.removeAllComponents) {
Node.prototype.removeAllComponents = function (
excludeTypes: (new () => Component)[] = []
): void {
const componentsToRemove = this.components.filter(
(comp) => {
return !excludeTypes.some(
(excludeType) => comp instanceof excludeType
);
}
);
Node.prototype.removeAllComponents = function (excludeTypes: (new () => Component)[] = []): void {
const componentsToRemove = this.components.filter((comp) => {
return !excludeTypes.some((excludeType) => comp instanceof excludeType);
});
for (const comp of componentsToRemove) {
LogUtils.debug(
TAG,
`移除组件: ${comp.constructor.name} 从节点: ${this.name}`
);
LogUtils.debug(TAG, `移除组件: ${comp.constructor.name} 从节点: ${this.name}`);
comp.destroy();
}
};
@@ -96,18 +80,18 @@ export default class NodeUtils {
// 扩展 Node 原型,添加 hasComponent 方法
if (!Node.prototype.hasComponent) {
Node.prototype.hasComponent = function <T extends Component>(
classConstructor: new () => T
): boolean {
Node.prototype.hasComponent = function <T extends Component>(classConstructor: new () => T): boolean {
return this.getComponent(classConstructor) !== null;
};
}
// 扩展 Node 原型,添加 findChildRecursive 方法
if (!Node.prototype.findChildRecursive) {
Node.prototype.findChildRecursive = function (
name: string
): Node | null {
Node.prototype.findChildRecursive = function (name: string): Node | null {
if (StringUtils.isEmpty(name)) {
LogUtils.warn(TAG, `findChildRecursive 方法参数 name 为空`);
return null;
}
// 先在直接子节点中查找
const directChild = this.getChildByName(name);
if (directChild) {
@@ -127,31 +111,18 @@ export default class NodeUtils {
}
if (!Node.prototype.onClick) {
Node.prototype.onClick = function (
callback: (event: EventTouch) => void,
target?: any
): void {
Node.prototype.onClick = function (callback: (event: EventTouch) => void, target?: any): void {
if (callback == null) {
LogUtils.warn(
TAG,
`节点 ${this.name} 点击事件回调为空`
);
LogUtils.warn(TAG, `节点 ${this.name} 点击事件回调为空`);
return;
}
if (this.hasComponent(Button)) {
const button: Button = this.getComponent(Button);
// 避免按钮事件重复监听
button.node.targetOff(target);
button.node.on(
Button.EventType.CLICK,
callback,
target
);
button.node.on(Button.EventType.CLICK, callback, target);
} else {
LogUtils.warn(
TAG,
`节点 ${this.name} 没有 Button 组件,无法添加点击事件`
);
LogUtils.warn(TAG, `节点 ${this.name} 没有 Button 组件,无法添加点击事件`);
}
};
}
@@ -177,10 +148,7 @@ export default class NodeUtils {
targetNode.setParent(parent);
LogUtils.debug(TAG, `创建节点: ${name} 在父节点: ${parent.name}`);
} else {
LogUtils.debug(
TAG,
`找到现有节点: ${name} 在父节点: ${parent.name}`
);
LogUtils.debug(TAG, `找到现有节点: ${name} 在父节点: ${parent.name}`);
}
return targetNode;
@@ -192,22 +160,13 @@ export default class NodeUtils {
* @param classConstructor 组件类构造函数
* @returns 组件实例
*/
public static getOrAddComponent<T extends Component>(
node: Node,
classConstructor: new () => T
): T {
public static getOrAddComponent<T extends Component>(node: Node, classConstructor: new () => T): T {
let component = node.getComponent(classConstructor);
if (!component) {
component = node.addComponent(classConstructor);
LogUtils.debug(
TAG,
`添加组件: ${classConstructor.name} 到节点: ${node.name}`
);
LogUtils.debug(TAG, `添加组件: ${classConstructor.name} 到节点: ${node.name}`);
} else {
LogUtils.debug(
TAG,
`获取现有组件: ${classConstructor.name} 从节点: ${node.name}`
);
LogUtils.debug(TAG, `获取现有组件: ${classConstructor.name} 从节点: ${node.name}`);
}
return component;
}
@@ -241,21 +200,13 @@ export default class NodeUtils {
* @param node 目标节点
* @param excludeTypes 要保留的组件类型数组
*/
public static cleanComponents(
node: Node,
excludeTypes: (new () => Component)[] = []
): void {
public static cleanComponents(node: Node, excludeTypes: (new () => Component)[] = []): void {
const componentsToRemove = node.components.filter((comp) => {
return !excludeTypes.some(
(excludeType) => comp instanceof excludeType
);
return !excludeTypes.some((excludeType) => comp instanceof excludeType);
});
for (const comp of componentsToRemove) {
LogUtils.debug(
TAG,
`移除组件: ${comp.constructor.name} 从节点: ${node.name}`
);
LogUtils.debug(TAG, `移除组件: ${comp.constructor.name} 从节点: ${node.name}`);
comp.destroy();
}
}
@@ -290,10 +241,7 @@ export default class NodeUtils {
* @param classConstructor 组件类构造函数
* @returns 是否包含该组件
*/
public static hasComponent<T extends Component>(
node: Node,
classConstructor: new () => T
): boolean {
public static hasComponent<T extends Component>(node: Node, classConstructor: new () => T): boolean {
return node.getComponent(classConstructor) !== null;
}
}
@@ -304,10 +252,11 @@ declare module "cc" {
/**
* 获取或添加组件
* 如果节点上已有该组件则直接返回,否则添加新组件
* @param classConstructor 组件类构造函数
* @param path 组件路径(可选)
* @returns 组件实例
*/
getOrAddComponent<T extends Component>(
classConstructor: new () => T
): T;
getOrAddComponent<T extends Component>(classConstructor: new () => T, path?: string): T;
/**
* 获取或创建子节点
@@ -324,9 +273,7 @@ declare module "cc" {
/**
* 检查是否包含指定组件
*/
hasComponent<T extends Component>(
classConstructor: new () => T
): boolean;
hasComponent<T extends Component>(classConstructor: new () => T): boolean;
/**
* 递归查找子节点

View File

@@ -1,7 +1,6 @@
import { native, Asset, JsonAsset } from "cc";
import { native } from "cc";
import { HotupdateState } from "./HotupdateState";
import { sys } from "cc";
import { resources } from "cc";
import { HotupdateEventCallback, HotupdateEventData } from "./HotupdateEvent";
import HotupdateConfig from "./HotupdateConfig";
import { EDITOR, NATIVE } from "cc/env";
@@ -27,11 +26,11 @@ export class HotupdateInstance {
private _am: native.AssetsManager;
private versionCompareHandle = (versionA: string, versionB: string) => {
let vA = versionA.split(".");
let vB = versionB.split(".");
const vA = versionA.split(".");
const vB = versionB.split(".");
for (let i = 0; i < vA.length; ++i) {
let a = parseInt(vA[i]);
let b = parseInt(vB[i] || "0");
const a = parseInt(vA[i]);
const b = parseInt(vB[i] || "0");
if (a === b) {
continue;
} else {
@@ -100,7 +99,7 @@ export class HotupdateInstance {
}
public async checkUpdate(config: HotupdateConfig) {
let event: HotupdateEventData = {
const event: HotupdateEventData = {
state: HotupdateState.UPDATE_IDLE,
};
@@ -125,7 +124,7 @@ export class HotupdateInstance {
return;
}
if (!!this._am) {
if (this._am) {
this._am = null;
}
@@ -137,10 +136,10 @@ export class HotupdateInstance {
this.versionCompareHandle,
);
this._am.setVerifyCallback((path: string, asset: any) => {
let compressed = asset.compressed;
let expectedMD5 = asset.md5;
let relativePath = asset.path;
let size = asset.size;
const compressed = asset.compressed;
const expectedMD5 = asset.md5;
const relativePath = asset.path;
const size = asset.size;
if (compressed) {
return true;
} else {
@@ -170,7 +169,7 @@ export class HotupdateInstance {
this._am.update();
this._updating = true;
} else {
let event: HotupdateEventData = {
const event: HotupdateEventData = {
state: HotupdateState.UPDATE_FAILED,
};
this.emitEvent(event);
@@ -195,7 +194,7 @@ export class HotupdateInstance {
private checkCb(event: native.EventAssetsManager) {
console.log("checkCb", event.getEventCode(), event.getMessage());
let data: HotupdateEventData = {
const data: HotupdateEventData = {
state: HotupdateState.UPDATE_IDLE,
};
@@ -225,7 +224,7 @@ export class HotupdateInstance {
console.log("updateCb", event.getEventCode(), event.getMessage());
let finished = false;
let data: HotupdateEventData = {
const data: HotupdateEventData = {
state: HotupdateState.UPDATE_PROGRESSION,
};
@@ -247,8 +246,8 @@ export class HotupdateInstance {
finished = true;
if (this._setSearchPath) {
let searchPaths = native.fileUtils.getSearchPaths();
let newPaths = this._am.getLocalManifest().getSearchPaths();
const searchPaths = native.fileUtils.getSearchPaths();
const newPaths = this._am.getLocalManifest().getSearchPaths();
for (let i = 0; i < newPaths.length; i++) {
if (searchPaths.indexOf(newPaths[i]) === -1) {
// 使用 indexOf 替代 includes
@@ -280,7 +279,7 @@ export class HotupdateInstance {
}
private emitEvent(data: HotupdateEventData) {
if (!!this._hotupdateCallback) {
if (this._hotupdateCallback) {
this._hotupdateCallback(data);
}
}
@@ -342,18 +341,18 @@ export class HotupdateInstance {
private async loadRemoteManifest(config: HotupdateConfig) {
return new Promise<void>((resolve, _) => {
let url = config.packageUrl + "/project.manifest";
let xhr = new XMLHttpRequest();
const url = config.packageUrl + "/project.manifest";
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status > 0) {
try {
if (xhr.status >= 200 && xhr.status < 400) {
let manifestStr = JSON.parse(xhr.responseText);
const manifestStr = JSON.parse(xhr.responseText);
manifestStr.packageUrl = config.packageUrl;
manifestStr.remoteManifestUrl = config.packageUrl + "/project.manifest";
manifestStr.remoteVersionUrl = config.packageUrl + "/version.manifest";
let manifestPath =
const manifestPath =
native.fileUtils.getWritablePath() + config.storageDirPath + "/remote.manifest";
native.fileUtils.writeStringToFile(JSON.stringify(manifestStr), manifestPath);
console.log("已创建远程 manifest:", manifestPath, JSON.stringify(manifestStr));
@@ -380,8 +379,8 @@ export class HotupdateInstance {
private async _parseLocalManifest(localUrl: string, remoteUrl: string) {
if (native.fileUtils.isFileExist(localUrl)) {
let manifestStr = native.fileUtils.getStringFromFile(localUrl);
let manifest = JSON.parse(manifestStr);
const manifestStr = native.fileUtils.getStringFromFile(localUrl);
const manifest = JSON.parse(manifestStr);
manifest.packageUrl = remoteUrl;
manifest.remoteManifestUrl = remoteUrl + "/project.manifest";
manifest.remoteVersionUrl = remoteUrl + "/version.manifest";