跳到主要内容

场景管理

场景(Scene)是 Unity 组织游戏内容的基本单位。有效的场景管理对于游戏的流畅运行、内存控制和用户体验至关重要。本章将详细讲解 Unity 的场景系统,包括场景加载、卸载、异步加载、多场景编辑等核心功能。

场景概念

什么是场景?

场景是游戏对象的容器,可以理解为游戏中的一个"关卡"或"画面"。每个场景包含:

  • 游戏对象(GameObject):场景中的实体
  • 环境设置:天空盒、雾效、环境光
  • 导航数据:NavMesh 信息
  • 光照数据:光照贴图、探针

场景组织策略

合理的场景划分可以提高开发效率和运行性能:

策略说明适用场景
按关卡划分每个关卡一个场景关卡制游戏
按功能划分菜单、游戏、UI 分别成场景大多数游戏
按区域划分开放世界的不同区域开放世界游戏
按功能模块地形、角色、UI 作为独立场景大型项目

典型场景结构

项目场景结构:
├── Boot.unity # 启动场景(加载核心系统)
├── MainMenu.unity # 主菜单
├── Loading.unity # 加载场景
├── Level_01.unity # 关卡1
├── Level_02.unity # 关卡2
├── GameUI.unity # 游戏UI(附加加载)
└── Environment/ # 环境场景(附加加载)
├── Terrain.unity
├── Lighting.unity
└── Audio.unity

场景基础操作

编辑器操作

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;

public class EditorSceneOperations
{
// 创建新场景
[MenuItem("Scenes/Create New Scene")]
static void CreateNewScene()
{
Scene newScene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
EditorSceneManager.SaveScene(newScene, "Assets/Scenes/NewScene.unity");
}

// 打开场景
[MenuItem("Scenes/Open Main Menu")]
static void OpenMainMenu()
{
EditorSceneManager.OpenScene("Assets/Scenes/MainMenu.unity");
}

// 保存当前场景
[MenuItem("Scenes/Save Current Scene")]
static void SaveCurrentScene()
{
Scene activeScene = EditorSceneManager.GetActiveScene();
EditorSceneManager.SaveScene(activeScene);
}
}
#endif

运行时场景信息

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneInfo : MonoBehaviour
{
void Start()
{
// 获取当前活动场景
Scene activeScene = SceneManager.GetActiveScene();

Debug.Log($"场景名称: {activeScene.name}");
Debug.Log($"场景路径: {activeScene.path}");
Debug.Log($"构建索引: {activeScene.buildIndex}");
Debug.Log($"是否已加载: {activeScene.isLoaded}");
Debug.Log($"根对象数量: {activeScene.rootCount}");

// 获取所有已加载的场景
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
Debug.Log($"已加载场景 {i}: {scene.name}");
}

// 遍历场景中的根对象
GameObject[] rootObjects = activeScene.GetRootGameObjects();
foreach (var obj in rootObjects)
{
Debug.Log($"根对象: {obj.name}");
}
}
}

同步场景加载

LoadScene 基本用法

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
// 按名称加载场景
public void LoadSceneByName(string sceneName)
{
SceneManager.LoadScene(sceneName);
}

// 按构建索引加载场景
public void LoadSceneByIndex(int buildIndex)
{
SceneManager.LoadScene(buildIndex);
}

// 加载场景示例
public void LoadMainMenu()
{
SceneManager.LoadScene("MainMenu");
}

public void LoadLevel1()
{
SceneManager.LoadScene(1); // 假设关卡1的构建索引是1
}
}

加载模式

public class SceneLoadModes : MonoBehaviour
{
// Single 模式:卸载当前所有场景,加载新场景
public void LoadSingle(string sceneName)
{
SceneManager.LoadScene(sceneName, LoadSceneMode.Single);
}

// Additive 模式:保留当前场景,额外加载新场景
public void LoadAdditive(string sceneName)
{
SceneManager.LoadScene(sceneName, LoadSceneMode.Additive);
}

// 示例:加载游戏UI作为附加场景
public void LoadGameUI()
{
SceneManager.LoadScene("GameUI", LoadSceneMode.Additive);
}
}

LoadSceneMode 对比

模式行为使用场景
Single卸载所有当前场景,只保留新加载的场景关卡切换、返回主菜单
Additive保留当前场景,额外加载新场景动态加载地形、UI、环境

场景卸载

public class SceneUnloader : MonoBehaviour
{
// 卸载指定场景
public void UnloadScene(string sceneName)
{
// 检查场景是否已加载
Scene scene = SceneManager.GetSceneByName(sceneName);

if (scene.isLoaded)
{
SceneManager.UnloadSceneAsync(sceneName);
}
}

// 卸载除指定场景外的所有场景
public void UnloadAllExcept(string sceneToKeep)
{
for (int i = SceneManager.sceneCount - 1; i >= 0; i--)
{
Scene scene = SceneManager.GetSceneAt(i);

if (scene.name != sceneToKeep && scene.isLoaded)
{
SceneManager.UnloadSceneAsync(scene);
}
}
}
}

异步场景加载

异步加载允许在后台加载场景,避免游戏卡顿,同时可以显示加载进度。

LoadSceneAsync 基本用法

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class AsyncSceneLoader : MonoBehaviour
{
public string sceneToLoad = "Level_01";

public void LoadSceneAsync()
{
StartCoroutine(LoadSceneCoroutine());
}

IEnumerator LoadSceneCoroutine()
{
// 开始异步加载
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneToLoad);

// 禁止场景加载完成后自动切换
asyncLoad.allowSceneActivation = false;

// 等待加载完成
while (!asyncLoad.isDone)
{
// 输出进度(0 到 0.9)
Debug.Log($"加载进度: {asyncLoad.progress * 100}%");

// 进度达到 0.9 表示加载完成
if (asyncLoad.progress >= 0.9f)
{
Debug.Log("场景加载完成,准备切换");
asyncLoad.allowSceneActivation = true;
}

yield return null;
}

Debug.Log("场景切换完成");
}
}

异步加载进度说明

Unity 的异步加载进度有一个特殊机制:

  • 0.0 - 0.9:实际加载进度
  • 0.9:加载完成,等待激活
  • 1.0:场景已激活
// 正确理解进度
IEnumerator LoadWithProgress(string sceneName)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;

float displayProgress = 0f;
float targetProgress = 0f;

while (!operation.isDone)
{
// 实际进度(最高 0.9)
targetProgress = operation.progress;

// 平滑显示进度
displayProgress = Mathf.MoveTowards(displayProgress, targetProgress, Time.deltaTime);

Debug.Log($"显示进度: {displayProgress * 100}%");

if (operation.progress >= 0.9f)
{
// 加载完成,可以激活
displayProgress = 1f;
operation.allowSceneActivation = true;
}

yield return null;
}
}

加载画面实现

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class LoadingScreen : MonoBehaviour
{
[Header("UI 组件")]
public GameObject loadingPanel;
public Slider progressBar;
public Text progressText;
public Text tipText;

[Header("提示文本")]
public string[] tips = {
"提示:按空格键可以跳跃",
"提示:收集金币可以获得额外分数",
"提示:注意躲避敌人的攻击"
};

public void LoadLevel(string levelName)
{
StartCoroutine(LoadLevelAsync(levelName));
}

IEnumerator LoadLevelAsync(string levelName)
{
// 显示加载面板
loadingPanel.SetActive(true);

// 显示随机提示
if (tipText != null && tips.Length > 0)
{
tipText.text = tips[Random.Range(0, tips.Length)];
}

// 开始加载
AsyncOperation operation = SceneManager.LoadSceneAsync(levelName);
operation.allowSceneActivation = false;

float displayProgress = 0f;

while (!operation.isDone)
{
// 平滑进度条
float targetProgress = Mathf.Clamp01(operation.progress / 0.9f);
displayProgress = Mathf.Lerp(displayProgress, targetProgress, Time.deltaTime * 5f);

// 更新 UI
if (progressBar != null)
{
progressBar.value = displayProgress;
}

if (progressText != null)
{
progressText.text = $"加载中... {(int)(displayProgress * 100)}%";
}

// 加载完成
if (operation.progress >= 0.9f)
{
// 等待一小段时间让玩家看到 100%
yield return new WaitForSeconds(0.5f);

// 激活场景
operation.allowSceneActivation = true;
}

yield return null;
}

// 隐藏加载面板
loadingPanel.SetActive(false);
}
}

带动画的加载画面

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class AnimatedLoadingScreen : MonoBehaviour
{
[Header("动画设置")]
public Animator fadeAnimator;
public float fadeDuration = 0.5f;

[Header("UI 组件")]
public GameObject loadingScreen;
public Slider progressBar;
public Text progressText;

private string nextSceneName;

// 从当前场景切换到目标场景
public void TransitionToScene(string sceneName)
{
nextSceneName = sceneName;
StartCoroutine(TransitionRoutine());
}

IEnumerator TransitionRoutine()
{
// 淡出当前场景
fadeAnimator.SetTrigger("FadeOut");
yield return new WaitForSeconds(fadeDuration);

// 显示加载画面
loadingScreen.SetActive(true);

// 淡入加载画面
fadeAnimator.SetTrigger("FadeIn");
yield return new WaitForSeconds(fadeDuration);

// 开始加载
yield return StartCoroutine(LoadSceneRoutine(nextSceneName));

// 淡出加载画面
fadeAnimator.SetTrigger("FadeOut");
yield return new WaitForSeconds(fadeDuration);

// 隐藏加载画面
loadingScreen.SetActive(false);

// 淡入新场景
fadeAnimator.SetTrigger("FadeIn");
}

IEnumerator LoadSceneRoutine(string sceneName)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;

while (!operation.isDone)
{
float progress = Mathf.Clamp01(operation.progress / 0.9f);

if (progressBar != null)
progressBar.value = progress;

if (progressText != null)
progressText.text = $"加载中... {(int)(progress * 100)}%";

if (operation.progress >= 0.9f)
{
yield return new WaitForSeconds(0.3f);
operation.allowSceneActivation = true;
}

yield return null;
}
}
}

多场景管理

Additive 加载策略

使用 Additive 模式可以同时运行多个场景,适合以下场景:

  • 大型开放世界:动态加载卸载区域
  • 持久化 UI:UI 场景始终存在
  • 共享资源:环境、光照作为独立场景
public class MultiSceneManager : MonoBehaviour
{
[Header("持久化场景")]
public string persistentScene = "PersistentManagers";
public string uiScene = "GameUI";

[Header("关卡场景")]
public string[] levelScenes;

private int currentLevelIndex = -1;

void Start()
{
StartCoroutine(InitializeGame());
}

IEnumerator InitializeGame()
{
// 加载持久化管理场景
yield return SceneManager.LoadSceneAsync(persistentScene, LoadSceneMode.Additive);

// 加载 UI 场景
yield return SceneManager.LoadSceneAsync(uiScene, LoadSceneMode.Additive);

Debug.Log("游戏初始化完成");
}

// 切换关卡
public void LoadLevel(int levelIndex)
{
StartCoroutine(SwitchLevel(levelIndex));
}

IEnumerator SwitchLevel(int levelIndex)
{
// 卸载当前关卡
if (currentLevelIndex >= 0)
{
yield return SceneManager.UnloadSceneAsync(levelScenes[currentLevelIndex]);
}

// 加载新关卡
yield return SceneManager.LoadSceneAsync(levelScenes[levelIndex], LoadSceneMode.Additive);

currentLevelIndex = levelIndex;

// 设置为新关卡为活动场景(用于实例化对象)
Scene newScene = SceneManager.GetSceneByName(levelScenes[levelIndex]);
SceneManager.SetActiveScene(newScene);
}
}

动态区域加载

开放世界游戏中,根据玩家位置动态加载卸载区域:

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;

public class WorldStreamer : MonoBehaviour
{
[System.Serializable]
public class WorldRegion
{
public string sceneName;
public Vector3 center;
public Vector3 size;
public bool isLoaded;
}

public Transform player;
public WorldRegion[] regions;
public float checkInterval = 1f;
public float loadDistance = 50f;

private Dictionary<string, AsyncOperation> loadingOperations = new Dictionary<string, AsyncOperation>();

void Start()
{
StartCoroutine(CheckRegionsCoroutine());
}

IEnumerator CheckRegionsCoroutine()
{
while (true)
{
UpdateRegions();
yield return new WaitForSeconds(checkInterval);
}
}

void UpdateRegions()
{
if (player == null) return;

foreach (var region in regions)
{
float distance = GetDistanceToRegion(region);

if (distance < loadDistance && !region.isLoaded && !IsLoading(region.sceneName))
{
LoadRegion(region);
}
else if (distance > loadDistance * 1.5f && region.isLoaded)
{
UnloadRegion(region);
}
}
}

float GetDistanceToRegion(WorldRegion region)
{
// 计算玩家到区域边界的最近距离
Vector3 closestPoint = new Vector3(
Mathf.Clamp(player.position.x, region.center.x - region.size.x / 2, region.center.x + region.size.x / 2),
Mathf.Clamp(player.position.y, region.center.y - region.size.y / 2, region.center.y + region.size.y / 2),
Mathf.Clamp(player.position.z, region.center.z - region.size.z / 2, region.center.z + region.size.z / 2)
);

return Vector3.Distance(player.position, closestPoint);
}

void LoadRegion(WorldRegion region)
{
StartCoroutine(LoadRegionAsync(region));
}

IEnumerator LoadRegionAsync(WorldRegion region)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(region.sceneName, LoadSceneMode.Additive);
loadingOperations[region.sceneName] = operation;

yield return operation;

loadingOperations.Remove(region.sceneName);
region.isLoaded = true;

Debug.Log($"区域加载完成: {region.sceneName}");
}

void UnloadRegion(WorldRegion region)
{
StartCoroutine(UnloadRegionAsync(region));
}

IEnumerator UnloadRegionAsync(WorldRegion region)
{
yield return SceneManager.UnloadSceneAsync(region.sceneName);

region.isLoaded = false;
Debug.Log($"区域卸载完成: {region.sceneName}");
}

bool IsLoading(string sceneName)
{
return loadingOperations.ContainsKey(sceneName);
}

// 调试可视化
void OnDrawGizmosSelected()
{
if (regions == null) return;

foreach (var region in regions)
{
Gizmos.color = region.isLoaded ? Color.green : Color.red;
Gizmos.DrawWireCube(region.center, region.size);
}
}
}

场景合并

有时需要将多个场景合并为一个:

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public class SceneMerger
{
[MenuItem("Tools/Merge Scenes")]
static void MergeSelectedScenes()
{
// 获取选中的场景文件
var selectedScenes = Selection.GetFiltered<SceneAsset>(SelectionMode.DeepAssets);

if (selectedScenes.Length < 2)
{
Debug.LogError("请选择至少两个场景");
return;
}

// 加载第一个场景作为目标场景
string firstScenePath = AssetDatabase.GetAssetPath(selectedScenes[0]);
Scene targetScene = EditorSceneManager.OpenScene(firstScenePath);

// 合并其他场景
for (int i = 1; i < selectedScenes.Length; i++)
{
string scenePath = AssetDatabase.GetAssetPath(selectedScenes[i]);
EditorSceneManager.OpenSceneAdditive(scenePath);
}

// 保存合并后的场景
EditorSceneManager.SaveScene(targetScene);

Debug.Log("场景合并完成");
}
}
#endif

场景事件

场景加载事件

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneEventHandler : MonoBehaviour
{
void OnEnable()
{
// 订阅场景事件
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
SceneManager.activeSceneChanged += OnActiveSceneChanged;
}

void OnDisable()
{
// 取消订阅
SceneManager.sceneLoaded -= OnSceneLoaded;
SceneManager.sceneUnloaded -= OnSceneUnloaded;
SceneManager.activeSceneChanged -= OnActiveSceneChanged;
}

// 场景加载完成时调用
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log($"场景已加载: {scene.name}, 模式: {mode}");

// 根据场景执行初始化
switch (scene.name)
{
case "MainMenu":
InitMainMenu();
break;
case "Level_01":
InitLevel();
break;
}
}

// 场景卸载完成时调用
void OnSceneUnloaded(Scene scene)
{
Debug.Log($"场景已卸载: {scene.name}");

// 清理资源
Resources.UnloadUnusedAssets();
System.GC.Collect();
}

// 活动场景变更时调用
void OnActiveSceneChanged(Scene oldScene, Scene newScene)
{
Debug.Log($"活动场景从 {oldScene.name} 变更为 {newScene.name}");
}

void InitMainMenu()
{
// 主菜单初始化逻辑
}

void InitLevel()
{
// 关卡初始化逻辑
}
}

场景加载完成回调

public class SceneLoadCallback : MonoBehaviour
{
public void LoadSceneWithCallback(string sceneName, System.Action<Scene> onLoaded)
{
StartCoroutine(LoadSceneCoroutine(sceneName, onLoaded));
}

IEnumerator LoadSceneCoroutine(string sceneName, System.Action<Scene> callback)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
yield return operation;

Scene loadedScene = SceneManager.GetSceneByName(sceneName);
callback?.Invoke(loadedScene);
}
}

// 使用示例
public class ExampleUsage : MonoBehaviour
{
void Start()
{
var loader = GetComponent<SceneLoadCallback>();

loader.LoadSceneWithCallback("Level_01", (scene) =>
{
Debug.Log($"{scene.name} 加载完成,开始初始化");
InitializeLevel(scene);
});
}

void InitializeLevel(Scene scene)
{
// 初始化逻辑
}
}

场景状态保存

使用 DontDestroyOnLoad

让对象在场景切换时保持存在:

public class PersistentObject : MonoBehaviour
{
public static PersistentObject Instance { get; private set; }

void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}

管理多个持久化对象

public class PersistentManager : MonoBehaviour
{
public GameObject[] persistentPrefabs;

void Awake()
{
// 检查是否已存在
if (FindObjectsOfType<PersistentManager>().Length > 1)
{
Destroy(gameObject);
return;
}

DontDestroyOnLoad(gameObject);

// 实例化其他持久化对象
foreach (var prefab in persistentPrefabs)
{
var instance = Instantiate(prefab);
DontDestroyOnLoad(instance);
}
}
}

场景数据传递

在场景之间传递数据:

// 方式1:使用静态类
public static class SceneData
{
public static int selectedLevel;
public static string playerName;
public static Dictionary<string, object> customData = new Dictionary<string, object>();

public static void Clear()
{
selectedLevel = 0;
playerName = "";
customData.Clear();
}
}

// 方式2:使用 ScriptableObject
[CreateAssetMenu(fileName = "GameSession", menuName = "Game/Session Data")]
public class GameSessionData : ScriptableObject
{
public int currentLevel;
public int score;
public float health;

public void Reset()
{
currentLevel = 0;
score = 0;
health = 100f;
}
}

// 使用
public class GameManager : MonoBehaviour
{
public GameSessionData sessionData;

public void StartGame(int level)
{
sessionData.currentLevel = level;
SceneManager.LoadScene("Game");
}
}

Build Settings 管理

场景构建设置

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;

public class BuildSettingsManager
{
// 自动添加所有场景到构建设置
[MenuItem("Build/Auto Add Scenes")]
static void AutoAddScenes()
{
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" });

EditorBuildSettingsScene[] buildScenes = new EditorBuildSettingsScene[sceneGuids.Length];

for (int i = 0; i < sceneGuids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(sceneGuids[i]);
buildScenes[i] = new EditorBuildSettingsScene(path, true);
}

EditorBuildSettings.scenes = buildScenes;

Debug.Log($"已添加 {buildScenes.Length} 个场景到构建设置");
}

// 获取场景构建索引
public static int GetSceneBuildIndex(string sceneName)
{
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
if (EditorBuildSettings.scenes[i].path.Contains(sceneName))
{
return i;
}
}
return -1;
}
}
#endif

运行时获取场景信息

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneBuildInfo : MonoBehaviour
{
void Start()
{
// 获取构建设置中的场景数量
int sceneCount = SceneManager.sceneCountInBuildSettings;

Debug.Log($"构建设置中的场景数量: {sceneCount}");

// 遍历所有场景
for (int i = 0; i < sceneCount; i++)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
Debug.Log($"场景 {i}: {scenePath}");
}
}

// 检查场景是否在构建设置中
public bool IsSceneInBuildSettings(string sceneName)
{
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
string path = SceneUtility.GetScenePathByBuildIndex(i);
if (path.Contains(sceneName))
{
return true;
}
}
return false;
}

// 获取场景构建索引
public int GetBuildIndex(string sceneName)
{
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
string path = SceneUtility.GetScenePathByBuildIndex(i);
if (System.IO.Path.GetFileNameWithoutExtension(path) == sceneName)
{
return i;
}
}
return -1;
}
}

完整场景管理器

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// 完整的场景管理器,支持同步/异步加载、加载进度、多场景管理
/// </summary>
public class SceneManagerEx : MonoBehaviour
{
public static SceneManagerEx Instance { get; private set; }

[Header("加载设置")]
public float minimumLoadTime = 1f;
public bool showLoadingScreen = true;

[Header("UI 引用")]
public GameObject loadingScreenPrefab;

// 状态
private bool isLoading = false;
private GameObject loadingScreenInstance;
private string currentSceneName;

// 场景栈(用于返回上一个场景)
private Stack<string> sceneHistory = new Stack<string>();

// 事件
public event System.Action<string> OnSceneLoadStart;
public event System.Action<string, float> OnSceneLoadProgress;
public event System.Action<string> OnSceneLoadComplete;
public event System.Action<string> OnSceneUnloadComplete;

void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);

// 订阅场景事件
SceneManager.sceneLoaded += HandleSceneLoaded;
SceneManager.sceneUnloaded += HandleSceneUnloaded;
}
else
{
Destroy(gameObject);
}
}

void OnDestroy()
{
SceneManager.sceneLoaded -= HandleSceneLoaded;
SceneManager.sceneUnloaded -= HandleSceneUnloaded;
}

#region 公共方法

/// <summary>
/// 同步加载场景
/// </summary>
public void LoadScene(string sceneName)
{
if (isLoading)
{
Debug.LogWarning("正在加载中,请等待");
return;
}

// 记录历史
sceneHistory.Push(currentSceneName);
currentSceneName = sceneName;

OnSceneLoadStart?.Invoke(sceneName);
SceneManager.LoadScene(sceneName);
}

/// <summary>
/// 异步加载场景(带加载画面)
/// </summary>
public void LoadSceneAsync(string sceneName)
{
if (isLoading)
{
Debug.LogWarning("正在加载中,请等待");
return;
}

StartCoroutine(LoadSceneAsyncRoutine(sceneName));
}

/// <summary>
/// 异步加载场景(附加模式)
/// </summary>
public void LoadSceneAdditive(string sceneName, System.Action onComplete = null)
{
StartCoroutine(LoadSceneAdditiveRoutine(sceneName, onComplete));
}

/// <summary>
/// 卸载场景(附加模式)
/// </summary>
public void UnloadSceneAsync(string sceneName, System.Action onComplete = null)
{
StartCoroutine(UnloadSceneRoutine(sceneName, onComplete));
}

/// <summary>
/// 返回上一个场景
/// </summary>
public void GoBack()
{
if (sceneHistory.Count > 0)
{
string previousScene = sceneHistory.Pop();
LoadSceneAsync(previousScene);
}
else
{
Debug.LogWarning("没有上一个场景");
}
}

/// <summary>
/// 重新加载当前场景
/// </summary>
public void ReloadCurrentScene()
{
if (!string.IsNullOrEmpty(currentSceneName))
{
LoadSceneAsync(currentSceneName);
}
}

#endregion

#region 协程

IEnumerator LoadSceneAsyncRoutine(string sceneName)
{
isLoading = true;

// 记录历史
sceneHistory.Push(currentSceneName);
currentSceneName = sceneName;

OnSceneLoadStart?.Invoke(sceneName);

// 显示加载画面
if (showLoadingScreen && loadingScreenPrefab != null)
{
loadingScreenInstance = Instantiate(loadingScreenPrefab);
DontDestroyOnLoad(loadingScreenInstance);
}

// 开始加载
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;

float elapsedTime = 0f;
float displayProgress = 0f;

while (!operation.isDone)
{
elapsedTime += Time.deltaTime;

// 计算进度
float actualProgress = Mathf.Clamp01(operation.progress / 0.9f);
displayProgress = Mathf.Lerp(displayProgress, actualProgress, Time.deltaTime * 5f);

// 触发进度事件
OnSceneLoadProgress?.Invoke(sceneName, displayProgress);

// 更新加载画面
UpdateLoadingScreen(displayProgress);

// 确保最短加载时间
if (operation.progress >= 0.9f && elapsedTime >= minimumLoadTime)
{
operation.allowSceneActivation = true;
}

yield return null;
}

// 隐藏加载画面
if (loadingScreenInstance != null)
{
Destroy(loadingScreenInstance);
}

isLoading = false;
OnSceneLoadComplete?.Invoke(sceneName);
}

IEnumerator LoadSceneAdditiveRoutine(string sceneName, System.Action onComplete)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
yield return operation;

onComplete?.Invoke();
}

IEnumerator UnloadSceneRoutine(string sceneName, System.Action onComplete)
{
AsyncOperation operation = SceneManager.UnloadSceneAsync(sceneName);
yield return operation;

// 卸载未使用的资源
yield return Resources.UnloadUnusedAssets();

onComplete?.Invoke();
}

#endregion

#region 事件处理

void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 更新当前场景名
if (mode == LoadSceneMode.Single)
{
currentSceneName = scene.name;
}
}

void HandleSceneUnloaded(Scene scene)
{
OnSceneUnloadComplete?.Invoke(scene.name);
}

#endregion

#region 辅助方法

void UpdateLoadingScreen(float progress)
{
if (loadingScreenInstance == null) return;

// 更新进度条
var progressBar = loadingScreenInstance.GetComponentInChildren<UnityEngine.UI.Slider>();
if (progressBar != null)
{
progressBar.value = progress;
}

// 更新进度文本
var progressText = loadingScreenInstance.GetComponentInChildren<UnityEngine.UI.Text>();
if (progressText != null)
{
progressText.text = $"加载中... {(int)(progress * 100)}%";
}
}

/// <summary>
/// 检查场景是否已加载
/// </summary>
public bool IsSceneLoaded(string sceneName)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
if (SceneManager.GetSceneAt(i).name == sceneName)
{
return true;
}
}
return false;
}

/// <summary>
/// 获取当前场景名
/// </summary>
public string GetCurrentSceneName()
{
return currentSceneName;
}

#endregion
}

// 使用示例
public class SceneLoadExample : MonoBehaviour
{
void Start()
{
// 订阅事件
SceneManagerEx.Instance.OnSceneLoadStart += OnLoadStart;
SceneManagerEx.Instance.OnSceneLoadProgress += OnLoadProgress;
SceneManagerEx.Instance.OnSceneLoadComplete += OnLoadComplete;
}

public void LoadLevel(int levelIndex)
{
SceneManagerEx.Instance.LoadSceneAsync($"Level_{levelIndex:00}");
}

void OnLoadStart(string sceneName)
{
Debug.Log($"开始加载场景: {sceneName}");
}

void OnLoadProgress(string sceneName, float progress)
{
Debug.Log($"加载进度: {progress * 100}%");
}

void OnLoadComplete(string sceneName)
{
Debug.Log($"场景加载完成: {sceneName}");
}
}

最佳实践

1. 场景划分原则

  • 按功能划分:菜单、游戏、结算分为不同场景
  • 控制场景大小:单个场景不宜过大,影响加载速度和内存
  • 合理使用 Additive:共享资源作为独立场景附加加载

2. 加载优化

  • 预加载:在合适的时机预加载即将需要的场景
  • 异步加载:使用异步加载避免卡顿
  • 最小加载时间:设置最小加载时间,让玩家看到加载信息

3. 内存管理

  • 及时卸载:不需要的场景及时卸载
  • 资源清理:卸载场景后调用 Resources.UnloadUnusedAssets()
  • 监控内存:使用 Profiler 监控场景切换时的内存变化

4. 用户体验

  • 加载进度反馈:提供清晰的加载进度提示
  • 加载动画:使用动画过渡让切换更平滑
  • 错误处理:处理场景加载失败的情况

参考资源