Unity3D 场景 Scene 管理(加载/卸载/切换/DontDestroyOnLoad)

举报
William 发表于 2026/01/09 10:07:04 2026/01/09
【摘要】 一、引言与技术背景在游戏开发中,场景(Scene)是组织和划分游戏内容的 fundamental unit(基本单元)。一个典型的游戏可能由多个场景构成,例如:主菜单场景:包含开始、设置、退出等选项。游戏关卡场景:承载具体的游戏玩法、角色和世界。加载场景:在切换大型关卡时显示一个进度条,提升用户体验。结算/暂停场景:叠加在主游戏场景之上,显示得分或暂停菜单。因此,高效地管理场景的生命周期——...


一、引言与技术背景

在游戏开发中,场景(Scene)是组织和划分游戏内容的 fundamental unit(基本单元)。一个典型的游戏可能由多个场景构成,例如:
  • 主菜单场景:包含开始、设置、退出等选项。
  • 游戏关卡场景:承载具体的游戏玩法、角色和世界。
  • 加载场景:在切换大型关卡时显示一个进度条,提升用户体验。
  • 结算/暂停场景:叠加在主游戏场景之上,显示得分或暂停菜单。
因此,高效地管理场景的生命周期——包括加载、卸载、切换以及跨场景对象的持久化——是构建一个结构良好、性能优异的游戏的核心环节。Unity 提供了一套完善的 API 来处理这些需求,理解其背后的原理和最佳实践对于开发者至关重要。

二、核心概念与原理

1. 场景基础

  • Scene 对象:在 Unity 中,Scene是一个类 (UnityEngine.SceneManagement.Scene),它代表一个场景实例,包含了场景中的所有游戏对象(GameObjects)及其层级关系。
  • 场景的唯一标识:每个场景通过其文件路径(在 Build Settings 中注册)来唯一标识。
  • 场景管理类UnityEngine.SceneManagement.SceneManager是核心的静态类,提供了所有场景操作的 API。

2. 场景加载模式

  • 单一模式 (Single):这是最常用的模式。当加载一个新场景时,当前活动场景中的所有对象都会被销毁,并由新场景的内容完全取代。适用于大多数关卡切换。
  • 叠加模式 (Additive):新场景会加载到当前已存在的场景之上,而不会销毁现有对象。这使得开发者可以创建“大世界”地图(将多个小场景流式加载进来)或实现 HUD、暂停菜单等叠加界面。

3. DontDestroyOnLoad (DontDestroy)

  • 作用:当一个游戏对象被标记为 DontDestroyOnLoad后,它在场景切换时将不会被销毁
  • 应用场景:用于存储需要在多个场景间共享的数据或持久存在的对象,例如:
    • 游戏管理器 (GameManager)
    • 音频管理器 (AudioManager)
    • 玩家分数/存档数据
    • 常驻的 UI 界面(如主菜单)
  • 注意:滥用 DontDestroyOnLoad会导致“场景污染”,即旧场景的对象残留在新场景中,可能引发逻辑错误或内存泄漏。需要谨慎管理这些“孤儿”对象。

4. 原理流程图

场景切换流程 (Single Mode):
[当前场景 A (活动)]
      |
      V
[调用 SceneManager.LoadScene("B", LoadSceneMode.Single)]
      |
      V
[引擎开始异步加载场景 B 的资源]
      |
      V
[触发所有场景 A 中对象的 OnDisable() -> OnDestroy()]
      |
      V
[激活场景 B,触发其中所有对象的 Awake() -> OnEnable() -> Start()]
      |
      V
[场景 B (活动)]
场景叠加流程 (Additive Mode):
[当前场景 A (活动)]
      |
      V
[调用 SceneManager.LoadScene("B", LoadSceneMode.Additive)]
      |
      V
[引擎开始异步加载场景 B 的资源]
      |
      V
[场景 B 加载完成,其中的对象被激活 (Awake/OnEnable/Start)]
      |
      V
[场景 A 仍保持活动状态,场景 B 也被加载]
DontDestroyOnLoad 流程:
[场景 A 中存在 Obj_X]
      |
      V
[Obj_X 调用 DontDestroyOnLoad(gameObject)]
      |
      V
[切换至场景 B]
      |
      V
[场景 A 中除 Obj_X 外的所有对象被销毁]
      |
      V
[场景 B 加载,Obj_X 被保留并出现在场景 B 的根层级]

三、应用使用场景

场景
技术方案
描述
主菜单 -> 第一关
LoadSceneMode.Single
标准关卡切换,清空旧内容,加载新内容。
开放世界地图加载
LoadSceneMode.Additive+ 流式加载
将大地图分割成多个小场景,玩家靠近时动态加载相邻区域,远离时卸载。
HUD/暂停菜单
LoadSceneMode.Additive
将 HUD 或暂停菜单做成一个独立场景,需要时叠加到游戏主场景上,关闭时卸载。
跨场景数据持久化
DontDestroyOnLoad
创建 GameManager 来存储玩家生命值、分数等,确保切换场景时数据不丢失。
异步加载与进度条
SceneManager.LoadSceneAsync
在后台加载场景,同时显示一个 UI 进度条,避免游戏卡死,提升用户体验。

四、环境准备

  • Unity 版本:2019.3 LTS 或更高(引入了更清晰的场景管理 API)。
  • 脚本语言:C#。
  • 场景设置
    1. 创建三个场景:MainMenu, Level1, LoadingScreen
    2. 打开 File -> Build Settings,将所有三个场景拖入 Scenes In Build​ 列表中,并记录它们的索引(Index)或名称。
    3. MainMenu中创建一个 UI Canvas,添加一个 Button(命名为 StartGameButton)和一个 Text(命名为 TitleText)。
    4. Level1中创建一个 3D Cube 和一个 Directional Light。
    5. LoadingScreen中创建一个全屏的 UI Image(作为背景)和一个 Slider(作为进度条),以及一个 Text 用于显示百分比。

五、不同场景的代码实现

我们将创建以下几个脚本来覆盖所有核心功能。

1. 场景加载器:SceneLoader.cs

这是一个通用的场景加载工具类,使用单例模式以便于全局访问。
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

/// <summary>
/// 场景加载器工具类,使用单例模式。
/// 负责处理所有场景的同步/异步加载和卸载逻辑。
/// </summary>
public class SceneLoader : MonoBehaviour
{
    public static SceneLoader Instance;

    [Header("Loading Screen UI References")]
    public GameObject loadingScreen; // 加载界面的根 GameObject
    public Slider progressBar;
    public Text progressText;

    void Awake()
    {
        // 实现单例模式
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 让加载器本身常驻
        }
        else
        {
            Destroy(gameObject);
            return;
        }

        // 确保加载界面初始时是隐藏的
        if (loadingScreen != null)
            loadingScreen.SetActive(false);
    }

    /// <summary>
    /// 同步加载场景(会卡住主线程,不推荐用于大型场景)
    /// </summary>
    /// <param name="sceneName">场景名称</param>
    public void LoadSceneSync(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    /// <summary>
    /// 异步加载场景,并显示加载进度
    /// </summary>
    /// <param name="sceneName">场景名称</param>
    public void LoadSceneAsync(string sceneName)
    {
        StartCoroutine(LoadSceneAsyncRoutine(sceneName));
    }

    /// <summary>
    /// 协程:处理异步加载的细节
    /// </summary>
    private IEnumerator LoadSceneAsyncRoutine(string sceneName)
    {
        // 激活加载界面
        if (loadingScreen != null)
            loadingScreen.SetActive(true);

        // 异步加载场景
        AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);

        // 阻止场景在加载完成瞬间自动激活
        // 这允许我们完全控制何时切换场景,确保进度条能走到 100%
        operation.allowSceneActivation = false;

        // 循环更新进度条,直到加载完成
        while (!operation.isDone)
        {
            // operation.progress 的值范围是 [0, 1],但在 allowSceneActivation=false 时,最大只能到 0.9
            float progress = Mathf.Clamp01(operation.progress / 0.9f);
            
            if (progressBar != null)
                progressBar.value = progress;
            if (progressText != null)
                progressText.text = $"{Mathf.RoundToInt(progress * 100)}%";

            // 当进度达到 90% 以上时,等待用户按键或自动激活
            if (operation.progress >= 0.9f)
            {
                if (progressBar != null)
                    progressBar.value = 1f;
                if (progressText != null)
                    progressText.text = "100%";

                // 这里可以添加逻辑,例如按任意键继续,或者等待一小段时间后自动继续
                // 为了演示,我们在这里自动激活
                // Debug.Log("Press any key to continue...");
                // yield return new WaitUntil(() => Input.anyKeyDown);
                
                operation.allowSceneActivation = true;
            }

            yield return null; // 等待下一帧
        }

        // 加载完成,隐藏加载界面
        if (loadingScreen != null)
            loadingScreen.SetActive(false);
    }


    /// <summary>
    /// 卸载指定的叠加场景
    /// </summary>
    /// <param name="sceneName">场景名称</param>
    public void UnloadScene(string sceneName)
    {
        if (SceneManager.GetSceneByName(sceneName).IsValid())
        {
            SceneManager.UnloadSceneAsync(sceneName);
            Debug.Log($"Scene '{sceneName}' has been unloaded.");
        }
        else
        {
            Debug.LogWarning($"Cannot unload scene '{sceneName}', it was not loaded.");
        }
    }
}

2. 游戏状态管理器:GameManager.cs

这是一个典型的需要跨场景存在的“管理者”对象。
using UnityEngine;
using UnityEngine.SceneManagement;

/// <summary>
/// 游戏状态管理器,使用单例模式和 DontDestroyOnLoad。
/// 负责存储跨场景共享的游戏数据。
/// </summary>
public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    public int playerScore = 0;
    public int playerLives = 3;

    void Awake()
    {
        // 单例模式实现
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 关键:使其在场景切换时不被销毁
        }
        else
        {
            Destroy(gameObject); // 如果已存在实例,则销毁新的
            return;
        }

        Debug.Log("GameManager Initialized. It will persist across scenes.");
    }

    public void AddScore(int points)
    {
        playerScore += points;
        Debug.Log($"Score: {playerScore}");
    }

    public void LoseLife()
    {
        playerLives--;
        Debug.Log($"Lives remaining: {playerLives}");
        if (playerLives <= 0)
        {
            // 游戏结束,返回主菜单
            SceneLoader.Instance.LoadSceneAsync("MainMenu");
        }
    }

    // 示例:响应场景加载完成后的回调
    void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    void OnDisable()
    {
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    // 当任何场景加载完成时,这个方法都会被调用
    void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        Debug.Log($"Scene loaded: {scene.name} in {mode} mode.");
        // 可以在这里根据加载的场景名执行特定的逻辑
        // 例如,如果加载的是 Level1,就重置一些状态
        if (scene.name == "Level1")
        {
            // Reset level-specific state if needed
        }
    }
}

3. 主菜单逻辑:MainMenuController.cs

挂载在 MainMenu场景的 Canvas 上。
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 主菜单界面的控制器
/// </summary>
public class MainMenuController : MonoBehaviour
{
    public Button startGameButton;

    void Start()
    {
        // 确保 GameManager 存在(从之前场景保留或新建)
        if (GameManager.Instance == null)
        {
            GameObject gm = new GameObject("GameManager");
            gm.AddComponent<GameManager>();
            Debug.Log("A new GameManager was created.");
        }

        if (startGameButton != null)
        {
            startGameButton.onClick.AddListener(OnStartGameClicked);
        }
    }

    void OnStartGameClicked()
    {
        Debug.Log("Start button clicked. Loading Level 1...");
        // 使用 SceneLoader 异步加载游戏关卡
        SceneLoader.Instance.LoadSceneAsync("Level1");
    }
}

4. 关卡逻辑:Level1Controller.cs

挂载在 Level1场景中的一个空对象上。
using UnityEngine;

/// <summary>
/// 第一关卡的控制器
/// </summary>
public class Level1Controller : MonoBehaviour
{
    void Update()
    {
        // 简单的测试:按空格键加分,按 R 键减命
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if(GameManager.Instance != null)
                GameManager.Instance.AddScore(10);
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            if (GameManager.Instance != null)
                GameManager.Instance.LoseLife();
        }

        // 按 Esc 键返回主菜单
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            SceneLoader.Instance.LoadSceneAsync("MainMenu");
        }
    }
}

5. 加载界面设置

  • SceneLoader.cs挂载到一个在 LoadingScreen场景中创建的空 GameObject(如 SceneLoaderManager)上。
  • 将 UI 控件(loadingScreenGameObject, progressBar, progressText)拖拽到脚本对应的槽位中。
  • 为了让 SceneLoader能在游戏一开始就存在,你需要首先构建并运行 LoadingScreen场景,或者在 MainMenu场景中创建一个 SceneLoader的实例。一个更简单的方法是,在 MainMenu场景中也创建一个 SceneLoader对象,由于单例模式,它会成为唯一的实例。

六、运行结果与测试步骤

  1. 基础流程测试
    • MainMenu设为启动场景。
    • 运行游戏。点击 “Start Game” 按钮。
    • 观察 LoadingScreen出现,进度条从 0% 加载到 100%,然后切换到 Level1场景。控制台会打印出 GameManager的初始化信息和分数变化。
  2. 跨场景数据持久化测试
    • Level1中,按 空格键​ 增加分数,按 R​ 键减少生命值。
    • Esc​ 键返回 MainMenu
    • 再次点击 “Start Game” 进入 Level1。观察控制台,分数和生命值是延续上次的值,而不是重置。这证明了 GameManager通过 DontDestroyOnLoad成功保留了数据。
  3. 异步加载与进度条测试
    • 故意在 Level1中放入一个很大的模型或贴图,使加载时间变长。
    • 观察在切换场景时,画面不会卡住,而是平滑地显示加载界面和进度条的增长。
  4. 叠加场景测试 (扩展)
    • 创建一个新的场景 HUD,在其中放置一个显示分数的 Text。
    • Level1ControllerStart方法中,添加 SceneManager.LoadScene("HUD", LoadSceneMode.Additive);
    • 运行游戏进入 Level1,你会发现 HUD 被叠加显示了。在 Level1ControllerOnDestroy或通过一个明确的退出方法中,调用 SceneLoader.Instance.UnloadScene("HUD");来卸载它。

七、部署场景与疑难解答

部署场景

  • 所有平台通用:场景管理是 Unity 引擎的核心功能,对所有构建平台(PC, Mac, Mobile, WebGL, Consoles)的支持是一致的。
  • 构建设置:务必在 File -> Build Settings​ 中添加所有需要用到的场景,否则 SceneManager.LoadScene会失败。

疑难解答

  1. 问题:SceneManager.LoadScene报错 “Scene 'XXX' couldn't be loaded because it has not been added to the build settings.”
    • 原因:要加载的场景没有被添加到 Build Settings 的 Scenes In Build 列表中。
    • 解决:打开 Build Settings,将对应的场景文件拖入列表中。
  2. 问题:DontDestroyOnLoad的对象重复创建。
    • 原因:在多个场景中都存在该对象的原始版本,导致单例逻辑失效,创建了多个实例。
    • 解决:确保只有一个场景(通常是主菜单或启动场景)包含该对象的原始版本。其他场景应通过代码查找 (FindObjectOfType) 来获取已有的单例实例,而不是重新创建。
  3. 问题:场景切换时出现明显的卡顿或冻结。
    • 原因:使用了同步加载 (LoadScene) 加载了一个非常大的场景。
    • 解决:改用异步加载 (LoadSceneAsync) 并结合加载界面,将加载工作分摊到多帧中执行。
  4. 问题:卸载叠加场景失败或不彻底。
    • 原因:可能有其他对象(尤其是通过 DontDestroyOnLoad的对象)仍然持有对该场景中对象的引用,导致 Unity 的垃圾回收机制无法完全清理。
    • 解决:在卸载场景前,手动清除对这些对象的引用(例如,将引用设为 null),并调用 Resources.UnloadUnusedAssets()来帮助垃圾回收。

八、未来展望与技术趋势

  • Addressables 与场景管理:Unity 的 Addressable Assets 系统正在改变资源的加载方式。未来,场景本身也可以作为 Addressable 资源进行远程加载和卸载,实现真正的动态内容更新(DLC)和巨型世界的流式加载,而无需将所有场景都打入主包体。
  • DOTS 与 SubScene:在 Data-Oriented Technology Stack (DOTS) 中,传统的 GameObject 场景概念正在演变为 SubScene。SubScene 允许将一部分世界流式传输到 ECS 世界中,这为构建超大规模、高并发的物理模拟世界提供了可能,其管理方式与传统场景管理有本质区别。
  • 更智能的场景合并与依赖分析:随着项目变得日益复杂,Unity 可能会提供更强大的工具来分析场景之间的依赖关系,优化打包体积,并实现更智能的场景合并与拆分。

九、总结

特性/API
核心概念
主要优势
使用场景
SceneManager.LoadScene
同步加载单一场景
简单直接
小型项目或加载内容极少的场景
SceneManager.LoadSceneAsync
异步加载单一场景
不阻塞主线程,可实现加载界面
绝大多数关卡切换场景
LoadSceneMode.Additive
叠加加载场景
可实现大世界、UI 叠加
开放世界、HUD、暂停菜单
SceneManager.UnloadSceneAsync
异步卸载场景
管理内存,动态构建世界
卸载远处的区域、关闭弹窗
Object.DontDestroyOnLoad
跨场景保留对象
数据持久化,全局管理器
GameManager, AudioManager, 玩家数据
核心工作流建议
  1. 拥抱异步:养成使用 LoadSceneAsync的习惯,这是专业游戏的标配。
  2. 善用单例:对于 GameManager这类全局对象,使用单例模式配合 DontDestroyOnLoad是可靠的做法,但要警惕其副作用。
  3. 规划场景结构:提前思考好哪些部分是单一场景,哪些需要叠加,哪些需要动态加载/卸载,这能避免后期的重构痛苦。
  4. 利用事件:使用 SceneManager.sceneLoaded等事件来响应场景变化,而不是在每个加载调用后写一堆零散的逻辑。
掌握 Unity 的场景管理是驾驭复杂项目的关键一步,它不仅是技术的体现,更是优秀软件架构思想的实践。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。