ResourcePreloader.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { _decorator, Component, SpriteFrame, sp } from 'cc';
  2. import { BundleLoader } from './BundleLoader';
  3. import { LevelConfigManager } from '../LevelSystem/LevelConfigManager';
  4. import { SaveDataManager } from '../LevelSystem/SaveDataManager';
  5. import { ConfigManager } from './ConfigManager';
  6. import EventBus, { GameEvents } from './EventBus';
  7. const { ccclass } = _decorator;
  8. /**
  9. * 资源预加载管理器
  10. * 解决微信开发者平台动态加载资源不及时的问题
  11. * 在进入战斗前预加载当前关卡的背景图片和敌人资源
  12. */
  13. @ccclass('ResourcePreloader')
  14. export class ResourcePreloader {
  15. private static instance: ResourcePreloader = null;
  16. private bundleLoader: BundleLoader;
  17. private levelConfigManager: LevelConfigManager;
  18. private saveDataManager: SaveDataManager;
  19. private configManager: ConfigManager;
  20. // 预加载缓存
  21. private preloadedBackgrounds: Map<string, SpriteFrame> = new Map();
  22. private preloadedEnemyAnimations: Map<string, sp.SkeletonData> = new Map();
  23. // 预加载状态
  24. private isPreloading: boolean = false;
  25. private preloadProgress: number = 0;
  26. private preloadErrors: string[] = [];
  27. public static getInstance(): ResourcePreloader {
  28. if (!ResourcePreloader.instance) {
  29. ResourcePreloader.instance = new ResourcePreloader();
  30. }
  31. return ResourcePreloader.instance;
  32. }
  33. constructor() {
  34. this.bundleLoader = BundleLoader.getInstance();
  35. this.levelConfigManager = LevelConfigManager.getInstance();
  36. this.saveDataManager = SaveDataManager.getInstance();
  37. this.configManager = ConfigManager.getInstance();
  38. }
  39. /**
  40. * 预加载指定关卡的资源
  41. * @param levelId 关卡ID
  42. */
  43. public async preloadLevelResources(levelId: number): Promise<boolean> {
  44. try {
  45. console.log(`[ResourcePreloader] 开始预加载关卡 ${levelId} 的资源`);
  46. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_START);
  47. const levelConfig = await this.levelConfigManager.getLevelConfig(levelId);
  48. if (!levelConfig) {
  49. console.warn(`[ResourcePreloader] 找不到关卡 ${levelId} 的配置`);
  50. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_ERROR, { error: `找不到关卡 ${levelId} 的配置` });
  51. return false;
  52. }
  53. let totalResources = 0;
  54. let loadedResources = 0;
  55. // 计算总资源数
  56. if (levelConfig.backgroundImage) {
  57. totalResources++;
  58. }
  59. const enemyTypes = new Set<string>();
  60. if (levelConfig.waves) {
  61. levelConfig.waves.forEach(wave => {
  62. if (wave.enemies && Array.isArray(wave.enemies)) {
  63. wave.enemies.forEach(enemy => {
  64. if (enemy.enemyType) {
  65. enemyTypes.add(enemy.enemyType);
  66. }
  67. });
  68. }
  69. });
  70. }
  71. totalResources += enemyTypes.size;
  72. console.log(`[ResourcePreloader] 关卡 ${levelId} 需要预加载 ${totalResources} 个资源`);
  73. // 预加载背景图片
  74. if (levelConfig.backgroundImage) {
  75. try {
  76. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_PROGRESS, {
  77. progress: (loadedResources / totalResources) * 100,
  78. message: `正在加载背景图片: ${levelConfig.backgroundImage}`
  79. });
  80. await this.preloadBackground(levelConfig.backgroundImage);
  81. loadedResources++;
  82. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_PROGRESS, {
  83. progress: (loadedResources / totalResources) * 100,
  84. message: `背景图片加载完成: ${levelConfig.backgroundImage}`
  85. });
  86. console.log(`[ResourcePreloader] 背景图片预加载完成: ${levelConfig.backgroundImage}`);
  87. } catch (error) {
  88. console.warn(`[ResourcePreloader] 背景图片预加载失败: ${levelConfig.backgroundImage}`, error);
  89. }
  90. }
  91. // 预加载敌人动画
  92. for (const enemyType of enemyTypes) {
  93. try {
  94. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_PROGRESS, {
  95. progress: (loadedResources / totalResources) * 100,
  96. message: `正在加载敌人动画: ${enemyType}`
  97. });
  98. await this.preloadEnemyAnimation(enemyType);
  99. loadedResources++;
  100. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_PROGRESS, {
  101. progress: (loadedResources / totalResources) * 100,
  102. message: `敌人动画加载完成: ${enemyType}`
  103. });
  104. console.log(`[ResourcePreloader] 敌人动画预加载完成: ${enemyType}`);
  105. } catch (error) {
  106. console.warn(`[ResourcePreloader] 敌人动画预加载失败: ${enemyType}`, error);
  107. }
  108. }
  109. console.log(`[ResourcePreloader] 关卡 ${levelId} 资源预加载完成,成功加载 ${loadedResources}/${totalResources} 个资源`);
  110. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_COMPLETE);
  111. return true;
  112. } catch (error) {
  113. console.error(`[ResourcePreloader] 关卡 ${levelId} 资源预加载失败:`, error);
  114. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_ERROR, { error: error.toString() });
  115. return false;
  116. }
  117. }
  118. /**
  119. * 预加载当前关卡的所有资源
  120. */
  121. public async preloadCurrentLevelResources(): Promise<void> {
  122. const currentLevel = this.saveDataManager.getCurrentLevel();
  123. const success = await this.preloadLevelResources(currentLevel);
  124. if (!success) {
  125. throw new Error('资源预加载失败');
  126. }
  127. }
  128. /**
  129. * 预加载背景图片
  130. * @param backgroundPath 背景图片路径
  131. */
  132. private async preloadBackground(backgroundPath: string): Promise<void> {
  133. // 如果已经预加载过,直接返回
  134. if (this.preloadedBackgrounds.has(backgroundPath)) {
  135. return;
  136. }
  137. // 转换路径格式:从 "images/LevelBackground/BG1" 转换为 "LevelBackground/BG1"
  138. const bundlePath = backgroundPath.replace('images/', '');
  139. const spriteFramePath = `${bundlePath}/spriteFrame`;
  140. try {
  141. const spriteFrame = await this.bundleLoader.loadSpriteFrame(spriteFramePath);
  142. this.preloadedBackgrounds.set(backgroundPath, spriteFrame);
  143. console.log(`[ResourcePreloader] 背景图片缓存成功: ${backgroundPath}`);
  144. } catch (error) {
  145. console.error(`[ResourcePreloader] 背景图片预加载失败: ${backgroundPath}`, error);
  146. throw error;
  147. }
  148. }
  149. /**
  150. * 预加载敌人动画资源
  151. * @param enemyType 敌人类型
  152. */
  153. private async preloadEnemyAnimation(enemyType: string): Promise<void> {
  154. // 如果已经预加载过,直接返回
  155. if (this.preloadedEnemyAnimations.has(enemyType)) {
  156. return;
  157. }
  158. try {
  159. // 获取敌人配置
  160. const enemiesConfig = this.configManager.getAllEnemies();
  161. if (!enemiesConfig || enemiesConfig.length === 0) {
  162. throw new Error('敌人配置未加载');
  163. }
  164. const enemyConfig = enemiesConfig.find((enemy: any) => enemy.id === enemyType);
  165. if (!enemyConfig) {
  166. throw new Error(`未找到敌人类型: ${enemyType}`);
  167. }
  168. // 获取敌人动画路径
  169. let spinePath = enemyConfig.visualConfig?.spritePath || enemyConfig.visual?.sprite_path;
  170. if (!spinePath) {
  171. throw new Error(`敌人 ${enemyType} 没有配置动画路径`);
  172. }
  173. // 路径转换逻辑(与EnemyController保持一致)
  174. if (spinePath.startsWith('@EnemyAni')) {
  175. spinePath = spinePath.replace('@EnemyAni', 'Animation/EnemyAni');
  176. }
  177. if (spinePath.startsWith('@')) {
  178. spinePath = spinePath.substring(1);
  179. }
  180. // 对于Animation/EnemyAni路径,需要添加子目录和文件名
  181. if (spinePath.startsWith('Animation/EnemyAni/')) {
  182. const parts = spinePath.split('/');
  183. if (parts.length === 3) {
  184. const animId = parts[2];
  185. spinePath = `${spinePath}/${animId}`;
  186. }
  187. }
  188. // 移除Animation/前缀,因为BundleLoader会从Bundle中加载
  189. if (spinePath.startsWith('Animation/')) {
  190. spinePath = spinePath.replace('Animation/', '');
  191. }
  192. // 预加载骨骼动画数据
  193. const skeletonData = await BundleLoader.loadSkeletonData(spinePath);
  194. this.preloadedEnemyAnimations.set(enemyType, skeletonData);
  195. console.log(`[ResourcePreloader] 敌人动画缓存成功: ${enemyType} -> ${spinePath}`);
  196. } catch (error) {
  197. console.error(`[ResourcePreloader] 敌人动画预加载失败: ${enemyType}`, error);
  198. throw error;
  199. }
  200. }
  201. /**
  202. * 计算需要预加载的资源总数
  203. * @param levelConfig 关卡配置
  204. * @returns 资源总数
  205. */
  206. private calculateTotalResources(levelConfig: any): number {
  207. let total = 0;
  208. // 背景图片
  209. if (levelConfig.backgroundImage) {
  210. total += 1;
  211. }
  212. // 敌人动画
  213. const enemyTypes = this.extractEnemyTypes(levelConfig);
  214. total += enemyTypes.length;
  215. return total;
  216. }
  217. /**
  218. * 从关卡配置中提取所有敌人类型
  219. * @param levelConfig 关卡配置
  220. * @returns 敌人类型数组
  221. */
  222. private extractEnemyTypes(levelConfig: any): string[] {
  223. const enemyTypes = new Set<string>();
  224. if (levelConfig.waves && Array.isArray(levelConfig.waves)) {
  225. for (const wave of levelConfig.waves) {
  226. if (wave.enemies && Array.isArray(wave.enemies)) {
  227. for (const enemy of wave.enemies) {
  228. if (enemy.enemyType) {
  229. enemyTypes.add(enemy.enemyType);
  230. }
  231. }
  232. }
  233. }
  234. }
  235. return Array.from(enemyTypes);
  236. }
  237. /**
  238. * 更新预加载进度
  239. * @param loaded 已加载数量
  240. * @param total 总数量
  241. */
  242. private updateProgress(loaded: number, total: number): void {
  243. this.preloadProgress = total > 0 ? (loaded / total) * 100 : 100;
  244. // 发送进度更新事件
  245. EventBus.getInstance().emit(GameEvents.RESOURCE_PRELOAD_PROGRESS, {
  246. progress: this.preloadProgress,
  247. loaded: loaded,
  248. total: total
  249. });
  250. }
  251. /**
  252. * 获取预加载的背景图片
  253. * @param backgroundPath 背景图片路径
  254. * @returns SpriteFrame或null
  255. */
  256. public getPreloadedBackground(backgroundPath: string): SpriteFrame | null {
  257. return this.preloadedBackgrounds.get(backgroundPath) || null;
  258. }
  259. /**
  260. * 获取预加载的敌人动画
  261. * @param enemyType 敌人类型
  262. * @returns SkeletonData或null
  263. */
  264. public getPreloadedEnemyAnimation(enemyType: string): sp.SkeletonData | null {
  265. return this.preloadedEnemyAnimations.get(enemyType) || null;
  266. }
  267. /**
  268. * 清理预加载缓存
  269. */
  270. public clearCache(): void {
  271. this.preloadedBackgrounds.clear();
  272. this.preloadedEnemyAnimations.clear();
  273. console.log('[ResourcePreloader] 预加载缓存已清理');
  274. }
  275. /**
  276. * 获取预加载状态
  277. */
  278. public getPreloadStatus(): {
  279. isPreloading: boolean;
  280. progress: number;
  281. errors: string[];
  282. } {
  283. return {
  284. isPreloading: this.isPreloading,
  285. progress: this.preloadProgress,
  286. errors: [...this.preloadErrors]
  287. };
  288. }
  289. }