Mirror Networking
NetworkClient.cs
1using System;
2using System.Collections.Generic;
3using System.Linq;
5using UnityEngine;
6
7namespace Mirror
8{
9 public enum ConnectState
10 {
11 None,
12 // connecting between Connect() and OnTransportConnected()
13 Connecting,
14 Connected,
15 // disconnecting between Disconnect() and OnTransportDisconnected()
16 Disconnecting,
17 Disconnected
18 }
19
21 public static class NetworkClient
22 {
23 // message handlers by messageId
24 internal static readonly Dictionary<ushort, NetworkMessageDelegate> handlers =
25 new Dictionary<ushort, NetworkMessageDelegate>();
26
28 // client sees OBSERVED spawned ones.
29 public static readonly Dictionary<uint, NetworkIdentity> spawned =
30 new Dictionary<uint, NetworkIdentity>();
31
33 public static NetworkConnection connection { get; internal set; }
34
36 // TODO redundant state. point it to .connection.isReady instead (& test)
37 // TODO OR remove NetworkConnection.isReady? unless it's used on server
38 //
39 // TODO maybe ClientState.Connected/Ready/AddedPlayer/etc.?
40 // way better for security if we can check states in callbacks
41 public static bool ready;
42
44 public static NetworkIdentity localPlayer { get; internal set; }
45
46 // NetworkClient state
47 internal static ConnectState connectState = ConnectState.None;
48
50 // empty if the client has not connected yet.
51 public static string serverIp => connection.address;
52
54 // (= while the network is active)
55 public static bool active => connectState == ConnectState.Connecting ||
56 connectState == ConnectState.Connected;
57
59 public static bool isConnecting => connectState == ConnectState.Connecting;
60
62 public static bool isConnected => connectState == ConnectState.Connected;
63
66
67 // OnConnected / OnDisconnected used to be NetworkMessages that were
68 // invoked. this introduced a bug where external clients could send
69 // Connected/Disconnected messages over the network causing undefined
70 // behaviour.
71 // => public so that custom NetworkManagers can hook into it
72 public static Action OnConnectedEvent;
73 public static Action OnDisconnectedEvent;
74 public static Action<TransportError, string> OnErrorEvent;
75
77 public static readonly Dictionary<Guid, GameObject> prefabs =
78 new Dictionary<Guid, GameObject>();
79
80 // custom spawn / unspawn handlers.
81 // useful to support prefab pooling etc.:
82 // https://mirror-networking.gitbook.io/docs/guides/gameobjects/custom-spawnfunctions
83 internal static readonly Dictionary<Guid, SpawnHandlerDelegate> spawnHandlers =
84 new Dictionary<Guid, SpawnHandlerDelegate>();
85 internal static readonly Dictionary<Guid, UnSpawnDelegate> unspawnHandlers =
86 new Dictionary<Guid, UnSpawnDelegate>();
87
88 // spawning
89 // internal for tests
90 internal static bool isSpawnFinished;
91
92 // Disabled scene objects that can be spawned again, by sceneId.
93 internal static readonly Dictionary<ulong, NetworkIdentity> spawnableObjects =
94 new Dictionary<ulong, NetworkIdentity>();
95
96 static Unbatcher unbatcher = new Unbatcher();
97
98 // interest management component (optional)
99 // only needed for SetHostVisibility
100 public static InterestManagement aoi;
101
102 // scene loading
103 public static bool isLoadingScene;
104
105 // initialization //////////////////////////////////////////////////////
106 static void AddTransportHandlers()
107 {
108 // += so that other systems can also hook into it (i.e. statistics)
109 Transport.activeTransport.OnClientConnected += OnTransportConnected;
110 Transport.activeTransport.OnClientDataReceived += OnTransportData;
111 Transport.activeTransport.OnClientDisconnected += OnTransportDisconnected;
112 Transport.activeTransport.OnClientError += OnTransportError;
113 }
114
115 static void RemoveTransportHandlers()
116 {
117 // -= so that other systems can also hook into it (i.e. statistics)
118 Transport.activeTransport.OnClientConnected -= OnTransportConnected;
119 Transport.activeTransport.OnClientDataReceived -= OnTransportData;
120 Transport.activeTransport.OnClientDisconnected -= OnTransportDisconnected;
121 Transport.activeTransport.OnClientError -= OnTransportError;
122 }
123
124 internal static void RegisterSystemHandlers(bool hostMode)
125 {
126 // host mode client / remote client react to some messages differently.
127 // but we still need to add handlers for all of them to avoid
128 // 'message id not found' errors.
129 if (hostMode)
130 {
131 RegisterHandler<ObjectDestroyMessage>(OnHostClientObjectDestroy);
132 RegisterHandler<ObjectHideMessage>(OnHostClientObjectHide);
133 RegisterHandler<NetworkPongMessage>(_ => {}, false);
134 RegisterHandler<SpawnMessage>(OnHostClientSpawn);
135 // host mode doesn't need spawning
136 RegisterHandler<ObjectSpawnStartedMessage>(_ => {});
137 // host mode doesn't need spawning
138 RegisterHandler<ObjectSpawnFinishedMessage>(_ => {});
139 // host mode doesn't need state updates
140 RegisterHandler<EntityStateMessage>(_ => {});
141 }
142 else
143 {
144 RegisterHandler<ObjectDestroyMessage>(OnObjectDestroy);
145 RegisterHandler<ObjectHideMessage>(OnObjectHide);
146 RegisterHandler<NetworkPongMessage>(NetworkTime.OnClientPong, false);
147 RegisterHandler<SpawnMessage>(OnSpawn);
148 RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
149 RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
150 RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
151 }
152
153 // These handlers are the same for host and remote clients
154 RegisterHandler<ChangeOwnerMessage>(OnChangeOwner);
155 RegisterHandler<RpcMessage>(OnRPCMessage);
156 }
157
158 // connect /////////////////////////////////////////////////////////////
160 public static void Connect(string address)
161 {
162 // Debug.Log($"Client Connect: {address}");
163 Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first");
164
165 RegisterSystemHandlers(false);
166 Transport.activeTransport.enabled = true;
167 AddTransportHandlers();
168
169 connectState = ConnectState.Connecting;
171
173 }
174
176 public static void Connect(Uri uri)
177 {
178 // Debug.Log($"Client Connect: {uri}");
179 Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first");
180
181 RegisterSystemHandlers(false);
182 Transport.activeTransport.enabled = true;
183 AddTransportHandlers();
184
185 connectState = ConnectState.Connecting;
187
189 }
190
191 // TODO why are there two connect host methods?
192 // called from NetworkManager.FinishStartHost()
193 public static void ConnectHost()
194 {
195 //Debug.Log("Client Connect Host to Server");
196
197 RegisterSystemHandlers(true);
198
199 connectState = ConnectState.Connected;
200
201 // create local connection objects and connect them
202 LocalConnectionToServer connectionToServer = new LocalConnectionToServer();
203 LocalConnectionToClient connectionToClient = new LocalConnectionToClient();
204 connectionToServer.connectionToClient = connectionToClient;
205 connectionToClient.connectionToServer = connectionToServer;
206
207 connection = connectionToServer;
208
209 // create server connection to local client
210 NetworkServer.SetLocalConnection(connectionToClient);
211 }
212
214 // called from NetworkManager.StartHostClient
215 // TODO why are there two connect host methods?
216 public static void ConnectLocalServer()
217 {
218 // call server OnConnected with server's connection to client
220
221 // call client OnConnected with client's connection to server
222 // => previously we used to send a ConnectMessage to
223 // NetworkServer.localConnection. this would queue the message
224 // until NetworkClient.Update processes it.
225 // => invoking the client's OnConnected event directly here makes
226 // tests fail. so let's do it exactly the same order as before by
227 // queueing the event for next Update!
228 //OnConnectedEvent?.Invoke(connection);
229 ((LocalConnectionToServer)connection).QueueConnectedEvent();
230 }
231
232 // disconnect //////////////////////////////////////////////////////////
234 public static void Disconnect()
235 {
236 // only if connected or connecting.
237 // don't disconnect() again if already in the process of
238 // disconnecting or fully disconnected.
239 if (connectState != ConnectState.Connecting &&
240 connectState != ConnectState.Connected)
241 return;
242
243 // we are disconnecting until OnTransportDisconnected is called.
244 // setting state to Disconnected would stop OnTransportDisconnected
245 // from calling cleanup code because it would think we are already
246 // disconnected fully.
247 // TODO move to 'cleanup' code below if safe
248 connectState = ConnectState.Disconnecting;
249 ready = false;
250
251 // call Disconnect on the NetworkConnection
253
254 // IMPORTANT: do NOT clear connection here yet.
255 // we still need it in OnTransportDisconnected for callbacks.
256 // connection = null;
257 }
258
259 // transport events ////////////////////////////////////////////////////
260 // called by Transport
261 static void OnTransportConnected()
262 {
263 if (connection != null)
264 {
265 // reset network time stats
266 NetworkTime.ResetStatics();
267
268 // reset unbatcher in case any batches from last session remain.
269 unbatcher = new Unbatcher();
270
271 // the handler may want to send messages to the client
272 // thus we should set the connected state before calling the handler
273 connectState = ConnectState.Connected;
274 NetworkTime.UpdateClient();
275 OnConnectedEvent?.Invoke();
276 }
277 else Debug.LogError("Skipped Connect message handling because connection is null.");
278 }
279
280 // helper function
281 static bool UnpackAndInvoke(NetworkReader reader, int channelId)
282 {
283 if (MessagePacking.Unpack(reader, out ushort msgType))
284 {
285 // try to invoke the handler for that message
286 if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler))
287 {
288 handler.Invoke(connection, reader, channelId);
289
290 // message handler may disconnect client, making connection = null
291 // therefore must check for null to avoid NRE.
292 if (connection != null)
293 connection.lastMessageTime = Time.time;
294
295 return true;
296 }
297 else
298 {
299 // message in a batch are NOT length prefixed to save bandwidth.
300 // every message needs to be handled and read until the end.
301 // otherwise it would overlap into the next message.
302 // => need to warn and disconnect to avoid undefined behaviour.
303 // => WARNING, not error. can happen if attacker sends random data.
304 Debug.LogWarning($"Unknown message id: {msgType}. This can happen if no handler was registered for this message.");
305 // simply return false. caller is responsible for disconnecting.
306 //connection.Disconnect();
307 return false;
308 }
309 }
310 else
311 {
312 // => WARNING, not error. can happen if attacker sends random data.
313 Debug.LogWarning("Invalid message header.");
314 // simply return false. caller is responsible for disconnecting.
315 //connection.Disconnect();
316 return false;
317 }
318 }
319
320 // called by Transport
321 internal static void OnTransportData(ArraySegment<byte> data, int channelId)
322 {
323 if (connection != null)
324 {
325 // server might batch multiple messages into one packet.
326 // feed it to the Unbatcher.
327 // NOTE: we don't need to associate a channelId because we
328 // always process all messages in the batch.
329 if (!unbatcher.AddBatch(data))
330 {
331 Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting.");
333 return;
334 }
335
336 // process all messages in the batch.
337 // only while NOT loading a scene.
338 // if we get a scene change message, then we need to stop
339 // processing. otherwise we might apply them to the old scene.
340 // => fixes https://github.com/vis2k/Mirror/issues/2651
341 //
342 // NOTE: is scene starts loading, then the rest of the batch
343 // would only be processed when OnTransportData is called
344 // the next time.
345 // => consider moving processing to NetworkEarlyUpdate.
346 while (!isLoadingScene &&
347 unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp))
348 {
349 // enough to read at least header size?
350 if (reader.Remaining >= MessagePacking.HeaderSize)
351 {
352 // make remoteTimeStamp available to the user
353 connection.remoteTimeStamp = remoteTimestamp;
354
355 // handle message
356 if (!UnpackAndInvoke(reader, channelId))
357 {
358 // warn, disconnect and return if failed
359 // -> warning because attackers might send random data
360 // -> messages in a batch aren't length prefixed.
361 // failing to read one would cause undefined
362 // behaviour for every message afterwards.
363 // so we need to disconnect.
364 // -> return to avoid the below unbatches.count error.
365 // we already disconnected and handled it.
366 Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
368 return;
369 }
370 }
371 // otherwise disconnect
372 else
373 {
374 // WARNING, not error. can happen if attacker sends random data.
375 Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)");
377 return;
378 }
379 }
380
381 // if we weren't interrupted by a scene change,
382 // then all batched messages should have been processed now.
383 // if not, we need to log an error to avoid debugging hell.
384 // otherwise batches would silently grow.
385 // we need to log an error to avoid debugging hell.
386 //
387 // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882
388 // -> UnpackAndInvoke silently returned because no handler for id
389 // -> Reader would never be read past the end
390 // -> Batch would never be retired because end is never reached
391 //
392 // NOTE: prefixing every message in a batch with a length would
393 // avoid ever not reading to the end. for extra bandwidth.
394 //
395 // IMPORTANT: always keep this check to detect memory leaks.
396 // this took half a day to debug last time.
397 if (!isLoadingScene && unbatcher.BatchesCount > 0)
398 {
399 Debug.LogError($"Still had {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.");
400 }
401 }
402 else Debug.LogError("Skipped Data message handling because connection is null.");
403 }
404
405 // called by Transport
406 // IMPORTANT: often times when disconnecting, we call this from Mirror
407 // too because we want to remove the connection and handle
408 // the disconnect immediately.
409 // => which is fine as long as we guarantee it only runs once
410 // => which we do by setting the state to Disconnected!
411 internal static void OnTransportDisconnected()
412 {
413 // StopClient called from user code triggers Disconnected event
414 // from transport which calls StopClient again, so check here
415 // and short circuit running the Shutdown process twice.
416 if (connectState == ConnectState.Disconnected) return;
417
418 // Raise the event before changing ConnectState
419 // because 'active' depends on this during shutdown
420 if (connection != null) OnDisconnectedEvent?.Invoke();
421
422 connectState = ConnectState.Disconnected;
423 ready = false;
424
425 // now that everything was handled, clear the connection.
426 // previously this was done in Disconnect() already, but we still
427 // need it for the above OnDisconnectedEvent.
428 connection = null;
429
430 // transport handlers are only added when connecting.
431 // so only remove when actually disconnecting.
432 RemoveTransportHandlers();
433 }
434
435 // transport errors are forwarded to high level
436 static void OnTransportError(TransportError error, string reason)
437 {
438 // transport errors will happen. logging a warning is enough.
439 // make sure the user does not panic.
440 Debug.LogWarning($"Client Transport Error: {error}: {reason}. This is fine.");
441 OnErrorEvent?.Invoke(error, reason);
442 }
443
444 // send ////////////////////////////////////////////////////////////////
446 public static void Send<T>(T message, int channelId = Channels.Reliable)
447 where T : struct, NetworkMessage
448 {
449 if (connection != null)
450 {
451 if (connectState == ConnectState.Connected)
452 {
453 connection.Send(message, channelId);
454 }
455 else Debug.LogError("NetworkClient Send when not connected to a server");
456 }
457 else Debug.LogError("NetworkClient Send with no connection");
458 }
459
460 // message handlers ////////////////////////////////////////////////////
462 public static void RegisterHandler<T>(Action<T> handler, bool requireAuthentication = true)
463 where T : struct, NetworkMessage
464 {
465 ushort msgType = MessagePacking.GetId<T>();
466 if (handlers.ContainsKey(msgType))
467 {
468 Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning.");
469 }
470 // we use the same WrapHandler function for server and client.
471 // so let's wrap it to ignore the NetworkConnection parameter.
472 // it's not needed on client. it's always NetworkClient.connection.
473 void HandlerWrapped(NetworkConnection _, T value) => handler(value);
474 handlers[msgType] = MessagePacking.WrapHandler((Action<NetworkConnection, T>) HandlerWrapped, requireAuthentication);
475 }
476
478 // RegisterHandler throws a warning (as it should) if a handler is assigned twice
479 // Use of ReplaceHandler makes it clear the user intended to replace the handler
480 public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool requireAuthentication = true)
481 where T : struct, NetworkMessage
482 {
483 ushort msgType = MessagePacking.GetId<T>();
484 handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication);
485 }
486
488 // RegisterHandler throws a warning (as it should) if a handler is assigned twice
489 // Use of ReplaceHandler makes it clear the user intended to replace the handler
490 public static void ReplaceHandler<T>(Action<T> handler, bool requireAuthentication = true)
491 where T : struct, NetworkMessage
492 {
493 ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication);
494 }
495
497 public static bool UnregisterHandler<T>()
498 where T : struct, NetworkMessage
499 {
500 // use int to minimize collisions
501 ushort msgType = MessagePacking.GetId<T>();
502 return handlers.Remove(msgType);
503 }
504
505 // spawnable prefabs ///////////////////////////////////////////////////
507 // Useful for debuggers
508 public static bool GetPrefab(Guid assetId, out GameObject prefab)
509 {
510 prefab = null;
511 return assetId != Guid.Empty &&
512 prefabs.TryGetValue(assetId, out prefab) && prefab != null;
513 }
514
516 static void RegisterPrefabIdentity(NetworkIdentity prefab)
517 {
518 if (prefab.assetId == Guid.Empty)
519 {
520 Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
521 return;
522 }
523
524 if (prefab.sceneId != 0)
525 {
526 Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
527 return;
528 }
529
530 NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
531 if (identities.Length > 1)
532 {
533 Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
534 }
535
536 if (prefabs.ContainsKey(prefab.assetId))
537 {
538 GameObject existingPrefab = prefabs[prefab.assetId];
539 Debug.LogWarning($"Replacing existing prefab with assetId '{prefab.assetId}'. Old prefab '{existingPrefab.name}', New prefab '{prefab.name}'");
540 }
541
542 if (spawnHandlers.ContainsKey(prefab.assetId) || unspawnHandlers.ContainsKey(prefab.assetId))
543 {
544 Debug.LogWarning($"Adding prefab '{prefab.name}' with assetId '{prefab.assetId}' when spawnHandlers with same assetId already exists. If you want to use custom spawn handling, then remove the prefab from NetworkManager's registered prefabs first.");
545 }
546
547 // Debug.Log($"Registering prefab '{prefab.name}' as asset:{prefab.assetId}");
548
549 prefabs[prefab.assetId] = prefab.gameObject;
550 }
551
553 // Note: newAssetId can not be set on GameObjects that already have an assetId
554 // Note: registering with assetId is useful for assetbundles etc. a lot
555 // of people use this.
556 public static void RegisterPrefab(GameObject prefab, Guid newAssetId)
557 {
558 if (prefab == null)
559 {
560 Debug.LogError("Could not register prefab because it was null");
561 return;
562 }
563
564 if (newAssetId == Guid.Empty)
565 {
566 Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty");
567 return;
568 }
569
570 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
571 if (identity == null)
572 {
573 Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
574 return;
575 }
576
577 if (identity.assetId != Guid.Empty && identity.assetId != newAssetId)
578 {
579 Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}");
580 return;
581 }
582
583 identity.assetId = newAssetId;
584
585 RegisterPrefabIdentity(identity);
586 }
587
589 public static void RegisterPrefab(GameObject prefab)
590 {
591 if (prefab == null)
592 {
593 Debug.LogError("Could not register prefab because it was null");
594 return;
595 }
596
597 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
598 if (identity == null)
599 {
600 Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
601 return;
602 }
603
604 RegisterPrefabIdentity(identity);
605 }
606
608 // Note: newAssetId can not be set on GameObjects that already have an assetId
609 // Note: registering with assetId is useful for assetbundles etc. a lot
610 // of people use this.
611 // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
612 public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
613 {
614 // We need this check here because we don't want a null handler in the lambda expression below
615 if (spawnHandler == null)
616 {
617 Debug.LogError($"Can not Register null SpawnHandler for {newAssetId}");
618 return;
619 }
620
621 RegisterPrefab(prefab, newAssetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
622 }
623
625 // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
626 public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
627 {
628 if (prefab == null)
629 {
630 Debug.LogError("Could not register handler for prefab because the prefab was null");
631 return;
632 }
633
634 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
635 if (identity == null)
636 {
637 Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
638 return;
639 }
640
641 if (identity.sceneId != 0)
642 {
643 Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
644 return;
645 }
646
647 Guid assetId = identity.assetId;
648
649 if (assetId == Guid.Empty)
650 {
651 Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
652 return;
653 }
654
655 // We need this check here because we don't want a null handler in the lambda expression below
656 if (spawnHandler == null)
657 {
658 Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
659 return;
660 }
661
662 RegisterPrefab(prefab, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
663 }
664
666 // Note: newAssetId can not be set on GameObjects that already have an assetId
667 // Note: registering with assetId is useful for assetbundles etc. a lot
668 // of people use this.
669 // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
670 public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
671 {
672 if (newAssetId == Guid.Empty)
673 {
674 Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty");
675 return;
676 }
677
678 if (prefab == null)
679 {
680 Debug.LogError("Could not register handler for prefab because the prefab was null");
681 return;
682 }
683
684 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
685 if (identity == null)
686 {
687 Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
688 return;
689 }
690
691 if (identity.assetId != Guid.Empty && identity.assetId != newAssetId)
692 {
693 Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}");
694 return;
695 }
696
697 if (identity.sceneId != 0)
698 {
699 Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
700 return;
701 }
702
703 identity.assetId = newAssetId;
704 Guid assetId = identity.assetId;
705
706 if (spawnHandler == null)
707 {
708 Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
709 return;
710 }
711
712 if (unspawnHandler == null)
713 {
714 Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
715 return;
716 }
717
718 if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
719 {
720 Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'");
721 }
722
723 if (prefabs.ContainsKey(assetId))
724 {
725 // this is error because SpawnPrefab checks prefabs before handler
726 Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler");
727 }
728
729 NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
730 if (identities.Length > 1)
731 {
732 Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
733 }
734
735 //Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
736
737 spawnHandlers[assetId] = spawnHandler;
738 unspawnHandlers[assetId] = unspawnHandler;
739 }
740
742 // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
743 public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
744 {
745 if (prefab == null)
746 {
747 Debug.LogError("Could not register handler for prefab because the prefab was null");
748 return;
749 }
750
751 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
752 if (identity == null)
753 {
754 Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
755 return;
756 }
757
758 if (identity.sceneId != 0)
759 {
760 Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
761 return;
762 }
763
764 Guid assetId = identity.assetId;
765
766 if (assetId == Guid.Empty)
767 {
768 Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
769 return;
770 }
771
772 if (spawnHandler == null)
773 {
774 Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
775 return;
776 }
777
778 if (unspawnHandler == null)
779 {
780 Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
781 return;
782 }
783
784 if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
785 {
786 Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'");
787 }
788
789 if (prefabs.ContainsKey(assetId))
790 {
791 // this is error because SpawnPrefab checks prefabs before handler
792 Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler");
793 }
794
795 NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
796 if (identities.Length > 1)
797 {
798 Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
799 }
800
801 //Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
802
803 spawnHandlers[assetId] = spawnHandler;
804 unspawnHandlers[assetId] = unspawnHandler;
805 }
806
808 public static void UnregisterPrefab(GameObject prefab)
809 {
810 if (prefab == null)
811 {
812 Debug.LogError("Could not unregister prefab because it was null");
813 return;
814 }
815
816 NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
817 if (identity == null)
818 {
819 Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component");
820 return;
821 }
822
823 Guid assetId = identity.assetId;
824
825 prefabs.Remove(assetId);
826 spawnHandlers.Remove(assetId);
827 unspawnHandlers.Remove(assetId);
828 }
829
830 // spawn handlers //////////////////////////////////////////////////////
832 // This can be used to register custom spawning methods for an assetId -
833 // instead of the usual method of registering spawning methods for a
834 // prefab. This should be used when no prefab exists for the spawned
835 // objects - such as when they are constructed dynamically at runtime
836 // from configuration data.
837 public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
838 {
839 // We need this check here because we don't want a null handler in the lambda expression below
840 if (spawnHandler == null)
841 {
842 Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
843 return;
844 }
845
846 RegisterSpawnHandler(assetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
847 }
848
850 // This can be used to register custom spawning methods for an assetId -
851 // instead of the usual method of registering spawning methods for a
852 // prefab. This should be used when no prefab exists for the spawned
853 // objects - such as when they are constructed dynamically at runtime
854 // from configuration data.
855 public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
856 {
857 if (spawnHandler == null)
858 {
859 Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
860 return;
861 }
862
863 if (unspawnHandler == null)
864 {
865 Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
866 return;
867 }
868
869 if (assetId == Guid.Empty)
870 {
871 Debug.LogError("Can not Register SpawnHandler for empty Guid");
872 return;
873 }
874
875 if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
876 {
877 Debug.LogWarning($"Replacing existing spawnHandlers for {assetId}");
878 }
879
880 if (prefabs.ContainsKey(assetId))
881 {
882 // this is error because SpawnPrefab checks prefabs before handler
883 Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}'");
884 }
885
886 // Debug.Log("RegisterSpawnHandler asset {assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
887
888 spawnHandlers[assetId] = spawnHandler;
889 unspawnHandlers[assetId] = unspawnHandler;
890 }
891
893 public static void UnregisterSpawnHandler(Guid assetId)
894 {
895 spawnHandlers.Remove(assetId);
896 unspawnHandlers.Remove(assetId);
897 }
898
900 public static void ClearSpawners()
901 {
902 prefabs.Clear();
903 spawnHandlers.Clear();
904 unspawnHandlers.Clear();
905 }
906
907 internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj)
908 {
909 if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null)
910 {
911 handler(obj);
912 return true;
913 }
914 return false;
915 }
916
917 // ready ///////////////////////////////////////////////////////////////
919 // This could be for example when a client enters an ongoing game and
920 // has finished loading the current scene. The server should respond to
921 // the SYSTEM_READY event with an appropriate handler which instantiates
922 // the players object for example.
923 public static bool Ready()
924 {
925 // Debug.Log($"NetworkClient.Ready() called with connection {conn}");
926 if (ready)
927 {
928 Debug.LogError("NetworkClient is already ready. It shouldn't be called twice.");
929 return false;
930 }
931
932 // need a valid connection to become ready
933 if (connection == null)
934 {
935 Debug.LogError("Ready() called with invalid connection object: conn=null");
936 return false;
937 }
938
939 // Set these before sending the ReadyMessage, otherwise host client
940 // will fail in InternalAddPlayer with null readyConnection.
941 // TODO this is redundant. have one source of truth for .ready
942 ready = true;
943 connection.isReady = true;
944
945 // Tell server we're ready to have a player object spawned
946 connection.Send(new ReadyMessage());
947 return true;
948 }
949
950 // add player //////////////////////////////////////////////////////////
951 // called from message handler for Owner message
952 internal static void InternalAddPlayer(NetworkIdentity identity)
953 {
954 //Debug.Log("NetworkClient.InternalAddPlayer");
955
956 // NOTE: It can be "normal" when changing scenes for the player to be destroyed and recreated.
957 // But, the player structures are not cleaned up, we'll just replace the old player
958 localPlayer = identity;
959
960 // NOTE: we DONT need to set isClient=true here, because OnStartClient
961 // is called before OnStartLocalPlayer, hence it's already set.
962 // localPlayer.isClient = true;
963
964 // TODO this check might not be necessary
965 //if (readyConnection != null)
966 if (ready && connection != null)
967 {
968 connection.identity = identity;
969 }
970 else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer");
971 }
972
974 public static bool AddPlayer()
975 {
976 // ensure valid ready connection
977 if (connection == null)
978 {
979 Debug.LogError("AddPlayer requires a valid NetworkClient.connection.");
980 return false;
981 }
982
983 // UNET checked 'if readyConnection != null'.
984 // in other words, we need a connection and we need to be ready.
985 if (!ready)
986 {
987 Debug.LogError("AddPlayer requires a ready NetworkClient.");
988 return false;
989 }
990
991 if (connection.identity != null)
992 {
993 Debug.LogError("NetworkClient.AddPlayer: a PlayerController was already added. Did you call AddPlayer twice?");
994 return false;
995 }
996
997 // Debug.Log($"NetworkClient.AddPlayer() called with connection {readyConnection}");
998 connection.Send(new AddPlayerMessage());
999 return true;
1000 }
1001
1002 // spawning ////////////////////////////////////////////////////////////
1003 internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message)
1004 {
1005 if (message.assetId != Guid.Empty)
1006 identity.assetId = message.assetId;
1007
1008 if (!identity.gameObject.activeSelf)
1009 {
1010 identity.gameObject.SetActive(true);
1011 }
1012
1013 // apply local values for VR support
1014 identity.transform.localPosition = message.position;
1015 identity.transform.localRotation = message.rotation;
1016 identity.transform.localScale = message.scale;
1017 identity.hasAuthority = message.isOwner;
1018 identity.netId = message.netId;
1019
1020 if (message.isLocalPlayer)
1021 InternalAddPlayer(identity);
1022
1023 // deserialize components if any payload
1024 // (Count is 0 if there were no components)
1025 if (message.payload.Count > 0)
1026 {
1027 using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload))
1028 {
1029 identity.OnDeserializeAllSafely(payloadReader, true);
1030 }
1031 }
1032
1033 spawned[message.netId] = identity;
1034
1035 // the initial spawn with OnObjectSpawnStarted/Finished calls all
1036 // object's OnStartClient/OnStartLocalPlayer after they were all
1037 // spawned.
1038 // this only happens once though.
1039 // for all future spawns, we need to call OnStartClient/LocalPlayer
1040 // here immediately since there won't be another OnObjectSpawnFinished.
1041 if (isSpawnFinished)
1042 {
1043 identity.NotifyAuthority();
1044 identity.OnStartClient();
1045 CheckForLocalPlayer(identity);
1046 }
1047 }
1048
1049 // Finds Existing Object with NetId or spawns a new one using AssetId or sceneId
1050 internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity)
1051 {
1052 // was the object already spawned?
1053 identity = GetExistingObject(message.netId);
1054
1055 // if found, return early
1056 if (identity != null)
1057 {
1058 return true;
1059 }
1060
1061 if (message.assetId == Guid.Empty && message.sceneId == 0)
1062 {
1063 Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId");
1064 return false;
1065 }
1066
1067 identity = message.sceneId == 0 ? SpawnPrefab(message) : SpawnSceneObject(message.sceneId);
1068
1069 if (identity == null)
1070 {
1071 Debug.LogError($"Could not spawn assetId={message.assetId} scene={message.sceneId:X} netId={message.netId}");
1072 return false;
1073 }
1074
1075 return true;
1076 }
1077
1078 static NetworkIdentity GetExistingObject(uint netid)
1079 {
1080 spawned.TryGetValue(netid, out NetworkIdentity localObject);
1081 return localObject;
1082 }
1083
1084 static NetworkIdentity SpawnPrefab(SpawnMessage message)
1085 {
1086 // custom spawn handler for this prefab? (for prefab pools etc.)
1087 //
1088 // IMPORTANT: look for spawn handlers BEFORE looking for registered
1089 // prefabs. Unspawning also looks for unspawn handlers
1090 // before falling back to regular Destroy. this needs to
1091 // be consistent.
1092 // https://github.com/vis2k/Mirror/issues/2705
1093 if (spawnHandlers.TryGetValue(message.assetId, out SpawnHandlerDelegate handler))
1094 {
1095 GameObject obj = handler(message);
1096 if (obj == null)
1097 {
1098 Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'");
1099 return null;
1100 }
1101 NetworkIdentity identity = obj.GetComponent<NetworkIdentity>();
1102 if (identity == null)
1103 {
1104 Debug.LogError($"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{message.assetId}'");
1105 return null;
1106 }
1107 return identity;
1108 }
1109
1110 // otherwise look in NetworkManager registered prefabs
1111 if (GetPrefab(message.assetId, out GameObject prefab))
1112 {
1113 GameObject obj = GameObject.Instantiate(prefab, message.position, message.rotation);
1114 //Debug.Log($"Client spawn handler instantiating [netId{message.netId} asset ID:{message.assetId} pos:{message.position} rotation:{message.rotation}]");
1115 return obj.GetComponent<NetworkIdentity>();
1116 }
1117
1118 Debug.LogError($"Failed to spawn server object, did you forget to add it to the NetworkManager? assetId={message.assetId} netId={message.netId}");
1119 return null;
1120 }
1121
1122 static NetworkIdentity SpawnSceneObject(ulong sceneId)
1123 {
1124 NetworkIdentity identity = GetAndRemoveSceneObject(sceneId);
1125 if (identity == null)
1126 {
1127 Debug.LogError($"Spawn scene object not found for {sceneId:X}. Make sure that client and server use exactly the same project. This only happens if the hierarchy gets out of sync.");
1128
1129 // dump the whole spawnable objects dict for easier debugging
1130 //foreach (KeyValuePair<ulong, NetworkIdentity> kvp in spawnableObjects)
1131 // Debug.Log($"Spawnable: SceneId={kvp.Key:X} name={kvp.Value.name}");
1132 }
1133 //else Debug.Log($"Client spawn for [netId:{msg.netId}] [sceneId:{msg.sceneId:X}] obj:{identity}");
1134 return identity;
1135 }
1136
1137 static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId)
1138 {
1139 if (spawnableObjects.TryGetValue(sceneId, out NetworkIdentity identity))
1140 {
1141 spawnableObjects.Remove(sceneId);
1142 return identity;
1143 }
1144 return null;
1145 }
1146
1147 // Checks if identity is not spawned yet, not hidden and has sceneId
1148 static bool ConsiderForSpawning(NetworkIdentity identity)
1149 {
1150 // not spawned yet, not hidden, etc.?
1151 return !identity.gameObject.activeSelf &&
1152 identity.gameObject.hideFlags != HideFlags.NotEditable &&
1153 identity.gameObject.hideFlags != HideFlags.HideAndDontSave &&
1154 identity.sceneId != 0;
1155 }
1156
1158 public static void PrepareToSpawnSceneObjects()
1159 {
1160 // remove existing items, they will be re-added below
1161 spawnableObjects.Clear();
1162
1163 // finds all NetworkIdentity currently loaded by unity (includes disabled objects)
1164 NetworkIdentity[] allIdentities = Resources.FindObjectsOfTypeAll<NetworkIdentity>();
1165 foreach (NetworkIdentity identity in allIdentities)
1166 {
1167 // add all unspawned NetworkIdentities to spawnable objects
1168 if (ConsiderForSpawning(identity))
1169 {
1170 if (spawnableObjects.TryGetValue(identity.sceneId, out NetworkIdentity existingIdentity))
1171 {
1172 string msg = $"NetworkClient: Duplicate sceneId {identity.sceneId} detected on {identity.gameObject.name} and {existingIdentity.gameObject.name}\n" +
1173 $"This can happen if a networked object is persisted in DontDestroyOnLoad through loading / changing to the scene where it originated,\n" +
1174 $"otherwise you may need to open and re-save the {identity.gameObject.scene} to reset scene id's.";
1175 Debug.LogWarning(msg, identity.gameObject);
1176 }
1177 else
1178 {
1179 spawnableObjects.Add(identity.sceneId, identity);
1180 }
1181 }
1182 }
1183 }
1184
1185 internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _)
1186 {
1187 // Debug.Log("SpawnStarted");
1189 isSpawnFinished = false;
1190 }
1191
1192 internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _)
1193 {
1194 //Debug.Log("SpawnFinished");
1195 ClearNullFromSpawned();
1196
1197 // paul: Initialize the objects in the same order as they were
1198 // initialized in the server. This is important if spawned objects
1199 // use data from scene objects
1200 foreach (NetworkIdentity identity in spawned.Values.OrderBy(uv => uv.netId))
1201 {
1202 identity.NotifyAuthority();
1203 identity.OnStartClient();
1204 CheckForLocalPlayer(identity);
1205 }
1206 isSpawnFinished = true;
1207 }
1208
1209 static readonly List<uint> removeFromSpawned = new List<uint>();
1210 static void ClearNullFromSpawned()
1211 {
1212 // spawned has null objects after changing scenes on client using
1213 // NetworkManager.ServerChangeScene remove them here so that 2nd
1214 // loop below does not get NullReferenceException
1215 // see https://github.com/vis2k/Mirror/pull/2240
1216 // TODO fix scene logic so that client scene doesn't have null objects
1217 foreach (KeyValuePair<uint, NetworkIdentity> kvp in spawned)
1218 {
1219 if (kvp.Value == null)
1220 {
1221 removeFromSpawned.Add(kvp.Key);
1222 }
1223 }
1224
1225 // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove
1226 foreach (uint id in removeFromSpawned)
1227 {
1228 spawned.Remove(id);
1229 }
1230 removeFromSpawned.Clear();
1231 }
1232
1233 // host mode callbacks /////////////////////////////////////////////////
1234 static void OnHostClientObjectDestroy(ObjectDestroyMessage message)
1235 {
1236 //Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}");
1237 spawned.Remove(message.netId);
1238 }
1239
1240 static void OnHostClientObjectHide(ObjectHideMessage message)
1241 {
1242 //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}");
1243 if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) &&
1244 localObject != null)
1245 {
1246 if (aoi != null)
1247 aoi.SetHostVisibility(localObject, false);
1248 }
1249 }
1250
1251 internal static void OnHostClientSpawn(SpawnMessage message)
1252 {
1253 // on host mode, the object already exist in NetworkServer.spawned.
1254 // simply add it to NetworkClient.spawned too.
1255 if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null)
1256 {
1257 spawned[message.netId] = localObject;
1258
1259 // now do the actual 'spawning' on host mode
1260 if (message.isLocalPlayer)
1261 InternalAddPlayer(localObject);
1262
1263 localObject.hasAuthority = message.isOwner;
1264 localObject.NotifyAuthority();
1265 localObject.OnStartClient();
1266
1267 if (aoi != null)
1268 aoi.SetHostVisibility(localObject, true);
1269
1270 CheckForLocalPlayer(localObject);
1271 }
1272 }
1273
1274 // client-only mode callbacks //////////////////////////////////////////
1275 static void OnEntityStateMessage(EntityStateMessage message)
1276 {
1277 // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
1278 if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null)
1279 {
1280 using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload))
1281 localObject.OnDeserializeAllSafely(networkReader, false);
1282 }
1283 else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
1284 }
1285
1286 static void OnRPCMessage(RpcMessage message)
1287 {
1288 // Debug.Log($"NetworkClient.OnRPCMessage hash:{msg.functionHash} netId:{msg.netId}");
1289 if (spawned.TryGetValue(message.netId, out NetworkIdentity identity))
1290 {
1291 using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload))
1292 identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, networkReader);
1293 }
1294 }
1295
1296 static void OnObjectHide(ObjectHideMessage message) => DestroyObject(message.netId);
1297
1298 internal static void OnObjectDestroy(ObjectDestroyMessage message) => DestroyObject(message.netId);
1299
1300 internal static void OnSpawn(SpawnMessage message)
1301 {
1302 // Debug.Log($"Client spawn handler instantiating netId={msg.netId} assetID={msg.assetId} sceneId={msg.sceneId:X} pos={msg.position}");
1303 if (FindOrSpawnObject(message, out NetworkIdentity identity))
1304 {
1305 ApplySpawnPayload(identity, message);
1306 }
1307 }
1308
1309 internal static void OnChangeOwner(ChangeOwnerMessage message)
1310 {
1311 NetworkIdentity identity = GetExistingObject(message.netId);
1312
1313 if (identity != null)
1314 ChangeOwner(identity, message);
1315 else
1316 Debug.LogError($"OnChangeOwner: Could not find object with netId {message.netId}");
1317 }
1318
1319 // ChangeOwnerMessage contains new 'owned' and new 'localPlayer'
1320 // that we need to apply to the identity.
1321 internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage message)
1322 {
1323 // local player before, but not anymore?
1324 // call OnStopLocalPlayer before setting new values.
1325 if (identity.isLocalPlayer && !message.isLocalPlayer)
1326 {
1327 identity.OnStopLocalPlayer();
1328 }
1329
1330 // set ownership flag (aka authority)
1331 identity.hasAuthority = message.isOwner;
1332 identity.NotifyAuthority();
1333
1334 // set localPlayer flag
1335 identity.isLocalPlayer = message.isLocalPlayer;
1336
1337 // identity is now local player. set our static helper field to it.
1338 if (identity.isLocalPlayer)
1339 {
1340 localPlayer = identity;
1341 }
1342 // identity's isLocalPlayer was set to false.
1343 // clear our static localPlayer IF (and only IF) it was that one before.
1344 else if (localPlayer == identity)
1345 {
1346 localPlayer = null;
1347 }
1348
1349 // call OnStartLocalPlayer if it's the local player now.
1350 CheckForLocalPlayer(identity);
1351 }
1352
1353 internal static void CheckForLocalPlayer(NetworkIdentity identity)
1354 {
1355 if (identity == localPlayer)
1356 {
1357 // Set isLocalPlayer to true on this NetworkIdentity and trigger
1358 // OnStartLocalPlayer in all scripts on the same GO
1359 identity.connectionToServer = connection;
1360 identity.OnStartLocalPlayer();
1361 // Debug.Log($"NetworkClient.OnOwnerMessage player:{identity.name}");
1362 }
1363 }
1364
1365 // destroy /////////////////////////////////////////////////////////////
1366 static void DestroyObject(uint netId)
1367 {
1368 // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}");
1369 if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null)
1370 {
1371 if (localObject.isLocalPlayer)
1372 localObject.OnStopLocalPlayer();
1373
1374 localObject.OnStopClient();
1375
1376 // custom unspawn handler for this prefab? (for prefab pools etc.)
1377 if (InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject))
1378 {
1379 // reset object after user's handler
1380 localObject.Reset();
1381 }
1382 // otherwise fall back to default Destroy
1383 else if (localObject.sceneId == 0)
1384 {
1385 // don't call reset before destroy so that values are still set in OnDestroy
1386 GameObject.Destroy(localObject.gameObject);
1387 }
1388 // scene object.. disable it in scene instead of destroying
1389 else
1390 {
1391 localObject.gameObject.SetActive(false);
1392 spawnableObjects[localObject.sceneId] = localObject;
1393 // reset for scene objects
1394 localObject.Reset();
1395 }
1396
1397 // remove from dictionary no matter how it is unspawned
1398 spawned.Remove(netId);
1399 }
1400 //else Debug.LogWarning($"Did not find target for destroy message for {netId}");
1401 }
1402
1403 // update //////////////////////////////////////////////////////////////
1404 // NetworkEarlyUpdate called before any Update/FixedUpdate
1405 // (we add this to the UnityEngine in NetworkLoop)
1406 internal static void NetworkEarlyUpdate()
1407 {
1408 // process all incoming messages first before updating the world
1409 if (Transport.activeTransport != null)
1411 }
1412
1413 // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate
1414 // (we add this to the UnityEngine in NetworkLoop)
1415 internal static void NetworkLateUpdate()
1416 {
1417 // local connection?
1418 if (connection is LocalConnectionToServer localConnection)
1419 {
1420 localConnection.Update();
1421 }
1422 // remote connection?
1423 else if (connection is NetworkConnectionToServer remoteConnection)
1424 {
1425 // only update things while connected
1426 if (active && connectState == ConnectState.Connected)
1427 {
1428 // update NetworkTime
1429 NetworkTime.UpdateClient();
1430
1431 // update connection to flush out batched messages
1432 remoteConnection.Update();
1433 }
1434 }
1435
1436 // process all outgoing messages after updating the world
1437 if (Transport.activeTransport != null)
1438 Transport.activeTransport.ClientLateUpdate();
1439 }
1440
1441 // shutdown ////////////////////////////////////////////////////////////
1443 // Note: NetworkServer.CleanupNetworkIdentities does the same on server.
1444 public static void DestroyAllClientObjects()
1445 {
1446 // user can modify spawned lists which causes InvalidOperationException
1447 // list can modified either in UnSpawnHandler or in OnDisable/OnDestroy
1448 // we need the Try/Catch so that the rest of the shutdown does not get stopped
1449 try
1450 {
1451 foreach (NetworkIdentity identity in spawned.Values)
1452 {
1453 if (identity != null && identity.gameObject != null)
1454 {
1455 if (identity.isLocalPlayer)
1456 identity.OnStopLocalPlayer();
1457
1458 identity.OnStopClient();
1459
1460 // NetworkClient.Shutdown calls DestroyAllClientObjects.
1461 // which destroys all objects in NetworkClient.spawned.
1462 // => NC.spawned contains owned & observed objects
1463 // => in host mode, we CAN NOT destroy observed objects.
1464 // => that would destroy them other connection's objects
1465 // on the host server, making them disconnect.
1466 // https://github.com/vis2k/Mirror/issues/2954
1467 bool hostOwned = identity.connectionToServer is LocalConnectionToServer;
1468 bool shouldDestroy = !identity.isServer || hostOwned;
1469 if (shouldDestroy)
1470 {
1471 bool wasUnspawned = InvokeUnSpawnHandler(identity.assetId, identity.gameObject);
1472
1473 // unspawned objects should be reset for reuse later.
1474 if (wasUnspawned)
1475 {
1476 identity.Reset();
1477 }
1478 // without unspawn handler, we need to disable/destroy.
1479 else
1480 {
1481 // scene objects are reset and disabled.
1482 // they always stay in the scene, we don't destroy them.
1483 if (identity.sceneId != 0)
1484 {
1485 identity.Reset();
1486 identity.gameObject.SetActive(false);
1487 }
1488 // spawned objects are destroyed
1489 else
1490 {
1491 GameObject.Destroy(identity.gameObject);
1492 }
1493 }
1494 }
1495 }
1496 }
1497 spawned.Clear();
1498 }
1499 catch (InvalidOperationException e)
1500 {
1501 Debug.LogException(e);
1502 Debug.LogError("Could not DestroyAllClientObjects because spawned list was modified during loop, make sure you are not modifying NetworkIdentity.spawned by calling NetworkServer.Destroy or NetworkServer.Spawn in OnDestroy or OnDisable.");
1503 }
1504 }
1505
1507 // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
1508 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
1509 public static void Shutdown()
1510 {
1511 //Debug.Log("Shutting down client.");
1512
1513 // calls prefabs.Clear();
1514 // calls spawnHandlers.Clear();
1515 // calls unspawnHandlers.Clear();
1516 ClearSpawners();
1517
1518 // calls spawned.Clear() if no exception occurs
1520
1521 spawned.Clear();
1522 handlers.Clear();
1523 spawnableObjects.Clear();
1524
1525 // IMPORTANT: do NOT call NetworkIdentity.ResetStatics() here!
1526 // calling StopClient() in host mode would reset nextNetId to 1,
1527 // causing next connection to have a duplicate netId accidentally.
1528 // => see also: https://github.com/vis2k/Mirror/issues/2954
1529 //NetworkIdentity.ResetStatics();
1530 // => instead, reset only the client sided statics.
1531 NetworkIdentity.ResetClientStatics();
1532
1533 // disconnect the client connection.
1534 // we do NOT call Transport.Shutdown, because someone only called
1535 // NetworkClient.Shutdown. we can't assume that the server is
1536 // supposed to be shut down too!
1537 if (Transport.activeTransport != null)
1539
1540 // reset statics
1541 connectState = ConnectState.None;
1542 connection = null;
1543 localPlayer = null;
1544 ready = false;
1545 isSpawnFinished = false;
1546 isLoadingScene = false;
1547
1548 unbatcher = new Unbatcher();
1549
1550 // clear events. someone might have hooked into them before, but
1551 // we don't want to use those hooks after Shutdown anymore.
1552 OnConnectedEvent = null;
1553 OnDisconnectedEvent = null;
1554 OnErrorEvent = null;
1555 }
1556 }
1557}
NetworkClient with connection to server.
static bool active
active is true while a client is connecting/connected
static void ClearSpawners()
This clears the registered spawn prefabs and spawn handler functions for this client.
static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers.
static bool isHostClient
True if client is running in host mode.
static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
This is an advanced spawning function that registers a custom assetId with the spawning system.
static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
Register a spawnable prefab with custom spawn/unspawn handlers.
static void DestroyAllClientObjects()
Destroys all networked objects on the client.
static void Connect(string address)
Connect client to a NetworkServer by address.
static void ConnectLocalServer()
Connect host mode
static bool GetPrefab(Guid assetId, out GameObject prefab)
Find the registered prefab for this asset id.
static void UnregisterPrefab(GameObject prefab)
Removes a registered spawn prefab that was setup with NetworkClient.RegisterPrefab.
static void Send< T >(T message, int channelId=Channels.Reliable)
Send a NetworkMessage to the server over the given channel.
static readonly Dictionary< Guid, GameObject > prefabs
Registered spawnable prefabs by assetId.
static bool ready
True if client is ready (= joined world).
static readonly Dictionary< uint, NetworkIdentity > spawned
All spawned NetworkIdentities by netId.
static bool isConnecting
Check if client is connecting (before connected).
static void Connect(Uri uri)
Connect client to a NetworkServer by Uri.
static void RegisterPrefab(GameObject prefab)
Register spawnable prefab.
static void ReplaceHandler< T >(Action< NetworkConnection, T > handler, bool requireAuthentication=true)
Replace a handler for a particular message type. Should require authentication by default.
static void RegisterPrefab(GameObject prefab, Guid newAssetId)
Register spawnable prefab with custom assetId.
static void Shutdown()
Shutdown the client.
static void PrepareToSpawnSceneObjects()
Call this after loading/unloading a scene in the client after connection to register the spawnable ob...
static bool Ready()
Sends Ready message to server, indicating that we loaded the scene, ready to enter the game.
static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers.
static void Disconnect()
Disconnect from server.
static bool AddPlayer()
Sends AddPlayer message to the server, indicating that we want to join the world.
static void RegisterHandler< T >(Action< T > handler, bool requireAuthentication=true)
Register a handler for a message type T. Most should require authentication.
static bool UnregisterHandler< T >()
Unregister a message handler of type T.
static bool isConnected
Check if client is connected (after connecting).
static string serverIp
IP address of the connection to server.
static void UnregisterSpawnHandler(Guid assetId)
Removes a registered spawn handler function that was registered with NetworkClient....
static NetworkConnection connection
Client's NetworkConnection to server.
static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
This is an advanced spawning function that registers a custom assetId with the spawning system.
static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
Register a spawnable prefab with custom spawn/unspawn handlers.
static NetworkIdentity localPlayer
NetworkIdentity of the localPlayer
Base NetworkConnection class for server-to-client and client-to-server connection.
NetworkIdentity identity
This connection's main object (usually the player object).
abstract string address
IP address of the connection. Can be useful for game master IP bans etc.
abstract void Disconnect()
Disconnects this connection.
NetworkIdentity identifies objects across the network.
Guid assetId
Prefab GUID used to spawn prefabs across the network.
ulong sceneId
Unique identifier for NetworkIdentity objects within a scene, used for spawning scene objects.
bool isLocalPlayer
Return true if this object represents the player on the local machine.
Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool....
Pool of NetworkReaders to avoid allocations.
static NetworkReaderPooled Get(byte[] bytes)
Get the next reader in the pool. If pool is empty, creates a new Reader
Pooled NetworkReader, automatically returned to pool when using 'using'
NetworkServer handles remote connections and has a local connection for a local client.
static NetworkConnectionToClient localConnection
Connection to host mode client (if any)
static readonly Dictionary< uint, NetworkIdentity > spawned
All spawned NetworkIdentities by netId.
Synchronizes server time to clients.
Definition: NetworkTime.cs:12
Abstract transport layer component
Definition: Transport.cs:32
virtual void ClientEarlyUpdate()
NetworkLoop NetworkEarly/LateUpdate were added for a proper network update order. the goal is to: pro...
Definition: Transport.cs:174
static Transport activeTransport
The current transport used by Mirror.
Definition: Transport.cs:35
abstract void ClientConnect(string address)
Connects the client to the server at the address.
abstract void ClientDisconnect()
Disconnects the client from the server