携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第34天,点击查看活动详情
前言
本篇中,我们详细分析AddPlayerForConnection函数,从而在理解Mirror服务器和客户端对象的关系的基础上,探寻服务器上生成Player对象的机制。其实这个机制对于其他普通对象也是一样的。
AddPlayerForConnection
public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player)
{
NetworkIdentity identity = player.GetComponent<NetworkIdentity>();
//省略检查操作
conn.identity = identity;
// Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients)
identity.SetClientOwner(conn);
// special case, we are in host mode, set hasAuthority to true so that all overrides see it
if (conn is LocalConnectionToClient)
{
identity.hasAuthority = true;
NetworkClient.InternalAddPlayer(identity);
}
// set ready if not set yet
SetClientReady(conn);
// Debug.Log($"Adding new playerGameObject object netId: {identity.netId} asset ID: {identity.assetId}");
Respawn(identity);
return true;
}
Player identity和连接绑定
conn.identity = identity将这player对象的network identity设置给当前客户端连接对象,这样该player就和这个连接绑定到一起了。identity.SetClientOwner(conn);这是一个反向的绑定,方便player使用连接对象。
Host模式分支处理
Host模式,对于本地Player调用NetworkClient.InternalAddPlayer(identity);
这儿简单说说Host模式,由于Host模型下服务器和客户端共存于同一进程中,他们是共享内存的,所以所有服务器上的对象不需要再生成一遍了。这儿InternalAddPlayer内部只是设置了NetworkClient.localPlayer为当前生成的Player对象的identity。当然Host模式的处理不仅仅是这儿,会对很多情况都做特殊处理。其实Mirror为了支持局域网Host模式还是增加了整个系统的复杂性。
SetClientReady
只从这个函数名来看,似乎是个简单的状态设置。但其实内部做了很重要的事情。我们看一下它的实现:
public static void SetClientReady(NetworkConnectionToClient conn)
{
// set ready
conn.isReady = true;
// client is ready to start spawning objects
if (conn.identity != null)
SpawnObserversForConnection(conn);
}
- 首先,设置
conn.isReady,这真的仅仅是设置一个标志,表示该链接已经就绪。 - 调用
SpawnObserversForConnection。我们上篇说过,所谓观察者,其实是客户端上对应于服务器上对象的镜像对象,这些镜像对象观察着服务器上对应的对象,同步它们状态的改变。当连接已经就绪时,我们就可以为这个客户端创建所有服务器上当前已经存在的对象的观察者。而当前服务器上存在的对象,是位于NetworkServer.spawned这个字典中,当在服务器上Spawn对象时,会使用对象NetworkIdentity组件的netId属性作为key,将对象加入到这个字典中(我们很快就会看到)。继续深入,最终会调用identity.AddObserver(conn);这个identity是服务器上NetworkServer.spawned中各个对象的identity。这个调用内部最终会调用SendSpawnMessage(identity, conn);发送SpawnMessage给客户端,从而在客户端上生成对象。需要注意的时,此时Player尚未在服务器上真正生成,即还没有加入到NetworkServer.spawned字典中,因此也不会在这儿向客户端发送消息生成客户端上的Player镜像(观察者)。
对象可见性和aoi系统
上面创建观察者时其实用到了对象的可见性和aoi系统,简单介绍一下。服务器可以看到所有的对象,而客户端不一定能看到全部的对象,而是否可以看到就由可见性和aoi系统共同决定。
对象可见性
对于对象可见性来说,有三种:
public enum Visibility { Default, ForceHidden, ForceShown }
Default是使用aoi系统;ForceHidden是在某些情况下不让客户端看到该对象,比如怪物正在重生;ForceShown则是让所有客户端总能看到该对象。看一下这儿的关键代码:
// ForceShown: add no matter what
if (identity.visible == Visibility.ForceShown)
{
identity.AddObserver(conn);
}
// ForceHidden: don't show no matter what
else if (identity.visible == Visibility.ForceHidden)
{
// do nothing
}
// default: legacy system / new system / no system support
else if (identity.visible == Visibility.Default)
{
// aoi system
if (aoi != null)
{
// call OnCheckObserver
if (aoi.OnCheckObserver(identity, conn))
identity.AddObserver(conn);
}
// no system: add all observers by default
else
{
identity.AddObserver(conn);
}
}
很明显,只有可见的对象才会调用identity.AddObserver(conn);
aoi系统
aoi即InterestManagement,兴趣管理。我们从上面的代码可以看到,aoi对象如果存在,就会调用它的OnCheckObserver方法检查某identity对于某connection是否是可见的。而InterestManagement是一个抽象类,主要是提供了一些接口来进行"兴趣管理"。Mirror默认提供了几种兴趣管理对象:
- 比如基本的基于距离的兴趣管理(根据距离来判断可见性)
- 还有基于网格的(SpatialHashing)空间哈希可见性管理
- 以及基于场景的(可以将对象放置到不同的场景中,场景是隔离的),这对于大型游戏,即服务器上同时有很多玩家处于不同的场景中,是很有用的。
- 还有基于队伍的,比如队伍内部可以互相看到,还可以共享视野,而对面的队伍就不能轻易看到了。
待续
AddPlayerForConnection进行到这儿已经做了大量的工作,但是最重要的工作还没做,即SpawnPlayer。这是在Respawn中进行的。这是真正在服务器上Spawn Player的地方,并且也会给所有已经连接的客户端发送消息生成这个Player的镜像(而不仅仅是当前连接的客户端),我们下篇继续。