实现思路
这里只考虑游戏中只存在单场景的情况,也就是等价于LoadSceneMode.Single的场景切换
缓存
因为如果直接用LoadSceneMode.Single加载场景,原场景会直接被卸载掉而无法缓存,所以首先要做的就是使用LoadSceneMode.Additive加载场景。
然后对于场景缓存,或者说把一个加载好的场景放到缓存中实际上只需要两步操作:
把场景中的根对象GetRootGameObjects全部禁用掉
通过SetActiveScene将激活场景切换为另一个
当需要重新切换回到此场景时,只需要重新激活根对象并设置为激活场景即可。
预加载
按照场景缓存的思路,预加载只需要在加载完成后立即禁用全部根对象。
这里一开始还想过,既然只是预加载而不激活,那能否直接设置op.allowSceneActivation = false,然后等到需要激活时再设置为true呢?
答案是不能,因为按照官方文档的说法,场景加载是有一个内部队列的,只要前面的场景没有激活,后面的场景无论如何也无法激活。
遇到的坑点
切换场景后,由于使用了缓存,原场景中的对象其实还在,只是被禁掉了。如果保存了诸如Camera.main之类的引用,也需要在切换后进行更新,否则会出现很多异常问题。
使用LoadSceneMode.Additive加载场景后,新场景会很暗,即便新场景中全部都是Realtime光照,而且也执行了SetActiveScene。
根据论坛上的方案,只需要单独打开新场景进行一次光照烘焙即可。(看起来是一个bug,新场景没有自己的光照设置,切换场景后天空盒的漫反射没有切换过来)
完整代码
以下代码使用了UniTask库和Defer 。
SceneManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 using System;using System.Collections;using System.Collections.Generic;using System.Linq;using Cysharp.Threading.Tasks;using UnityEngine;using UnityEngine.Events;using UnityEngine.SceneManagement;using UnitySceneManager = UnityEngine.SceneManagement.SceneManager;public static class SceneManager { static readonly Stack<string > _sceneStack = new (); static readonly HashSet<string > _loadingScenes = new (); static readonly Dictionary<string , AsyncOperation> _sceneNameToOps = new (); class LoadSceneProgress : IProgress <float > { public Action<float > OnProgress; public void Report (float value ) { OnProgress?.Invoke(value ); } } public static void PreloadScene (string sceneName, Action<float > onProgress = null , Action onComplete = null ) { PreloadSceneAsync(sceneName, onProgress, onComplete).Forget(); } public static void LoadScene (string sceneName, Action callback = null ) { LoadSceneAsync(sceneName, callback).Forget(); } public static void UnloadScene (string sceneName, Action callback = null ) { if (UnitySceneManager.GetActiveScene().name != sceneName) { callback?.Invoke(); return ; } var last = _sceneStack.Pop(); LoadScene(last, callback); } public static bool IsSceneActiveOrLoading (string sceneName ) { var cur = UnitySceneManager.GetActiveScene(); if (cur.name == sceneName) return true ; if (_loadingScenes.Contains(sceneName)) return true ; return false ; } static async UniTask PreloadSceneAsync (string sceneName, Action<float > onProgress, Action onComplete ) { using var _ = Defer.OnLeave(() => { onProgress?.Invoke(1 ); onComplete?.Invoke(); }); onProgress?.Invoke(0 ); if (_sceneNameToOps.ContainsKey(sceneName)) return ; var op = UnitySceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); _sceneNameToOps[sceneName] = op; await op.ToUniTask(new LoadSceneProgress { OnProgress = onProgress }); var scene = UnitySceneManager.GetSceneByName(sceneName); if (scene != UnitySceneManager.GetActiveScene()) SetSceneObjectsActive(scene, false ); } static async UniTask LoadSceneAsync (string sceneName, Action callback ) { var cur = UnitySceneManager.GetActiveScene(); if (cur.name == sceneName) { callback?.Invoke(); return ; } _sceneStack.Push(cur.name); if (!_sceneNameToOps.TryGetValue(sceneName, out var op)) _sceneNameToOps[sceneName] = op = UnitySceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); _loadingScenes.Add(sceneName); using var _ = Defer.OnLeave(() => { _loadingScenes.Remove(sceneName); callback?.Invoke(); }); await op; var oldScene = UnitySceneManager.GetActiveScene(); var newScene = UnitySceneManager.GetSceneByName(sceneName); SetSceneObjectsActive(oldScene, false ); SetSceneObjectsActive(newScene, true ); UnitySceneManager.SetActiveScene(newScene); } static void SetSceneObjectsActive (Scene scene, bool active ) { var rootObjs = scene.GetRootGameObjects(); foreach (var obj in rootObjs) obj.SetActive(active); } }