【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.pswp.cn/pingmian/92698.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/92698.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

B 树与 B + 树解析与实现

一、磁盘存储优化的核心逻辑 在大规模数据处理场景中&#xff0c;磁盘 I/O 效率是性能瓶颈的核心。磁盘访问具有以下特性&#xff1a; 随机访问成本高&#xff1a;磁头寻道时间&#xff08;Seek Time&#xff09;可达毫秒级&#xff0c;相比内存访问&#xff08;纳秒级&#…

MySQL 查询相同记录并保留时间最晚的一条

要在 MySQL 中查询相同记录并仅保留时间最晚的那一条&#xff0c;你可以使用以下几种方法&#xff1a;方法一&#xff1a;使用子查询和 GROUP BY假设你的表名为 your_table&#xff0c;时间字段为 create_time&#xff0c;其他用于判断记录相同的字段为 field1, field2 等&…

在 .NET Core 5.0 中启用 Gzip 压缩 Response

在 .NET Core 5.0 中启用 Gzip 压缩 Response 在 .NET Core 5.0 (ASP.NET Core 5.0) 中启用 Gzip 压缩主要通过响应压缩中间件实现。以下是详细配置步骤&#xff1a; 1. 安装必要的 NuGet 包 首先确保已安装响应压缩包&#xff1a; dotnet add package Microsoft.AspNetCore.Re…

[Oracle] TRUNC()函数

TRUNC() 是 Oracle 中一个多功能函数&#xff0c;主要用于对数值、日期进行截断操作1.TRUNC()函数用于数值处理语法格式TRUNC(number, decimal_places)参数说明number&#xff1a;要截断的数值 decimal_places&#xff1a;保留的小数位数(可选)&#xff0c;默认为0(截断所有小数…

GPT-oss:OpenAI再次开源新模型,技术报告解读

1.简介OpenAI 发布了两款开源权重推理模型 gpt-oss-120b 与 gpt-oss-20b&#xff0c;均采用 Apache 2.0 许可&#xff0c;主打在代理工作流中执行复杂推理、调用工具&#xff08;如搜索、Python 代码执行&#xff09;并严格遵循指令。120b 为 36 层 MoE 结构&#xff0c;活跃参…

python tcp 框架

目录 python tcp 框架 asyncio websockets python tcp 框架 asyncio import asyncio import json import timeclass TCPClient:def __init__(self, host, port, heartbeat_interval10):self.host hostself.port portself.heartbeat_interval heartbeat_intervalself.read…

HTML 与 CSS:从 “认识标签” 到 “美化页面” 的入门指南

个人主页&#xff1a;♡喜欢做梦 目录 &#x1f3a0;HTML &#x1f3a1;一、什么是HTML&#xff1f; ⛲️1.定义 ⛲️2.核心特点 ⛲️3.HTML的基本结构 ⛲️4.标签的层次结构关系 &#x1f3a1;二、HTML的常用标签 &#x1f305;1.文本列表标签 标题标签&#xff1a;h…

【MATLAB 2025a】安装离线帮助文档

文章目录一、在 MATLAB 设置中安装二、从math works 网站下载ISO&#xff1a;适用于给无法联网的电脑安装或自定义路径三、startup文件说明四、重要说明&#x1f9e9;&#x1f9e9;【Matlab】最新版2025a发布&#xff0c;深色模式、Copilot编程助手上线&#xff01; 版本&#…

Linux系统编程Day8 -- Git 教程(初阶)

往期内容回顾 基于Linux系统知识的第一个程序 自动化构建工具-make/Makefile gcc/g编译及链接 Vim工具的使用 Linux常用工具&#xff08;yum与vim&#xff09; ​​​​​​ Linux系统编程Day4-- Shell与权限 回顾进度条程序的编写&#xff1a; //.h文件内容 #include<stdio…

React18 Transition特性详解

Transition 核心概念&#xff1a;Transition是一种标记非紧急任务更新的机制&#xff0c;它允许React在用户交互&#xff08;如输入&#xff09;期间保持界面的响应&#xff0c;同时准备后台更新 主要特点&#xff1a; 区分优先级&#xff1a;可以将更新分为紧急非紧急任务可中…

OpenHarmony概述与使用

1. OpenHarmony Hi3861 学习目标与任务 硬件基础知识&#xff1a;涵盖嵌入式硬件体系架构&#xff08;如 MCU 基础、硬件接口原理 &#xff09;、硬件设计流程&#xff08;原理图绘制、PCB Layout 规范 &#xff09;&#xff0c;了解常见硬件外设&#xff08;传感器、通信模…

大模型提示词工程实践:大语言模型文本转换实践

大模型文本转换 学习目标 在本课程中&#xff0c;我们将探究如何使用大语言模型来完成文本转换任务&#xff0c;例如语言翻译、拼写和语法检查、语气调整以及格式转换。 相关知识点 大模型文本转换 学习内容 1. 大模型文本转换 文本转换的核心定义与范畴 文本转换 是指通过技术…

力扣LCR024:反转链表206.反转链表双解法(经典面试题)

LCR 024. 反转链表 - 力扣&#xff08;LeetCode&#xff09;LCR 024. 反转链表 - 给定单链表的头节点 head &#xff0c;请反转链表&#xff0c;并返回反转后的链表的头节点。 示例 1&#xff1a;[https://assets.leetcode.com/uploads/2021/02/19/rev1ex1.jpg]输入&#xff1a…

Day 6: CNN卷积神经网络 - 计算机视觉的核心引擎

Day 6: CNN卷积神经网络 - 计算机视觉的核心引擎 📚 核心概念(5分钟理解) 什么是CNN卷积神经网络? 核心概念解释: CNN(Convolutional Neural Network): 专门处理具有网格状拓扑结构数据的深度学习模型,特别擅长图像识别 为什么需要: 传统全连接神经网络处理图像时参数量…

MacBook 本地化部署 Dify 指南

Dify 安装前的准备工作 确认系统满足最低配置要求&#xff0c;包括操作系统版本、内存、CPU 和存储空间。 检查是否已安装必要的依赖项&#xff0c;如 Python、Docker 确保网络环境稳定&#xff0c;能够访问所需的软件源或镜像仓库。 获取 Dify 安装包 https://docs.dify.ai…

疫情可视化:基孔肯雅热风险地图实战解析

> 一只白纹伊蚊的飞行半径是100米,而一套WebGIS系统能将疫情防控范围精确到每平方米。 2025年夏季,基孔肯雅热疫情在广东佛山爆发,短短一个月内感染病例占全省95%以上。这种由伊蚊传播的病毒性疾病,以**突发高热、剧烈关节痛和全身皮疹**为特征,患者关节疼痛可能持续数…

【14-模型训练细节】

训练步骤 1、指定输入和输出&#xff0c;即模型定义&#xff1b; 2、指定损失函数和成本函数&#xff1b; 3、指定训练算法&#xff0c;如梯度下降算法&#xff1b;训练细节 损失函数和成本函数用梯度下降算法训练模型 主要是求成本函数的偏导数&#xff0c;使用的是反向传播算…

ConcurrentDictionary 详解:.NET 中的线程安全字典

什么是 ConcurrentDictionary&#xff1f; ConcurrentDictionary<TKey, TValue> 是 .NET Framework 4.0 和 .NET Core/.NET 5 中引入的线程安全字典实现&#xff0c;位于 System.Collections.Concurrent 命名空间。它解决了多线程环境下操作字典时的同步问题&#xff0c…

集成电路学习:什么是URDF Parser统一机器人描述格式解析器

URDF Parser(URDF解析器)是ROS(Robot Operating System,机器人操作系统)中用于解析URDF(Unified Robot Description Format,统一机器人描述格式)文件的工具。URDF是一种基于XML(Extensible Markup Language,可扩展标记语言)规范的格式,用于描述机器人的结构、关节、…

老式大头显示器(CRT)和当前最高分辨率的LED显示器对比

老式 CRT&#xff08;阴极射线管&#xff09;和当前最顶尖的 LED&#xff08;包括 MicroLED / 高端 MiniLED / OLED&#xff09;显示器在画面清晰度极限相关的参数并列分析。1. 分辨率与像素密度指标老式 CRT&#xff08;PC/电视用&#xff09;顶级 LED 显示器&#xff08;2025…