Unity场景预加载和缓存

实现思路

这里只考虑游戏中只存在单场景的情况,也就是等价于LoadSceneMode.Single的场景切换

缓存

因为如果直接用LoadSceneMode.Single加载场景,原场景会直接被卸载掉而无法缓存,所以首先要做的就是使用LoadSceneMode.Additive加载场景。

然后对于场景缓存,或者说把一个加载好的场景放到缓存中实际上只需要两步操作:

  1. 把场景中的根对象GetRootGameObjects全部禁用掉
  2. 通过SetActiveScene将激活场景切换为另一个

当需要重新切换回到此场景时,只需要重新激活根对象并设置为激活场景即可。

预加载

按照场景缓存的思路,预加载只需要在加载完成后立即禁用全部根对象。

这里一开始还想过,既然只是预加载而不激活,那能否直接设置op.allowSceneActivation = false,然后等到需要激活时再设置为true呢?

答案是不能,因为按照官方文档的说法,场景加载是有一个内部队列的,只要前面的场景没有激活,后面的场景无论如何也无法激活。

遇到的坑点

  1. 切换场景后,由于使用了缓存,原场景中的对象其实还在,只是被禁掉了。如果保存了诸如Camera.main之类的引用,也需要在切换后进行更新,否则会出现很多异常问题。

  2. 使用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);
}
}

/// <summary>
/// 预加载场景,但不激活
/// </summary>
public static void PreloadScene(string sceneName, Action<float> onProgress = null, Action onComplete = null)
{
PreloadSceneAsync(sceneName, onProgress, onComplete).Forget();
}

/// <summary>
/// 加载场景并激活
/// </summary>
public static void LoadScene(string sceneName, Action callback = null)
{
LoadSceneAsync(sceneName, callback).Forget();
}

/// <summary>
/// 卸载当前场景并返回上一个场景
/// </summary>
public static void UnloadScene(string sceneName, Action callback = null)
{
if (UnitySceneManager.GetActiveScene().name != sceneName)
{
callback?.Invoke();
return;
}

var last = _sceneStack.Pop();
LoadScene(last, callback);
}

/// <summary>
/// 判断场景是否已激活或正在加载
/// </summary>
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);
}

}