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

【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题

【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题

本文将完整梳理 Unity 中通过 SceneManager.LoadSceneAsync 使用 Additive 模式加载子场景时出现的卡顿问题,分析其本质,提出不同阶段的优化策略,并最终实现一个从预热、加载到资源释放的高性能、低内存场景管理系统。本文适用于(不使用Addressables 的情况下)需要频繁加载子场景的 VR/AR/大地图/分区模块化项目。


前文主要是一些发现问题,解决问题的文档记录。
查看源码,请跳转至文末!


在这里插入图片描述


文章目录

    • 【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题
      • 一、问题起点:LoadSceneAsync 导致的卡顿
      • 二、卡顿原因分析
      • 三、常规优化尝试
        • 1. allowSceneActivation = false
        • 2. 延迟帧 / 加载动画
      • 四、核心解决方案:预热 + 资源卸载
        • 1. 什么是场景预热(Prewarm)?
        • 2. 场景资源未释放问题
      • 五、完善场景管理系统:SceneFlowManager
        • 1. 支持配置化管理 EqSceneConfig
        • 2. 支持 Key 方式加载
        • 3. 支持场景预热接口
      • 六、新增释放资源接口
      • 七、完整流程总结
      • 八、性能实测对比
      • 九、扩展:自动预热与内存调度
      • 十、结语:让 Unity 多场景系统真正高效
        • 1. 总结
        • 2. 源码


一、问题起点:LoadSceneAsync 导致的卡顿

在项目开发过程中,当我们使用如下代码进行 Additive 场景加载时:

AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("YourScene", LoadSceneMode.Additive);

你会发现:

  • 第一次加载某个场景时卡顿极为明显
  • 后续加载相同场景不卡顿,表现正常
  • 即使使用 allowSceneActivation = false 先加载至 0.9,再激活,也无法解决卡顿。

二、卡顿原因分析

Unity 场景加载包括两个阶段:

  1. 资源加载阶段(读取场景所需的纹理、Mesh、Prefab 等)
  2. 激活阶段(触发 Awake/Start、构建场景结构)

而第一次加载时会触发:

  • Shader Compile
  • 静态 Batching
  • Occlusion Culling 计算
  • 实例化所有场景对象

这些过程即使异步,也依然可能在 allowSceneActivation=true 时集中执行,导致帧冻结。


三、常规优化尝试

1. allowSceneActivation = false
asyncLoad.allowSceneActivation = false;
while (asyncLoad.progress < 0.9f) yield return null;
yield return new WaitForSeconds(0.5f);
asyncLoad.allowSceneActivation = true;

结果:激活时依旧卡顿。

2. 延迟帧 / 加载动画

只能缓解体验,不能真正解决第一次激活的卡顿


四、核心解决方案:预热 + 资源卸载

1. 什么是场景预热(Prewarm)?

在用户进入目标场景之前,提前加载该场景、触发资源加载、初始化内存,再卸载掉。

这样用户真正进入场景时:

  • 所有资源都在缓存中(Unity 会延后释放)
  • 场景结构早已解析,第二次加载快很多
IEnumerator PrewarmSceneCoroutine(string sceneName)
{var loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);loadOp.allowSceneActivation = true;while (!loadOp.isDone) yield return null;yield return null;yield return null; // 等待几帧确保初始化var unloadOp = SceneManager.UnloadSceneAsync(sceneName);while (!unloadOp.isDone) yield return null;
}
2. 场景资源未释放问题

你会发现:预热+卸载后并不会立即释放资源!

Unity 会保留一部分资源在内存中,直到调用:

Resources.UnloadUnusedAssets();

所以你必须加入如下逻辑:

yield return Resources.UnloadUnusedAssets();

五、完善场景管理系统:SceneFlowManager

在项目中,我们将所有的加载逻辑封装在 SceneFlowManager 中。

1. 支持配置化管理 EqSceneConfig
[System.Serializable]
public class EqSceneEntry
{public string key;public string sceneName;
}[CreateAssetMenu]
public class EqSceneConfig : ScriptableObject
{public List<EqSceneEntry> scenes;
}
2. 支持 Key 方式加载
public void LoadSceneAdditiveByKey(string key) => LoadSceneAdditive(GetSceneNameByKey(key));
3. 支持场景预热接口
public void PrewarmScene(string sceneName)
{if (IsSceneLoaded(sceneName)) return;StartCoroutine(PrewarmSceneCoroutine(sceneName));
}

六、新增释放资源接口

为了真正释放场景相关的资源,新增 ReleaseSceneResources 方法:

public void ReleaseSceneResources(string sceneName)
{if (IsSceneLoaded(sceneName)){StartCoroutine(UnloadAndReleaseCoroutine(sceneName));}else{StartCoroutine(ReleaseOnlyCoroutine());}
}private IEnumerator UnloadAndReleaseCoroutine(string sceneName)
{yield return SceneManager.UnloadSceneAsync(sceneName);yield return Resources.UnloadUnusedAssets();
}private IEnumerator ReleaseOnlyCoroutine()
{yield return Resources.UnloadUnusedAssets();
}

七、完整流程总结

  1. 项目启动时

    • 初始化 SceneFlowManager
    • 预热即将访问的场景(不会激活)
  2. 进入新场景

    • 调用 LoadSceneAdditiveByKey(key) 平滑加载场景
  3. 离开场景

    • 调用 ReleaseSceneResourcesByKey(key) 卸载并释放内存
  4. 避免过早 Resources.UnloadUnusedAssets()

    • 建议只在真正切场景后调用,避免误删仍在用资源

八、性能实测对比

流程首次加载帧耗时第二次加载帧耗时内存占用卡顿感受
直接加载80ms+40ms+300MB↑明显卡顿
预热+加载30ms↓20ms↓200MB几乎无卡顿
加载+释放资源40ms40ms150MB↓无卡顿

直接加载,出现卡顿(掉帧)
在这里插入图片描述

预热+加载,无掉帧
在这里插入图片描述


九、扩展:自动预热与内存调度

你可以设置:

  • 定时自动预热(玩家未操作时)
  • 内存压力大时调用 ReleaseSceneResources
  • 按访问频率记录预热优先级

十、结语:让 Unity 多场景系统真正高效

1. 总结

本方案从 SceneManager.LoadSceneAsync 的卡顿问题出发,经历:

  • allowSceneActivation 控制加载
  • 手动预热场景
  • 引入资源释放

最终构建了一个完整的 SceneFlowManager

2. 源码

完整代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;namespace Eqgis.Runtime.Scene
{public class SceneFlowManager : MonoBehaviour{public static SceneFlowManager Instance { get; private set; }[Tooltip("常驻场景名称,不参与卸载")]private string persistentSceneName;[Tooltip("场景配置文件")]public EqSceneConfig sceneConfig;private Dictionary<string, string> keyToSceneMap;public void Awake(){// 自动记录当前激活场景为 PersistentScenepersistentSceneName = SceneManager.GetActiveScene().name;Android.EqLog.d("SceneFlowManager", $"[SceneFlowManager] PersistentScene 自动设置为:{persistentSceneName}");if (Instance != null && Instance != this){Destroy(gameObject);return;}Instance = this;DontDestroyOnLoad(gameObject);InitSceneMap();}private void InitSceneMap(){keyToSceneMap = new Dictionary<string, string>();if (sceneConfig != null){foreach (var entry in sceneConfig.scenes){if (!keyToSceneMap.ContainsKey(entry.key)){keyToSceneMap.Add(entry.key, entry.sceneName);}else{Debug.LogWarning($"重复的场景 Key:{entry.key}");}}}else{Debug.LogWarning("未指定 EqSceneConfig,SceneFlowManager 无法使用 key 加载场景");}}// 根据 key 获取真实场景名private string GetSceneNameByKey(string key){if (keyToSceneMap != null && keyToSceneMap.TryGetValue(key, out var sceneName))return sceneName;Debug.LogError($"未找到 key 对应的场景名: {key}");return null;}// 通过 Key 加载 Additive 场景public void LoadSceneAdditiveByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){LoadSceneAdditive(sceneName);}}// 通过 Key 加载 Single 场景public void LoadSceneSingleByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){LoadSceneSingle(sceneName);}}// 通过 Key 卸载场景public void UnloadSceneByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){UnloadScene(sceneName);}}// 加载场景名(Additive)public void LoadSceneAdditive(string sceneName){if (!IsSceneLoaded(sceneName)){//SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);StartCoroutine(LoadSceneAdditiveCoroutine(sceneName));}}// 加载场景名(Additive)private IEnumerator LoadSceneAdditiveCoroutine(string sceneName){AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);//asyncLoad.allowSceneActivation = false;//while (asyncLoad.progress < 0.9f)//{//    yield return null; // 等待加载完成(进度最多到0.9)//}//// 此时可以延迟几帧或做加载动画等处理//yield return new WaitForSeconds(0.5f);//asyncLoad.allowSceneActivation = true; // 手动激活场景// 参考:https://docs.unity3d.com/2021.3/Documentation/ScriptReference/SceneManagement.SceneManager.LoadSceneAsync.htmlwhile (!asyncLoad.isDone){yield return null;}}// 加载场景名(Single)public void LoadSceneSingle(string sceneName){if (!IsSceneLoaded(sceneName)){SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);}}// 卸载指定场景public void UnloadScene(string sceneName){if (sceneName == persistentSceneName) return;if (IsSceneLoaded(sceneName)){SceneManager.UnloadSceneAsync(sceneName);}}// 卸载所有非常驻场景public void UnloadAllNonPersistentScenes(){StartCoroutine(UnloadAllExceptPersistent());}private IEnumerator UnloadAllExceptPersistent(){List<string> scenesToUnload = new List<string>();for (int i = 0; i < SceneManager.sceneCount; i++){var scene = SceneManager.GetSceneAt(i);if (scene.name != persistentSceneName){scenesToUnload.Add(scene.name);}}foreach (string sceneName in scenesToUnload){AsyncOperation op = SceneManager.UnloadSceneAsync(sceneName);while (!op.isDone){yield return null;}}}public bool IsSceneLoaded(string sceneName){for (int i = 0; i < SceneManager.sceneCount; i++){if (SceneManager.GetSceneAt(i).name == sceneName)return true;}return false;}public void SetActiveScene(string sceneName){if (IsSceneLoaded(sceneName)){SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneName));}}public void SetActiveSceneByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){SetActiveScene(sceneName);}}// 通过 Key 预热一个场景(Additive 预加载后立即卸载)public void PrewarmSceneByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){PrewarmScene(sceneName);}}// 通过场景名预热一个场景public void PrewarmScene(string sceneName){// 若已加载,无需预热if (IsSceneLoaded(sceneName)){Debug.Log($"[SceneFlowManager] 场景 {sceneName} 已加载,跳过预热");return;}StartCoroutine(PrewarmSceneCoroutine(sceneName));}private IEnumerator PrewarmSceneCoroutine(string sceneName){Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 开始预热场景:{sceneName}");AsyncOperation loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);loadOp.allowSceneActivation = true;while (!loadOp.isDone)yield return null;// 延迟几帧以确保资源初始化完成yield return null;yield return null;Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 场景 {sceneName} 加载完毕,开始卸载");AsyncOperation unloadOp = SceneManager.UnloadSceneAsync(sceneName);while (!unloadOp.isDone)yield return null;Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 场景 {sceneName} 预热完成并卸载");}/// <summary>/// 释放指定场景对应的未被引用资源,确保卸载后内存回收/// </summary>public void ReleaseSceneResourcesByKey(string key){string sceneName = GetSceneNameByKey(key);if (!string.IsNullOrEmpty(sceneName)){ReleaseSceneResources(sceneName);}}public void ReleaseSceneResources(string sceneName){if (sceneName == persistentSceneName){Debug.LogWarning($"不能释放常驻场景[{sceneName}]的资源");return;}if (IsSceneLoaded(sceneName)){// 场景已加载,先卸载后释放资源AsyncOperation unloadOp = SceneManager.UnloadSceneAsync(sceneName);StartCoroutine(ReleaseResourcesAfterUnload(unloadOp, sceneName));}else{// 场景已卸载,直接释放资源StartCoroutine(ReleaseResourcesDirect(sceneName));}}private IEnumerator ReleaseResourcesAfterUnload(AsyncOperation unloadOp, string sceneName){yield return unloadOp;Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 已卸载,开始释放未使用资源");AsyncOperation unloadUnused = Resources.UnloadUnusedAssets();yield return unloadUnused;Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 资源释放完成");}private IEnumerator ReleaseResourcesDirect(string sceneName){Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 已卸载,直接释放未使用资源");AsyncOperation unloadUnused = Resources.UnloadUnusedAssets();yield return unloadUnused;Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 资源释放完成");}}
}
http://www.lryc.cn/news/594942.html

相关文章:

  • 【c++】leetcode438 找到字符串中所有字母异位词
  • Three.js 从零入门:构建你的第一个 Web 3D 世界
  • 小孙学变频学习笔记(十一)关于V/F曲线的讨论
  • 本地部署AI新选择!LocalAI+cpolar轻松实现隐私安全的远程访问
  • 深入解析Hadoop YARN:三层调度模型与资源管理机制
  • 星游路-个人日志-学习积累法
  • 【PTA数据结构 | C语言版】验证六度空间理论
  • Unity VR多人手术系统恢复3:Agora语音通讯系统问题解决全记录
  • Hadoop数据完整性校验机制深度解析:CRC32校验和与后台扫描线程
  • 低空经济展 | 约克科技携小型化测试设备亮相2025深圳eVTOL展
  • Spring Boot 3核心技术面试指南:从迁移升级到云原生实战,9轮技术攻防(含架构解析)
  • 树链剖分-苹果树
  • EMBMS1820芯祥科技18单元电池监控器芯片数据手册
  • 有关Spring的总结
  • 网络编程之 UDP:用户数据报协议详解与实战
  • 19.TaskExecutor与ResourceManager建立连接
  • Openlayers 面试题及答案180道(161-180)
  • 线上问题排查之【CPU飙高100%】
  • 在幸狐RV1106板子上用gcc14.2本地编译安装mysql-8.0.42数据库
  • 一维DP深度解析
  • ElasticSearch是什么
  • 如何使用Ansible一键部署Nacos集群?
  • Android 蓝牙通讯全解析:从基础到实战
  • 【STM32】485接口原理
  • 元图 CAD:PDF 与 CAD 格式互转的完美解决方案
  • 部署 Zabbix 企业级分布式监控
  • WPF 初始界面启动时播放背景音乐
  • 合并pdf工具下载
  • Redis进阶--缓存
  • 如何使用python网络爬虫批量获取公共资源数据