携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第36天,点击查看活动详情
客户端生成Player机制
注册SpawnMessage
处理器
客户端连接时,调用Connect
方法:
public static void Connect(string address)
{
RegisterSystemHandlers(false);
Transport.activeTransport.enabled = true;
AddTransportHandlers();
connectState = ConnectState.Connecting;
Transport.activeTransport.ClientConnect(address);
connection = new NetworkConnectionToServer();
}
再实际使用Transport连接服务器之前,会先调用RegisterSystemHandlers
注册系统处理器。
RegisterSystemHandlers
方法中,会根据当前是Host模式还是纯客户端模式,注册不同的处理器。而对于SpawnMessage,会注册如下处理器,我们只关注纯客户端模式。
RegisterHandler<SpawnMessage>(OnSpawn);
OnSpawn方法
internal static void OnSpawn(SpawnMessage message)
{
if (FindOrSpawnObject(message, out NetworkIdentity identity))
{
ApplySpawnPayload(identity, message);
}
}
FindOrSpawnObject
该方法首先调用FindOrSpawnObject
来查询该message中所描述的对象是否已经在客户端存在,如果不存在则返回true表示需要生成对象。
internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity)
{
identity = GetExistingObject(message.netId);
// if found, return early
if (identity != null)
{
return true;
}
identity = message.sceneId == 0 ? SpawnPrefab(message) : SpawnSceneObject(message.sceneId);
if (identity == null)
{
Debug.LogError($"Could not spawn assetId={message.assetId} scene={message.sceneId:X} netId={message.netId}");
return false;
}
return true;
}
- 首先我们要关注一下
GetExistingObject
方法,该方法接受的参数为netId。
static NetworkIdentity GetExistingObject(uint netid)
{
spawned.TryGetValue(netid, out NetworkIdentity localObject);
return localObject;
}
该方法内部的spawned
是NetworkClient类的一个静态字典,包含了客户端上所有的网络对象。和服务器类似,在客户端上spawn对象,也需要加入字典中。所以查询这个字典就可以知道该对象是否已经存在于客户端中。
- 如果客户端还没有该对象,则使用
SpawnPrefab
或SpawnSceneObject
生成。这儿是根据是否存在sceneId来判断该对象是否是场景游戏对象
,如果没有sceneID,则需要从prefab动态生成,否则会特殊处理。关于场景游戏对象,请参考Unity Mirror联网游戏开发(4) 场景中的Game Objects
SpawnPrefab
该方法会根据message.assetId,去获取注册到Mirror中的prefab,然后使用该prefab实例化出游戏对象。不过该方法内部也处理了自定义实例化,即spawnHandler。本文就不细说了。
SpawnSceneObject
场景中原有的网络对象,会存在于NetworkClient的spawnableObjects字典中,这些对象会在场景载入后被隐藏。而SpawnSceneObject方法会从中取出对象,并将对象从该字典中删除。
ApplySpawnPayload
如果对象找到了或者生成了,则调用该方法进行设置。
internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message)
{
if (message.assetId != Guid.Empty)
identity.assetId = message.assetId;
if (!identity.gameObject.activeSelf)
{
identity.gameObject.SetActive(true);
}
// apply local values for VR support
identity.transform.localPosition = message.position;
identity.transform.localRotation = message.rotation;
identity.transform.localScale = message.scale;
identity.hasAuthority = message.isOwner;
identity.netId = message.netId;
if (message.isLocalPlayer)
InternalAddPlayer(identity);
// deserialize components if any payload
// (Count is 0 if there were no components)
if (message.payload.Count > 0)
{
using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload))
{
identity.OnDeserializeAllSafely(payloadReader, true);
}
}
spawned[message.netId] = identity;
// the initial spawn with OnObjectSpawnStarted/Finished calls all
// object's OnStartClient/OnStartLocalPlayer after they were all
// spawned.
// this only happens once though.
// for all future spawns, we need to call OnStartClient/LocalPlayer
// here immediately since there won't be another OnObjectSpawnFinished.
if (isSpawnFinished)
{
identity.NotifyAuthority();
identity.OnStartClient();
CheckForLocalPlayer(identity);
}
}
- 如果对象不可见(主要是对于scene game object),则会调用
SetActive(true)
- 然后,最重要的是调用了
spawned[message.netId] = identity;
将其添加到客户端的spawned字典中,这样该对象就真正在客户端spawn成功了。 - 这儿的isSpawnFinished其实是针对之前客户端刚连接时,生成所有已经在服务器上存在的对象时,会有
ObjectSpawnStarted
和ObjectSpawnFinished
两个消息发送。当客户端接收到ObjectSpawnFinished
消失时,表示已经生成了所有需要生成的对象,会设置isSpawnFinished为true。
总结
本系列文章深入分析了默认情况下Mirror自动创建Player的机制,该机制其实是基于Mirror的Spawn System。
- 客户端连接时会发送
AddPlayerMessage
消息给服务器请求添加player - 服务器会调用
OnServerAddPlayer
生成player对象实例 - 服务器会继续调用
NetworkServer.AddPlayerForConnection
进行处理。首先会根据可见性和aoi系统,找到该客户端可以看到的所有对象,发送消息让客户端生成这些对象 - 之后服务器会对于所有连接上的客户端,不仅仅是刚刚连接的这个客户端,发送消息,让这些客户端生成player
- 客户端收到消息后在客户端生成对象。