perf: 优化关卡图片加载策略为按需加载

- 初始化时只预加载第一关图片,大幅减少启动时间
- 进入关卡后自动预加载下一关图片(静默加载)
- 新增 ensureLevelReady 和 preloadNextLevel 方法支持按需加载
- 使用 Map 存储关卡配置,Set 跟踪加载中状态避免重复加载
- 提取 _createRuntimeConfig 方法减少代码重复
This commit is contained in:
richarjiang
2026-03-16 20:54:26 +08:00
parent f99bc12f52
commit b05ef71368
2 changed files with 137 additions and 54 deletions

View File

@@ -172,6 +172,9 @@ export class PageLevel extends BaseView {
// 更新倒计时显示 // 更新倒计时显示
this.updateClockLabel(); this.updateClockLabel();
// 预加载下一关图片(静默加载,不阻塞)
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex);
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`); console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`);
} }

View File

@@ -11,7 +11,7 @@ export type ProgressCallback = (progress: number, message: string) => void;
/** /**
* 关卡数据管理器 * 关卡数据管理器
* 单例模式,负责从 API 获取关卡数据并加载图片 * 单例模式,负责从 API 获取关卡数据并按需加载图片
*/ */
export class LevelDataManager { export class LevelDataManager {
private static _instance: LevelDataManager | null = null; private static _instance: LevelDataManager | null = null;
@@ -22,8 +22,11 @@ export class LevelDataManager {
/** 请求超时时间(毫秒) */ /** 请求超时时间(毫秒) */
private readonly REQUEST_TIMEOUT = 8000; private readonly REQUEST_TIMEOUT = 8000;
/** 运行时关卡配置缓存 */ /** API 返回的原始关卡数据 */
private _levelConfigs: RuntimeLevelConfig[] = []; private _apiData: ApiLevelData[] = [];
/** 运行时关卡配置缓存(按需填充) */
private _levelConfigs: Map<number, RuntimeLevelConfig> = new Map();
/** 是否已成功从 API 获取数据 */ /** 是否已成功从 API 获取数据 */
private _hasApiData: boolean = false; private _hasApiData: boolean = false;
@@ -31,6 +34,9 @@ export class LevelDataManager {
/** 图片缓存URL -> SpriteFrame */ /** 图片缓存URL -> SpriteFrame */
private _imageCache: Map<string, SpriteFrame> = new Map(); private _imageCache: Map<string, SpriteFrame> = new Map();
/** 正在加载中的关卡索引集合 */
private _loadingLevels: Set<number> = new Set();
/** /**
* 获取单例实例 * 获取单例实例
*/ */
@@ -47,7 +53,7 @@ export class LevelDataManager {
private constructor() {} private constructor() {}
/** /**
* 初始化:从 API 获取数据并预加载图片 * 初始化:从 API 获取数据并预加载第一关图片
* @param onProgress 进度回调 * @param onProgress 进度回调
* @returns 是否初始化成功 * @returns 是否初始化成功
*/ */
@@ -55,34 +61,37 @@ export class LevelDataManager {
console.log('[LevelDataManager] 开始初始化'); console.log('[LevelDataManager] 开始初始化');
try { try {
// 阶段1: 获取 API 数据 (0-20%) // 阶段1: 获取 API 数据 (0-30%)
onProgress?.(0, '正在获取关卡数据...'); onProgress?.(0, '正在获取关卡数据...');
const apiData = await this._fetchApiData(); const apiData = await this._fetchApiData();
if (!apiData || apiData.length === 0) { if (!apiData || apiData.length === 0) {
console.warn('[LevelDataManager] API 返回空数据'); console.warn('[LevelDataManager] API 返回空数据');
onProgress?.(0.2, 'API 数据为空,使用本地配置'); onProgress?.(0.3, 'API 数据为空,使用本地配置');
return false; return false;
} }
console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`); console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`);
onProgress?.(0.2, `获取到 ${apiData.length} 个关卡`); this._apiData = apiData;
// 阶段2: 预加载所有图片 (20-80%)
const configs = await this._preloadImages(apiData, (progress) => {
onProgress?.(0.2 + progress * 0.6, '正在加载关卡资源...');
});
this._levelConfigs = configs;
this._hasApiData = true; this._hasApiData = true;
onProgress?.(0.3, `获取到 ${apiData.length} 个关卡`);
console.log('[LevelDataManager] 初始化完成'); // 阶段2: 只预加载第一关图片 (30-80%)
onProgress?.(0.8, '关卡资源加载完成'); 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; return true;
} catch (error) { } catch (error) {
console.error('[LevelDataManager] 初始化失败:', error); console.error('[LevelDataManager] 初始化失败:', error);
onProgress?.(0.2, '获取数据失败,使用本地配置'); onProgress?.(0.3, '获取数据失败,使用本地配置');
return false; return false;
} }
} }
@@ -92,24 +101,107 @@ export class LevelDataManager {
* @param index 关卡索引 * @param index 关卡索引
*/ */
getLevelConfig(index: number): RuntimeLevelConfig | null { getLevelConfig(index: number): RuntimeLevelConfig | null {
if (index < 0 || index >= this._levelConfigs.length) { return this._levelConfigs.get(index) ?? null;
return null;
}
return this._levelConfigs[index];
} }
/** /**
* 获取关卡总数 * 获取关卡总数
*/ */
getLevelCount(): number { getLevelCount(): number {
return this._levelConfigs.length; return this._apiData.length;
} }
/** /**
* 检查是否有 API 数据 * 检查是否有 API 数据
*/ */
hasApiData(): boolean { hasApiData(): boolean {
return this._hasApiData && this._levelConfigs.length > 0; 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);
});
} }
/** /**
@@ -132,35 +224,20 @@ export class LevelDataManager {
} }
/** /**
* 预加载所有图片 * 创建运行时关卡配置
* @param apiData API 返回的关卡数据 * @param data API 关卡数据
* @param onProgress 进度回调 * @param spriteFrame 已加载的精灵帧
*/ */
private async _preloadImages( private _createRuntimeConfig(data: ApiLevelData, spriteFrame: SpriteFrame | null): RuntimeLevelConfig {
apiData: ApiLevelData[], return {
onProgress?: (progress: number) => void id: data.id,
): Promise<RuntimeLevelConfig[]> { name: `${data.level}`,
const configs: RuntimeLevelConfig[] = []; spriteFrame: spriteFrame,
const total = apiData.length; clue1: data.hint1,
clue2: data.hint2,
for (let i = 0; i < total; i++) { clue3: data.hint3,
const data = apiData[i]; answer: data.answer
const spriteFrame = await this._loadImage(data.imageUrl); };
configs.push({
id: data.id,
name: `${data.level}`,
spriteFrame: spriteFrame,
clue1: data.hint1,
clue2: data.hint2,
clue3: data.hint3,
answer: data.answer
});
onProgress?.((i + 1) / total);
}
return configs;
} }
/** /**
@@ -169,8 +246,9 @@ export class LevelDataManager {
*/ */
private async _loadImage(url: string): Promise<SpriteFrame | null> { private async _loadImage(url: string): Promise<SpriteFrame | null> {
// 检查缓存 // 检查缓存
if (this._imageCache.has(url)) { const cached = this._imageCache.get(url);
return this._imageCache.get(url)!; if (cached) {
return cached;
} }
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -199,7 +277,9 @@ export class LevelDataManager {
* 清除缓存 * 清除缓存
*/ */
clearCache(): void { clearCache(): void {
this._levelConfigs = []; this._apiData = [];
this._levelConfigs.clear();
this._loadingLevels.clear();
this._hasApiData = false; this._hasApiData = false;
this._imageCache.clear(); this._imageCache.clear();
console.log('[LevelDataManager] 缓存已清除'); console.log('[LevelDataManager] 缓存已清除');