Unity Mirror 从入门到入神(二)

2024-05-16 06:44
文章标签 入门 unity mirror 入神

本文主要是介绍Unity Mirror 从入门到入神(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

        • Spawn
        • SpawnObject
      • NetworkIdentity
        • Awake
        • InitializeNetworkBehaviours
        • ValidateComponents
      • NetworkBehaviour
      • NetworkServer
        • SpawnObject
        • OnStartServer
        • RebuildObservers
        • RebuildObserversDefault
        • AddAllReadyServerConnectionsToObservers
      • NetworkIdentity
        • AddObserver
      • NetworkConnectionToClient
        • AddToObserving
      • NetworkServer
        • ShowForConnection
        • SendSpawnMessage
      • LocalConnectionToServer
        • Send
      • LocalConnectionToClient
        • Update
      • NetworkServer
        • OnTransportData
        • UnpackAndInvoke
      • NetworkMessageDelegate
      • NetworkClient
        • RegisterMessageHandlers
        • RegisterHandler
      • NetworkMessageId
        • NetworkMessageId
        • GetStableHashCode
      • NetworkClient
        • OnSpawn
        • FindOrSpawnObject
      • NetworkBehaviour
        • OnSerialize
        • OnDeserialize

前序文章

我们跟踪下源码看看,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#Spawnpublic static void Spawn(GameObject obj, NetworkConnection ownerConnection = null){SpawnObject(obj, ownerConnection);}
SpawnObject
 //NetworkServer.cs#SpawnObjectstatic 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 modeif (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 setidentity.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 onefor (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 setidentity.OnStartServer();}...

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

OnStartServer
    //NetworkIdentity.cs#OnStartServerinternal 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 connectionsif (aoi == null || identity.visibility == Visibility.ForceShown){RebuildObserversDefault(identity, initialize);}// otherwise let interest management system rebuildelse{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#AddAllReadyServerConnectionsToObserversinternal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity){// add all server connectionsforeach (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 connNetworkServer.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 observersusing (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 supportposition = 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#Sendinternal 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 timewhile (queue.Count > 0){// call receive on queued writer's content, return to poolNetworkWriterPooled 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();}elseDebug.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 userconnection.remoteTimeStamp = remoteTimestamp;// handle messageif (!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();}elseDebug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}.");return;}}// otherwise disconnectelse{if (exceptionsDisconnect){Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");connection.Disconnect();}elseDebug.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 messageif (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 spawningRegisterHandler<ObjectSpawnStartedMessage>(_ => { });// host mode doesn't need spawningRegisterHandler<ObjectSpawnFinishedMessage>(_ => { });// host mode doesn't need state updatesRegisterHandler<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 clientsRegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage);RegisterHandler<ChangeOwnerMessage>(OnChangeOwner);RegisterHandler<RpcMessage>(OnRPCMessage);
}
RegisterHandler
//NetworkClient.cs#RegisterHandlerpublic 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#OnSpawninternal 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#FindOrSpawnObjectinternal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity){// was the object already spawned?identity = GetExistingObject(message.netId);// if found, return earlyif (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#ApplySpawnPayloadinternal 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 supportidentity.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/3259identity.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/3259InitializeIdentityFlags(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,现阶段不合适直接上手敲代码,先多了解了解概念,为后续的编写打好基础

未完待续…

这篇关于Unity Mirror 从入门到入神(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/994166

相关文章

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

Java List 使用举例(从入门到精通)

《JavaList使用举例(从入门到精通)》本文系统讲解JavaList,涵盖基础概念、核心特性、常用实现(如ArrayList、LinkedList)及性能对比,介绍创建、操作、遍历方法,结合实... 目录一、List 基础概念1.1 什么是 List?1.2 List 的核心特性1.3 List 家族成

C#和Unity中的中介者模式使用方式

《C#和Unity中的中介者模式使用方式》中介者模式通过中介者封装对象交互,降低耦合度,集中控制逻辑,适用于复杂系统组件交互场景,C#中可用事件、委托或MediatR实现,提升可维护性与灵活性... 目录C#中的中介者模式详解一、中介者模式的基本概念1. 定义2. 组成要素3. 模式结构二、中介者模式的特点

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

史上最全MybatisPlus从入门到精通

《史上最全MybatisPlus从入门到精通》MyBatis-Plus是MyBatis增强工具,简化开发并提升效率,支持自动映射表名/字段与实体类,提供条件构造器、多种查询方式(等值/范围/模糊/分页... 目录1.简介2.基础篇2.1.通用mapper接口操作2.2.通用service接口操作3.进阶篇3

Python自定义异常的全面指南(入门到实践)

《Python自定义异常的全面指南(入门到实践)》想象你正在开发一个银行系统,用户转账时余额不足,如果直接抛出ValueError,调用方很难区分是金额格式错误还是余额不足,这正是Python自定义异... 目录引言:为什么需要自定义异常一、异常基础:先搞懂python的异常体系1.1 异常是什么?1.2

Python实现Word转PDF全攻略(从入门到实战)

《Python实现Word转PDF全攻略(从入门到实战)》在数字化办公场景中,Word文档的跨平台兼容性始终是个难题,而PDF格式凭借所见即所得的特性,已成为文档分发和归档的标准格式,下面小编就来和大... 目录一、为什么需要python处理Word转PDF?二、主流转换方案对比三、五套实战方案详解方案1:

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与

Spring Boot 与微服务入门实战详细总结

《SpringBoot与微服务入门实战详细总结》本文讲解SpringBoot框架的核心特性如快速构建、自动配置、零XML与微服务架构的定义、演进及优缺点,涵盖开发环境准备和HelloWorld实战... 目录一、Spring Boot 核心概述二、微服务架构详解1. 微服务的定义与演进2. 微服务的优缺点三