Files
mp-xieyingeng/assets/scripts/utils/LevelDataManager.ts
richarjiang b05ef71368 perf: 优化关卡图片加载策略为按需加载
- 初始化时只预加载第一关图片,大幅减少启动时间
- 进入关卡后自动预加载下一关图片(静默加载)
- 新增 ensureLevelReady 和 preloadNextLevel 方法支持按需加载
- 使用 Map 存储关卡配置,Set 跟踪加载中状态避免重复加载
- 提取 _createRuntimeConfig 方法减少代码重复
2026-03-16 20:54:26 +08:00

288 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
import { HttpUtil } from './HttpUtil';
import { ApiLevelData, ApiResponse, RuntimeLevelConfig } from '../types/LevelTypes';
/**
* 进度回调类型
* @param progress 进度值 0-1
* @param message 进度消息
*/
export type ProgressCallback = (progress: number, message: string) => void;
/**
* 关卡数据管理器
* 单例模式,负责从 API 获取关卡数据并按需加载图片
*/
export class LevelDataManager {
private static _instance: LevelDataManager | null = null;
/** API 地址 */
private readonly API_URL = 'https://ilookai.cn/api/v1/wechat-game/levels';
/** 请求超时时间(毫秒) */
private readonly REQUEST_TIMEOUT = 8000;
/** API 返回的原始关卡数据 */
private _apiData: ApiLevelData[] = [];
/** 运行时关卡配置缓存(按需填充) */
private _levelConfigs: Map<number, RuntimeLevelConfig> = new Map();
/** 是否已成功从 API 获取数据 */
private _hasApiData: boolean = false;
/** 图片缓存URL -> SpriteFrame */
private _imageCache: Map<string, SpriteFrame> = new Map();
/** 正在加载中的关卡索引集合 */
private _loadingLevels: Set<number> = new Set();
/**
* 获取单例实例
*/
static get instance(): LevelDataManager {
if (!this._instance) {
this._instance = new LevelDataManager();
}
return this._instance;
}
/**
* 私有构造函数
*/
private constructor() {}
/**
* 初始化:从 API 获取数据并预加载第一关图片
* @param onProgress 进度回调
* @returns 是否初始化成功
*/
async initialize(onProgress?: ProgressCallback): Promise<boolean> {
console.log('[LevelDataManager] 开始初始化');
try {
// 阶段1: 获取 API 数据 (0-30%)
onProgress?.(0, '正在获取关卡数据...');
const apiData = await this._fetchApiData();
if (!apiData || apiData.length === 0) {
console.warn('[LevelDataManager] API 返回空数据');
onProgress?.(0.3, 'API 数据为空,使用本地配置');
return false;
}
console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`);
this._apiData = apiData;
this._hasApiData = true;
onProgress?.(0.3, `获取到 ${apiData.length} 个关卡`);
// 阶段2: 只预加载第一关图片 (30-80%)
const firstLevel = apiData[0];
onProgress?.(0.3, '正在加载第一关资源...');
const spriteFrame = await this._loadImage(firstLevel.imageUrl);
if (spriteFrame) {
this._levelConfigs.set(0, this._createRuntimeConfig(firstLevel, spriteFrame));
}
console.log('[LevelDataManager] 初始化完成,第一关资源已加载');
onProgress?.(0.8, '第一关资源加载完成');
return true;
} catch (error) {
console.error('[LevelDataManager] 初始化失败:', error);
onProgress?.(0.3, '获取数据失败,使用本地配置');
return false;
}
}
/**
* 获取指定关卡配置
* @param index 关卡索引
*/
getLevelConfig(index: number): RuntimeLevelConfig | null {
return this._levelConfigs.get(index) ?? null;
}
/**
* 获取关卡总数
*/
getLevelCount(): number {
return this._apiData.length;
}
/**
* 检查是否有 API 数据
*/
hasApiData(): boolean {
return this._hasApiData && this._apiData.length > 0;
}
/**
* 检查指定关卡图片是否已加载
* @param index 关卡索引
*/
isLevelImageLoaded(index: number): boolean {
return this._levelConfigs.has(index);
}
/**
* 确保指定关卡资源已准备好
* 如果资源未加载,会立即加载
* @param index 关卡索引
* @returns 加载的关卡配置,失败返回 null
*/
async ensureLevelReady(index: number): Promise<RuntimeLevelConfig | null> {
// 检查索引有效性
if (index < 0 || index >= this._apiData.length) {
console.warn(`[LevelDataManager] 关卡索引无效: ${index}`);
return null;
}
// 检查缓存
const cached = this._levelConfigs.get(index);
if (cached) {
return cached;
}
// 检查是否正在加载
if (this._loadingLevels.has(index)) {
console.log(`[LevelDataManager] 关卡 ${index} 正在加载中...`);
return null;
}
// 开始加载
this._loadingLevels.add(index);
console.log(`[LevelDataManager] 开始加载关卡 ${index} 资源...`);
const data = this._apiData[index];
const spriteFrame = await this._loadImage(data.imageUrl);
this._loadingLevels.delete(index);
if (!spriteFrame) {
console.error(`[LevelDataManager] 加载关卡 ${index} 图片失败`);
return null;
}
const config = this._createRuntimeConfig(data, spriteFrame);
this._levelConfigs.set(index, config);
console.log(`[LevelDataManager] 关卡 ${index} 资源加载完成`);
return config;
}
/**
* 预加载下一关图片(静默加载,不阻塞)
* 在进入当前关卡后调用,提前加载下一关资源
* @param currentIndex 当前关卡索引
*/
preloadNextLevel(currentIndex: number): void {
const nextIndex = currentIndex + 1;
// 检查是否有下一关
if (nextIndex >= this._apiData.length) {
console.log('[LevelDataManager] 没有下一关了');
return;
}
// 检查是否已加载
if (this._levelConfigs.has(nextIndex)) {
console.log(`[LevelDataManager] 下一关 ${nextIndex} 已加载`);
return;
}
// 检查是否正在加载
if (this._loadingLevels.has(nextIndex)) {
console.log(`[LevelDataManager] 下一关 ${nextIndex} 正在加载中`);
return;
}
// 异步加载,不等待
console.log(`[LevelDataManager] 开始预加载下一关 ${nextIndex}...`);
this.ensureLevelReady(nextIndex).catch(err => {
console.error(`[LevelDataManager] 预加载下一关失败:`, err);
});
}
/**
* 从 API 获取关卡数据
*/
private async _fetchApiData(): Promise<ApiLevelData[] | null> {
try {
const response = await HttpUtil.get<ApiResponse>(this.API_URL, this.REQUEST_TIMEOUT);
if (!response.success) {
console.warn(`[LevelDataManager] API 返回失败, 消息: ${response.message}`);
return null;
}
return response.data.levels;
} catch (error) {
console.error('[LevelDataManager] API 请求失败:', error);
return null;
}
}
/**
* 创建运行时关卡配置
* @param data API 关卡数据
* @param spriteFrame 已加载的精灵帧
*/
private _createRuntimeConfig(data: ApiLevelData, spriteFrame: SpriteFrame | null): RuntimeLevelConfig {
return {
id: data.id,
name: `${data.level}`,
spriteFrame: spriteFrame,
clue1: data.hint1,
clue2: data.hint2,
clue3: data.hint3,
answer: data.answer
};
}
/**
* 加载远程图片为 SpriteFrame
* @param url 图片 URL
*/
private async _loadImage(url: string): Promise<SpriteFrame | null> {
// 检查缓存
const cached = this._imageCache.get(url);
if (cached) {
return cached;
}
return new Promise((resolve) => {
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
if (err) {
console.error(`[LevelDataManager] 加载图片失败: ${url}`, err);
resolve(null);
return;
}
const texture = new Texture2D();
texture.image = imageAsset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
// 缓存
this._imageCache.set(url, spriteFrame);
resolve(spriteFrame);
});
});
}
/**
* 清除缓存
*/
clearCache(): void {
this._apiData = [];
this._levelConfigs.clear();
this._loadingLevels.clear();
this._hasApiData = false;
this._imageCache.clear();
console.log('[LevelDataManager] 缓存已清除');
}
}