Unity游戏框架设计之场景管理器

Unity游戏框架设计之场景管理器

简单介绍

在游戏开发过程中,我们经常对 Scene 进行切换。为了不使场景切换时造成的游戏卡顿,可以 Unity 官方 API 利用协程的方式异步加载场景。

同时,为提升 Scene 切换的玩家体验,我们经常会在场景切换的开始,先显示过渡 UI ,然后才对目标场景进行加载。在对目标场景加载的过程中,还必须不断将加载进度更新到过渡 UI 上,以便玩家观察。

对于一个场景的管理,除了加载场景和卸载场景之外,还必须管理场景中的游戏对象和组件。因此我们还必须提供对场景游戏对象的创建、加载、激活、禁用、卸载和搜索等方法。组件也同理。这部分代码相对简单,因此下述代码未给出。

代码设计

public class SceneManager : SingletonMono<SceneManager>
{
    public float loadingProcess;
    private bool _isLoadingScene;
    private readonly HashSet<string> _sceneAssetPathSet = new();
    private readonly WaitForEndOfFrame _waitForEndOfFrame = new();
    private static string _launchSceneAssetPath;
    private static Action _onLaunchSceneLoaded;
    private static Action<string> _onSceneUnload;

    public static void Initialize(string launchSceneAssetPath, Action onLaunchSceneLoaded, Action<string> onSceneUnload)
    {
        _launchSceneAssetPath = launchSceneAssetPath;
        _onLaunchSceneLoaded = onLaunchSceneLoaded;
        _onSceneUnload = onSceneUnload;
    }

    private void OnEnable()
    {
        UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
        UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnLoad;
    }

    private void OnDisable()
    {
        UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
        UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnLoad;
    }

    private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, LoadSceneMode mode)
    {
        if (scene.path.Equals(_launchSceneAssetPath))
        {
            if (_onLaunchSceneLoaded == null)
            {
                return;
            }
            _onLaunchSceneLoaded();
            return;
        }
    }

    private void OnSceneUnLoad(UnityEngine.SceneManagement.Scene scene)
    {
        if (_onSceneUnload == null)
        {
            return;
        }
        _onSceneUnload(scene.path);
    }

    public string GetSceneGameObjectName(string sceneAssetPath)
    {
        return StringUtils.GetFileNameWithoutExtension(sceneAssetPath);
    }

    private string SceneAssetPathToSceneName(string sceneAssetPath)
    {
        string substring = sceneAssetPath.Substring("Assets/".Length);
        return substring.Substring(0, substring.IndexOf('.'));
    }

    private bool IsSceneLoaded(string sceneAssetPath)
    {
        if (_sceneAssetPathSet.Contains(sceneAssetPath))
        {
            return true;
        }
        return UnityEngine.SceneManagement.SceneManager.GetSceneByName(SceneAssetPathToSceneName(sceneAssetPath)).isLoaded;
    }

    private IEnumerator OpenScene(string sceneAssetPath, float waitingTime = 0f, Action openedCallback = null, Action callback = null)
    {
        string sceneName = SceneAssetPathToSceneName(sceneAssetPath);
        if (!_sceneAssetPathSet.Contains(sceneAssetPath))
        {
            ResourceManager.Instance.LoadSceneAsset(sceneAssetPath);
            _sceneAssetPathSet.Add(sceneAssetPath);
        }
        UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
        if (scene.isLoaded)
        {
            yield break;
        }
        if (_isLoadingScene)
        {
            yield break;
        }
        _isLoadingScene = true;
        AsyncOperation operation = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
        operation.allowSceneActivation = false;
        loadingProcess = 0f;
        while (!operation.isDone)
        {
            loadingProcess = operation.progress;
            if (operation.progress >= 0.9f)
            {
                operation.allowSceneActivation = true;
                if (openedCallback != null)
                {
                    openedCallback();
                    openedCallback = null;
                }
                if (FloatUtils.IsGreaterThan(waitingTime, 0f))
                {
                    yield return new WaitForSeconds(waitingTime);
                }
            }
            yield return _waitForEndOfFrame;
        }
        loadingProcess = 1f;
        _isLoadingScene = false;
        if (callback != null)
        {
            callback();
        }
    }

    private void CoroutineOpenScene<T>(string sceneAssetPath, float waitingTime = 0f, Action openedCallback = null, Action callback = null) where T : Component
    {
        if (IsSceneLoaded(sceneAssetPath))
        {
            if (openedCallback != null)
            {
                openedCallback();
            }
            if (FloatUtils.IsGreaterThan(waitingTime, 0f))
            {
                CoroutineUtils.Instance.CoroutineSleep(waitingTime, () =>
                {
                    if (callback != null)
                    {
                        callback();
                    }
                });
                return;
            }
            if (callback != null)
            {
                callback();
            }
            return;
        }
        StartCoroutine(OpenScene(sceneAssetPath, waitingTime, openedCallback, () =>
        {
            UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(SceneAssetPathToSceneName(sceneAssetPath));
            string rootGameObjectName = GetSceneGameObjectName(sceneAssetPath);
            if (IsRootGameObjectExists(sceneAssetPath, rootGameObjectName))
            {
                GameObject rootGameObject = FindRootPrefab(sceneAssetPath, rootGameObjectName);
                if (rootGameObject.GetComponent<T>() != null)
                {
                    return;
                }
                rootGameObject.AddComponent<T>();
            }
            else
            {
                GameObject rootGameObject = new GameObject
                {
                    transform =
                    {
                        parent = null,
                        position = Vector3.zero
                    },
                    name = rootGameObjectName
                };
                UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(rootGameObject, scene);
                rootGameObject.AddComponent<T>();
            }
            if (callback != null)
            {
                callback();
            }
        }));
    }

    private IEnumerator CloseScene(string sceneAssetPath, Action callback = null)
    {
        if (!IsSceneLoaded(sceneAssetPath))
        {
            yield break;
        }
        string sceneName = SceneAssetPathToSceneName(sceneAssetPath);
        UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
        foreach (GameObject rootGameObject in scene.GetRootGameObjects())
        {
            rootGameObject.SetActive(false);
        }
        AsyncOperation operation = UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(sceneName);
        while (!operation.isDone)
        {
            yield return _waitForEndOfFrame;
        }
        _sceneAssetPathSet.Remove(sceneAssetPath);
        if (callback != null)
        {
            callback();
        }
    }

    private void CoroutineCloseScene(string sceneAssetPath, Action callback = null)
    {
        StartCoroutine(CloseScene(sceneAssetPath, callback));
    }

    public void EnterFirstScene<T>(string sceneAssetPath, float waitingTime = 0f, Action openedCallback = null, Action callback = null) where T : Component
    {
        CoroutineOpenScene<T>(sceneAssetPath, waitingTime, openedCallback, callback);
    }
    
    public void SwitchScene<T>(string currentSceneAssetPath, string targetSceneAssetPath, float waitTimeBeforeEnterScene, ILoadingUI loadingUI, string loadingUIName,
        string[] retainPrefabNameSet = null, Action callbackBeforeLoadScene = null, Action callback = null) where T : Component
    {
        UIManager.Instance.OpenUI(currentSceneAssetPath, loadingUIName);
        loadingUI.StartProcess();
        if (callbackBeforeLoadScene != null)
        {
            callbackBeforeLoadScene();
        }
        string sceneRootGameObjectName = GetSceneGameObjectName(currentSceneAssetPath);
        UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(SceneAssetPathToSceneName(currentSceneAssetPath));
        foreach (GameObject rootGameObject in scene.GetRootGameObjects())
        {
            string rootGameObjectName = rootGameObject.name;
            if (rootGameObjectName.Equals(sceneRootGameObjectName) || rootGameObjectName.Equals(loadingUIName))
            {
                continue;
            }
            if (retainPrefabNameSet != null)
            {
                bool isRetain = false;
                foreach (string retainObjectName in retainPrefabNameSet)
                {
                    if (rootGameObjectName.Equals(retainObjectName))
                    {
                        isRetain = true;
                        break;
                    }
                }
                if (isRetain)
                {
                    continue;
                }
            }
            rootGameObject.SetActive(false);
        }
        InputSystemManager.Instance.Disable();
        CoroutineOpenScene<T>(targetSceneAssetPath, waitTimeBeforeEnterScene, () =>
        {
            CoroutineUtils.Instance.CoroutineSleep(waitTimeBeforeEnterScene * 0.9f, loadingUI.EndProcess);
        }, () =>
        {
            InputSystemManager.Instance.Enable();
            CoroutineCloseScene(currentSceneAssetPath, callback);
        });
    }
}

代码执行流程

EnterFirstScene() 进入首个 Scene。

(一)加载 Scene 的 Asset 资源。

(二)通过 UnityEngine.SceneManagement.SceneManager.LoadSceneAsync 异步加载场景。

(三)创建场景游戏对象,并将场景脚本添加到场景游戏对象上。


SwitchScene() 切换 Scene。

(一)激活在当前 Scene 中已加载但被禁用的过渡 UI,然后开始过渡 UI 的伪进度加载。

(二)禁用当前 Scene 中的所有游戏对象,除了场景游戏对象、过渡 UI 游戏对象和指定不禁用的游戏对象。指定不禁用的游戏对象通常包括 Camera 游戏对象、AudioListener 游戏对象等等。

(三)禁用输入系统(可选的)。

(四)加载目标 Scene 的 Asset 资源。

(五)通过 UnityEngine.SceneManagement.SceneManager.LoadSceneAsync 异步加载场景。

(六)当目标场景打开成功后,先睡眠小于 waitTimeBeforeEnterScene 时间,可以取 waitTimeBeforeEnterScene * 0.9f,然后才结束过渡 UI 的伪进度加载。引入 waitTimeBeforeEnterScene 的原因是,防止场景加载速度过快时导致过渡 UI 页面一闪而过、用户无法观察进度条变化等问题。

(七)当目标场景打开成功后,先睡眠 waitTimeBeforeEnterScene 时间,然后创建目标场景游戏对象并将目标场景脚本添加到目标场景游戏对象上,然后激活输入系统(可选的)并卸载当前场景。第六步的睡眠时间小于第七步的睡眠时间,因此可以保证用户先观察到进度值为 100 % ,然后才进入场景。

(八)卸载当前场景的流程为,先禁止用当前场景中所有的游戏对象,然后通过 UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync 异步卸载当前场景。

代码说明

(一)实现场景的异步加载。

(二)引入过渡 UI,实现场景的丝滑切换。

(三)场景必须为空场景,场景中所有的游戏对象必须通过场景脚本来加载。

后记

由于个人能力有限,文中不免存在疏漏之处,恳求大家斧正,一起交流,共同进步。

热门相关:我是单身(我是SOLO成人版)   龙皇武神   惊悚乐园   惊悚乐园   顶级气运,悄悄修炼千年