9.场景模块

9.场景模块


9.1 知识点

为什么需要场景切换模块

在游戏开发中,我们经常需要进行场景切换。只要存在场景切换,我们往往需要在切换场景时切换场景结束后进行一些操作。

常见需求:

  1. 切换场景中
    • 更新Loading界面进度条
    • 显示加载百分比
    • 播放加载动画
  2. 切换场景后
    • 隐藏Loading界面
    • 动态创建场景中关卡信息:如角色、怪物、场景物件等
    • 播放场景背景音乐
    • 初始化游戏逻辑

问题分析:

  • 场景切换逻辑分散在各个地方,缺乏统一管理
  • 无法统一监听加载进度事件
  • 异步加载进度获取困难

场景模块实现思路

主要实现思路:

  1. 制作SceneMgr单例模式管理器:统一管理场景切换
  2. 实现同步加载场景的方法:封装SceneManager.LoadScene
  3. 实现异步加载场景的方法:使用协程实现非阻塞加载
  4. 实现外部获取异步加载场景进度:通过事件中心发送进度事件

设计要点:

  1. 进度监听:使用协程每帧检测AsyncOperation.progress
  2. 事件分发:通过EventCenter发送加载进度事件
  3. 回调机制:加载完成后执行回调函数
  4. 协程管理:通过MonoMgr启动协程

场景模块实现

源码实现:SceneMgr.cs

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

/// <summary>
/// 场景切换管理器
/// 主要用于切换场景,提供同步和异步加载功能
/// </summary>
public class SceneMgr : Singleton<SceneMgr>
{
    #region 构造函数

    private SceneMgr() { }

    #endregion

    #region 同步加载场景

    /// <summary>
    /// 同步切换场景的方法
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    public void LoadScene(string name, UnityAction callBack = null)
    {
        // 切换场景
        SceneManager.LoadScene(name);
        // 调用回调
        callBack?.Invoke();
        callBack = null;
    }

    #endregion

    #region 异步加载场景

    /// <summary>
    /// 异步切换场景的方法
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    public void LoadSceneAsyn(string name, UnityAction callBack = null)
    {
        // 通过MonoMgr启动协程进行异步加载
        MonoMgr.Instance.StartCoroutine(ReallyLoadSceneAsyn(name, callBack));
    }

    /// <summary>
    /// 真正的异步加载场景协程
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction callBack)
    {
        // 开始异步加载场景
        AsyncOperation ao = SceneManager.LoadSceneAsync(name);
        
        // 【核心功能】不停地每帧检测是否加载结束
        // 如果加载结束就不会进这个循环,每帧执行了
        while (!ao.isDone)
        {
            // 可以在这里利用事件中心每帧将进度发送给想要得到的地方
            EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, ao.progress);
            yield return 0;
        }
        
        // 【重要】避免最后一帧直接结束了,没有同步1出去
        EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, 1);

        // 加载完成,调用回调
        callBack?.Invoke();
        callBack = null;
    }

    #endregion
}

场景模块使用测试

创建测试脚本:SceneManagerUsageTest.cs

using UnityEngine;

/// <summary>
/// 场景管理器使用测试
/// 演示场景管理器的所有功能
/// </summary>
public class SceneManagerUsageTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("场景管理器使用测试:开始");

        // 1. 测试同步加载场景
        TestSynchronousLoad();

        // 2. 测试异步加载场景
        TestAsynchronousLoad();
    }

    /// <summary>
    /// 测试同步加载场景
    /// </summary>
    private void TestSynchronousLoad()
    {
        Debug.Log("测试同步加载场景");
        
        // 同步加载场景
        SceneMgr.Instance.LoadScene("GameScene", () =>
        {
            Debug.Log("场景加载完成");
            // 输出:场景加载完成
            // 注意:场景加载完成后,当前场景的对象都会被销毁
            // 因此这里的回调在场景切换后不会执行
        });
    }

    /// <summary>
    /// 测试异步加载场景
    /// </summary>
    private void TestAsynchronousLoad()
    {
        Debug.Log("测试异步加载场景");

        // 先监听场景加载进度事件
        EventCenter.Instance.AddEventListener<float>(E_EventType.E_SceneLoadChange, OnSceneLoadProgress);
        
        // 显示Loading界面
        ShowLoadingPanel();

        // 异步加载场景
        SceneMgr.Instance.LoadSceneAsyn("GameScene", () =>
        {
            Debug.Log("场景加载完成");
            
            // 隐藏Loading界面
            HideLoadingPanel();
            
            // 移除事件监听
            EventCenter.Instance.RemoveEventListener<float>(E_EventType.E_SceneLoadChange, OnSceneLoadProgress);
            
            // 可以在这里做场景加载后的初始化工作
            // 例如:创建角色、初始化游戏逻辑等
        });
    }

    /// <summary>
    /// 场景加载进度回调
    /// </summary>
    /// <param name="progress">加载进度(0-1)</param>
    private void OnSceneLoadProgress(float progress)
    {
        Debug.Log($"场景加载进度:{progress * 100}%");
        // 输出:场景加载进度:0% ... 100%
        
        // 更新进度条
        UpdateLoadingProgress(progress);
    }

    /// <summary>
    /// 显示Loading界面
    /// </summary>
    private void ShowLoadingPanel()
    {
        Debug.Log("显示Loading界面");
        // 实际操作:UIMgr.Instance.ShowPanel<LoadingPanel>();
    }

    /// <summary>
    /// 隐藏Loading界面
    /// </summary>
    private void HideLoadingPanel()
    {
        Debug.Log("隐藏Loading界面");
        // 实际操作:UIMgr.Instance.HidePanel<LoadingPanel>();
    }

    /// <summary>
    /// 更新加载进度
    /// </summary>
    /// <param name="progress">加载进度</param>
    private void UpdateLoadingProgress(float progress)
    {
        // 实际操作:更新进度条的显示
        // loadingPanel.SetProgress(progress);
    }
}

Loading面板实现示例

创建Loading面板:LoadingPanel.cs

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// Loading面板
/// 显示场景加载进度
/// </summary>
public class LoadingPanel : BasePanel
{
    private Slider progressSlider;
    private Text progressText;

    protected override void Awake()
    {
        base.Awake();
        
        // 获取进度条和文本
        progressSlider = GetControl<Slider>("progressSlider");
        progressText = GetControl<Text>("progressText");
    }

    private void Start()
    {
        // 监听场景加载进度事件
        EventCenter.Instance.AddEventListener<float>(E_EventType.E_SceneLoadChange, UpdateProgress);
        
        // 初始化进度
        UpdateProgress(0f);
    }

    /// <summary>
    /// 更新加载进度
    /// </summary>
    /// <param name="progress">加载进度(0-1)</param>
    private void UpdateProgress(float progress)
    {
        // 更新进度条
        if (progressSlider != null)
        {
            progressSlider.value = progress;
        }
        
        // 更新进度文本
        if (progressText != null)
        {
            progressText.text = $"{(int)(progress * 100)}%";
        }
    }

    /// <summary>
    /// 显示面板时的逻辑
    /// </summary>
    public override void ShowMe()
    {
        Debug.Log("LoadingPanel显示");
    }

    /// <summary>
    /// 隐藏面板时的逻辑
    /// </summary>
    public override void HideMe()
    {
        Debug.Log("LoadingPanel隐藏");
    }

    private void OnDestroy()
    {
        // 移除事件监听
        EventCenter.Instance.RemoveEventListener<float>(E_EventType.E_SceneLoadChange, UpdateProgress);
    }
}

完整场景切换流程

创建完整的场景切换流程:GameSceneTransition.cs

using UnityEngine;

/// <summary>
/// 游戏场景切换流程
/// 演示完整的场景切换流程
/// </summary>
public class GameSceneTransition : MonoBehaviour
{
    void Start()
    {
        Debug.Log("游戏场景切换流程:开始");

        // 开始切换到游戏场景
        StartGameScene();
    }

    /// <summary>
    /// 开始游戏场景
    /// </summary>
    private void StartGameScene()
    {
        Debug.Log("开始切换到游戏场景");
        
        // 1. 显示Loading面板
        UIMgr.Instance.ShowPanel<LoadingPanel>(E_UILayer.System, (panel) =>
        {
            Debug.Log("Loading面板显示完成");
            
            // 2. 异步加载场景
            SceneMgr.Instance.LoadSceneAsyn("GameScene", () =>
            {
                Debug.Log("游戏场景加载完成");
                
                // 3. 隐藏Loading面板
                UIMgr.Instance.HidePanel<LoadingPanel>(true);
                
                // 4. 播放场景背景音乐
                MusicMgr.Instance.PlayBKMusic("GameBGM");
                
                // 5. 初始化游戏逻辑
                InitializeGameLogic();
            });
        });
    }

    /// <summary>
    /// 初始化游戏逻辑
    /// </summary>
    private void InitializeGameLogic()
    {
        Debug.Log("初始化游戏逻辑");
        
        // 创建角色
        CreatePlayer();
        
        // 创建敌人
        CreateEnemies();
        
        // 加载关卡数据
        LoadLevelData();
    }

    /// <summary>
    /// 创建角色
    /// </summary>
    private void CreatePlayer()
    {
        Debug.Log("创建角色");
        // 从资源管理器加载角色预设体
        ABResMgr.Instance.LoadResAsync<GameObject>("player", "Player", (player) =>
        {
            GameObject playerObj = Instantiate(player);
            playerObj.name = "Player";
            Debug.Log("角色创建完成");
        });
    }

    /// <summary>
    /// 创建敌人
    /// </summary>
    private void CreateEnemies()
    {
        Debug.Log("创建敌人");
        // 从资源管理器加载敌人预设体
        for (int i = 0; i < 5; i++)
        {
            ABResMgr.Instance.LoadResAsync<GameObject>("enemy", "Enemy", (enemy) =>
            {
                GameObject enemyObj = Instantiate(enemy);
                enemyObj.name = "Enemy";
                Debug.Log("敌人创建完成");
            });
        }
    }

    /// <summary>
    /// 加载关卡数据
    /// </summary>
    private void LoadLevelData()
    {
        Debug.Log("加载关卡数据");
        // 实际项目中可以从配置文件加载关卡数据
        // LevelMgr.Instance.LoadLevel(levelID);
    }
}

场景模块进阶和优化

场景预加载优化

在实际项目中,可以通过预加载场景资源来提高切换速度。

优化思路:

  1. 资源预加载:在Loading阶段预加载场景所需资源
  2. 分块加载:将大场景分块加载,提高加载体验
  3. 场景预热:在后台预热下一个场景的资源

示例代码:

/// <summary>
/// 场景预加载
/// </summary>
/// <param name="sceneName">场景名称</param>
/// <param name="callBack">预加载完成回调</param>
public void PreloadScene(string sceneName, UnityAction callBack = null)
{
    MonoMgr.Instance.StartCoroutine(ReallyPreloadScene(sceneName, callBack));
}

private IEnumerator ReallyPreloadScene(string sceneName, UnityAction callBack)
{
    // 1. 预加载场景资源
    // 根据场景名称预加载该场景所需的资源
    // 例如:ABResMgr.Instance.LoadResAsync<GameObject>("scene1", "scene_res1");
    
    // 2. 预加载场景
    AsyncOperation ao = SceneManager.LoadSceneAsync(sceneName);
    ao.allowSceneActivation = false; // 不激活场景,只加载资源
    
    // 3. 等待加载到90%
    while (ao.progress < 0.9f)
    {
        yield return null;
    }
    
    // 4. 预加载完成,调用回调
    callBack?.Invoke();
}

场景资源管理

结合资源管理模块,在场景切换时自动清理不用的资源。

使用示例:

/// <summary>
/// 切换场景并清理资源
/// </summary>
/// <param name="sceneName">场景名称</param>
/// <param name="callBack">切换完成回调</param>
public void LoadSceneWithCleanup(string sceneName, UnityAction callBack = null)
{
    // 1. 隐藏当前场景的UI
    UIMgr.Instance.HideAllPanels();
    
    // 2. 清空对象池
    PoolMgr.Instance.ClearPool();
    
    // 3. 清空音效
    MusicMgr.Instance.ClearSound();
    
    // 4. 卸载未使用的资源
    ResMgr.Instance.UnloadUnusedAssets(() =>
    {
        // 5. 加载新场景
        SceneMgr.Instance.LoadSceneAsyn(sceneName, callBack);
    });
}

9.2 知识点代码

SceneMgr.cs(场景管理器)

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

/// <summary>
/// 场景切换管理器
/// 主要用于切换场景,提供同步和异步加载功能
/// 支持加载进度监听和回调机制
/// </summary>
public class SceneMgr : Singleton<SceneMgr>
{
    #region 构造函数

    private SceneMgr() { }

    #endregion

    #region 同步加载场景

    /// <summary>
    /// 同步切换场景的方法
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    public void LoadScene(string name, UnityAction callBack = null)
    {
        // 切换场景
        SceneManager.LoadScene(name);
        // 调用回调
        callBack?.Invoke();
        callBack = null;
    }

    #endregion

    #region 异步加载场景

    /// <summary>
    /// 异步切换场景的方法
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    public void LoadSceneAsyn(string name, UnityAction callBack = null)
    {
        // 通过MonoMgr启动协程进行异步加载
        MonoMgr.Instance.StartCoroutine(ReallyLoadSceneAsyn(name, callBack));
    }

    /// <summary>
    /// 真正的异步加载场景协程
    /// </summary>
    /// <param name="name">场景名称</param>
    /// <param name="callBack">加载完成后的回调函数</param>
    /// <returns>协程迭代器</returns>
    private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction callBack)
    {
        // 开始异步加载场景
        AsyncOperation ao = SceneManager.LoadSceneAsync(name);
        
        // 【核心功能】不停地每帧检测是否加载结束
        // 如果加载结束就不会进这个循环,每帧执行了
        while (!ao.isDone)
        {
            // 可以在这里利用事件中心每帧将进度发送给想要得到的地方
            EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, ao.progress);
            yield return 0;
        }
        
        // 【重要】避免最后一帧直接结束了,没有同步1出去
        EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, 1);

        // 加载完成,调用回调
        callBack?.Invoke();
        callBack = null;
    }

    #endregion
}

LoadingPanel.cs(Loading面板)

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// Loading面板
/// 显示场景加载进度
/// 监听场景加载进度事件并更新UI
/// </summary>
public class LoadingPanel : BasePanel
{
    #region 字段

    /// <summary>
    /// 进度条
    /// </summary>
    private Slider progressSlider;

    /// <summary>
    /// 进度文本
    /// </summary>
    private Text progressText;

    #endregion

    #region Unity生命周期

    protected override void Awake()
    {
        base.Awake();
        
        // 获取进度条和文本
        progressSlider = GetControl<Slider>("progressSlider");
        progressText = GetControl<Text>("progressText");
    }

    private void Start()
    {
        // 监听场景加载进度事件
        EventCenter.Instance.AddEventListener<float>(E_EventType.E_SceneLoadChange, UpdateProgress);
        
        // 初始化进度
        UpdateProgress(0f);
    }

    private void OnDestroy()
    {
        // 移除事件监听
        EventCenter.Instance.RemoveEventListener<float>(E_EventType.E_SceneLoadChange, UpdateProgress);
    }

    #endregion

    #region 进度更新

    /// <summary>
    /// 更新加载进度
    /// </summary>
    /// <param name="progress">加载进度(0-1)</param>
    private void UpdateProgress(float progress)
    {
        // 更新进度条
        if (progressSlider != null)
        {
            progressSlider.value = progress;
        }
        
        // 更新进度文本
        if (progressText != null)
        {
            progressText.text = $"{(int)(progress * 100)}%";
        }
    }

    #endregion

    #region BasePanel实现

    /// <summary>
    /// 显示面板时的逻辑
    /// </summary>
    public override void ShowMe()
    {
        Debug.Log("LoadingPanel显示");
    }

    /// <summary>
    /// 隐藏面板时的逻辑
    /// </summary>
    public override void HideMe()
    {
        Debug.Log("LoadingPanel隐藏");
    }

    #endregion
}

SceneManagerUsageTest.cs(场景管理器使用测试)

using UnityEngine;

/// <summary>
/// 场景管理器使用测试
/// 演示场景管理器的所有功能
/// </summary>
public class SceneManagerUsageTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log("场景管理器使用测试:开始");

        // 测试异步加载场景
        TestAsynchronousLoad();
    }

    /// <summary>
    /// 测试异步加载场景
    /// </summary>
    private void TestAsynchronousLoad()
    {
        Debug.Log("测试异步加载场景");

        // 先监听场景加载进度事件
        EventCenter.Instance.AddEventListener<float>(E_EventType.E_SceneLoadChange, OnSceneLoadProgress);
        
        // 显示Loading界面
        ShowLoadingPanel();

        // 异步加载场景
        SceneMgr.Instance.LoadSceneAsyn("GameScene", () =>
        {
            Debug.Log("场景加载完成");
            
            // 隐藏Loading界面
            HideLoadingPanel();
            
            // 移除事件监听
            EventCenter.Instance.RemoveEventListener<float>(E_EventType.E_SceneLoadChange, OnSceneLoadProgress);
            
            // 可以在这里做场景加载后的初始化工作
            // 例如:创建角色、初始化游戏逻辑等
        });
    }

    /// <summary>
    /// 场景加载进度回调
    /// </summary>
    /// <param name="progress">加载进度(0-1)</param>
    private void OnSceneLoadProgress(float progress)
    {
        Debug.Log($"场景加载进度:{progress * 100}%");
        // 输出:场景加载进度:0% ... 100%
        
        // 更新进度条
        UpdateLoadingProgress(progress);
    }

    /// <summary>
    /// 显示Loading界面
    /// </summary>
    private void ShowLoadingPanel()
    {
        Debug.Log("显示Loading界面");
        UIMgr.Instance.ShowPanel<LoadingPanel>(E_UILayer.System, (panel) =>
        {
            Debug.Log("Loading面板显示完成");
        });
    }

    /// <summary>
    /// 隐藏Loading界面
    /// </summary>
    private void HideLoadingPanel()
    {
        Debug.Log("隐藏Loading界面");
        UIMgr.Instance.HidePanel<LoadingPanel>(true);
    }

    /// <summary>
    /// 更新加载进度
    /// </summary>
    /// <param name="progress">加载进度</param>
    private void UpdateLoadingProgress(float progress)
    {
        // 实际操作:更新进度条的显示
        // LoadingPanel.Instance.SetProgress(progress);
    }
}

GameSceneTransition.cs(完整场景切换流程)

using UnityEngine;

/// <summary>
/// 游戏场景切换流程
/// 演示完整的场景切换流程
/// </summary>
public class GameSceneTransition : MonoBehaviour
{
    void Start()
    {
        Debug.Log("游戏场景切换流程:开始");

        // 开始切换到游戏场景
        StartGameScene();
    }

    /// <summary>
    /// 开始游戏场景
    /// </summary>
    private void StartGameScene()
    {
        Debug.Log("开始切换到游戏场景");
        
        // 1. 显示Loading面板
        UIMgr.Instance.ShowPanel<LoadingPanel>(E_UILayer.System, (panel) =>
        {
            Debug.Log("Loading面板显示完成");
            
            // 2. 异步加载场景
            SceneMgr.Instance.LoadSceneAsyn("GameScene", () =>
            {
                Debug.Log("游戏场景加载完成");
                
                // 3. 隐藏Loading面板
                UIMgr.Instance.HidePanel<LoadingPanel>(true);
                
                // 4. 播放场景背景音乐
                MusicMgr.Instance.PlayBKMusic("GameBGM");
                
                // 5. 初始化游戏逻辑
                InitializeGameLogic();
            });
        });
    }

    /// <summary>
    /// 初始化游戏逻辑
    /// </summary>
    private void InitializeGameLogic()
    {
        Debug.Log("初始化游戏逻辑");
        
        // 创建角色
        CreatePlayer();
        
        // 创建敌人
        CreateEnemies();
        
        // 加载关卡数据
        LoadLevelData();
    }

    /// <summary>
    /// 创建角色
    /// </summary>
    private void CreatePlayer()
    {
        Debug.Log("创建角色");
        // 从资源管理器加载角色预设体
        ABResMgr.Instance.LoadResAsync<GameObject>("player", "Player", (player) =>
        {
            GameObject playerObj = Instantiate(player);
            playerObj.name = "Player";
            Debug.Log("角色创建完成");
        });
    }

    /// <summary>
    /// 创建敌人
    /// </summary>
    private void CreateEnemies()
    {
        Debug.Log("创建敌人");
        // 从资源管理器加载敌人预设体
        for (int i = 0; i < 5; i++)
        {
            ABResMgr.Instance.LoadResAsync<GameObject>("enemy", "Enemy", (enemy) =>
            {
                GameObject enemyObj = Instantiate(enemy);
                enemyObj.name = "Enemy";
                Debug.Log("敌人创建完成");
            });
        }
    }

    /// <summary>
    /// 加载关卡数据
    /// </summary>
    private void LoadLevelData()
    {
        Debug.Log("加载关卡数据");
        // 实际项目中可以从配置文件加载关卡数据
        // LevelMgr.Instance.LoadLevel(levelID);
    }
}

SceneTransitionWithCleanup.cs(场景切换并清理资源)

using UnityEngine;

/// <summary>
/// 场景切换并清理资源
/// 演示如何在场景切换时清理资源
/// </summary>
public class SceneTransitionWithCleanup : MonoBehaviour
{
    void Start()
    {
        Debug.Log("场景切换并清理资源测试:开始");

        // 切换场景并清理资源
        SwitchSceneWithCleanup();
    }

    /// <summary>
    /// 切换场景并清理资源
    /// </summary>
    private void SwitchSceneWithCleanup()
    {
        Debug.Log("准备切换场景并清理资源");
        
        // 1. 隐藏当前场景的UI
        UIMgr.Instance.HideAllPanels();
        Debug.Log("隐藏所有UI面板");
        
        // 2. 清空音效
        MusicMgr.Instance.ClearSound();
        Debug.Log("清空所有音效");
        
        // 3. 显示Loading面板
        UIMgr.Instance.ShowPanel<LoadingPanel>(E_UILayer.System, (panel) =>
        {
            Debug.Log("Loading面板显示");
            
            // 4. 卸载未使用的资源
            ResMgr.Instance.UnloadUnusedAssets(() =>
            {
                Debug.Log("资源卸载完成");
                
                // 5. 加载新场景
                SceneMgr.Instance.LoadSceneAsyn("NextScene", () =>
                {
                    Debug.Log("场景切换完成");
                    
                    // 6. 隐藏Loading面板
                    UIMgr.Instance.HidePanel<LoadingPanel>(true);
                });
            });
        });
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏