携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第31天,点击查看活动详情
客户端场景加载机制
本篇中,将详细探讨客户端加载场景的核心函数ClientChangeScene,并讨论自定义场景操作。
ClientChangeScene
该函数中会根据SceneMessage指定的场景名和操作类型,在客户端进行场景操作。该函数较长,我们分段研究。
internal void ClientChangeScene(string newSceneName, SceneOperation sceneOperation = SceneOperation.Normal, bool customHandling = false)
{
if (string.IsNullOrWhiteSpace(newSceneName))
{
return;
}
OnClientChangeScene(newSceneName, sceneOperation, customHandling);
if (NetworkServer.active)
return;
NetworkClient.isLoadingScene = true;
clientSceneOperation = sceneOperation;
if (customHandling)
return;
- 首先,会调用回调
OnClientChangeScene, 这是在实际场景操作之前的回调。另外还有一个回调OnClientSceneChanged,是在场景操作成功之后调用,这两个都是NetworkManager类的虚函数,需要子类override提供功能。OnClientSceneChanged并不只是在ClientChangeScene中调用的,而是在场景操作完成之后调用,具体来说是在NetworkManager.UpdateScene中判断loadingSceneAsync.isDone,如果判断成立,则调用FinishLoadScene,其中会进一步调用OnClientSceneChanged。 if (NetworkServer.active) return;乍一看挺奇怪,客户端代码里面为啥要判断服务器是否激活? 其实这是在判断Host模式,此模式下服务器和客户端在一个机子上运行,是同一个进程,他们的场景和对象都是共享的,因此对于Host模式的客户端,他的场景已经在启动服务器时载入了,因此这儿就不需要载入了,直接返回。if(customHandling) return如果msg中设置了customHanding,说明需要使用自定义场景操作,因此这儿直接返回。因为下面就是要进行场景操作了。
switch (sceneOperation)
{
case SceneOperation.Normal:
loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName);
break;
case SceneOperation.LoadAdditive:
if (!SceneManager.GetSceneByName(newSceneName).IsValid() && !SceneManager.GetSceneByPath(newSceneName).IsValid())
loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, LoadSceneMode.Additive);
else
{
NetworkClient.isLoadingScene = false;
}
break;
case SceneOperation.UnloadAdditive:
if (SceneManager.GetSceneByName(newSceneName).IsValid() || SceneManager.GetSceneByPath(newSceneName).IsValid())
loadingSceneAsync = SceneManager.UnloadSceneAsync(newSceneName, UnloadSceneOptions.UnloadAllEmbeddedSceneObjects);
else
{
NetworkClient.isLoadingScene = false;
}
break;
}
}
- 默认提供的场景操作都是异步操作,会返回一个
loadingSceneAsync,这是一个AsyncOperation类型的变量。前面说过,如果是默认的场景操作,就会使用这个loadingSceneAsync来判断场景操作是否完成。 - 对应了
SceneMessage的sceneOperation参数的类型,这儿分别进行3种操作:载入单一场景,载入Additive场景,卸载Additive场景。 - 其中对于Additive场景的操作,都需要检查场景是否已经载入,因为对于客户端来说,并不知道消息中传递的场景名是什么,这是一种防御性编程。
- 需要注意卸载Additive场景时,使用
UnloadSceneAsync的第二个参数UnloadSceneOptions.UnloadAllEmbeddedSceneObjects。这个参数的含义是卸载场景时需要卸载掉所有从场景资源文件中反序列化出来的对象。即载入时就存在于该场景中的对象。如果不使用这个参数,则只会卸载掉场景树形结构中的对象。比如场景中原本有一把斧头,玩家捡起了这把斧子,将其父节点设置为玩家手上的骨骼。然后玩家走出了这个场景,我们将其动态卸载掉。如果使用这个参数,这个斧子也会被卸载掉。这显然不是我们想要的,所以如果有这种需求,就需要自定义Additive场景的卸载操作了。而Mirror默认提供的操作,是要保证无论我们将这个斧子扔到哪儿,都需要卸载掉。
自定义场景操作
当SceneMessage的customHandling参数为true时,说明我们要自定义场景操作。而ClientChangeScene中如果发现customHandling为true,则会跳过默认的场景操作。此时我们需要在OnClientChangeScene或OnClientSceneChanged中进行自定义场景操作。需要注意的是,我们要设置loadingSceneAsync这个变量,因为Mirror会在UpdateScene中检查这个变量判断场景操作完成。
例子:
public override void OnClientChangeScene(string sceneName, SceneOperation sceneOperation, bool customHandling)
{
if (sceneOperation == SceneOperation.UnloadAdditive)
StartCoroutine(UnloadAdditive(sceneName));
if (sceneOperation == SceneOperation.LoadAdditive)
StartCoroutine(LoadAdditive(sceneName));
}
IEnumerator LoadAdditive(string sceneName)
{
if (mode == NetworkManagerMode.ClientOnly)
{
loadingSceneAsync = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (loadingSceneAsync != null && !loadingSceneAsync.isDone)
yield return null;
}
NetworkClient.isLoadingScene = false;
OnClientSceneChanged();
}
IEnumerator UnloadAdditive(string sceneName)
{
if (mode == NetworkManagerMode.ClientOnly)
{
yield return SceneManager.UnloadSceneAsync(sceneName);
yield return Resources.UnloadUnusedAssets();
}
NetworkClient.isLoadingScene = false;
OnClientSceneChanged();
}
这个例子中在OnClientChangeScene中使用协程加载或卸载Additive场景,即对于这两类操作进行自定义(对于单一场景操作仍然使用默认操作)。在Load/UnLoad协程中,使用mode == NetworkManagerMode.ClientOnly来排除Host模式的主机客户端。由于是协程,因此可以一直等待操作成功,然后调用OnClientSceneChanged。