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

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al

MySQL-CRUD入门1

文章目录 认识配置文件client节点mysql节点mysqld节点 数据的添加(Create)添加一行数据添加多行数据两种添加数据的效率对比 数据的查询(Retrieve)全列查询指定列查询查询中带有表达式关于字面量关于as重命名 临时表引入distinct去重order by 排序关于NULL 认识配置文件 在我们的MySQL服务安装好了之后, 会有一个配置文件, 也就

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非

MySQL入门到精通

一、创建数据库 CREATE DATABASE 数据库名称; 如果数据库存在,则会提示报错。 二、选择数据库 USE 数据库名称; 三、创建数据表 CREATE TABLE 数据表名称; 四、MySQL数据类型 MySQL支持多种类型,大致可以分为三类:数值、日期/时间和字符串类型 4.1 数值类型 数值类型 类型大小用途INT4Bytes整数值FLOAT4By

【QT】基础入门学习

文章目录 浅析Qt应用程序的主函数使用qDebug()函数常用快捷键Qt 编码风格信号槽连接模型实现方案 信号和槽的工作机制Qt对象树机制 浅析Qt应用程序的主函数 #include "mywindow.h"#include <QApplication>// 程序的入口int main(int argc, char *argv[]){// argc是命令行参数个数,argv是