【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
英文地址,請注明出處:http://en.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…