核心思路是把场景拆成"静态"和"动态"两块。静态部分用SceneManager.LoadSceneAsync异步加载,配合allowSceneActivation控制激活时机。关键是这个加载进度条的处理:当progress达到0.9时卡住,等后台预加载完关键资源再允许激活。具体实现可以挂个MonoBehaviour协程:
动态对象管理要用对象池。比如射击游戏里的子弹,频繁实例化会引发GC卡顿。我们创建ObjectPool<T>泛型类,初始化时预生成20发子弹放入Stack。射击时从栈顶取对象,回收时重置状态再压栈。注意要给池子设上限,避免内存泄漏。实测这样能把GC分配从每帧2KB降到0.2KB。
场景过渡时的内存管理有个细节:用Resources.UnloadUnusedAssets()前得先调用GC.Collect(),否则资源引用没清理干净。我们在场景切换时启动这个组合拳:
对于开放世界这种大场景,需要实现分块加载。把地图切成16x16的区块,根据玩家坐标动态加载周围9个区块。关键是要在Update里做距离检测,但必须加时间间隔限制:
遇到需要跨场景保留的对象(比如玩家属性),别用DontDestroyOnLoad就了事。最好弄个GameManager单例,在Awake里自检是否存在重复实例,有就立即Destroy。记得把核心数据序列化到ScriptableObject里,这样重启游戏也能读取初始值。
调试时可以在SceneManager.sceneLoaded事件里注册回调,记录每个场景加载耗时。遇到过最坑的是场景里扔了上百个未激活的NPC,虽然不渲染但Update照跑。后来改成按距离启用,CPU使用率直接降了30%。
最后提醒:别在场景里放一堆空GameObject挂脚本,该用静态类就用静态类。曾经见过有人每个场景都摆个SoundManager实例,其实整个游戏只需要一个音频管理器常驻内存就行。场景管理本质是资源调度,把握"用时加载,完事即焚"八个字,性能就不会差到哪去。