using System; using System.Collections; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif #if MULTIPLAYER_TOOLS using Unity.Multiplayer.Tools; #endif using Unity.Profiling; using UnityEngine.SceneManagement; using System.Runtime.CompilerServices; using Debug = UnityEngine.Debug; namespace Unity.Netcode { /// /// The main component of the library /// [AddComponentMenu("Netcode/" + nameof(NetworkManager), -100)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `public` internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters); // RuntimeAccessModifiersILPP will make this `public` internal static readonly Dictionary __rpc_func_table = new Dictionary(); #if DEVELOPMENT_BUILD || UNITY_EDITOR // RuntimeAccessModifiersILPP will make this `public` internal static readonly Dictionary __rpc_name_table = new Dictionary(); #endif #pragma warning restore IDE1006 // restore naming rule violation check #if DEVELOPMENT_BUILD || UNITY_EDITOR private static ProfilerMarker s_SyncTime = new ProfilerMarker($"{nameof(NetworkManager)}.SyncTime"); private static ProfilerMarker s_TransportPoll = new ProfilerMarker($"{nameof(NetworkManager)}.TransportPoll"); private static ProfilerMarker s_TransportConnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportConnect"); private static ProfilerMarker s_HandleIncomingData = new ProfilerMarker($"{nameof(NetworkManager)}.{nameof(HandleIncomingData)}"); private static ProfilerMarker s_TransportDisconnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportDisconnect"); #endif private const double k_TimeSyncFrequency = 1.0d; // sync every second private const float k_DefaultBufferSizeSec = 0.05f; // todo talk with UX/Product, find good default value for this internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) { return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\""; } internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; } internal MessagingSystem MessagingSystem { get; private set; } private NetworkPrefabHandler m_PrefabHandler; public NetworkPrefabHandler PrefabHandler { get { if (m_PrefabHandler == null) { m_PrefabHandler = new NetworkPrefabHandler(); } return m_PrefabHandler; } } private bool m_ShuttingDown; private bool m_StopProcessingMessages; private class NetworkManagerHooks : INetworkHooks { private NetworkManager m_NetworkManager; internal NetworkManagerHooks(NetworkManager manager) { m_NetworkManager = manager; } public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage { } public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage { } public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { } public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { } public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) { } public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) { } public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) { } public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) { } public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) { return !m_NetworkManager.m_StopProcessingMessages; } public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) && (client.ConnectionState == PendingClient.State.PendingApproval || (client.ConnectionState == PendingClient.State.PendingConnection && messageType != typeof(ConnectionRequestMessage)))) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted"); } return false; } return !m_NetworkManager.m_StopProcessingMessages; } public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { } public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage { } } private class NetworkManagerMessageSender : IMessageSender { private NetworkManager m_NetworkManager; public NetworkManagerMessageSender(NetworkManager manager) { m_NetworkManager = manager; } public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData) { var sendBuffer = batchData.ToTempByteArray(); m_NetworkManager.NetworkConfig.NetworkTransport.Send(m_NetworkManager.ClientIdToTransportId(clientId), sendBuffer, delivery); } } /// /// Returns the to use as the override as could be defined within the NetworkPrefab list /// Note: This should be used to create pools (with components) /// under the scenario where you are using the Host model as it spawns everything locally. As such, the override /// will not be applied when spawning locally on a Host. /// Related Classes and Interfaces: /// /// /// /// the to be checked for a defined NetworkPrefab override /// a that is either the override or if no overrides exist it returns the same as the one passed in as a parameter public GameObject GetNetworkPrefabOverride(GameObject gameObject) { var networkObject = gameObject.GetComponent(); if (networkObject != null) { if (NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(networkObject.GlobalObjectIdHash)) { switch (NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].Override) { case NetworkPrefabOverride.Hash: case NetworkPrefabOverride.Prefab: { return NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].OverridingTargetPrefab; } } } } return gameObject; } public NetworkTimeSystem NetworkTimeSystem { get; private set; } public NetworkTickSystem NetworkTickSystem { get; private set; } public NetworkTime LocalTime => NetworkTickSystem?.LocalTime ?? default; public NetworkTime ServerTime => NetworkTickSystem?.ServerTime ?? default; /// /// Gets or sets if the application should be set to run in background /// [HideInInspector] public bool RunInBackground = true; /// /// The log level to use /// [HideInInspector] public LogLevel LogLevel = LogLevel.Normal; /// /// The singleton instance of the NetworkManager /// public static NetworkManager Singleton { get; private set; } /// /// Gets the SpawnManager for this NetworkManager /// public NetworkSpawnManager SpawnManager { get; private set; } internal IDeferredMessageManager DeferredMessageManager { get; private set; } public CustomMessagingManager CustomMessagingManager { get; private set; } public NetworkSceneManager SceneManager { get; private set; } public const ulong ServerClientId = 0; /// /// Gets the networkId of the server /// private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ?? throw new NullReferenceException($"The transport in the active {nameof(NetworkConfig)} is null"); /// /// Returns ServerClientId if IsServer or LocalClientId if not /// public ulong LocalClientId { get => IsServer ? NetworkConfig.NetworkTransport.ServerClientId : m_LocalClientId; internal set => m_LocalClientId = value; } private ulong m_LocalClientId; private Dictionary m_ConnectedClients = new Dictionary(); private ulong m_NextClientId = 1; private Dictionary m_ClientIdToTransportIdMap = new Dictionary(); private Dictionary m_TransportIdToClientIdMap = new Dictionary(); private List m_ConnectedClientsList = new List(); private List m_ConnectedClientIds = new List(); /// /// Gets a dictionary of connected clients and their clientId keys. This is only accessible on the server. /// public IReadOnlyDictionary ConnectedClients { get { if (IsServer == false) { throw new NotServerException($"{nameof(ConnectedClients)} should only be accessed on server."); } return m_ConnectedClients; } } /// /// Gets a list of connected clients. This is only accessible on the server. /// public IReadOnlyList ConnectedClientsList { get { if (IsServer == false) { throw new NotServerException($"{nameof(ConnectedClientsList)} should only be accessed on server."); } return m_ConnectedClientsList; } } /// /// Gets a list of just the IDs of all connected clients. This is only accessible on the server. /// public IReadOnlyList ConnectedClientsIds { get { if (IsServer == false) { throw new NotServerException($"{nameof(m_ConnectedClientIds)} should only be accessed on server."); } return m_ConnectedClientIds; } } /// /// Gets the local for this client. /// public NetworkClient LocalClient { get; internal set; } /// /// Gets a dictionary of the clients that have been accepted by the transport but are still pending by the Netcode. This is only populated on the server. /// public readonly Dictionary PendingClients = new Dictionary(); /// /// Gets Whether or not a server is running /// public bool IsServer { get; internal set; } /// /// Gets Whether or not a client is running /// public bool IsClient { get; internal set; } /// /// Gets if we are running as host /// public bool IsHost => IsServer && IsClient; /// /// Gets Whether or not we are listening for connections /// public bool IsListening { get; internal set; } /// /// Gets if we are connected as a client /// public bool IsConnectedClient { get; internal set; } public bool ShutdownInProgress { get { return m_ShuttingDown; } } /// /// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects. /// public event Action OnClientConnectedCallback = null; internal void InvokeOnClientConnectedCallback(ulong clientId) => OnClientConnectedCallback?.Invoke(clientId); /// /// The callback to invoke when a client disconnects. This callback is only ran on the server and on the local client that disconnects. /// public event Action OnClientDisconnectCallback = null; /// /// The callback to invoke once the server is ready /// public event Action OnServerStarted = null; /// /// Delegate type called when connection has been approved. This only has to be set on the server. /// /// If true, a player object will be created. Otherwise the client will have no object. /// The prefabHash to use for the client. If createPlayerObject is false, this is ignored. If playerPrefabHash is null, the default player prefab is used. /// Whether or not the client was approved /// The position to spawn the client at. If null, the prefab position is used. /// The rotation to spawn the client with. If null, the prefab position is used. public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation); /// /// The callback to invoke during connection approval /// public event Action ConnectionApprovalCallback = null; internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) => ConnectionApprovalCallback?.Invoke(payload, clientId, action); /// /// The current NetworkConfig /// [HideInInspector] public NetworkConfig NetworkConfig; /// /// The current host name we are connected to, used to validate certificate /// public string ConnectedHostname { get; private set; } internal INetworkMetrics NetworkMetrics { get; private set; } internal static event Action OnSingletonReady; #if UNITY_EDITOR private void OnValidate() { if (NetworkConfig == null) { return; // May occur when the component is added } if (GetComponentInChildren() != null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}."); } } var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); // If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information if (!activeScene.isDirty || EditorApplication.isUpdating) { return; } // During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it NetworkConfig.NetworkPrefabOverrideLinks.Clear(); // Check network prefabs and assign to dictionary for quick look up for (int i = 0; i < NetworkConfig.NetworkPrefabs.Count; i++) { var networkPrefab = NetworkConfig.NetworkPrefabs[i]; var networkPrefabGo = networkPrefab?.Prefab; if (networkPrefabGo != null) { var networkObject = networkPrefabGo.GetComponent(); if (networkObject == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogError($"Cannot register {PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root"); } } else { { var childNetworkObjects = new List(); networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects); if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)"); } } } // Default to the standard NetworkPrefab.Prefab's NetworkObject first var globalObjectIdHash = networkObject.GlobalObjectIdHash; // Now check to see if it has an override switch (networkPrefab.Override) { case NetworkPrefabOverride.Prefab: { if (NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride == null && NetworkConfig.NetworkPrefabs[i].Prefab != null) { if (networkPrefab.SourcePrefabToOverride == null) { networkPrefab.SourcePrefabToOverride = networkPrefabGo; } globalObjectIdHash = networkPrefab.SourcePrefabToOverride.GetComponent().GlobalObjectIdHash; } break; } case NetworkPrefabOverride.Hash: globalObjectIdHash = networkPrefab.SourceHashToOverride; break; } // Add to the NetworkPrefabOverrideLinks or handle a new (blank) entries if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) { NetworkConfig.NetworkPrefabOverrideLinks.Add(globalObjectIdHash, networkPrefab); } else { // Duplicate entries can happen when adding a new entry into a list of existing entries // Either this is user error or a new entry, either case we replace it with a new, blank, NetworkPrefab under this condition NetworkConfig.NetworkPrefabs[i] = new NetworkPrefab(); } } } } } #endif /// /// Adds a new prefab to the network prefab list. /// This can be any GameObject with a NetworkObject component, from any source (addressables, asset /// bundles, Resource.Load, dynamically created, etc) /// /// There are three limitations to this method: /// - If you have NetworkConfig.ForceSamePrefabs enabled, you can only do this before starting /// networking, and the server and all connected clients must all have the same exact set of prefabs /// added via this method before connecting /// - Adding a prefab on the server does not automatically add it on the client - it's up to you /// to make sure the client and server are synchronized via whatever method makes sense for your game /// (RPCs, configs, deterministic loading, etc) /// - If the server sends a Spawn message to a client that has not yet added a prefab for, the spawn message /// and any other relevant messages will be held for a configurable time (default 1 second, configured via /// NetworkConfig.SpawnTimeout) before an error is logged. This is intented to enable the SDK to gracefully /// handle unexpected conditions (slow disks, slow network, etc) that slow down asset loading. This timeout /// should not be relied on and code shouldn't be written around it - your code should be written so that /// the asset is expected to be loaded before it's needed. /// /// /// public void AddNetworkPrefab(GameObject prefab) { if (IsListening && NetworkConfig.ForceSamePrefabs) { throw new Exception($"All prefabs must be registered before starting {nameof(NetworkManager)} when {nameof(NetworkConfig.ForceSamePrefabs)} is enabled."); } var networkObject = prefab.GetComponent(); if (!networkObject) { throw new Exception($"All {nameof(NetworkPrefab)}s must contain a {nameof(NetworkObject)} component."); } var networkPrefab = new NetworkPrefab { Prefab = prefab }; NetworkConfig.NetworkPrefabs.Add(networkPrefab); if (IsListening) { var sourcePrefabGlobalObjectIdHash = (uint)0; var targetPrefabGlobalObjectIdHash = (uint)0; if (!ShouldAddPrefab(networkPrefab, out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash)) { NetworkConfig.NetworkPrefabs.Remove(networkPrefab); return; } if (!AddPrefabRegistration(networkPrefab, sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash)) { NetworkConfig.NetworkPrefabs.Remove(networkPrefab); return; } DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnAddPrefab, networkObject.GlobalObjectIdHash); } } private bool ShouldAddPrefab(NetworkPrefab networkPrefab, out uint sourcePrefabGlobalObjectIdHash, out uint targetPrefabGlobalObjectIdHash, int index = -1) { sourcePrefabGlobalObjectIdHash = 0; targetPrefabGlobalObjectIdHash = 0; var networkObject = (NetworkObject)null; if (networkPrefab == null || (networkPrefab.Prefab == null && networkPrefab.Override == NetworkPrefabOverride.None)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning( $"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {index})"); } return false; } else if (networkPrefab.Override == NetworkPrefabOverride.None) { networkObject = networkPrefab.Prefab.GetComponent(); if (networkObject == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} is missing " + $"a {nameof(NetworkObject)} component (entry will be ignored)."); } return false; } // Otherwise get the GlobalObjectIdHash value sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; } else // Validate Overrides { // Validate source prefab override values first switch (networkPrefab.Override) { case NetworkPrefabOverride.Hash: { if (networkPrefab.SourceHashToOverride == 0) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourceHashToOverride)} is zero " + "(entry will be ignored)."); } return false; } sourcePrefabGlobalObjectIdHash = networkPrefab.SourceHashToOverride; break; } case NetworkPrefabOverride.Prefab: { if (networkPrefab.SourcePrefabToOverride == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourcePrefabToOverride)} is null (entry will be ignored)."); } Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored."); return false; } else { networkObject = networkPrefab.SourcePrefabToOverride.GetComponent(); if (networkObject == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({networkPrefab.SourcePrefabToOverride.name}) " + $"is missing a {nameof(NetworkObject)} component (entry will be ignored)."); } Debug.LogWarning($"{nameof(NetworkPrefab)} override entry (\"{networkPrefab.SourcePrefabToOverride.name}\") will be removed and ignored."); return false; } sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; } break; } } // Validate target prefab override values next if (networkPrefab.OverridingTargetPrefab == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.OverridingTargetPrefab)} is null!"); } switch (networkPrefab.Override) { case NetworkPrefabOverride.Hash: { Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored."); break; } case NetworkPrefabOverride.Prefab: { Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({networkPrefab.SourcePrefabToOverride.name}) will be removed and ignored."); break; } } return false; } else { targetPrefabGlobalObjectIdHash = networkPrefab.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; } } return true; } internal bool AddPrefabRegistration(NetworkPrefab networkPrefab, uint sourcePrefabGlobalObjectIdHash, uint targetPrefabGlobalObjectIdHash) { // Assign the appropriate GlobalObjectIdHash to the appropriate NetworkPrefab if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(sourcePrefabGlobalObjectIdHash)) { if (networkPrefab.Override == NetworkPrefabOverride.None) { NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); } else { if (!NetworkConfig.OverrideToNetworkPrefab.ContainsKey(targetPrefabGlobalObjectIdHash)) { switch (networkPrefab.Override) { case NetworkPrefabOverride.Prefab: { NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); } break; case NetworkPrefabOverride.Hash: { NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); } break; } } else { var networkObject = networkPrefab.Prefab.GetComponent(); // This can happen if a user tries to make several GlobalObjectIdHash values point to the same target Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {targetPrefabGlobalObjectIdHash}! Removing entry from list!"); return false; } } } else { var networkObject = networkPrefab.Prefab.GetComponent(); // This should never happen, but in the case it somehow does log an error and remove the duplicate entry Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {sourcePrefabGlobalObjectIdHash}! Removing entry from list!"); return false; } return true; } private void InitializePrefabs(int startIdx = 0) { // This is used to remove entries not needed or invalid var removeEmptyPrefabs = new List(); // Build the NetworkPrefabOverrideLinks dictionary for (int i = startIdx; i < NetworkConfig.NetworkPrefabs.Count; i++) { var sourcePrefabGlobalObjectIdHash = (uint)0; var targetPrefabGlobalObjectIdHash = (uint)0; if (!ShouldAddPrefab(NetworkConfig.NetworkPrefabs[i], out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash, i)) { removeEmptyPrefabs.Add(i); continue; } if (!AddPrefabRegistration(NetworkConfig.NetworkPrefabs[i], sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash)) { removeEmptyPrefabs.Add(i); continue; } } // Clear out anything that is invalid or not used (for invalid entries we already logged warnings to the user earlier) // Iterate backwards so indices don't shift as we remove for (int i = removeEmptyPrefabs.Count - 1; i >= 0; i--) { NetworkConfig.NetworkPrefabs.RemoveAt(removeEmptyPrefabs[i]); } removeEmptyPrefabs.Clear(); } private void Initialize(bool server) { // Don't allow the user to start a network session if the NetworkManager is // still parented under another GameObject if (NetworkManagerCheckForParent(true)) { return; } ComponentFactory.SetDefaults(); if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(Initialize)); } this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate); MessagingSystem = new MessagingSystem(new NetworkManagerMessageSender(this), this); MessagingSystem.Hook(new NetworkManagerHooks(this)); #if DEVELOPMENT_BUILD || UNITY_EDITOR MessagingSystem.Hook(new ProfilingHooks()); #endif #if MULTIPLAYER_TOOLS MessagingSystem.Hook(new MetricHooks(this)); #endif LocalClientId = ulong.MaxValue; ClearClients(); // Create spawn manager instance SpawnManager = new NetworkSpawnManager(this); DeferredMessageManager = ComponentFactory.Create(this); CustomMessagingManager = new CustomMessagingManager(this); SceneManager = new NetworkSceneManager(this); BehaviourUpdater = new NetworkBehaviourUpdater(); if (NetworkMetrics == null) { #if MULTIPLAYER_TOOLS NetworkMetrics = new NetworkMetrics(); #else NetworkMetrics = new NullNetworkMetrics(); #endif } #if MULTIPLAYER_TOOLS NetworkSolutionInterface.SetInterface(new NetworkSolutionInterfaceParameters { NetworkObjectProvider = new NetworkObjectProvider(this), }); #endif if (NetworkConfig.NetworkTransport == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogError("No transport has been selected!"); } return; } NetworkConfig.NetworkTransport.NetworkMetrics = NetworkMetrics; if (server) { NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem(); } else { NetworkTimeSystem = new NetworkTimeSystem(1.0 / NetworkConfig.TickRate, k_DefaultBufferSizeSec, 0.2); } NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0); NetworkTickSystem.Tick += OnNetworkManagerTick; this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); // Always clear our prefab override links before building NetworkConfig.NetworkPrefabOverrideLinks.Clear(); NetworkConfig.OverrideToNetworkPrefab.Clear(); InitializePrefabs(); // If we have a player prefab, then we need to verify it is in the list of NetworkPrefabOverrideLinks for client side spawning. if (NetworkConfig.PlayerPrefab != null) { var playerPrefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent(); if (playerPrefabNetworkObject != null) { //In the event there is no NetworkPrefab entry (i.e. no override for default player prefab) if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(playerPrefabNetworkObject .GlobalObjectIdHash)) { //Then add a new entry for the player prefab var playerNetworkPrefab = new NetworkPrefab(); playerNetworkPrefab.Prefab = NetworkConfig.PlayerPrefab; NetworkConfig.NetworkPrefabs.Insert(0, playerNetworkPrefab); NetworkConfig.NetworkPrefabOverrideLinks.Add(playerPrefabNetworkObject.GlobalObjectIdHash, playerNetworkPrefab); } } else { // Provide the name of the prefab with issues so the user can more easily find the prefab and fix it Debug.LogError($"{nameof(NetworkConfig.PlayerPrefab)} (\"{NetworkConfig.PlayerPrefab.name}\") has no NetworkObject assigned to it!."); } } NetworkConfig.NetworkTransport.OnTransportEvent += HandleRawTransportPoll; NetworkConfig.NetworkTransport.Initialize(this); } private void ClearClients() { PendingClients.Clear(); m_ConnectedClients.Clear(); m_ConnectedClientsList.Clear(); m_ConnectedClientIds.Clear(); LocalClient = null; NetworkObject.OrphanChildren.Clear(); } /// /// Starts a server /// public bool StartServer() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartServer)); } if (!CanStart(StartType.Server)) { return false; } Initialize(true); // If we failed to start then shutdown and notify user that the transport failed to start if (NetworkConfig.NetworkTransport.StartServer()) { IsServer = true; IsClient = false; IsListening = true; SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); OnServerStarted?.Invoke(); return true; } else { Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); Shutdown(); } return false; } /// /// Starts a client /// public bool StartClient() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartClient)); } if (!CanStart(StartType.Client)) { return false; } Initialize(false); MessagingSystem.ClientConnected(ServerClientId); if (!NetworkConfig.NetworkTransport.StartClient()) { Debug.LogError($"Client is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); Shutdown(); return false; } IsServer = false; IsClient = true; IsListening = true; return true; } /// /// Starts a Host /// public bool StartHost() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartHost)); } if (!CanStart(StartType.Host)) { return false; } Initialize(true); // If we failed to start then shutdown and notify user that the transport failed to start if (!NetworkConfig.NetworkTransport.StartServer()) { Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); Shutdown(); return false; } MessagingSystem.ClientConnected(ServerClientId); LocalClientId = ServerClientId; NetworkMetrics.SetConnectionId(LocalClientId); IsServer = true; IsClient = true; IsListening = true; if (NetworkConfig.ConnectionApproval) { InvokeConnectionApproval(NetworkConfig.ConnectionData, ServerClientId, (createPlayerObject, playerPrefabHash, approved, position, rotation) => { // You cannot decline the local server. Force approved to true if (!approved) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning( "You cannot decline the host connection. The connection was automatically approved."); } } HandleApproval(ServerClientId, createPlayerObject, playerPrefabHash, true, position, rotation); }); } else { HandleApproval(ServerClientId, NetworkConfig.PlayerPrefab != null, null, true, null, null); } SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); OnServerStarted?.Invoke(); return true; } private enum StartType { Server, Host, Client } private bool CanStart(StartType type) { if (IsListening) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("Cannot start " + type + " while an instance is already running"); } return false; } // Only if it is starting as a server or host do we need to check this // Clients don't invoke the ConnectionApprovalCallback if (NetworkConfig.ConnectionApproval && type != StartType.Client) { if (ConnectionApprovalCallback == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning( "No ConnectionApproval callback defined. Connection approval will timeout"); } } } if (ConnectionApprovalCallback != null) { if (!NetworkConfig.ConnectionApproval) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning( "A ConnectionApproval callback is defined but ConnectionApproval is disabled. In order to use ConnectionApproval it has to be explicitly enabled "); } } } return true; } public void SetSingleton() { Singleton = this; OnSingletonReady?.Invoke(); } private void OnEnable() { if (RunInBackground) { Application.runInBackground = true; } if (Singleton == null) { SetSingleton(); } if (!NetworkManagerCheckForParent()) { DontDestroyOnLoad(gameObject); } } private void Awake() { UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded; NetworkVariableHelper.InitializeAllBaseDelegates(); } /// /// Handle runtime detection for parenting the NetworkManager's GameObject under another GameObject /// private void OnTransformParentChanged() { NetworkManagerCheckForParent(); } /// /// Determines if the NetworkManager's GameObject is parented under another GameObject and /// notifies the user that this is not allowed for the NetworkManager. /// internal bool NetworkManagerCheckForParent(bool ignoreNetworkManagerCache = false) { #if UNITY_EDITOR var isParented = NetworkManagerHelper.NotifyUserOfNestedNetworkManager(this, ignoreNetworkManagerCache); #else var isParented = transform.root != transform; if (isParented) { throw new Exception(GenerateNestedNetworkManagerMessage(transform)); } #endif return isParented; } static internal string GenerateNestedNetworkManagerMessage(Transform transform) { return $"{transform.name} is nested under {transform.root.name}. NetworkManager cannot be nested.\n"; } #if UNITY_EDITOR static internal INetworkManagerHelper NetworkManagerHelper; /// /// Interface for NetworkManagerHelper /// internal interface INetworkManagerHelper { bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false); void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false); } #endif // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when unloading a scene with a NetworkManager private void OnSceneUnloaded(Scene scene) { if (scene == gameObject.scene) { OnDestroy(); } } // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application. private void OnApplicationQuit() { OnDestroy(); } // Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit private void OnDestroy() { ShutdownInternal(); UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded; if (Singleton == this) { Singleton = null; } } private void DisconnectRemoteClient(ulong clientId) { var transportId = ClientIdToTransportId(clientId); NetworkConfig.NetworkTransport.DisconnectRemoteClient(transportId); } /// /// Globally shuts down the library. /// Disconnects clients if connected and stops server if running. /// /// /// If false, any messages that are currently in the incoming queue will be handled, /// and any messages in the outgoing queue will be sent, before the shutdown is processed. /// If true, NetworkManager will shut down immediately, and any unprocessed or unsent messages /// will be discarded. /// public void Shutdown(bool discardMessageQueue = false) { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(Shutdown)); } // If we're not running, don't start shutting down, it would only cause an immediate // shutdown the next time the manager is started. if (IsServer || IsClient) { m_ShuttingDown = true; m_StopProcessingMessages = discardMessageQueue; } NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; } internal void ShutdownInternal() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(ShutdownInternal)); } if (IsServer) { // make sure all messages are flushed before transport disconnect clients if (MessagingSystem != null) { MessagingSystem.ProcessSendQueues(); } var disconnectedIds = new HashSet(); //Don't know if I have to disconnect the clients. I'm assuming the NetworkTransport does all the cleaning on shutdown. But this way the clients get a disconnect message from server (so long it does't get lost) foreach (KeyValuePair pair in ConnectedClients) { if (!disconnectedIds.Contains(pair.Key)) { disconnectedIds.Add(pair.Key); if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId) { continue; } DisconnectRemoteClient(pair.Key); } } foreach (KeyValuePair pair in PendingClients) { if (!disconnectedIds.Contains(pair.Key)) { disconnectedIds.Add(pair.Key); if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId) { continue; } DisconnectRemoteClient(pair.Key); } } } if (IsClient && IsConnectedClient) { // Client only, send disconnect to server NetworkConfig.NetworkTransport.DisconnectLocalClient(); } IsConnectedClient = false; IsServer = false; IsClient = false; this.UnregisterAllNetworkUpdates(); if (NetworkTickSystem != null) { NetworkTickSystem.Tick -= OnNetworkManagerTick; NetworkTickSystem = null; } if (MessagingSystem != null) { MessagingSystem.Dispose(); MessagingSystem = null; } if (NetworkConfig?.NetworkTransport != null) { NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; } if (SpawnManager != null) { SpawnManager.DespawnAndDestroyNetworkObjects(); SpawnManager.ServerResetShudownStateForSceneObjects(); SpawnManager = null; } if (DeferredMessageManager != null) { DeferredMessageManager.CleanupAllTriggers(); } if (SceneManager != null) { // Let the NetworkSceneManager clean up its two SceneEvenData instances SceneManager.Dispose(); SceneManager = null; } if (CustomMessagingManager != null) { CustomMessagingManager = null; } if (BehaviourUpdater != null) { BehaviourUpdater = null; } // This is required for handling the potential scenario where multiple NetworkManager instances are created. // See MTT-860 for more information if (IsListening) { //The Transport is set during initialization, thus it is possible for the Transport to be null NetworkConfig?.NetworkTransport?.Shutdown(); } m_ClientIdToTransportIdMap.Clear(); m_TransportIdToClientIdMap.Clear(); IsListening = false; m_ShuttingDown = false; m_StopProcessingMessages = false; ClearClients(); } // INetworkUpdateSystem public void NetworkUpdate(NetworkUpdateStage updateStage) { switch (updateStage) { case NetworkUpdateStage.EarlyUpdate: OnNetworkEarlyUpdate(); break; case NetworkUpdateStage.PreUpdate: OnNetworkPreUpdate(); break; case NetworkUpdateStage.PostLateUpdate: OnNetworkPostLateUpdate(); break; } } private void OnNetworkEarlyUpdate() { if (!IsListening) { return; } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportPoll.Begin(); #endif NetworkEvent networkEvent; do { networkEvent = NetworkConfig.NetworkTransport.PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime); HandleRawTransportPoll(networkEvent, clientId, payload, receiveTime); // Only do another iteration if: there are no more messages AND (there is no limit to max events or we have processed less than the maximum) } while (IsListening && networkEvent != NetworkEvent.Nothing); MessagingSystem.ProcessIncomingMessageQueue(); #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportPoll.End(); #endif } // TODO Once we have a way to subscribe to NetworkUpdateLoop with order we can move this out of NetworkManager but for now this needs to be here because we need strict ordering. private void OnNetworkPreUpdate() { if (IsServer == false && IsConnectedClient == false) { // As a client wait to run the time system until we are connected. return; } if (m_ShuttingDown && m_StopProcessingMessages) { return; } // Only update RTT here, server time is updated by time sync messages var reset = NetworkTimeSystem.Advance(Time.deltaTime); if (reset) { NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); } NetworkTickSystem.UpdateTick(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); if (IsServer == false) { NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.deltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d); } } private void OnNetworkPostLateUpdate() { if (!m_ShuttingDown || !m_StopProcessingMessages) { MessagingSystem.ProcessSendQueues(); NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count); NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1); NetworkMetrics.DispatchFrame(); NetworkObject.VerifyParentingStatus(); } DeferredMessageManager.CleanupStaleTriggers(); if (m_ShuttingDown) { ShutdownInternal(); } } /// /// This function runs once whenever the local tick is incremented and is responsible for the following (in order): /// - collect commands/inputs and send them to the server (TBD) /// - call NetworkFixedUpdate on all NetworkBehaviours in prediction/client authority mode /// private void OnNetworkManagerTick() { // Do NetworkVariable updates BehaviourUpdater.NetworkBehaviourUpdate(this); int timeSyncFrequencyTicks = (int)(k_TimeSyncFrequency * NetworkConfig.TickRate); if (IsServer && NetworkTickSystem.ServerTime.Tick % timeSyncFrequencyTicks == 0) { SyncTime(); } } private void SendConnectionRequest() { var message = new ConnectionRequestMessage { ConfigHash = NetworkConfig.GetConfig(), ShouldSendConnectionData = NetworkConfig.ConnectionApproval, ConnectionData = NetworkConfig.ConnectionData }; SendMessage(ref message, NetworkDelivery.ReliableSequenced, ServerClientId); } private IEnumerator ApprovalTimeout(ulong clientId) { NetworkTime timeStarted = LocalTime; //We yield every frame incase a pending client disconnects and someone else gets its connection id while ((LocalTime - timeStarted).Time < NetworkConfig.ClientConnectionBufferTimeout && PendingClients.ContainsKey(clientId)) { yield return null; } if (PendingClients.ContainsKey(clientId) && !ConnectedClients.ContainsKey(clientId)) { // Timeout if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo($"Client {clientId} Handshake Timed Out"); } DisconnectClient(clientId); } } internal ulong TransportIdToClientId(ulong transportId) { return transportId == m_ServerTransportId ? ServerClientId : m_TransportIdToClientIdMap[transportId]; } internal ulong ClientIdToTransportId(ulong clientId) { return clientId == ServerClientId ? m_ServerTransportId : m_ClientIdToTransportIdMap[clientId]; } private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, ArraySegment payload, float receiveTime) { var transportId = clientId; switch (networkEvent) { case NetworkEvent.Connect: #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportConnect.Begin(); #endif // Assumptions: // - When server receives a connection, it *must be* a client // - When client receives one, it *must be* the server // Client's can't connect to or talk to other clients. // Server is a sentinel so only one exists, if we are server, we can't be // connecting to it. if (IsServer) { clientId = m_NextClientId++; } else { clientId = ServerClientId; } m_ClientIdToTransportIdMap[clientId] = transportId; m_TransportIdToClientIdMap[transportId] = clientId; MessagingSystem.ClientConnected(clientId); if (IsServer) { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo("Client Connected"); } PendingClients.Add(clientId, new PendingClient() { ClientId = clientId, ConnectionState = PendingClient.State.PendingConnection }); StartCoroutine(ApprovalTimeout(clientId)); } else { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo("Connected"); } SendConnectionRequest(); StartCoroutine(ApprovalTimeout(clientId)); } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportConnect.End(); #endif break; case NetworkEvent.Data: { clientId = TransportIdToClientId(clientId); HandleIncomingData(clientId, payload, receiveTime); break; } case NetworkEvent.Disconnect: #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.Begin(); #endif clientId = TransportIdCleanUp(clientId, transportId); if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo($"Disconnect Event From {clientId}"); } OnClientDisconnectCallback?.Invoke(clientId); if (IsServer) { OnClientDisconnectFromServer(clientId); } else { // We must pass true here and not process any sends messages // as we are no longer connected and thus there is no one to // send any messages to and this will cause an exception within // UnityTransport as the client ID is no longer valid. Shutdown(true); } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.End(); #endif break; } } /// /// Handles cleaning up the transport id/client id tables after /// receiving a disconnect event from transport /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private ulong TransportIdCleanUp(ulong clientId, ulong transportId) { // This check is for clients that attempted to connect but failed. // When this happens, the client will not have an entry within the // m_TransportIdToClientIdMap or m_ClientIdToTransportIdMap lookup // tables so we exit early and just return 0 to be used for the // disconnect event. if (!IsServer && !m_TransportIdToClientIdMap.ContainsKey(clientId)) { return 0; } clientId = TransportIdToClientId(clientId); m_TransportIdToClientIdMap.Remove(transportId); m_ClientIdToTransportIdMap.Remove(clientId); return clientId; } internal unsafe int SendMessage(ref TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds) where TMessageType : INetworkMessage where TClientIdListType : IReadOnlyList { // Prevent server sending to itself if (IsServer) { ulong* nonServerIds = stackalloc ulong[clientIds.Count]; int newIdx = 0; for (int idx = 0; idx < clientIds.Count; ++idx) { if (clientIds[idx] == ServerClientId) { continue; } nonServerIds[newIdx++] = clientIds[idx]; } if (newIdx == 0) { return 0; } return MessagingSystem.SendMessage(ref message, delivery, nonServerIds, newIdx); } // else if (clientIds.Count != 1 || clientIds[0] != ServerClientId) { throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}"); } return MessagingSystem.SendMessage(ref message, delivery, clientIds); } internal unsafe int SendMessage(ref T message, NetworkDelivery delivery, ulong* clientIds, int numClientIds) where T : INetworkMessage { // Prevent server sending to itself if (IsServer) { ulong* nonServerIds = stackalloc ulong[numClientIds]; int newIdx = 0; for (int idx = 0; idx < numClientIds; ++idx) { if (clientIds[idx] == ServerClientId) { continue; } nonServerIds[newIdx++] = clientIds[idx]; } if (newIdx == 0) { return 0; } return MessagingSystem.SendMessage(ref message, delivery, nonServerIds, newIdx); } // else if (numClientIds != 1 || clientIds[0] != ServerClientId) { throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}"); } return MessagingSystem.SendMessage(ref message, delivery, clientIds, numClientIds); } internal unsafe int SendMessage(ref T message, NetworkDelivery delivery, in NativeArray clientIds) where T : INetworkMessage { return SendMessage(ref message, delivery, (ulong*)clientIds.GetUnsafePtr(), clientIds.Length); } internal int SendMessage(ref T message, NetworkDelivery delivery, ulong clientId) where T : INetworkMessage { // Prevent server sending to itself if (IsServer && clientId == ServerClientId) { return 0; } if (!IsServer && clientId != ServerClientId) { throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}"); } return MessagingSystem.SendMessage(ref message, delivery, clientId); } internal int SendPreSerializedMessage(in FastBufferWriter writer, int maxSize, ref T message, NetworkDelivery delivery, ulong clientId) where T : INetworkMessage { return MessagingSystem.SendPreSerializedMessage(writer, maxSize, ref message, delivery, clientId); } internal void HandleIncomingData(ulong clientId, ArraySegment payload, float receiveTime) { #if DEVELOPMENT_BUILD || UNITY_EDITOR s_HandleIncomingData.Begin(); #endif MessagingSystem.HandleIncomingData(clientId, payload, receiveTime); #if DEVELOPMENT_BUILD || UNITY_EDITOR s_HandleIncomingData.End(); #endif } /// /// Disconnects the remote client. /// /// The ClientId to disconnect public void DisconnectClient(ulong clientId) { if (!IsServer) { throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead."); } OnClientDisconnectFromServer(clientId); DisconnectRemoteClient(clientId); } private void OnClientDisconnectFromServer(ulong clientId) { PendingClients.Remove(clientId); if (ConnectedClients.TryGetValue(clientId, out NetworkClient networkClient)) { if (IsServer) { var playerObject = networkClient.PlayerObject; if (playerObject != null) { if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash)) { PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject); } else { Destroy(playerObject.gameObject); } } // Get the NetworkObjects owned by the disconnected client var clientOwnedObjects = SpawnManager.GetClientOwnedObjects(clientId); if (clientOwnedObjects == null) { // This could happen if a client is never assigned a player object and is disconnected // Only log this in verbose/developer mode if (LogLevel == LogLevel.Developer) { NetworkLog.LogWarning($"ClientID {clientId} disconnected with (0) zero owned objects! Was a player prefab not assigned?"); } } else { // Handle changing ownership and prefab handlers for (int i = clientOwnedObjects.Count - 1; i >= 0; i--) { var ownedObject = clientOwnedObjects[i]; if (ownedObject != null) { if (!ownedObject.DontDestroyWithOwner) { if (PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash)) { PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]); } else { Destroy(ownedObject.gameObject); } } else { ownedObject.RemoveOwnership(); } } } } // TODO: Could(should?) be replaced with more memory per client, by storing the visibility foreach (var sobj in SpawnManager.SpawnedObjectsList) { sobj.Observers.Remove(clientId); } } for (int i = 0; i < ConnectedClientsList.Count; i++) { if (ConnectedClientsList[i].ClientId == clientId) { m_ConnectedClientsList.RemoveAt(i); break; } } for (int i = 0; i < ConnectedClientsIds.Count; i++) { if (ConnectedClientsIds[i] == clientId) { m_ConnectedClientIds.RemoveAt(i); break; } } m_ConnectedClients.Remove(clientId); } MessagingSystem.ClientDisconnected(clientId); } private void SyncTime() { #if DEVELOPMENT_BUILD || UNITY_EDITOR s_SyncTime.Begin(); #endif if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo("Syncing Time To Clients"); } var message = new TimeSyncMessage { Tick = NetworkTickSystem.ServerTime.Tick }; SendMessage(ref message, NetworkDelivery.Unreliable, ConnectedClientsIds); #if DEVELOPMENT_BUILD || UNITY_EDITOR s_SyncTime.End(); #endif } /// /// Server Side: Handles the approval of a client /// /// client being approved /// whether we want to create a player or not /// the GlobalObjectIdHash value for the Network Prefab to create as the player /// Is the player approved or not? /// Used when createPlayerObject is true, position of the player when spawned /// Used when createPlayerObject is true, rotation of the player when spawned internal void HandleApproval(ulong ownerClientId, bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation) { if (approved) { // Inform new client it got approved PendingClients.Remove(ownerClientId); var client = new NetworkClient { ClientId = ownerClientId, }; m_ConnectedClients.Add(ownerClientId, client); m_ConnectedClientsList.Add(client); m_ConnectedClientIds.Add(client.ClientId); if (createPlayerObject) { var networkObject = SpawnManager.CreateLocalNetworkObject(false, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, null, position, rotation); SpawnManager.SpawnNetworkObjectLocally(networkObject, SpawnManager.GetNetworkObjectId(), false, true, ownerClientId, false); ConnectedClients[ownerClientId].PlayerObject = networkObject; } // Server doesn't send itself the connection approved message if (ownerClientId != ServerClientId) { var message = new ConnectionApprovedMessage { OwnerClientId = ownerClientId, NetworkTick = LocalTime.Tick }; if (!NetworkConfig.EnableSceneManagement) { if (SpawnManager.SpawnedObjectsList.Count != 0) { message.SpawnedObjectsList = SpawnManager.SpawnedObjectsList; } } SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); // If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization if (!NetworkConfig.EnableSceneManagement) { InvokeOnClientConnectedCallback(ownerClientId); } else { SceneManager.SynchronizeNetworkObjects(ownerClientId); } } else // Server just adds itself as an observer to all spawned NetworkObjects { LocalClient = client; SpawnManager.UpdateObservedNetworkObjects(ownerClientId); InvokeOnClientConnectedCallback(ownerClientId); } if (!createPlayerObject || (playerPrefabHash == null && NetworkConfig.PlayerPrefab == null)) { return; } // Separating this into a contained function call for potential further future separation of when this notification is sent. ApprovedPlayerSpawn(ownerClientId, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash); } else { PendingClients.Remove(ownerClientId); DisconnectRemoteClient(ownerClientId); } } /// /// Spawns the newly approved player /// /// new player client identifier /// the prefab GlobalObjectIdHash value for this player internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) { foreach (var clientPair in ConnectedClients) { if (clientPair.Key == clientId || clientPair.Key == ServerClientId || // Server already spawned it ConnectedClients[clientId].PlayerObject == null || !ConnectedClients[clientId].PlayerObject.Observers.Contains(clientPair.Key)) { continue; //The new client. } var message = new CreateObjectMessage { ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key) }; message.ObjectInfo.Header.Hash = playerPrefabHash; message.ObjectInfo.Header.IsSceneObject = false; message.ObjectInfo.Header.HasParent = false; message.ObjectInfo.Header.IsPlayerObject = true; message.ObjectInfo.Header.OwnerClientId = clientId; var size = SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientPair.Key); NetworkMetrics.TrackObjectSpawnSent(clientPair.Key, ConnectedClients[clientId].PlayerObject, size); } } } }