Unity3D 场景 Scene 管理(加载/卸载/切换/DontDestroyOnLoad)
【摘要】 一、引言与技术背景在游戏开发中,场景(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#。
-
场景设置:
-
创建三个场景:
MainMenu,Level1,LoadingScreen。 -
打开 File -> Build Settings,将所有三个场景拖入 Scenes In Build 列表中,并记录它们的索引(Index)或名称。
-
在
MainMenu中创建一个 UI Canvas,添加一个 Button(命名为StartGameButton)和一个 Text(命名为TitleText)。 -
在
Level1中创建一个 3D Cube 和一个 Directional Light。 -
在
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对象,由于单例模式,它会成为唯一的实例。
六、运行结果与测试步骤
-
基础流程测试:
-
将
MainMenu设为启动场景。 -
运行游戏。点击 “Start Game” 按钮。
-
观察
LoadingScreen出现,进度条从 0% 加载到 100%,然后切换到Level1场景。控制台会打印出GameManager的初始化信息和分数变化。
-
-
跨场景数据持久化测试:
-
在
Level1中,按 空格键 增加分数,按 R 键减少生命值。 -
按 Esc 键返回
MainMenu。 -
再次点击 “Start Game” 进入
Level1。观察控制台,分数和生命值是延续上次的值,而不是重置。这证明了GameManager通过DontDestroyOnLoad成功保留了数据。
-
-
异步加载与进度条测试:
-
故意在
Level1中放入一个很大的模型或贴图,使加载时间变长。 -
观察在切换场景时,画面不会卡住,而是平滑地显示加载界面和进度条的增长。
-
-
叠加场景测试 (扩展):
-
创建一个新的场景
HUD,在其中放置一个显示分数的 Text。 -
在
Level1Controller的Start方法中,添加SceneManager.LoadScene("HUD", LoadSceneMode.Additive);。 -
运行游戏进入
Level1,你会发现 HUD 被叠加显示了。在Level1Controller的OnDestroy或通过一个明确的退出方法中,调用SceneLoader.Instance.UnloadScene("HUD");来卸载它。
-
七、部署场景与疑难解答
部署场景
-
所有平台通用:场景管理是 Unity 引擎的核心功能,对所有构建平台(PC, Mac, Mobile, WebGL, Consoles)的支持是一致的。
-
构建设置:务必在 File -> Build Settings 中添加所有需要用到的场景,否则
SceneManager.LoadScene会失败。
疑难解答
-
问题:
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,将对应的场景文件拖入列表中。
-
-
问题:
DontDestroyOnLoad的对象重复创建。-
原因:在多个场景中都存在该对象的原始版本,导致单例逻辑失效,创建了多个实例。
-
解决:确保只有一个场景(通常是主菜单或启动场景)包含该对象的原始版本。其他场景应通过代码查找 (
FindObjectOfType) 来获取已有的单例实例,而不是重新创建。
-
-
问题:场景切换时出现明显的卡顿或冻结。
-
原因:使用了同步加载 (
LoadScene) 加载了一个非常大的场景。 -
解决:改用异步加载 (
LoadSceneAsync) 并结合加载界面,将加载工作分摊到多帧中执行。
-
-
问题:卸载叠加场景失败或不彻底。
-
原因:可能有其他对象(尤其是通过
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, 玩家数据
|
核心工作流建议:
-
拥抱异步:养成使用
LoadSceneAsync的习惯,这是专业游戏的标配。 -
善用单例:对于
GameManager这类全局对象,使用单例模式配合DontDestroyOnLoad是可靠的做法,但要警惕其副作用。 -
规划场景结构:提前思考好哪些部分是单一场景,哪些需要叠加,哪些需要动态加载/卸载,这能避免后期的重构痛苦。
-
利用事件:使用
SceneManager.sceneLoaded等事件来响应场景变化,而不是在每个加载调用后写一堆零散的逻辑。
掌握 Unity 的场景管理是驾驭复杂项目的关键一步,它不仅是技术的体现,更是优秀软件架构思想的实践。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)