Unity 场景管理核心教程:从 LoadScene 到 Loading Screen 实战 (Day 35)

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)



前言

欢迎来到《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 Slider
    public 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}%");

            // (可选)更新 UI
            if (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,这是 LoadSceneLoadSceneAsync 的默认模式),旧场景会自动卸载,通常不需要手动调用。

三、场景间数据传递

切换场景时,一个常见的需求是如何将数据(如玩家得分、选择的角色、关卡进度等)从一个场景传递到另一个场景。默认情况下,加载新场景会销毁前一个场景中的所有对象,导致数据丢失。以下是几种常用的数据传递方法:

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 是理想方案。
  • 组合使用: 实际项目中,常常是多种方法结合使用。例如,用 DontDestroyOnLoadGameManager 存储玩家动态数据,并持有对当前关卡 ScriptableObject 的引用。

四、制作加载界面(Loading Screen)

使用异步加载 LoadSceneAsync 时,可以制作一个加载界面,提升用户体验。

4.1 为何需要加载界面?

  • 提供反馈: 告知用户游戏没有卡死,正在加载中。
  • 改善感知性能: 即使用户需要等待,一个动态的加载界面(如进度条、提示信息、小动画)也比冻结的屏幕感觉更好。
  • 娱乐/信息: 可以在加载界面显示游戏提示、故事背景、或者有趣的动画。

4.2 实现思路

一种常见的实现方式是:

  1. 创建加载场景 (Loading Scene): 一个非常轻量的独立场景,包含 UI 元素(如背景图、进度条 Slider、百分比 Text)。
  2. 触发加载: 当需要加载目标场景(如 “GameLevel1”)时,首先同步加载这个轻量的 “LoadingScene”。
  3. 在 LoadingScene 中执行异步加载: 在 LoadingScene 中放置一个脚本(例如 LoadingScreenManager),其 Start() 方法或一个触发方法会启动异步加载真正的目标场景 (LoadSceneAsync("GameLevel1"))。
  4. 更新进度: 在该脚本的协程中,持续获取 AsyncOperation.progress 并更新 UI 上的进度条和百分比文本。
  5. (可选)控制激活: 可以设置 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. 同步加载 LoadingScene
        SceneManager.LoadScene("LoadingScene");
    }

    // 示例:按钮点击事件调用
    public void OnStartButtonClicked()
    {
        StartGameWithLoadingScreen("GameLevel1"); // 假设目标场景是 GameLevel1
    }
}

五、实践:主菜单与游戏场景切换

现在,我们将运用所学知识,创建一个简单的主菜单场景和一个游戏场景,并实现它们之间的切换。

5.1 创建场景

  1. 主菜单场景 (MainMenu):
    • 创建一个新场景,保存为 “MainMenu.unity”。
    • 在场景中添加 Canvas。
    • 在 Canvas 下添加一个 Button,修改其 Text 为 “开始游戏”。
    • (可选)添加一些背景图、标题文本等。
  2. 游戏场景 (GameScene):
    • 创建另一个新场景,保存为 “GameScene.unity”。
    • 在场景中添加一些简单的内容,比如一个 Cube 或 Sphere,以便区分。
    • (可选)添加一个 Button,文本为 “返回主菜单”。

5.2 主菜单UI与脚本

  1. 在 “MainMenu” 场景中,创建一个新的 C# 脚本,命名为 MainMenuController
  2. 将以下代码添加到 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");
    }
}
  1. 在 “MainMenu” 场景中,创建一个空 GameObject,命名为 “MainMenuManager”。
  2. MainMenuController 脚本挂载到 “MainMenuManager” GameObject 上。
  3. 选中 “开始游戏” 按钮,在 Inspector 窗口找到 Button 组件下的 On Click () 事件列表。
  4. 点击 + 号添加一个事件。
  5. 将 “MainMenuManager” GameObject 拖拽到事件的 None (Object) 字段上。
  6. 在右侧的下拉菜单中,选择 MainMenuController -> StartGame()

5.3 游戏场景返回菜单(可选)

  1. 在 “GameScene” 场景中,创建一个新的 C# 脚本,命名为 GameSceneController
  2. 添加代码:
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameSceneController : MonoBehaviour
{
    public string mainMenuSceneName = "MainMenu"; // 要返回的主菜单场景名称

    public void ReturnToMainMenu()
    {
        Debug.Log($"正在返回主菜单: {mainMenuSceneName}");
        SceneManager.LoadScene(mainMenuSceneName);
    }
}
  1. 类似地,在 “GameScene” 创建一个 “GameManager” GameObject,挂载 GameSceneController 脚本。
  2. 如果添加了 “返回主菜单” 按钮,配置其 On Click () 事件调用 GameSceneControllerReturnToMainMenu() 方法。

5.4 构建设置(Build Settings)

这是非常关键的一步!

  1. 打开 File -> Build Settings...
  2. Scenes In Build 区域:
    • 确保 “MainMenu” 场景在列表中。如果不在,打开 “MainMenu” 场景,然后点击 “Add Open Scenes” 按钮。
    • 确保 “GameScene” 场景也在列表中。打开 “GameScene” 场景,再次点击 “Add Open Scenes”。
    • (如果创建了)确保 “LoadingScene” 也在列表中。
  3. 注意场景的索引(列表中的数字)。通常,索引 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 中场景管理与切换的核心知识和技术:

  1. 场景概念与必要性: 理解了场景是组织游戏内容的基础单元,场景管理有助于项目结构化和性能优化。
  2. SceneManagement API: 掌握了使用 UnityEngine.SceneManagement 命名空间,特别是 SceneManager 类进行场景操作。
  3. 场景加载: 学会了使用 LoadScene (同步) 和 LoadSceneAsync (异步) 加载场景,并理解了它们的原理、优缺点及适用场景。异步加载配合协程可以实现非阻塞加载和进度反馈。
  4. 场景间数据传递: 探索了三种主要的数据传递方法——静态变量、DontDestroyOnLoad(常结合单例模式)和 ScriptableObject,并了解了各自的适用场景与利弊。
  5. 加载界面: 学习了如何利用异步加载制作加载界面 (Loading Screen),通过一个中间的 “LoadingScene” 或直接在当前场景显示进度,提升用户体验。
  6. 实践: 通过创建主菜单和游戏场景,并实现它们之间的切换,巩固了所学知识,特别是场景的添加(Build Settings)和通过脚本触发加载。

熟练掌握场景管理是构建流畅、完整游戏体验的关键一步。在后续的开发中,你会频繁地与场景打交道。继续努力,下一课我们将探索更多 Unity 的高级主题!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴师兄大模型

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值