Unity Mirror 从入门到入神(二)


前序文章

我们跟踪下源码看看,Spawn是如何完成远端生成的,这里以Mirror提供的例子为例,看看Spawn是如何生效的。

        [Command(requiresAuthority = false)]
        public void SpawnVehicle(int vehicle,NetworkConnectionToClient networkConnection = null) {
            var newVehicle = Instantiate(vehicles[vehicle]);
            newVehicle.transform.position = NetworkManager.startPositions[Random.Range(0, NetworkManager.startPositions.Count-1)].transform.position;

            NetworkServer.Spawn(newVehicle, networkConnection);
            newVehicleNetId = newVehicle.GetComponent<NetworkIdentity>().netId;
        }

上面的这段代码位于某一个NetworkBehavior内,Command 表示这是一个有客户端到服务器的调用,且执行逻辑由服务器完成。这里在服务器通过指定预制体的形式实例化了一个vehicle然后将位置信息设置为 场景中的startPosition的最后一个位置 关于startPositions后面会补充,这里只需要知道这个事先在场景内设置的节点并附加了NetworkStartPosition组件的节点就行,该位置和PlayerPrefab的出生位置有着直接关系,PlayerPrefab在Mirror是必须的,他指代的是一个客户端,通常情况下我们可以直接用该PlayerPrefab作为玩家控制的角色进行使用,该内容后续会讲到。

PlayerPrefab 可以看作一个 带有NetworkIdentity的预制体,可选的在该预制体上附件其他游戏逻辑,与其他附加NetworkIdentity的联网预制体不同的是,该预制体的Spawn 和UnSpawn都有Mirror自行管理,不需要开发自己维护

这里的[Command(requiresAuthority = false)]中的requiresAuthority为了突破权限限制,默认情况下调用服务端的RPC允许的范围是该客户端是这个对象的Owner。比如有一道门,这个门一开始就在服务器中存在且不属于任何客户端,这个时候客户端Player要调用Door的open方法,Door检查这个玩家是不是有钥匙,那么这个时候就需要requiresAuthority=false来跳过Mirror的权限校验,这样就可以调用Door的方法,大概的逻辑代码就想下面这样

class Door:NetworkBehavior{

	[SyncVar]
	bool open;

	[Command(requiresAuthority)]
	public void open(NetworkConnectionToClient networkConnection = null){
		var keys = networkConnection.identity.gameObject.GetComponent<Player>().keys();
		if(hasKey(keys)){
			open = true;
		}
	}
	
	public boolean hasKey(keys){
		...
	}
}

class Player:NetworkBehavior{
   public Key[] keys;
}

我们接着继续Spawn的流程,注意代码有删减,只保留核心部分逻辑,如需查看完整版本代码请移步官网,贴出全部代码会让文章变得臃肿,打了...的就是代码被删了

Spawn
 //NetworkServer.cs#Spawn
 
 public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null)
 {
     SpawnObject(obj, ownerConnection);
 }

SpawnObject
 //NetworkServer.cs#SpawnObject
 static void SpawnObject(GameObject obj, NetworkConnection ownerConnection)
{
	...
    if (!obj.TryGetComponent(out NetworkIdentity identity))
    {
        Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", obj);
        return;
    }
	...
    identity.connectionToClient = (NetworkConnectionToClient)ownerConnection;

    // special case to make sure hasAuthority is set
    // on start server in host mode
    if (ownerConnection is LocalConnectionToClient)
        identity.isOwned = true;

    // NetworkServer.Unspawn sets object as inactive.
    // NetworkServer.Spawn needs to set them active again in case they were previously unspawned / inactive.
    identity.gameObject.SetActive(true);

    // only call OnStartServer if not spawned yet.
    // check used to be in NetworkIdentity. may not be necessary anymore.
    if (!identity.isServer && identity.netId == 0)
    {
        // configure NetworkIdentity
        // this may be called in host mode, so we need to initialize
        // isLocalPlayer/isClient flags too.
        identity.isLocalPlayer = NetworkClient.localPlayer == identity;
        identity.isClient = NetworkClient.active;
        identity.isServer = true;
        identity.netId = NetworkIdentity.GetNextNetworkId();

        // add to spawned (after assigning netId)
        spawned[identity.netId] = identity;

        // callback after all fields were set
        identity.OnStartServer();
    }
	...
    RebuildObservers(identity, true);
}

打住讲这部分内容之前需要先了解下NetworkIdentity

NetworkIdentity

但是这个好像没啥要说,关注下他的几个成员变量netId,spawned,assetId,sceneId,和Awake,其他的暂时用不上就不关注了。netId全网单位的唯一标识(从1自增,如果我没有记错的话),spawned持有引用,在client和server端都有,NetworkClient,NetworkServer也有,用于存储spawn出来的对象,当然有些单位只在服务器存在或者只在特定客户端存在。assetId表示来源于那个Prefab,可以通过该值,从NetworkManager的Prefabs中拿到NetworkManager.singleton.spawnPrefabs对应的预制体,SceneId表示该单位所在的场景ID生成方式如下

略…

还需要特别注意的,一个节点及其子子节点仅允许拥有一个NetworkIdentity,所以它必定被附加在父节点上。作为附加NetworkIdnetity的节点的所有父节点都不允许附加NetworkIdentity。

Awake
//NetworkIdentity.cs#Awake

  // Awake is only called in Play mode.
  // internal so we can call it during unit tests too.
  internal void Awake()
  {
      // initialize NetworkBehaviour components.
      // Awake() is called immediately after initialization.
      // no one can overwrite it because NetworkIdentity is sealed.
      // => doing it here is the fastest and easiest solution.
      InitializeNetworkBehaviours();

      if (hasSpawned)
      {
          Debug.LogError($"{name} has already spawned. Don't call Instantiate for NetworkIdentities that were in the scene since the beginning (aka scene objects).  Otherwise the client won't know which object to use for a SpawnSceneObject message.");
          SpawnedFromInstantiate = true;
          Destroy(gameObject);
      }
      hasSpawned = true;
  }



InitializeNetworkBehaviours
internal void InitializeNetworkBehaviours()
{
    // Get all NetworkBehaviour components, including children.
    // Some users need NetworkTransform on child bones, etc.
    // => Deterministic: https://forum.unity.com/threads/getcomponentsinchildren.4582/#post-33983
    // => Never null. GetComponents returns [] if none found.
    // => Include inactive. We need all child components.
    NetworkBehaviours = GetComponentsInChildren<NetworkBehaviour>(true);
    ValidateComponents();

    // initialize each one
    for (int i = 0; i < NetworkBehaviours.Length; ++i)
    {
        NetworkBehaviour component = NetworkBehaviours[i];
        component.netIdentity = this;
        component.ComponentIndex = (byte)i;
    }
}
ValidateComponents
void ValidateComponents()
{
    if (NetworkBehaviours == null)
    {
        Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" +
            $"Typically this can happen when a networked object is a child of a " +
            $"non-networked parent that's disabled, preventing Awake on the networked object " +
            $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject);
    }
    else if (NetworkBehaviours.Length > MaxNetworkBehaviours)
    {
        Debug.LogError($"NetworkIdentity {name} has too many NetworkBehaviour components: only {MaxNetworkBehaviours} NetworkBehaviour components are allowed in order to save bandwidth.", this);
    }
}

代码的注释部分很详细,一句话描述Awake,遍历所有的NetworkBehaviours子节点,最多不超过64个(因为使用64bit作为掩码,来判定NetworkBehavior中的数据是否需要同步)代码中是这么说的。每个NetworkBehavior都有一个索引ComponentIndex,用于细分同NetworkIdentity下的不同NetworkComponent。

// to save bandwidth, we send one 64 bit dirty mask
// instead of 1 byte index per dirty component.
// which means we can't allow > 64 components (it's enough).
const int MaxNetworkBehaviours = 64;

此时我们应该跳到NetworkBehaviour ,看下NetworkBehaviour的Awake干了嘛,学东西就是这样东拉西扯哈哈哈,就算是一坨毛线很乱,只要顺着线头就能理清

NetworkBehaviour

额… NetworkBehaviour的Awake方法并没有逻辑,

NetworkServer

很愉快我们可以继续接着Spawn了,请允许我再cv一次,凑一下字数

SpawnObject
  // NetworkServer.cs#SpawnObject
   ...
  // only call OnStartServer if not spawned yet.
  // check used to be in NetworkIdentity. may not be necessary anymore.
  if (!identity.isServer && identity.netId == 0)
  {
      // configure NetworkIdentity
      // this may be called in host mode, so we need to initialize
      // isLocalPlayer/isClient flags too.
      identity.isLocalPlayer = NetworkClient.localPlayer == identity;
      identity.isClient = NetworkClient.active;
      identity.isServer = true;
      identity.netId = NetworkIdentity.GetNextNetworkId();

      // add to spawned (after assigning netId)
      spawned[identity.netId] = identity;

      // callback after all fields were set
      identity.OnStartServer();
  }
 ...

通过查看identity的初始代码,可以明白这里就是首次identity生成执行的逻辑代码,注释也有说明 ,代码不做过多说明,这里跳转到NetworkIdentity.OnStartServer 然后会遍历所有的NetworkBehaviour的comp.OnStartServer方法,注意到目前为止所有的逻辑都是服务端执行的即执行环境是在服务器上,所以在服务器上Start的时间是在OnStartServer之前的(这里的描述是错的,因为执行OnStartServer是在当前ConnectServer.Update中需要等待当前Update代码执行完成之后 Identity.Start方法才会被执行),不过此时客户端上还没有执行一句有关Spawn的代码

OnStartServer
    
    //NetworkIdentity.cs#OnStartServer
    internal void OnStartServer()
    {
        foreach (NetworkBehaviour comp in NetworkBehaviours)
        {
            // an exception in OnStartServer should be caught, so that one
            // component's exception doesn't stop all other components from
            // being initialized
            // => this is what Unity does for Start() etc. too.
            //    one exception doesn't stop all the other Start() calls!
            try
            {
                comp.OnStartServer();
            }
            catch (Exception e)
            {
                Debug.LogException(e, comp);
            }
        }
    }

接下来直接跳过aoi,进入RebuildObservers(identity, true);直接假定aoi是null

RebuildObservers
//NetworkServer.cs#RebuildObservers
// RebuildObservers does a local rebuild for the NetworkIdentity.
// This causes the set of players that can see this object to be rebuild.
//
// IMPORTANT:
// => global rebuild would be more simple, BUT
// => local rebuild is way faster for spawn/despawn because we can
//    simply rebuild a select NetworkIdentity only
// => having both .observers and .observing is necessary for local
//    rebuilds
//
// in other words, this is the perfect solution even though it's not
// completely simple (due to .observers & .observing)
//
// Mirror maintains .observing automatically in the background. best of
// both worlds without any worrying now!
public static void RebuildObservers(NetworkIdentity identity, bool initialize)
{
    // if there is no interest management system,
    // or if 'force shown' then add all connections
    if (aoi == null || identity.visibility == Visibility.ForceShown)
    {
        RebuildObserversDefault(identity, initialize);
    }
    // otherwise let interest management system rebuild
    else
    {
        aoi.Rebuild(identity, initialize);
    }
}


RebuildObserversDefault
//NetworkServer.cs#RebuildObserversDefault
// interest management /
// Helper function to add all server connections as observers.
// This is used if none of the components provides their own
// OnRebuildObservers function.
// rebuild observers default method (no AOI) - adds all connections
static void RebuildObserversDefault(NetworkIdentity identity, bool initialize)
{
   // only add all connections when rebuilding the first time.
   // second time we just keep them without rebuilding anything.
   if (initialize)
   {
       // not force hidden?
       if (identity.visibility != Visibility.ForceHidden)
       {
           AddAllReadyServerConnectionsToObservers(identity);
       }
   }
}
AddAllReadyServerConnectionsToObservers
//NetworkServer.cs#AddAllReadyServerConnectionsToObservers
 internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity)
 {
     // add all server connections
     foreach (NetworkConnectionToClient conn in connections.Values)
     {
         // only if authenticated (don't send to people during logins)
         if (conn.isReady)
             identity.AddObserver(conn);
     }

     // add local host connection (if any)
     if (localConnection != null && localConnection.isReady)
     {
         identity.AddObserver(localConnection);
     }
 }

上面这部分代码就是服务器通知给所有额客户端,我要生娃了.AddAllReadyServerConnectionsToObservers变了当前所有的client如果状态没问题则将当前的这个client添加到identity的观察者队列中。Mirror源码中有大量的注释阐述了开发者当时是如何思考的,很有趣也有帮助,有时间可以看看,我就不看了,因为没时间。接下来看看 identity.AddObserver(conn);做了什么

NetworkIdentity

AddObserver
//NetworkIdentity#AddObserver
internal void AddObserver(NetworkConnectionToClient conn)
{	
    ...
    observers[conn.connectionId] = conn;
    conn.AddToObserving(this);
}

NetworkConnectionToClient

AddToObserving
    internal void AddToObserving(NetworkIdentity netIdentity)
    {
        observing.Add(netIdentity);

        // spawn identity for this conn
        NetworkServer.ShowForConnection(netIdentity, this);
    }

有点绕啊,主要逻辑是Identity和ClientConnect建立双向关联。NetworkServer.ShowForConnection(netIdentity, this);用于通知所有的Client生产该单位

NetworkServer

ShowForConnection
//NetworkServer.cs#ShowForConnection
// show / hide for connection //
internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn)
{
   if (conn.isReady)
       SendSpawnMessage(identity, conn);
}

SendSpawnMessage
//NetworkServer.cs#SendSpawnMessage
internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn)
{
    if (identity.serverOnly) return;

    //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}");

    // one writer for owner, one for observers
    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,
            // use local values for VR support
            position = identity.transform.localPosition,
            rotation = identity.transform.localRotation,
            scale = identity.transform.localScale,
            payload = payload
        };
        conn.Send(message);
    }
}

看到conn.Send 就知道服务端的代码终于到头了,这里拿到Writer然后构造message在通过conn发送消息出去,这里同时初始化了position,rotation,scale所以我说了除了transform以外的其他属性都需要是同步属性才能在客户端生效,这里CreateSpawnMessagePayload NetworkWriterPooled conn.Send不在过度深度只需要知道他们把消息发出去了。然后来看客户端干了什么?

LocalConnectionToServer

在Host模式的下的LocalClient,它的Send实现方式有助于我们定于到客户端的执行时机,

Send
       //LocalConnectionToServer#Send
       internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
       {
           if (segment.Count == 0)
           {
               Debug.LogError("LocalConnection.SendBytes cannot send zero bytes");
               return;
           }

           // instead of invoking it directly, we enqueue and process next update.
           // this way we can simulate a similar call flow as with remote clients.
           // the closer we get to simulating host as remote, the better!
           // both directions do this, so [Command] and [Rpc] behave the same way.

           //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
           NetworkWriterPooled writer = NetworkWriterPool.Get();
           writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
           connectionToClient.queue.Enqueue(writer);
       }

connectionToClient.queue.Enqueue(writer)他把消息压入到了LocalConnectionToClient的queue中,我们紧接着看下LocalConnectionToClient

LocalConnectionToClient

注意啊,从这里开始,我们的逻辑代码实际的执行环境已经属于客户端了

Update
//LocalConnectionToClient#Update
internal override void Update()
{
    base.Update();

    // process internal messages so they are applied at the correct time
    while (queue.Count > 0)
    {
        // call receive on queued writer's content, return to pool
        NetworkWriterPooled writer = queue.Dequeue();
        ArraySegment<byte> message = writer.ToArraySegment();

        // OnTransportData assumes a proper batch with timestamp etc.
        // let's make a proper batch and pass it to OnTransportData.
        Batcher batcher = GetBatchForChannelId(Channels.Reliable);
        batcher.AddMessage(message, NetworkTime.localTime);

        using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
        {
            // make a batch with our local time (double precision)
            if (batcher.GetBatch(batchWriter))
            {
                NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable);
            }
        }

        NetworkWriterPool.Return(writer);
    }
}

可以明确的看到Update中从queue里面读取出来,然后调用了 NetworkServer.OnTransportDataconnectionId是用来区分那个客户端的,localClient的该值一定是0,这是规约

NetworkServer

OnTransportData
//NetworkServer#OnTransportData
internal static void OnTransportData(int connectionId, ArraySegment<byte> data, int channelId)
 {
     if (connections.TryGetValue(connectionId, out NetworkConnectionToClient connection))
     {
         // client might batch multiple messages into one packet.
         // feed it to the Unbatcher.
         // NOTE: we don't need to associate a channelId because we
         //       always process all messages in the batch.
         if (!connection.unbatcher.AddBatch(data))
         {
             if (exceptionsDisconnect)
             {
                 Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
                 connection.Disconnect();
             }
             else
                 Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");

             return;
         }

         // process all messages in the batch.
         // only while NOT loading a scene.
         // if we get a scene change message, then we need to stop
         // processing. otherwise we might apply them to the old scene.
         // => fixes https://github.com/vis2k/Mirror/issues/2651
         //
         // NOTE: if scene starts loading, then the rest of the batch
         //       would only be processed when OnTransportData is called
         //       the next time.
         //       => consider moving processing to NetworkEarlyUpdate.
         while (!isLoadingScene &&
                connection.unbatcher.GetNextMessage(out ArraySegment<byte> message, out double remoteTimestamp))
         {
             using (NetworkReaderPooled reader = NetworkReaderPool.Get(message))
             {
                 // enough to read at least header size?
                 if (reader.Remaining >= NetworkMessages.IdSize)
                 {
                     // make remoteTimeStamp available to the user
                     connection.remoteTimeStamp = remoteTimestamp;

                     // handle message
                     if (!UnpackAndInvoke(connection, reader, channelId))
                     {
                         // warn, disconnect and return if failed
                         // -> warning because attackers might send random data
                         // -> messages in a batch aren't length prefixed.
                         //    failing to read one would cause undefined
                         //    behaviour for every message afterwards.
                         //    so we need to disconnect.
                         // -> return to avoid the below unbatches.count error.
                         //    we already disconnected and handled it.
                         if (exceptionsDisconnect)
                         {
                             Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}.");
                             connection.Disconnect();
                         }
                         else
                             Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}.");

                         return;
                     }
                 }
                 // otherwise disconnect
                 else
                 {
                     if (exceptionsDisconnect)
                     {
                         Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
                         connection.Disconnect();
                     }
                     else
                         Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");

                     return;
                 }
             }
         }

         // if we weren't interrupted by a scene change,
         // then all batched messages should have been processed now.
         // otherwise batches would silently grow.
         // we need to log an error to avoid debugging hell.
         //
         // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882
         // -> UnpackAndInvoke silently returned because no handler for id
         // -> Reader would never be read past the end
         // -> Batch would never be retired because end is never reached
         //
         // NOTE: prefixing every message in a batch with a length would
         //       avoid ever not reading to the end. for extra bandwidth.
         //
         // IMPORTANT: always keep this check to detect memory leaks.
         //            this took half a day to debug last time.
         if (!isLoadingScene && connection.unbatcher.BatchesCount > 0)
         {
             Debug.LogError($"Still had {connection.unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end.");
         }
     }
     else Debug.LogError($"HandleData Unknown connectionId:{connectionId}");
 }

好长,简化一下我们需要关注的,

 if (!UnpackAndInvoke(connection, reader, channelId))return;
UnpackAndInvoke
//NetworkServer.cs#UnpackAndInvoke
static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId)
{
    if (NetworkMessages.UnpackId(reader, out ushort msgType))
    {
        // try to invoke the handler for that message
        if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler))
        {
            handler.Invoke(connection, reader, channelId);
            connection.lastMessageTime = Time.time;
            return true;
        }
        else
        {
            // message in a batch are NOT length prefixed to save bandwidth.
            // every message needs to be handled and read until the end.
            // otherwise it would overlap into the next message.
            // => need to warn and disconnect to avoid undefined behaviour.
            // => WARNING, not error. can happen if attacker sends random data.
            Debug.LogWarning($"Unknown message id: {msgType} for connection: {connection}. This can happen if no handler was registered for this message.");
            // simply return false. caller is responsible for disconnecting.
            //connection.Disconnect();
            return false;
        }
    }
    else
    {
        // => WARNING, not error. can happen if attacker sends random data.
        Debug.LogWarning($"Invalid message header for connection: {connection}.");
        // simply return false. caller is responsible for disconnecting.
        //connection.Disconnect();
        return false;
    }
}

也好长,简化一下handler.Invoke(connection, reader, channelId); handlers是一个存在MsgType和Hander的字典

internal static Dictionary<ushort, NetworkMessageDelegate> handlers = new Dictionary<ushort, NetworkMessageDelegate>();

NetworkMessageDelegate

的定义如下 没啥好讲的

// Handles network messages on client and server
public delegate void NetworkMessageDelegate(NetworkConnection conn, NetworkReader reader, int channelId);

NetworkClient

在初始化的时候 Mirror会注册系统预制的消息类型及其Hander

RegisterMessageHandlers
//NetworkClient.cs#RegisterMessageHandlers
internal static void RegisterMessageHandlers(bool hostMode)
{
    // host mode client / remote client react to some messages differently.
    // but we still need to add handlers for all of them to avoid
    // 'message id not found' errors.
    if (hostMode)
    {
        RegisterHandler<ObjectDestroyMessage>(OnHostClientObjectDestroy);
        RegisterHandler<ObjectHideMessage>(OnHostClientObjectHide);
        RegisterHandler<NetworkPongMessage>(_ => { }, false);
        RegisterHandler<SpawnMessage>(OnHostClientSpawn);
        // host mode doesn't need spawning
        RegisterHandler<ObjectSpawnStartedMessage>(_ => { });
        // host mode doesn't need spawning
        RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
        // host mode doesn't need state updates
        RegisterHandler<EntityStateMessage>(_ => { });
    }
    else
    {
        RegisterHandler<ObjectDestroyMessage>(OnObjectDestroy);
        RegisterHandler<ObjectHideMessage>(OnObjectHide);
        RegisterHandler<NetworkPongMessage>(NetworkTime.OnClientPong, false);
        RegisterHandler<NetworkPingMessage>(NetworkTime.OnClientPing, false);
        RegisterHandler<SpawnMessage>(OnSpawn);
        RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
        RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
        RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
    }

    // These handlers are the same for host and remote clients
    RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage);
    RegisterHandler<ChangeOwnerMessage>(OnChangeOwner);
    RegisterHandler<RpcMessage>(OnRPCMessage);
}

RegisterHandler
//NetworkClient.cs#RegisterHandler
 public static void RegisterHandler<T>(Action<T> handler, bool requireAuthentication = true)
     where T : struct, NetworkMessage
 {
     ushort msgType = NetworkMessageId<T>.Id;
     if (handlers.ContainsKey(msgType))
     {
         Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning.");
     }

     // register Id <> Type in lookup for debugging.
     NetworkMessages.Lookup[msgType] = typeof(T);

     // we use the same WrapHandler function for server and client.
     // so let's wrap it to ignore the NetworkConnection parameter.
     // it's not needed on client. it's always NetworkClient.connection.
     void HandlerWrapped(NetworkConnection _, T value) => handler(value);
     handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
 }

NetworkMessageId

NetworkMessageId
    public static class NetworkMessageId<T> where T : struct, NetworkMessage
    {
        // automated message id from type hash.
        // platform independent via stable hashcode.
        // => convenient so we don't need to track messageIds across projects
        // => addons can work with each other without knowing their ids before
        // => 2 bytes is enough to avoid collisions.
        //    registering a messageId twice will log a warning anyway.
        public static readonly ushort Id = CalculateId();

        // Gets the 32bit fnv1a hash
        // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
        // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
        // This will create a more uniform 16bit hash, the method is described in:
        // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
        static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16();
    }
GetStableHashCode

这个Id通过Struct的名字 通过以下方式生成ushort 长度为两个字节,所以有概率会导致生成的MsgType变成一样的,这种时候调换一下单词的位置即可

 public static int GetStableHashCode(this string text)
 {
     unchecked
     {
         uint hash = 0x811c9dc5;
         uint prime = 0x1000193;

         for (int i = 0; i < text.Length; ++i)
         {
             byte value = (byte)text[i];
             hash = hash ^ value;
             hash *= prime;
         }

         //UnityEngine.Debug.Log($"Created stable hash {(ushort)hash} for {text}");
         return (int)hash;
     }
 }

通过以上流程我们知道接收Spawn的逻辑代码在NetworkClient.OnSpawn如果是host模式则为NetworkClient.OnHostClientSpawn

NetworkClient

OnSpawn
	//NetworkClient.cs#OnSpawn
    internal static void OnSpawn(SpawnMessage message)
   {
       // Debug.Log($"Client spawn handler instantiating netId={msg.netId} assetID={msg.assetId} sceneId={msg.sceneId:X} pos={msg.position}");
       if (FindOrSpawnObject(message, out NetworkIdentity identity))
       {
           ApplySpawnPayload(identity, message);
       }
   }
FindOrSpawnObject
//NetworkClient.cs#FindOrSpawnObject
    internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity)
    {
        // was the object already spawned?
        identity = GetExistingObject(message.netId);

        // if found, return early
        if (identity != null)
        {
            return true;
        }

        if (message.assetId == 0 && message.sceneId == 0)
        {
            Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId");
            return false;
        }

        identity = message.sceneId == 0 ? SpawnPrefab(message) :  );

        if (identity == null)
        {
            Debug.LogError($"Could not spawn assetId={message.assetId} scene={message.sceneId:X} netId={message.netId}");
            return false;
        }

        return true;
    }
//NetworkClient.cs#ApplySpawnPayload
	internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message)
{
    if (message.assetId != 0)
        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;

    // configure flags
    // the below DeserializeClient call invokes SyncVarHooks.
    // flags always need to be initialized before that.
    // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259
    identity.isOwned = message.isOwner;
    identity.netId = message.netId;

    if (message.isLocalPlayer)
        InternalAddPlayer(identity);

    // configure isClient/isLocalPlayer flags.
    // => after InternalAddPlayer. can't initialize .isLocalPlayer
    //    before InternalAddPlayer sets .localPlayer
    // => before DeserializeClient, otherwise SyncVar hooks wouldn't
    //    have isClient/isLocalPlayer set yet.
    //    fixes: https://github.com/MirrorNetworking/Mirror/issues/3259
    InitializeIdentityFlags(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.DeserializeClient(payloadReader, true);
        }
    }

    spawned[message.netId] = identity;
    if (identity.isOwned) connection?.owned.Add(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)
    {
        InvokeIdentityCallbacks(identity);
    }
}

FindOrSpawnObject判断是否允许生成,spawned存在则允许生成,SpawnMessage sceneId为0,所以会走SpawnPrefab,SpawnPrefab会先检查spawnHandlers中是否存在AssetId对应的SpawnHander,即之前提供的RegisterPrefab的功能,如果有则执行SpawnHandlerDelegate并拿到返回对象的NetworkIdentity,如果找不到SpawnHandlerDelegate执行默认的生成逻辑,Instantiate使用进行实例化,同时返回该对象的NetworkIdentity,注意这个阶段消息中的NetId和此时生成对象的NetworkIdentity中的数值是不一致的(可能一致)在 ApplySpawnPayload将统一该数值,并同时设置对应的transform数值,并将identity放入spawned,如果该预制体附加了其他的NetworkBehavior组件,则会通过附件 payload进行还原,通过payload中的mask来判断那些

NetworkBehaviour需要更新。

if (message.payload.Count > 0)
{
    using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload))
    {
        identity.DeserializeClient(payloadReader, true);
    }
}

NetworkBehaviour

在identity初始化的时候,会将所有的NetworkBehaviour都加到NetworkBehaviours并分配掩码,Mirror在NetworkBehaviours 提供了两个用于自主控制序列化的和反序列化的生命周期时间,预制体的结构一致保证了读写时的顺序一致。所以如果Spawn 在服务端调用Spawn方法前,它所有NetworkBehaviour的数值信息也会在Spawn时同步传递过来

OnSerialize
     public virtual void OnSerialize(NetworkWriter writer, bool initialState)
     {
         SerializeSyncObjects(writer, initialState);
         SerializeSyncVars(writer, initialState);
     }
OnDeserialize

     /// <summary>Override to do custom deserialization (instead of SyncVars/SyncLists). Use OnSerialize too.</summary>
     public virtual void OnDeserialize(NetworkReader reader, bool initialState)
     {
         DeserializeSyncObjects(reader, initialState);
         DeserializeSyncVars(reader, initialState);
     }

这样就完成了,一个Prefab的Spawn,现阶段不合适直接上手敲代码,先多了解了解概念,为后续的编写打好基础

未完待续…

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值