当前位置: 首页 > news >正文

Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战

Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战

学习目标:掌握场景管理、动态资源加载(AssetBundle)、预制体和对象池优化

预计时间:3 小时

难度:⭐⭐⭐⭐☆


为什么需要资源管理?

随着游戏规模扩大,你会遇到:

  • 场景切换时界面卡顿(资源未释放)
  • 频繁创建/销毁节点导致 GC 卡顿
  • 移动端内存不足崩溃

本章的三大武器:场景管理 + 动态加载 + 对象池,能解决这些问题。


1. 场景管理(director)

1.1 场景切换

import{_decorator,Component,director,Director}from'cc';const{ccclass}=_decorator;@ccclass('SceneManager')exportclassSceneManagerextendsComponent{// 场景名称需要在构建设置中添加才能打包staticSCENE_MENU='Menu';staticSCENE_GAME='Game';staticSCENE_RESULT='Result';// 加载场景(默认:加载完后立即切换,旧场景销毁)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}from'cc';const{ccclass}=_decorator;@ccclass('PersistNode')exportclassPersistNodeextendsComponent{onLoad(){// 将此节点标记为常驻节点(不被场景切换销毁)game.addPersistRootNode(this.node);}// 如需在某个场景中移除常驻removePersist(){game.removePersistRootNode(this.node);}}


1.3 场景生命周期与数据传递

// 场景间传递数据(通过单例管理器)exportclassGameData{privatestatic_instance:GameData|null=null;staticgetinstance():GameData{if(!GameData._instance){GameData._instance=newGameData();}returnGameData._instance;}// 游戏数据score:number=0;level:number=1;playerName:string='';reset(){this.score=0;this.level=1;}}// 在游戏场景中GameData.instance.score=1500;director.loadScene('Result');// 在结算场景中constfinalScore=GameData.instance.score;// 获取上一场景的得分

2. 动态资源加载

2.1 Resources 文件夹加载

将资源放在assets/resources/文件夹下,可以动态加载:

import{_decorator,Component,resources,SpriteFrame,AudioClip,Prefab,JsonAsset,instantiate}from'cc';const{ccclass}=_decorator;@ccclass('DynamicLoader')exportclassDynamicLoaderextendsComponent{start(){this.loadSprite();this.loadAudio();this.loadPrefab();this.loadJSON();}// 加载图片(SpriteFrame)loadSprite(){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;}constaudioSource=this.getComponent('AudioSource')asany;audioSource.clip=clip;audioSource.play();});}// 加载预制体loadPrefab(){resources.load('prefabs/Enemy',Prefab,(err,prefab)=>{if(err){console.error(err);return;}constenemy=instantiate(prefab);this.node.addChild(enemy);});}// 加载 JSON 配置文件loadJSON(){resources.load('data/level-config',JsonAsset,(err,asset)=>{if(err){console.error(err);return;}constconfig=asset.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}from'cc';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;}constboss=instantiate(prefab);this.node.addChild(boss);});}// 释放整个 BundlereleaseBundle(){assetManager.removeBundle(assetManager.getBundle('level2')!);}}

3. 预制体(Prefab)

预制体是可复用的节点模板,是 Cocos 游戏开发最核心的工作单元。

3.1 创建预制体

  1. 在场景中搭建好节点结构(例如:Enemy 节点 + Sprite + 碰撞体 + EnemyController 脚本)
  2. 将节点从层级管理器拖拽到资源管理器中 → 自动生成.prefab文件
  3. 原节点变为预制体实例(蓝色标识)

3.2 代码实例化预制体

import{_decorator,Component,Prefab,Node,instantiate,Vec3}from'cc';const{ccclass,property}=_decorator;@ccclass('EnemySpawner')exportclassEnemySpawnerextendsComponent{@property(Prefab)enemyPrefab:Prefab=null!;@property(Node)spawnParent:Node=null!;// 敌人的父节点(便于统一管理)spawnEnemy(x:number,y:number){constenemy=instantiate(this.enemyPrefab);this.spawnParent.addChild(enemy);enemy.setPosition(x,y,0);returnenemy;}spawnWave(count:number,spacing:number){for(leti=0;i<count;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}from'cc';const{ccclass,property}=_decorator;@ccclass('BulletPool')exportclassBulletPoolextendsComponent{@property(Prefab)bulletPrefab:Prefab=null!;// 子弹对象池private_pool:NodePool=newNodePool();// 预热(提前创建一批对象放入池中)start(){this.preheat(20);// 预先创建20个子弹}preheat(count:number){for(leti=0;i<count;i++){constbullet=instantiate(this.bulletPrefab);this._pool.put(bullet);// 放入池中(会自动禁用节点)}console.log(`对象池预热完成,池中有${this._pool.size()}个对象`);}// 从池中获取子弹(复用 > 新建)getBullet(x:number,y:number):Node{letbullet:Node;if(this._pool.size()>0){bullet=this._pool.get()!;// 从池中取出(会自动激活节点)}else{// 池中没有可用对象时,新建一个bullet=instantiate(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}from'cc';const{ccclass,property}=_decorator;// 子弹组件:实现 IPoolManager 接口@ccclass('Bullet')exportclassBulletextendsComponentimplementsIPoolManager{private_speed:number=600;private_pool:NodePool=null!;// 被放入池中时调用(做清理工作)unuse(){// 重置状态this._speed=600;this.node.setPosition(0,0,0);// 取消所有 tweenthis.node.stopAllActions();}// 从池中取出时调用(做初始化工作)reuse(...args:any[]){const[speed]=args;if(speed)this._speed=speed;}setPool(pool:NodePool){this._pool=pool;}update(deltaTime:number){// 向上移动this.node.setPosition(this.node.position.x,this.node.position.y+this._speed*deltaTime,0);// 超出屏幕时回收if(this.node.position.y>700){this._pool.put(this.node);}}}

5. 加载界面实现

import{_decorator,Component,ProgressBar,Label,director}from'cc';const{ccclass,property}=_decorator;@ccclass('LoadingScene')exportclassLoadingSceneextendsComponent{@property(ProgressBar)progressBar:ProgressBar=null!;@property(Label)progressLabel:Label=null!;@property({displayName:'目标场景名'})targetScene:string='Game';start(){this.loadTargetScene();}loadTargetScene(){director.preloadScene(this.targetScene,(completedCount,totalCount)=>{constprogress=completedCount/totalCount;this.progressBar.progress=progress;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 →

http://www.jsqmd.com/news/651803/

相关文章:

  • GNSS多系统星历下载资源全解析:从IGS到WUM的完整指南
  • 医学图像可视化终极指南:用MRIcroGL轻松玩转3D影像分析 [特殊字符][特殊字符]
  • 雀魂Mod Plus:3分钟解锁全角色皮肤的游戏增强方案
  • 如何高效使用TrafficMonitor插件:打造个性化桌面监控中心的完整指南
  • 如何3分钟实现Figma中文界面:设计师必备的汉化完整指南
  • 1.8万美金干掉顶级专家!Anthropic开启AI自主进化:Claude竟能自我「开颅」
  • 2026年最新Windows11下VSCode配置GCC开发C语言环境保姆级教程
  • Python实现斐波那契数列乱序加密与解密(附达芬奇密码案例)
  • 如何安全下载Android应用:APKMirror客户端的完整使用指南
  • Midscene.js:用AI视觉驱动彻底颠覆跨平台自动化测试
  • 手把手教你用Vector XL驱动库实现CAN总线通信(附完整代码解析)
  • 超元力XR剧场:技术革新,重构沉浸式体验的边界
  • STEP7新手避坑指南:手把手教你搞定S7-300硬件组态与IO地址分配(CPU315-2DP实战)
  • 氧化钕:一种带紫色气息的稀土材料
  • java的springboot输出配置文件配置值
  • 第N讲:C# 核心基石 从值类型与引用类型的内存布局理解.NET编程
  • 删除时遇到文件夹中有文件已经打开
  • 暗黑2存档编辑神器:5分钟快速掌握d2s-editor完整使用指南
  • Nginx | 从入门到精通:location匹配规则的实战解析与避坑指南
  • 一分钟了解JSON格式,使用场景,和它的优缺点
  • Hive ETL实战:用FROM_UNIXTIME和UNIX_TIMESTAMP处理混乱时间格式的完整流程
  • 邯郸市佳铭文化:Geo软文+社交媒体,解锁品牌传播新闭环
  • 告别红色感叹号!TortoiseGit冲突文件标记与手动合并技巧详解
  • CCRC 认证全攻略:助力企业提升安全服务能力
  • 广州仓储服务、行李寄存头部企业揭秘!广州家盛凭什么稳居第一? - 广州搬家老班长
  • 发期刊必看:虎贲等考 AI,把 “期刊论文” 做成标准化通关工具
  • 2026奇点大会语音助手技术路线图首度公开:LSTM→Neural Codec→神经声学建模的3阶段跃迁,错过本次将滞后整整18个月
  • Cursor设备指纹重置机制深度解析:突破AI开发工具的设备限制
  • Go:深入理解 go mod vendor 的离线编译实践
  • RabbitMQ 虚拟主机(vhost)全面解析:是什么、作用、使用场景+实战配置