携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第35天,点击查看活动详情
Player Spawn机制详解
关于对象Spawn
Spawn机制是Mirror最重要的机制,我觉得甚至比同步机制还重要。因为只有能正确的生成对象才有进一步的同步。而游戏场景中存在大量的对象,且在游戏过程中不断有新对象动态生成,而且客户端也可以随时连接上服务器(包含断线重连),因此在任意情况下,都能让客户端上生成正确的对象是非常复杂的事情。虽然本文是探讨Player生成机制,但其实Player的生成也是使用了对象的通用Spawn机制。我们先接着上篇看。
Respawn方法
static void Respawn(NetworkIdentity identity)
{
if (identity.netId == 0)
{
// If the object has not been spawned, then do a full spawn and update observers
Spawn(identity.gameObject, identity.connectionToClient);
}
else
{
// otherwise just replace his data
SendSpawnMessage(identity, identity.connectionToClient);
}
}
- 判断identity.netId,如果为0,说明该对象还没有注册到服务器上,需要进行完整的spawn操作。
- 否则该对象已经存在,但是由于某种原因,又触发了
AddPlayerForConnection,由于该对象已经在服务器上存在,有很多工作都不需要再做,因此这儿只是发送SpawnMessage,给客户端同步该对象的最新状态。
Spawn和SpawnObject
Spawn方法内部只是简单调用了SpawnObject方法,即Player的spawn和普通对象是使用相同的方法。
static void SpawnObject(GameObject obj, NetworkConnection ownerConnection)
{
//省略条件检测
NetworkIdentity identity = obj.GetComponent<NetworkIdentity>();
//省略一些不太重要的内容
identity.OnStartServer();
RebuildObservers(identity, true);
}
这个函数内部我们主要看两个地方,OnStartServer和RebuildObservers。
NetworkIdentity.OnStartServer
注意这儿省略了很多代码,只列出最核心的部分:
internal void OnStartServer()
{
netId = GetNextNetworkId();
observers = new Dictionary<int, NetworkConnectionToClient>();
NetworkServer.spawned[netId] = this;
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
try
{
comp.OnStartServer();
}
catch (Exception e)
{
Debug.LogException(e, comp);
}
}
}
- 设置netId,Mirror使用netId来标识网络对象,在服务器上和客户端上的同一对象使用相同的netId。
- 加入
NetworkServer.spawned字典 - 对该对象上的所有
NetworkBehaviour组件调用OnStartServer
至此,该Player对象已经在服务器上生成了,存在于spawned字典中并拥有了自己的netId。
RebuildObservers
上篇中我们介绍了观察者的概念,即在存在于客户端上,观察服务器上对象的对象,他和服务器上的对应对象具有相同的netId。
public static void RebuildObservers(NetworkIdentity identity, bool initialize)
{
// observers are null until OnStartServer creates them
if (identity.observers == null)
return;
// if there is no interest management system,
// or if 'force shown' then add all connections
if (aoi == null || identity.visible == Visibility.ForceShown)
{
RebuildObserversDefault(identity, initialize);
}
// otherwise let interest management system rebuild
else
{
RebuildObserversCustom(identity, initialize);
}
}
我们只看不使用aoi的默认情况,即RebuildObserversDefault方法。该方法内部调用了AddAllReadyServerConnectionsToObservers。这个函数会遍历所有已经存在的客户端连接,向当前对象的identity添加这些connection。
foreach (NetworkConnectionToClient conn in connections.Values)
{
if (conn.isReady)
identity.AddObserver(conn);
}
这来到了之前我们看过的一个函数AddObserver,这在客户端连接时,向它添加已经存在的对象时曾经使用过。看来我们已经接近最底层的机制了。
AddObserver(NetworkConnectionToClient conn)
该函数调用了conn.AddToObserving(this):
internal void AddToObserving(NetworkIdentity netIdentity)
{
observing.Add(netIdentity);
// spawn identity for this conn
NetworkServer.ShowForConnection(netIdentity, this);
}
记住,我们一直是在执行服务器代码。这儿终于来到了为这个connection执行spawn identity的地方:
internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn)
{
if (conn.isReady)
SendSpawnMessage(identity, conn);
}
有遇到了之前看到的函数SendSpawnMessage,来了,这就是最后一步了,发送网络消息。服务器和客户端代码的分界线就是发送消息,发送了消息就等着客户端执行了。我们先看下这个方法。
SendSpawnMessage
internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn)
{
using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get())
{
bool isOwner = identity.connectionToClient == conn;
ArraySegment<byte> payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter);
SpawnMessage message = new SpawnMessage
{
netId = identity.netId,
isLocalPlayer = conn.identity == identity,
isOwner = isOwner,
sceneId = identity.sceneId,
assetId = identity.assetId,
position = identity.transform.localPosition,
rotation = identity.transform.localRotation,
scale = identity.transform.localScale,
payload = payload
};
conn.Send(message);
}
}
这儿创建了SpawnMessage,这是Mirror预定义网络消息中最复杂的一个。我们看看他的定义:
public struct SpawnMessage : NetworkMessage
{
// netId of new or existing object
public uint netId;
public bool isLocalPlayer;
// Sets hasAuthority on the spawned object
public bool isOwner;
public ulong sceneId;
// If sceneId != 0 then it is used instead of assetId
public Guid assetId;
// Local position
public Vector3 position;
// Local rotation
public Quaternion rotation;
// Local scale
public Vector3 scale;
// serialized component data
// ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
最重要的,其实是3个id,即netId,sceneId和assetId。然后会发送对象当前的位置旋转缩放。
这儿的payload涉及到Mirror的消息打包机制,这儿就不展开了。
小结
本篇中,我们终于分析完服务器上Spawn Player的完整过程,最终走到了发送SpawnMessage。而本系列的最后一篇,将分析客户端接收SpawnMessage,在客户端上生成player观察者对象的机制。