fix: 更新提交
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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格式不正确");
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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": {}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
120
extensions/max-studio/assets/max-studio/core/ui/NetworkMask.ts
Normal file
120
extensions/max-studio/assets/max-studio/core/ui/NetworkMask.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "7969055e-3f6c-4f72-81cb-b30ac9a544a6",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "23692c6a-0fd9-4f54-8507-835c598c48a2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
8
extensions/max-studio/assets/max-studio/core/ui/index.ts
Normal file
8
extensions/max-studio/assets/max-studio/core/ui/index.ts
Normal 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";
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5e9b4e13-31ec-4d07-8d53-d5e882ddede8",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 递归查找子节点
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user