Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
27-Unity协程从入门到精通:告别卡顿,用Coroutine优雅处理异步与时序任务 (Day 27)
28-搞定玩家控制!Unity输入系统、物理引擎、碰撞检测实战指南 (Day 28)
29-# Unity动画控制核心:Animator状态机与C#脚本实战指南 (Day 29)
30-Unity UI 从零到精通 (第30天): Canvas、布局与C#交互实战 (Day 30)
31-Unity性能优化利器:彻底搞懂对象池技术(附C#实现与源码解析)
32-Unity C#进阶:用状态模式与FSM优雅管理复杂敌人AI,告别Spaghetti Code!(Day32)
33-Unity游戏开发实战:从PlayerPrefs到JSON,精通游戏存档与加载机制(Day 33)
34-Unity C# 实战:从零开始为游戏添加背景音乐与音效 (AudioSource/AudioClip/AudioMixer 详解)(Day 34)
35-Unity 场景管理核心教程:从 LoadScene 到 Loading Screen 实战 (Day 35)
文章目录
- Langchain系列文章目录
- PyTorch系列文章目录
- Python系列文章目录
- C#系列文章目录
- 前言
- 一、场景管理基础
- 1.1 什么是场景?
- 1.2 为何需要场景管理?
- 1.3 UnityEngine.SceneManagement 命名空间
- 二、场景加载与卸载
- 2.1 同步加载:LoadScene
- 2.1.1 工作原理
- 2.1.2 使用方法
- 2.1.3 优缺点与适用场景
- 2.2 异步加载:LoadSceneAsync
- 2.2.1 工作原理
- 2.2.2 使用方法
- 2.2.3 优缺点与适用场景
- 2.3 场景卸载 (UnloadSceneAsync)
- 三、场景间数据传递
- 3.1 静态变量(Static Variables)
- 3.1.1 原理与实现
- 3.1.2 优缺点
- 3.2 DontDestroyOnLoad
- 3.2.1 原理与实现
- 3.2.2 优缺点
- 3.3 ScriptableObject
- 3.3.1 原理与实现
- 3.3.2 优缺点
- 3.4 方法选择建议
- 四、制作加载界面(Loading Screen)
- 4.1 为何需要加载界面?
- 4.2 实现思路
- 4.3 结合 LoadSceneAsync 实现
- (1) 创建加载场景 (LoadingScene.unity)
- (2) 编写加载逻辑脚本 (LoadingScreenManager.cs)
- (3) 如何触发加载流程
- 五、实践:主菜单与游戏场景切换
- 5.1 创建场景
- 5.2 主菜单UI与脚本
- 5.3 游戏场景返回菜单(可选)
- 5.4 构建设置(Build Settings)
- 六、常见问题与注意事项
- 6.1 场景未添加到 Build Settings
- 6.2 DontDestroyOnLoad 对象重复
- 6.3 异步加载卡顿
- 6.4 数据传递方案的选择
- 七、总结
前言
欢迎来到《C# for Unity 学习之旅》的第 35 天!在之前的学习中,我们已经掌握了 C# 的核心语法、面向对象编程、数据结构以及 Unity 的一些关键机制,如动画、UI、对象池、状态机、存档和音频。今天,我们将聚焦于游戏开发中一个至关重要的环节——场景管理与切换。几乎所有的游戏都需要在不同的“世界”或“界面”之间进行跳转,例如从主菜单进入游戏关卡,从关卡1切换到关卡2,或者在游戏结束后返回得分界面。理解并熟练运用 Unity 的场景管理系统,是构建完整游戏流程的基础。本文将带你深入了解 UnityEngine.SceneManagement
命名空间,学习如何加载和卸载场景(包括同步与异步方式),探讨场景间数据传递的常用方法,并动手实践制作一个加载界面以及实现主菜单到游戏场景的切换。
一、场景管理基础
1.1 什么是场景?
在 Unity 中,一个场景 (Scene) 可以理解为一个独立的游戏世界或界面容器。它包含了游戏环境(地形、灯光、天空盒)、角色、道具、UI 元素以及驱动这一切的逻辑脚本等所有游戏对象。你可以将场景想象成电影的一个“场景”或戏剧的一“幕”,每个场景承载着特定的游戏内容或功能。
常见场景类型:
- 启动场景 (Splash Screen)
- 主菜单场景 (Main Menu)
- 游戏关卡场景 (Level 1, Level 2…)
- 加载场景 (Loading Screen)
- 设置场景 (Settings Menu)
- 游戏结束/得分场景 (Game Over / Score Screen)
1.2 为何需要场景管理?
将游戏内容组织到不同的场景中,主要有以下好处:
- 内容组织与模块化: 将不同功能或关卡分离到不同场景,使项目结构更清晰,便于团队协作和维护。
- 性能优化: 只加载当前需要的资源,避免一次性将所有游戏内容载入内存,降低内存占用和初始加载时间。尤其对于大型游戏,按需加载关卡至关重要。
- 工作流效率: 开发者可以专注于单个场景的编辑和测试,提高开发效率。
1.3 UnityEngine.SceneManagement 命名空间
Unity 提供了专门用于场景管理的 API,它们都位于 UnityEngine.SceneManagement
命名空间下。使用这些 API 前,需要在脚本开头添加 using UnityEngine.SceneManagement;
。
该命名空间下的核心类是 SceneManager
,它提供了加载、卸载、获取当前场景信息等静态方法。
// 引入场景管理命名空间
using UnityEngine.SceneManagement;public class ExampleScript : MonoBehaviour
{void Start(){// 获取当前活动场景的名字string currentSceneName = SceneManager.GetActiveScene().name;Debug.Log("当前场景名: " + currentSceneName);// 获取当前已加载场景的数量int sceneCount = SceneManager.sceneCount;Debug.Log("已加载场景数: " + sceneCount);}
}
二、场景加载与卸载
SceneManager
提供了多种加载场景的方式,最常用的是同步加载和异步加载。
2.1 同步加载:LoadScene
2.1.1 工作原理
SceneManager.LoadScene()
方法会立即开始加载指定的场景。在加载完成之前,它会阻塞游戏的主线程,导致游戏画面冻结,直到新场景完全加载并准备好运行。
2.1.2 使用方法
你可以通过场景的名称(字符串)或其在 Build Settings 中的索引(整数)来加载场景。
using UnityEngine;
using UnityEngine.SceneManagement; // 别忘了引入public class SceneLoaderSync : MonoBehaviour
{// 方法一:通过场景名称加载 (推荐,更直观)public void LoadSceneByName(string sceneName){Debug.Log($"开始同步加载场景: {sceneName}");SceneManager.LoadScene(sceneName);// 加载完成后,下面的代码不会立即执行,因为场景已经切换}// 方法二:通过场景在 Build Settings 中的索引加载public void LoadSceneByIndex(int sceneBuildIndex){// 确保索引有效if (sceneBuildIndex >= 0 && sceneBuildIndex < SceneManager.sceneCountInBuildSettings){Debug.Log($"开始同步加载场景,索引: {sceneBuildIndex}");SceneManager.LoadScene(sceneBuildIndex);}else{Debug.LogError($"无效的场景索引: {sceneBuildIndex}");}}// 示例:在某个事件触发时加载名为 "GameLevel1" 的场景public void StartGame(){LoadSceneByName("GameLevel1");}
}
注意: 要加载的场景必须被添加到项目的 File -> Build Settings... -> Scenes In Build
列表中。否则 LoadScene
会报错。
2.1.3 优缺点与适用场景
- 优点: 实现简单直接。
- 缺点: 加载过程中游戏会完全卡住,用户体验较差,尤其对于加载时间较长的场景。
- 适用场景:
- 加载非常轻量、快速的场景(如简单的菜单、Game Over 界面)。
- 游戏启动时从启动场景加载到主菜单(此时短暂的卡顿通常可以接受)。
- 对流畅度要求不高的原型开发阶段。
2.2 异步加载:LoadSceneAsync
2.2.1 工作原理
SceneManager.LoadSceneAsync()
方法会在后台线程中加载场景。这意味着加载过程不会阻塞主线程,游戏可以在场景加载时继续运行(例如播放动画、响应输入、显示加载进度)。
LoadSceneAsync
返回一个 AsyncOperation
对象,你可以通过这个对象查询加载进度、判断是否加载完成,甚至控制场景加载到 90% 后暂停,等待某个时机再激活。
2.2.2 使用方法
异步加载通常需要配合协程 (Coroutine) 来使用,以便在加载过程中进行轮询或等待。
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections; // 需要引入 Coroutine 相关命名空间
using UnityEngine.UI; // 如果需要更新 UI,比如进度条public class SceneLoaderAsync : MonoBehaviour
{public Slider loadingProgressBar; // (可选)用于显示进度的 UI Sliderpublic Text loadingPercentageText; // (可选)用于显示百分比的 UI Text// 启动异步加载的公共方法public void LoadSceneAsyncByName(string sceneName){StartCoroutine(LoadSceneInBackground(sceneName));}private IEnumerator LoadSceneInBackground(string sceneName){Debug.Log($"开始异步加载场景: {sceneName}");// 开始异步加载场景AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);// (可选)禁止场景在加载完成后自动激活// asyncLoad.allowSceneActivation = false;// 循环直到场景加载完成(但不一定激活)while (!asyncLoad.isDone){// 获取加载进度(范围 0.0 到 1.0)// 注意:进度到 0.9 时表示加载已完成,剩下的 0.1 是激活过程float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f);Debug.Log($"加载进度: {progress * 100}%");// (可选)更新 UIif (loadingProgressBar != null){loadingProgressBar.value = progress;}if (loadingPercentageText != null){loadingPercentageText.text = $"Loading... {Mathf.RoundToInt(progress * 100)}%";}// 如果设置了 allowSceneActivation = false,// 可以在这里检查条件,比如进度达到 100% 并且用户按下了某个键// if (progress >= 1.0f && Input.GetKeyDown(KeyCode.Space))// {// asyncLoad.allowSceneActivation = true; // 手动激活场景// }// 等待下一帧,避免阻塞主线程yield return null;}Debug.Log($"场景 {sceneName} 加载完成并激活!");// 场景加载并激活后,此协程所在的 GameObject 会被销毁(除非它是 DontDestroyOnLoad)}// 示例:在某个事件触发时异步加载名为 "GameLevel1" 的场景public void StartGameAsync(){// 通常在调用异步加载前,会先加载一个轻量的 "Loading" 场景// 然后在 Loading 场景的脚本中调用 LoadSceneAsyncByName("GameLevel1")// 这里为了简化,直接调用LoadSceneAsyncByName("GameLevel1");}
}
2.2.3 优缺点与适用场景
- 优点:
- 加载过程不阻塞主线程,游戏保持响应。
- 可以显示加载进度,提升用户体验。
- 可以控制场景激活时机(
allowSceneActivation
)。
- 缺点: 实现相对复杂,需要使用协程。
- 适用场景:
- 加载内容较多、耗时较长的游戏关卡。
- 需要制作加载界面 (Loading Screen) 的情况。
- 追求流畅用户体验的游戏。
2.3 场景卸载 (UnloadSceneAsync)
除了加载场景,有时也需要卸载不再需要的场景,特别是当使用叠加加载 (Additive Loading) 模式时(即同时加载多个场景,LoadSceneMode.Additive
)。SceneManager.UnloadSceneAsync()
用于异步卸载指定场景。对于我们主要讨论的单场景加载模式(LoadSceneMode.Single
,这是 LoadScene
和 LoadSceneAsync
的默认模式),旧场景会自动卸载,通常不需要手动调用。
三、场景间数据传递
切换场景时,一个常见的需求是如何将数据(如玩家得分、选择的角色、关卡进度等)从一个场景传递到另一个场景。默认情况下,加载新场景会销毁前一个场景中的所有对象,导致数据丢失。以下是几种常用的数据传递方法:
3.1 静态变量(Static Variables)
3.1.1 原理与实现
静态变量属于类本身,而不是类的任何特定实例。它们在程序的整个生命周期内存在,并且可以跨场景访问。
// 示例:一个静态类用于存储全局游戏数据
public static class GameData
{public static int PlayerScore = 0;public static string SelectedCharacter = "DefaultHero";public static int CurrentLevel = 1;
}// 在场景 A 中修改数据
public class SceneAScript : MonoBehaviour
{void UpdateScore(int points){GameData.PlayerScore += points;Debug.Log($"场景 A 更新分数: {GameData.PlayerScore}");}public void GoToSceneB(){GameData.CurrentLevel = 2; // 记录要去往的关卡SceneManager.LoadScene("SceneB");}
}// 在场景 B 中读取数据
public class SceneBScript : MonoBehaviour
{void Start(){Debug.Log($"场景 B 读取到分数: {GameData.PlayerScore}");Debug.Log($"场景 B 读取到角色: {GameData.SelectedCharacter}");Debug.Log($"场景 B 当前关卡: {GameData.CurrentLevel}");}
}
3.1.2 优缺点
- 优点: 实现简单,易于访问。
- 缺点:
- 滥用静态变量可能导致代码耦合度高,难以维护和测试(全局状态)。
- 数据在内存中持久存在,可能不适用于所有情况。
- 不利于面向对象的设计原则。
3.2 DontDestroyOnLoad
3.2.1 原理与实现
Object.DontDestroyOnLoad(target)
是一个 Unity 内置方法,它可以阻止目标 GameObject
在加载新场景时被销毁。通常将需要跨场景传递数据的脚本挂载到这样一个持久化的 GameObject 上。
这种方法常与单例模式 (Singleton Pattern) 结合使用,确保全局只有一个实例负责管理数据。
using UnityEngine;
using UnityEngine.SceneManagement;public class PersistentDataManager : MonoBehaviour
{public static PersistentDataManager Instance { get; private set; } // 静态实例引用public int PlayerScore = 0;public string PlayerName = "Guest";private void Awake(){// 单例模式实现:确保只有一个实例存在if (Instance == null){Instance = this;DontDestroyOnLoad(gameObject); // 让这个 GameObject 在场景切换时不被销毁Debug.Log("PersistentDataManager 初始化并设置为 DontDestroyOnLoad");}else if (Instance != this){// 如果已存在实例,并且不是当前这个,则销毁当前这个,避免重复Debug.LogWarning("已存在 PersistentDataManager 实例,销毁当前重复的 GameObject");Destroy(gameObject);}}// 提供一些方法来修改或获取数据public void AddScore(int points){PlayerScore += points;Debug.Log($"分数增加: {points}, 总分: {PlayerScore}");}
}// 在其他脚本中访问
public class ScoreUpdater : MonoBehaviour
{void Start(){// 通过单例访问数据if (PersistentDataManager.Instance != null){Debug.Log($"当前分数: {PersistentDataManager.Instance.PlayerScore}");PersistentDataManager.Instance.AddScore(10);}}
}
3.2.2 优缺点
- 优点:
- 非常适合管理需要贯穿整个游戏会话的数据(如玩家档案、游戏设置、全局管理器)。
- 保持了对象的封装性。
- 缺点:
- 需要谨慎管理,避免意外创建多个
DontDestroyOnLoad
对象,导致逻辑错误或资源浪费(单例模式有助于解决)。 - 如果持久化对象引用了只在特定场景存在的资源,可能导致内存泄漏。
- 需要谨慎管理,避免意外创建多个
3.3 ScriptableObject
3.3.1 原理与实现
ScriptableObject 是 Unity 中一种可以用来存储大量共享数据的资源文件。它们独立于场景存在,可以直接在 Project 窗口创建和编辑。虽然它们本身不直接“传递”动态运行时数据,但非常适合存储配置数据或预设状态,这些数据可以在任何场景中被加载和读取。
例如,你可以创建一个 LevelData
类型的 ScriptableObject 来存储每个关卡的配置(敌人类型、数量、时间限制等)。当加载某个关卡时,对应的 LevelData
资源会被读取。
// 1. 定义 ScriptableObject 类
using UnityEngine;[CreateAssetMenu(fileName = "LevelData", menuName = "Game/Level Data", order = 1)]
public class LevelData : ScriptableObject
{public string levelName = "New Level";public int enemyCount = 10;public float timeLimit = 120f;public GameObject enemyPrefab;// ... 其他关卡相关数据
}// 2. 在 Unity Editor 中创建 LevelData 资源 (e.g., Level1Data.asset)// 3. 在需要加载关卡的脚本中引用并使用
using UnityEngine;
using UnityEngine.SceneManagement;public class LevelLoader : MonoBehaviour
{public LevelData levelToLoad; // 在 Inspector 中拖入对应的 LevelData 资源public void LoadLevel(){if (levelToLoad != null){// 在加载场景前,可以将 LevelData 的引用传递给下一场景// (可以使用 DontDestroyOnLoad 的管理器,或静态变量,或 PlayerPrefs 等)// 这里简化,假设下一场景能自行获取需要的数据源Debug.Log($"准备加载关卡: {levelToLoad.levelName}, 敌人数量: {levelToLoad.enemyCount}");// 可以将 ScriptableObject 的引用存入一个静态变量或 DontDestroyOnLoad 对象中GameManager.CurrentLevelData = levelToLoad; // 假设 GameManager 是单例或静态类SceneManager.LoadScene(levelToLoad.levelName); // 或者使用场景索引}else{Debug.LogError("未指定要加载的 LevelData!");}}
}// 在新加载的关卡场景的脚本中读取数据
public class LevelSetup : MonoBehaviour
{void Start(){LevelData currentData = GameManager.CurrentLevelData; // 从数据源获取if (currentData != null){Debug.Log($"正在设置关卡: {currentData.levelName}");// 根据 currentData 中的信息生成敌人、设置计时器等// SpawnEnemies(currentData.enemyCount, currentData.enemyPrefab);}else{Debug.LogError("无法获取当前关卡的 LevelData!");}}
}
3.3.2 优缺点
- 优点:
- 数据与场景和代码解耦,非常适合管理游戏配置和设计数据。
- 易于在编辑器中创建和修改,方便策划调整。
- 作为资源文件,易于版本控制和管理。
- 缺点:
- 不直接适用于传递场景切换瞬间产生的动态运行时状态(如玩家精确位置、临时效果持续时间),仍需结合其他方法(如
DontDestroyOnLoad
的管理器)来传递或应用这些动态数据。
- 不直接适用于传递场景切换瞬间产生的动态运行时状态(如玩家精确位置、临时效果持续时间),仍需结合其他方法(如
3.4 方法选择建议
- 简单临时数据/标记: 少量、非关键数据,或仅用于标记下一个场景的行为,静态变量可能足够,但要注意风险。
- 全局状态/管理器: 需要贯穿游戏始终的数据(玩家信息、设置、进度)或系统(音频管理器、存档管理器),
DontDestroyOnLoad
+ 单例模式是常用且强大的选择。 - 关卡配置/预设数据: 定义关卡、角色、物品属性等不常变动的共享数据,ScriptableObject 是理想方案。
- 组合使用: 实际项目中,常常是多种方法结合使用。例如,用
DontDestroyOnLoad
的GameManager
存储玩家动态数据,并持有对当前关卡ScriptableObject
的引用。
四、制作加载界面(Loading Screen)
使用异步加载 LoadSceneAsync
时,可以制作一个加载界面,提升用户体验。
4.1 为何需要加载界面?
- 提供反馈: 告知用户游戏没有卡死,正在加载中。
- 改善感知性能: 即使用户需要等待,一个动态的加载界面(如进度条、提示信息、小动画)也比冻结的屏幕感觉更好。
- 娱乐/信息: 可以在加载界面显示游戏提示、故事背景、或者有趣的动画。
4.2 实现思路
一种常见的实现方式是:
- 创建加载场景 (Loading Scene): 一个非常轻量的独立场景,包含 UI 元素(如背景图、进度条 Slider、百分比 Text)。
- 触发加载: 当需要加载目标场景(如 “GameLevel1”)时,首先同步加载这个轻量的 “LoadingScene”。
- 在 LoadingScene 中执行异步加载: 在 LoadingScene 中放置一个脚本(例如
LoadingScreenManager
),其Start()
方法或一个触发方法会启动异步加载真正的目标场景 (LoadSceneAsync("GameLevel1")
)。 - 更新进度: 在该脚本的协程中,持续获取
AsyncOperation.progress
并更新 UI 上的进度条和百分比文本。 - (可选)控制激活: 可以设置
asyncLoad.allowSceneActivation = false;
,让场景加载到 90% 后暂停,直到满足某些条件(例如进度条动画播放完毕、或者等待至少一小段时间避免闪烁)再设置为true
以完成场景切换。
4.3 结合 LoadSceneAsync 实现
(1) 创建加载场景 (LoadingScene.unity)
- 创建一个新场景,命名为 “LoadingScene”。
- 在场景中添加 Canvas,并在 Canvas 下添加:
- 一个 Image 作为背景。
- 一个 Slider 作为进度条。
- 一个 Text (或 TextMeshPro) 显示 “Loading…” 或百分比。
- 确保该场景也添加到了 Build Settings 中。
(2) 编写加载逻辑脚本 (LoadingScreenManager.cs)
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;public class LoadingScreenManager : MonoBehaviour
{public Slider progressBar;public Text percentageText;// 需要静态变量或其他方式来传递要加载的目标场景名public static string sceneToLoad;void Start(){if (string.IsNullOrEmpty(sceneToLoad)){Debug.LogError("目标场景名未设置!请在加载 LoadingScene 前设置 LoadingScreenManager.sceneToLoad");// 可以考虑加载一个默认场景,比如主菜单// SceneManager.LoadScene("MainMenu");return;}// 启动异步加载协程StartCoroutine(LoadSceneAsyncProcess(sceneToLoad));}private IEnumerator LoadSceneAsyncProcess(string sceneName){AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);// (可选)禁止自动激活,直到加载快完成asyncLoad.allowSceneActivation = false;while (!asyncLoad.isDone){// progress 值在 0.0 到 0.9 之间表示真实加载进度// 当达到 0.9 时,表示加载完毕,等待激活float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f);if (progressBar != null){progressBar.value = progress;}if (percentageText != null){percentageText.text = $"Loading... {Mathf.RoundToInt(progress * 100)}%";}// 当加载即将完成时 (progress >= 1.0f 实际对应 asyncLoad.progress >= 0.9f)if (asyncLoad.progress >= 0.9f){if (percentageText != null){percentageText.text = "Press Space to continue..."; // 提示可以激活}// 在这里可以加一些延迟或者等待用户输入if (Input.GetKeyDown(KeyCode.Space)) // 示例:按空格键激活{Debug.Log("手动激活场景!");asyncLoad.allowSceneActivation = true;}// 或者无条件激活:// yield return new WaitForSeconds(0.5f); // 短暂等待,防止闪烁// asyncLoad.allowSceneActivation = true;}yield return null; // 等待下一帧}}
}
(3) 如何触发加载流程
在主菜单或其他需要切换场景的地方,执行以下操作:
using UnityEngine;
using UnityEngine.SceneManagement;public class MainMenu : MonoBehaviour
{public void StartGameWithLoadingScreen(string targetSceneName){// 1. 设置要加载的目标场景名LoadingScreenManager.sceneToLoad = targetSceneName;// 2. 同步加载 LoadingSceneSceneManager.LoadScene("LoadingScene");}// 示例:按钮点击事件调用public void OnStartButtonClicked(){StartGameWithLoadingScreen("GameLevel1"); // 假设目标场景是 GameLevel1}
}
五、实践:主菜单与游戏场景切换
现在,我们将运用所学知识,创建一个简单的主菜单场景和一个游戏场景,并实现它们之间的切换。
5.1 创建场景
- 主菜单场景 (MainMenu):
- 创建一个新场景,保存为 “MainMenu.unity”。
- 在场景中添加 Canvas。
- 在 Canvas 下添加一个 Button,修改其 Text 为 “开始游戏”。
- (可选)添加一些背景图、标题文本等。
- 游戏场景 (GameScene):
- 创建另一个新场景,保存为 “GameScene.unity”。
- 在场景中添加一些简单的内容,比如一个 Cube 或 Sphere,以便区分。
- (可选)添加一个 Button,文本为 “返回主菜单”。
5.2 主菜单UI与脚本
- 在 “MainMenu” 场景中,创建一个新的 C# 脚本,命名为
MainMenuController
。 - 将以下代码添加到
MainMenuController.cs
:
using UnityEngine;
using UnityEngine.SceneManagement;public class MainMenuController : MonoBehaviour
{public string gameSceneName = "GameScene"; // 要加载的游戏场景名称// 公共方法,将由按钮的 OnClick 事件调用public void StartGame(){Debug.Log($"正在加载场景: {gameSceneName}");// 使用同步加载(简单)SceneManager.LoadScene(gameSceneName);// 或者,如果想使用上面实现的带加载界面的异步加载:// LoadingScreenManager.sceneToLoad = gameSceneName;// SceneManager.LoadScene("LoadingScene");}
}
- 在 “MainMenu” 场景中,创建一个空 GameObject,命名为 “MainMenuManager”。
- 将
MainMenuController
脚本挂载到 “MainMenuManager” GameObject 上。 - 选中 “开始游戏” 按钮,在 Inspector 窗口找到
Button
组件下的On Click ()
事件列表。 - 点击
+
号添加一个事件。 - 将 “MainMenuManager” GameObject 拖拽到事件的
None (Object)
字段上。 - 在右侧的下拉菜单中,选择
MainMenuController
->StartGame()
。
5.3 游戏场景返回菜单(可选)
- 在 “GameScene” 场景中,创建一个新的 C# 脚本,命名为
GameSceneController
。 - 添加代码:
using UnityEngine;
using UnityEngine.SceneManagement;public class GameSceneController : MonoBehaviour
{public string mainMenuSceneName = "MainMenu"; // 要返回的主菜单场景名称public void ReturnToMainMenu(){Debug.Log($"正在返回主菜单: {mainMenuSceneName}");SceneManager.LoadScene(mainMenuSceneName);}
}
- 类似地,在 “GameScene” 创建一个 “GameManager” GameObject,挂载
GameSceneController
脚本。 - 如果添加了 “返回主菜单” 按钮,配置其
On Click ()
事件调用GameSceneController
的ReturnToMainMenu()
方法。
5.4 构建设置(Build Settings)
这是非常关键的一步!
- 打开
File -> Build Settings...
。 - 在
Scenes In Build
区域:- 确保 “MainMenu” 场景在列表中。如果不在,打开 “MainMenu” 场景,然后点击 “Add Open Scenes” 按钮。
- 确保 “GameScene” 场景也在列表中。打开 “GameScene” 场景,再次点击 “Add Open Scenes”。
- (如果创建了)确保 “LoadingScene” 也在列表中。
- 注意场景的索引(列表中的数字)。通常,索引 0 是游戏启动时默认加载的场景(除非有特定配置)。你可以拖动场景来调整它们的顺序。确保主菜单 (MainMenu) 通常是索引 0 或 1(如果在索引 0 有启动 Logo 场景的话)。
现在,运行游戏。从 “MainMenu” 场景开始,点击 “开始游戏” 按钮,应该能成功切换到 “GameScene”。如果在 “GameScene” 中添加了返回按钮,点击它应该能回到 “MainMenu”。
六、常见问题与注意事项
6.1 场景未添加到 Build Settings
最常见的问题是尝试加载一个没有添加到 Build Settings -> Scenes In Build
列表中的场景。这会导致运行时错误 Scene '...' couldn't be loaded because it has not been added to the build settings.
。务必将所有需要加载的场景添加到 Build Settings。
6.2 DontDestroyOnLoad 对象重复
如果使用 DontDestroyOnLoad
的对象(如单例管理器)没有正确实现单例检查逻辑(如 Awake
中的判断),每次重新加载包含该对象预制件的场景(例如返回主菜单)时,都可能创建一个新的实例。这会导致多个实例并存,引发逻辑混乱和性能问题。务必实现可靠的单例模式检查。
6.3 异步加载卡顿
虽然 LoadSceneAsync
在后台加载,但加载过程仍然消耗 CPU 和 I/O 资源。如果场景非常复杂或设备性能较低,即使是异步加载,也可能在加载过程中或场景激活时(allowSceneActivation = true
之后)引起短暂的卡顿。优化场景资源、使用 Addressables 等资源管理系统有助于缓解此问题。
6.4 数据传递方案的选择
再次强调,没有绝对最好的数据传递方式,应根据数据类型(配置 vs 动态状态)、作用域(临时 vs 全局)、复杂度以及项目架构来选择最合适的方法或组合。
七、总结
恭喜你完成了第 35 天的学习!今天我们深入探讨了 Unity 中场景管理与切换的核心知识和技术:
- 场景概念与必要性: 理解了场景是组织游戏内容的基础单元,场景管理有助于项目结构化和性能优化。
- SceneManagement API: 掌握了使用
UnityEngine.SceneManagement
命名空间,特别是SceneManager
类进行场景操作。 - 场景加载: 学会了使用
LoadScene
(同步) 和LoadSceneAsync
(异步) 加载场景,并理解了它们的原理、优缺点及适用场景。异步加载配合协程可以实现非阻塞加载和进度反馈。 - 场景间数据传递: 探索了三种主要的数据传递方法——静态变量、
DontDestroyOnLoad
(常结合单例模式)和 ScriptableObject,并了解了各自的适用场景与利弊。 - 加载界面: 学习了如何利用异步加载制作加载界面 (Loading Screen),通过一个中间的 “LoadingScene” 或直接在当前场景显示进度,提升用户体验。
- 实践: 通过创建主菜单和游戏场景,并实现它们之间的切换,巩固了所学知识,特别是场景的添加(Build Settings)和通过脚本触发加载。
熟练掌握场景管理是构建流畅、完整游戏体验的关键一步。在后续的开发中,你会频繁地与场景打交道。继续努力,下一课我们将探索更多 Unity 的高级主题!