深入Unity Mirror场景加载机制(1)

1,741 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

引言

使用Mirror这种高层网络API开发联网游戏,由于服务器代码和客户端代码是写在一个工程中的,虽然比较方便,但是也让服务器和客户端的界限比较模糊,特别是对于一开始当做单机开发的游戏,本身不是按照Mirror的方式开发,有很多单例的全局对象,其中混杂了服务器和客户端的代码,这就需要我们有非常清晰的认识。首先,最重要的是要理解Mirror是在服务器上进行大多数的计算。Unity场景首先必须在服务器上存在,需要加载进服务器内存,然后服务器发出消息让客户端也载入相应的场景。即服务器和客户端都有相同的场景存在。而场景中的对象分为两种,网络游戏对象和非网络游戏对象。对于网络游戏对象,就是在服务器上进行计算,并将其状态同步到客户端场景里面的镜像对象。而非网络游戏对象,是分别在服务器和客户端场景里面进行计算的。

本篇从场景加载分析,加强对于场景在服务器和客户端上分别存在的认知,主要内容包含onlineScene, offlineScene的自动加载,场景消息和客户端场景加载,手动加载任意场景等机制。在深入理解这些机制之后,我们就可以实现在游戏过程中,任意动态加载卸载场景和Additive子场景。

online场景自动加载机制

在NetworkManager中可以配置offline Scene和online Scene,当启动服务器后,如果online scene配置存在,则会自动载入它。我们以Host模式为例:

public void StartHost()
{
    SetupServer();
    OnStartHost();
    if (IsServerOnlineSceneChangeNeeded())
    {
        // call FinishStartHost after changing scene.
        finishStartHostPending = true;
        ServerChangeScene(onlineScene);
    }
    // otherwise call FinishStartHost directly
    else
    {
        FinishStartHost();
    }
}

启动服务器时,会调用ServerChangeScene(onlineScene);加载online scene。

ServerChangeScene

该函数会直接在服务器上加载场景,并且发送网络消息给所有的客户端去加载相应的场景。核心代码如下(省略参数检查等代码):

public virtual void ServerChangeScene(string newSceneName)
{
    NetworkServer.SetAllClientsNotReady();
    networkSceneName = newSceneName;

    // Let server prepare for scene change
    OnServerChangeScene(newSceneName);

    // set server flag to stop processing messages while changing scenes
    // it will be re-enabled in FinishLoadScene.
    NetworkServer.isLoadingScene = true;

    loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName);

    // ServerChangeScene can be called when stopping the server
    // when this happens the server is not active so does not need to tell clients about the change
    if (NetworkServer.active)
    {
        // notify all clients about the new scene
        NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName });
    }
}
  • 直接使用SceneManager.LoadSceneAsync异步加载场景
  • 同时使用NetworkServer.SendToAll发送SceneMessage给客户端去加载相同的场景。

客户端响应SceneMessage消息

SceneMessage消息

Mirror中场景加载必须是在服务器加载然后让客户端同步加载,没有客户端自己单独加载的情况,因为即便加载了也不能同步到其他客户端,相当于玩了个单机游戏,这就不属于联网游戏的范围了。因此客户端只要响应服务器发送过来的SceneMessage消息。首先看一下这个消息的定义:

public struct SceneMessage : NetworkMessage
{
    public string sceneName;
    // Normal = 0, LoadAdditive = 1, UnloadAdditive = 2
    public SceneOperation sceneOperation;
    public bool customHandling;
}

该消息中指定了场景的名字,这个名字和Unity场景加载函数中的要求一致,即:

  • 如果使用从Assets开始的路径,就要带有.unity后缀。例如: Assets/Scenes/Online.unity
  • 如果使用Build中的场景路径,则不需要包含Assets以及.unity后缀,例如:Scenes/Online
  • 也可以直接使用场景名,如Online。但是要避免同名字冲突,不建议这么用。

另外可以指定场景操作的类型,即SceneOperation:

  • 默认为0,即单一场景加载,这会卸载掉当前场景(如果存在)并加载指定的场景。
  • 值为1则是使用Additive加载模式,保留当前场景,并加载指定场景。
  • 值为2则是卸载掉Additive场景。
  • 注意并没有卸载掉单一场景的操作,因为并不需要,Unity游戏运行时总需要有一个场景存在,载入新的单一场景就会自动卸载掉之前的单一场景。

customHandling参数如果为false,则Mirror会自动进行场景操作,否则需要自己进行操作,后面会看到。

SceneMessage消息处理器的注册

客户端在RegisterClientMessages中注册SceneManager的处理器:

void RegisterClientMessages()
{
    NetworkClient.OnConnectedEvent = OnClientConnectInternal;
    NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal;
    NetworkClient.OnErrorEvent = OnClientError;
    NetworkClient.RegisterHandler<NotReadyMessage>(OnClientNotReadyMessageInternal);
    NetworkClient.RegisterHandler<SceneMessage>(OnClientSceneInternal, false);

    if (playerPrefab != null)
        NetworkClient.RegisterPrefab(playerPrefab);

    foreach (GameObject prefab in spawnPrefabs.Where(t => t != null))
        NetworkClient.RegisterPrefab(prefab);
}

该函数中会注册事件处理器和网络消息处理器,并且注册prefab。我们这儿只关注NetworkClient.RegisterHandler<SceneMessage>(OnClientSceneInternal, false);这句,找到SceneMessage的处理器为OnClientSceneInternal。另外RegisterClientMessages函数本身是在客户端连接函数中被调用,例如:void StartClient(Uri uri)

OnClientSceneInternal

OnClientSceneInternal只是检测客户端是否连接,如果已连接则使用ClientChangeScene让客户端加载场景。

void OnClientSceneInternal(SceneMessage msg)
{ 
    // This needs to run for host client too. NetworkServer.active is checked there
    if (NetworkClient.isConnected)
    {
        ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling);
    }
}

小结

  • 本篇中我们了解到Mirror是先在服务器端加载场景,然后发送消息让所有的客户端也加载场景。
  • 客户端通过响应SceneMessage来处理场景加载。
  • 下篇中我们将详细分析客户端场景加载过程。