Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战
Day 07 · 游戏也要管理状态场景切换·资源加载·对象池实战学习目标掌握场景管理、动态资源加载AssetBundle、预制体和对象池优化预计时间3 小时难度⭐⭐⭐⭐☆为什么需要资源管理随着游戏规模扩大你会遇到场景切换时界面卡顿资源未释放频繁创建/销毁节点导致 GC 卡顿移动端内存不足崩溃本章的三大武器场景管理 动态加载 对象池能解决这些问题。1. 场景管理director1.1 场景切换import{_decorator,Component,director,Director}fromcc;const{ccclass}_decorator;ccclass(SceneManager)exportclassSceneManagerextendsComponent{// 场景名称需要在构建设置中添加才能打包staticSCENE_MENUMenu;staticSCENE_GAMEGame;staticSCENE_RESULTResult;// 加载场景默认加载完后立即切换旧场景销毁staticloadGame(){director.loadScene(SceneManager.SCENE_GAME);}// 带回调的场景切换staticloadGameWithCallback(onProgress?:(completedCount:number,totalCount:number)void){director.loadScene(SceneManager.SCENE_GAME,(err){if(err){console.error(场景加载失败:,err);return;}console.log(场景加载成功);});}// 预加载场景提前加载到内存切换时无等待staticpreloadScene(sceneName:string){director.preloadScene(sceneName,(completedCount,totalCount){constprogress(completedCount/totalCount*100).toFixed(0);console.log(预加载进度:${progress}%);},(err){if(err)console.error(预加载失败:,err);elseconsole.log(预加载完成:,sceneName);});}}1.2 场景常驻节点默认情况下切换场景时旧场景的所有节点都会被销毁。如果某些节点需要跨场景保留如音乐管理器、玩家数据import{_decorator,Component,director,game,Node}fromcc;const{ccclass}_decorator;ccclass(PersistNode)exportclassPersistNodeextendsComponent{onLoad(){// 将此节点标记为常驻节点不被场景切换销毁game.addPersistRootNode(this.node);}// 如需在某个场景中移除常驻removePersist(){game.removePersistRootNode(this.node);}}1.3 场景生命周期与数据传递// 场景间传递数据通过单例管理器exportclassGameData{privatestatic_instance:GameData|nullnull;staticgetinstance():GameData{if(!GameData._instance){GameData._instancenewGameData();}returnGameData._instance;}// 游戏数据score:number0;level:number1;playerName:string;reset(){this.score0;this.level1;}}// 在游戏场景中GameData.instance.score1500;director.loadScene(Result);// 在结算场景中constfinalScoreGameData.instance.score;// 获取上一场景的得分2. 动态资源加载2.1 Resources 文件夹加载将资源放在assets/resources/文件夹下可以动态加载import{_decorator,Component,resources,SpriteFrame,AudioClip,Prefab,JsonAsset,instantiate}fromcc;const{ccclass}_decorator;ccclass(DynamicLoader)exportclassDynamicLoaderextendsComponent{start(){this.loadSprite();this.loadAudio();this.loadPrefab();this.loadJSON();}// 加载图片SpriteFrameloadSprite(){resources.load(textures/hero/hero-idle/spriteFrame,SpriteFrame,(err,spriteFrame){if(err){console.error(err);return;}// 使用 spriteFrame// this.sprite.spriteFrame spriteFrame;});}// 加载音频loadAudio(){resources.load(audio/bgm,AudioClip,(err,clip){if(err){console.error(err);return;}constaudioSourcethis.getComponent(AudioSource)asany;audioSource.clipclip;audioSource.play();});}// 加载预制体loadPrefab(){resources.load(prefabs/Enemy,Prefab,(err,prefab){if(err){console.error(err);return;}constenemyinstantiate(prefab);this.node.addChild(enemy);});}// 加载 JSON 配置文件loadJSON(){resources.load(data/level-config,JsonAsset,(err,asset){if(err){console.error(err);return;}constconfigasset.jsonas{levels:any[]};console.log(关卡数量:,config.levels.length);});}// 批量加载加载整个文件夹loadAllFrames(){resources.loadDir(textures/hero,SpriteFrame,(err,frames){if(err){console.error(err);return;}console.log(加载了,frames.length,帧);});}// 释放资源不再使用时必须释放releaseResources(path:string){resources.release(path);// 或者按资源引用释放// resources.release(myAsset);}}2.2 AssetBundle分包加载AssetBundle 允许将资源分包按需下载import{_decorator,Component,assetManager,AssetManager,Prefab,instantiate}fromcc;const{ccclass}_decorator;ccclass(BundleLoader)exportclassBundleLoaderextendsComponent{start(){this.loadLevel2Bundle();}loadLevel2Bundle(){// 加载 Bundle名称是在 Inspector 中配置的 Bundle 名assetManager.loadBundle(level2,(err,bundle){if(err){console.error(Bundle 加载失败:,err);return;}console.log(Level2 Bundle 加载成功);this.loadEnemyFromBundle(bundle);});}loadEnemyFromBundle(bundle:AssetManager.Bundle){bundle.load(prefabs/BossEnemy,Prefab,(err,prefab){if(err){console.error(err);return;}constbossinstantiate(prefab);this.node.addChild(boss);});}// 释放整个 BundlereleaseBundle(){assetManager.removeBundle(assetManager.getBundle(level2)!);}}3. 预制体Prefab预制体是可复用的节点模板是 Cocos 游戏开发最核心的工作单元。3.1 创建预制体在场景中搭建好节点结构例如Enemy 节点 Sprite 碰撞体 EnemyController 脚本将节点从层级管理器拖拽到资源管理器中 → 自动生成.prefab文件原节点变为预制体实例蓝色标识3.2 代码实例化预制体import{_decorator,Component,Prefab,Node,instantiate,Vec3}fromcc;const{ccclass,property}_decorator;ccclass(EnemySpawner)exportclassEnemySpawnerextendsComponent{property(Prefab)enemyPrefab:Prefabnull!;property(Node)spawnParent:Nodenull!;// 敌人的父节点便于统一管理spawnEnemy(x:number,y:number){constenemyinstantiate(this.enemyPrefab);this.spawnParent.addChild(enemy);enemy.setPosition(x,y,0);returnenemy;}spawnWave(count:number,spacing:number){for(leti0;icount;i){constx(i-count/2)*spacing;this.scheduleOnce((){this.spawnEnemy(x,400);},i*0.3);// 每隔0.3秒生成一个}}}4. 对象池NodePool频繁创建/销毁节点会导致GC垃圾回收卡顿在弹幕游戏、消消乐等需要大量临时节点的场景中必须使用对象池。import{_decorator,Component,Prefab,Node,instantiate,NodePool}fromcc;const{ccclass,property}_decorator;ccclass(BulletPool)exportclassBulletPoolextendsComponent{property(Prefab)bulletPrefab:Prefabnull!;// 子弹对象池private_pool:NodePoolnewNodePool();// 预热提前创建一批对象放入池中start(){this.preheat(20);// 预先创建20个子弹}preheat(count:number){for(leti0;icount;i){constbulletinstantiate(this.bulletPrefab);this._pool.put(bullet);// 放入池中会自动禁用节点}console.log(对象池预热完成池中有${this._pool.size()}个对象);}// 从池中获取子弹复用 新建getBullet(x:number,y:number):Node{letbullet:Node;if(this._pool.size()0){bulletthis._pool.get()!;// 从池中取出会自动激活节点}else{// 池中没有可用对象时新建一个bulletinstantiate(this.bulletPrefab);}this.node.addChild(bullet);bullet.setPosition(x,y,0);returnbullet;}// 回收子弹到池中recycleBullet(bullet:Node){this._pool.put(bullet);// 会自动从父节点移除并禁用}// 清空池场景切换时调用onDestroy(){this._pool.clear();}}4.1 对象池配合 IPoolManager 接口import{_decorator,Component,NodePool,IPoolManager}fromcc;const{ccclass,property}_decorator;// 子弹组件实现 IPoolManager 接口ccclass(Bullet)exportclassBulletextendsComponentimplementsIPoolManager{private_speed:number600;private_pool:NodePoolnull!;// 被放入池中时调用做清理工作unuse(){// 重置状态this._speed600;this.node.setPosition(0,0,0);// 取消所有 tweenthis.node.stopAllActions();}// 从池中取出时调用做初始化工作reuse(...args:any[]){const[speed]args;if(speed)this._speedspeed;}setPool(pool:NodePool){this._poolpool;}update(deltaTime:number){// 向上移动this.node.setPosition(this.node.position.x,this.node.position.ythis._speed*deltaTime,0);// 超出屏幕时回收if(this.node.position.y700){this._pool.put(this.node);}}}5. 加载界面实现import{_decorator,Component,ProgressBar,Label,director}fromcc;const{ccclass,property}_decorator;ccclass(LoadingScene)exportclassLoadingSceneextendsComponent{property(ProgressBar)progressBar:ProgressBarnull!;property(Label)progressLabel:Labelnull!;property({displayName:目标场景名})targetScene:stringGame;start(){this.loadTargetScene();}loadTargetScene(){director.preloadScene(this.targetScene,(completedCount,totalCount){constprogresscompletedCount/totalCount;this.progressBar.progressprogress;this.progressLabel.string${Math.floor(progress*100)}%;},(err){if(err){console.error(加载失败:,err);return;}// 加载完成后延迟 0.5 秒再切换给用户看清进度this.scheduleOnce((){director.loadScene(this.targetScene);},0.5);});}}6. 今日总结✅ 掌握场景切换、预加载和常驻节点✅ 掌握 resources 和 AssetBundle 动态加载✅ 掌握预制体的创建和实例化✅ 掌握对象池NodePool的完整使用流程✅ 实战加载场景与进度显示⚠️ 常见坑问题原因解决方案场景切换后内存暴涨动态加载的资源未释放场景销毁时调用resources.release()对象池节点位置错乱取出后未重置位置在reuse()中重置所有状态预制体修改后不生效场景中有旧的实例在层级管理器中右键预制体实例 → “还原到预制体”find() 跨场景找不到节点常驻节点不在当前场景中用单例管理器存引用而非 find()← Day 06 | 系列目录 | Day 08 →