using System; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine.SceneManagement; using Debug = UnityEngine.Debug; namespace Unity.Netcode { /// /// The main component of the library /// [AddComponentMenu("Netcode/Network Manager", -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 public void NetworkUpdate(NetworkUpdateStage updateStage) { switch (updateStage) { case NetworkUpdateStage.EarlyUpdate: { ConnectionManager.ProcessPendingApprovals(); ConnectionManager.PollAndHandleNetworkEvents(); MessageManager.ProcessIncomingMessageQueue(); MessageManager.CleanupDisconnectedClients(); } break; case NetworkUpdateStage.PreUpdate: { NetworkTimeSystem.UpdateTime(); } break; case NetworkUpdateStage.PostLateUpdate: { // This should be invoked just prior to the MessageManager processes its outbound queue. SceneManager.CheckForAndSendNetworkObjectSceneChanged(); // Process outbound messages MessageManager.ProcessSendQueues(); // Metrics update needs to be driven by NetworkConnectionManager's update to assure metrics are dispatched after the send queue is processed. MetricsManager.UpdateMetrics(); // TODO 2023-Q2: Determine a better way to handle this NetworkObject.VerifyParentingStatus(); // This is "ok" to invoke when not processing messages since it is just cleaning up messages that never got handled within their timeout period. DeferredMessageManager.CleanupStaleTriggers(); // TODO 2023-Q2: Determine a better way to handle this if (m_ShuttingDown) { ShutdownInternal(); } } break; } } /// /// The client id used to represent the server /// public const ulong ServerClientId = 0; /// /// Returns ServerClientId if IsServer or LocalClientId if not /// public ulong LocalClientId { get => ConnectionManager.LocalClient.ClientId; internal set => ConnectionManager.LocalClient.ClientId = value; } /// /// Gets a dictionary of connected clients and their clientId keys. This is only accessible on the server. /// public IReadOnlyDictionary ConnectedClients => IsServer ? ConnectionManager.ConnectedClients : throw new NotServerException($"{nameof(ConnectionManager.ConnectedClients)} should only be accessed on server."); /// /// Gets a list of connected clients. This is only accessible on the server. /// public IReadOnlyList ConnectedClientsList => IsServer ? ConnectionManager.ConnectedClientsList : throw new NotServerException($"{nameof(ConnectionManager.ConnectedClientsList)} should only be accessed on server."); /// /// Gets a list of just the IDs of all connected clients. This is only accessible on the server. /// public IReadOnlyList ConnectedClientsIds => IsServer ? ConnectionManager.ConnectedClientIds : throw new NotServerException($"{nameof(ConnectionManager.ConnectedClientIds)} should only be accessed on server."); /// /// Gets the local for this client. /// public NetworkClient LocalClient => ConnectionManager.LocalClient; /// /// 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. /// // See NetworkConnectionManager.AddPendingClient and NetworkConnectionManager.RemovePendingClient to see how this is now populated public readonly Dictionary PendingClients = new Dictionary(); /// /// Gets Whether or not a server is running /// public bool IsServer => ConnectionManager.LocalClient.IsServer; /// /// Gets Whether or not a client is running /// public bool IsClient => ConnectionManager.LocalClient.IsClient; /// /// Gets if we are running as host /// public bool IsHost => ConnectionManager.LocalClient.IsHost; /// /// When disconnected from the server, the server may send a reason. If a reason was sent, this property will /// tell client code what the reason was. It should be queried after the OnClientDisconnectCallback is called /// public string DisconnectReason => ConnectionManager.DisconnectReason; /// /// Is true when a server or host is listening for connections. /// Is true when a client is connecting or connected to a network session. /// Is false when not listening, connecting, or connected. /// public bool IsListening { get => ConnectionManager.IsListening; internal set => ConnectionManager.IsListening = value; } /// /// When true, the client is connected, approved, and synchronized with /// the server. ///
///
///
public bool IsConnectedClient { get => ConnectionManager.LocalClient.IsConnected; internal set => ConnectionManager.LocalClient.IsConnected = value; } /// /// Is true when the client has been approved. /// /// /// This only reflects the client's approved status and does not mean the client /// has finished the connection and synchronization process. The server-host will /// always be approved upon being starting the /// /// public bool IsApproved { get => ConnectionManager.LocalClient.IsApproved; internal set => ConnectionManager.LocalClient.IsApproved = value; } /// /// The callback to invoke if the fails. /// /// /// A failure of the transport is always followed by the shutting down. Recovering /// from a transport failure would normally entail reconfiguring the transport (e.g. re-authenticating, or /// recreating a new service allocation depending on the transport) and restarting the client/server/host. /// public event Action OnTransportFailure { add => ConnectionManager.OnTransportFailure += value; remove => ConnectionManager.OnTransportFailure -= value; } /// /// The callback to invoke during connection approval. Allows client code to decide whether or not to allow incoming client connection /// public Action ConnectionApprovalCallback { get => ConnectionManager.ConnectionApprovalCallback; set { if (value != null && value.GetInvocationList().Length > 1) { throw new InvalidOperationException($"Only one {nameof(ConnectionApprovalCallback)} can be registered at a time."); } ConnectionManager.ConnectionApprovalCallback = value; } } /// /// 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 { add => ConnectionManager.OnClientConnectedCallback += value; remove => ConnectionManager.OnClientConnectedCallback -= value; } /// /// 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 { add => ConnectionManager.OnClientDisconnectCallback += value; remove => ConnectionManager.OnClientDisconnectCallback -= value; } /// /// The current host name we are connected to, used to validate certificate /// public string ConnectedHostname => string.Empty; /// /// Connection Approval Response /// public class ConnectionApprovalResponse { /// /// Whether or not the client was approved /// public bool Approved; /// /// If true, a player object will be created. Otherwise the client will have no object. /// public bool CreatePlayerObject; /// /// The prefabHash to use for the client. If createPlayerObject is false, this is ignored. If playerPrefabHash is null, the default player prefab is used. /// public uint? PlayerPrefabHash; /// /// The position to spawn the client at. If null, the prefab position is used. /// public Vector3? Position; /// /// The rotation to spawn the client with. If null, the prefab position is used. /// public Quaternion? Rotation; /// /// If the Approval decision cannot be made immediately, the client code can set Pending to true, keep a reference to the ConnectionApprovalResponse object and write to it later. Client code must exercise care to setting all the members to the value it wants before marking Pending to false, to indicate completion. If the field is set as Pending = true, we'll monitor the object until it gets set to not pending anymore and use the parameters then. /// public bool Pending; /// /// Optional reason. If Approved is false, this reason will be sent to the client so they know why they /// were not approved. /// public string Reason; } /// /// Connection Approval Request /// public struct ConnectionApprovalRequest { /// /// The connection data payload /// public byte[] Payload; /// /// The Network Id of the client we are about to handle /// public ulong ClientNetworkId; } /// /// Can be used to determine if the is currently shutting itself down /// public bool ShutdownInProgress => m_ShuttingDown; private bool m_ShuttingDown; /// /// The current netcode project configuration /// [HideInInspector] public NetworkConfig NetworkConfig; /// /// The local /// public NetworkTime LocalTime => NetworkTickSystem?.LocalTime ?? default; /// /// The on the server /// 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; } internal static event Action OnSingletonReady; /// /// This callback is invoked when the local server is started and listening for incoming connections. /// public event Action OnServerStarted = null; /// /// The callback to invoke once the local client is ready /// public event Action OnClientStarted = null; /// /// This callback is invoked once the local server is stopped. /// /// The first parameter of this event will be set to when stopping a host instance and when stopping a server instance. public event Action OnServerStopped = null; /// /// The callback to invoke once the local client stops /// /// The parameter states whether the client was running in host mode /// The first parameter of this event will be set to when stopping the host client and when stopping a standard client instance. public event Action OnClientStopped = null; /// /// The instance created after starting the /// public NetworkPrefabHandler PrefabHandler { get { if (m_PrefabHandler == null) { m_PrefabHandler = new NetworkPrefabHandler(); m_PrefabHandler.Initialize(this); } return m_PrefabHandler; } } private NetworkPrefabHandler m_PrefabHandler; /// /// Gets the SpawnManager for this NetworkManager /// public NetworkSpawnManager SpawnManager { get; private set; } internal IDeferredNetworkMessageManager DeferredMessageManager { get; private set; } /// /// Gets the CustomMessagingManager for this NetworkManager /// public CustomMessagingManager CustomMessagingManager { get; private set; } /// /// The instance created after starting the /// public NetworkSceneManager SceneManager { get; private set; } internal NetworkBehaviourUpdater BehaviourUpdater { get; set; } /// /// Accessor property for the of the NetworkManager. /// Prefer the use of the LocalTime and ServerTime properties /// public NetworkTimeSystem NetworkTimeSystem { get; private set; } /// /// Accessor property for the of the NetworkManager. /// public NetworkTickSystem NetworkTickSystem { get; private set; } /// /// Used for time mocking in tests /// internal IRealTimeProvider RealTimeProvider { get; private set; } internal INetworkMetrics NetworkMetrics => MetricsManager.NetworkMetrics; internal NetworkMetricsManager MetricsManager = new NetworkMetricsManager(); internal NetworkConnectionManager ConnectionManager = new NetworkConnectionManager(); internal NetworkMessageManager MessageManager = null; #if UNITY_EDITOR internal static 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); } internal delegate void ResetNetworkManagerDelegate(NetworkManager manager); internal static ResetNetworkManagerDelegate OnNetworkManagerReset; private void Reset() { OnNetworkManagerReset?.Invoke(this); } internal 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.Prefabs.NetworkPrefabOverrideLinks.Clear(); var prefabs = NetworkConfig.Prefabs.Prefabs; // Check network prefabs and assign to dictionary for quick look up for (int i = 0; i < prefabs.Count; i++) { var networkPrefab = prefabs[i]; var networkPrefabGo = networkPrefab?.Prefab; if (networkPrefabGo == null) { continue; } var networkObject = networkPrefabGo.GetComponent(); if (networkObject == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogError($"Cannot register {NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root"); } continue; } { 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($"{NetworkPrefabHandler.PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)"); } } } } } #endif /// /// 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; } internal static string GenerateNestedNetworkManagerMessage(Transform transform) { return $"{transform.name} is nested under {transform.root.name}. NetworkManager cannot be nested.\n"; } /// /// Handle runtime detection for parenting the NetworkManager's GameObject under another GameObject /// private void OnTransformParentChanged() { NetworkManagerCheckForParent(); } /// /// Set this NetworkManager instance as the static NetworkManager singleton /// public void SetSingleton() { Singleton = this; OnSingletonReady?.Invoke(); } private void Awake() { NetworkConfig?.InitializePrefabs(); UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded; } private void OnEnable() { if (RunInBackground) { Application.runInBackground = true; } if (Singleton == null) { SetSingleton(); } if (!NetworkManagerCheckForParent()) { DontDestroyOnLoad(gameObject); } } /// /// /// /// 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) => PrefabHandler.GetNetworkPrefabOverride(gameObject); /// /// /// /// /// public void AddNetworkPrefab(GameObject prefab) => PrefabHandler.AddNetworkPrefab(prefab); /// /// /// /// public void RemoveNetworkPrefab(GameObject prefab) => PrefabHandler.RemoveNetworkPrefab(prefab); /// /// Sets the maximum size of a single non-fragmented message (or message batch) passed through the transport. /// This should represent the transport's MTU size, minus any transport-level overhead. /// /// public int MaximumTransmissionUnitSize { set => MessageManager.NonFragmentedMessageMaxSize = value; get => MessageManager.NonFragmentedMessageMaxSize; } /// /// Sets the maximum size of a message (or message batch) passed through the transport with the ReliableFragmented delivery. /// Warning: setting this value too low may result in the SDK becoming non-functional with projects that have a large number of NetworkBehaviours or NetworkVariables, as the SDK relies on the transport's ability to fragment some messages when they grow beyond the MTU size. /// /// public int MaximumFragmentedMessageSize { set => MessageManager.FragmentedMessageMaxSize = value; get => MessageManager.FragmentedMessageMaxSize; } internal 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; } if (NetworkConfig.NetworkTransport == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogError("No transport has been selected!"); } return; } // Logging initializes first for any logging during systems initialization if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(Initialize)); } this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate); // ComponentFactory needs to set its defaults next ComponentFactory.SetDefaults(); // UnityTransport dependencies are then initialized RealTimeProvider = ComponentFactory.Create(this); MetricsManager.Initialize(this); { MessageManager = new NetworkMessageManager(new DefaultMessageSender(this), this); MessageManager.Hook(new NetworkManagerHooks(this)); #if DEVELOPMENT_BUILD || UNITY_EDITOR MessageManager.Hook(new ProfilingHooks()); #endif #if MULTIPLAYER_TOOLS MessageManager.Hook(new MetricHooks(this)); #endif // Assures there is a server message queue available MessageManager.ClientConnected(ServerClientId); } // Now the connection manager can initialize (which initializes transport) ConnectionManager.Initialize(this); // The remaining systems can then be initialized NetworkTimeSystem = server ? NetworkTimeSystem.ServerTimeSystem() : new NetworkTimeSystem(1.0 / NetworkConfig.TickRate); NetworkTickSystem = NetworkTimeSystem.Initialize(this); // Create spawn manager instance SpawnManager = new NetworkSpawnManager(this); DeferredMessageManager = ComponentFactory.Create(this); CustomMessagingManager = new CustomMessagingManager(this); SceneManager = new NetworkSceneManager(this); BehaviourUpdater = new NetworkBehaviourUpdater(); BehaviourUpdater.Initialize(this); NetworkConfig.InitializePrefabs(); PrefabHandler.RegisterPlayerPrefab(); } private enum StartType { Server, Host, Client } /// /// Determines if NetworkManager can start based on the current /// NetworkManager instance state(s) /// 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; } /// /// Starts a server /// /// (/) returns true if started in server mode successfully. public bool StartServer() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartServer)); } if (!CanStart(StartType.Server)) { return false; } ConnectionManager.LocalClient.SetRole(true, false, this); ConnectionManager.LocalClient.ClientId = ServerClientId; Initialize(true); try { IsListening = NetworkConfig.NetworkTransport.StartServer(); // If we failed to start then shutdown and notify user that the transport failed to start if (IsListening) { SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); OnServerStarted?.Invoke(); ConnectionManager.LocalClient.IsApproved = true; return true; } ConnectionManager.TransportFailureEventHandler(true); } catch (Exception) { ConnectionManager.LocalClient.SetRole(false, false); IsListening = false; throw; } return IsListening; } /// /// Starts a client /// /// (/) returns true if started in client mode successfully. public bool StartClient() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartClient)); } if (!CanStart(StartType.Client)) { return false; } ConnectionManager.LocalClient.SetRole(false, true, this); Initialize(false); try { IsListening = NetworkConfig.NetworkTransport.StartClient(); // If we failed to start then shutdown and notify user that the transport failed to start if (!IsListening) { ConnectionManager.TransportFailureEventHandler(true); } else { OnClientStarted?.Invoke(); } } catch (Exception ex) { Debug.LogException(ex); ConnectionManager.LocalClient.SetRole(false, false); IsListening = false; } return IsListening; } /// /// Starts a Host /// /// (/) returns true if started in host mode successfully. public bool StartHost() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(StartHost)); } if (!CanStart(StartType.Host)) { return false; } ConnectionManager.LocalClient.SetRole(true, true, this); Initialize(true); try { IsListening = NetworkConfig.NetworkTransport.StartServer(); // If we failed to start then shutdown and notify user that the transport failed to start if (!IsListening) { ConnectionManager.TransportFailureEventHandler(true); } else { // Finalize host-client and server creation logic HostServerInitialize(); } } catch (Exception ex) { Debug.LogException(ex); ConnectionManager.LocalClient.SetRole(false, false); IsListening = false; } return IsListening; } /// /// Handles the host client creation logic along with /// additional server creation logic /// private void HostServerInitialize() { LocalClientId = ServerClientId; NetworkMetrics.SetConnectionId(LocalClientId); if (NetworkConfig.ConnectionApproval && ConnectionApprovalCallback != null) { var response = new ConnectionApprovalResponse(); ConnectionApprovalCallback(new ConnectionApprovalRequest { Payload = NetworkConfig.ConnectionData, ClientNetworkId = ServerClientId }, response); if (!response.Approved) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("You cannot decline the host connection. The connection was automatically approved."); } } response.Approved = true; ConnectionManager.HandleConnectionApproval(ServerClientId, response); } else { var response = new ConnectionApprovalResponse { Approved = true, CreatePlayerObject = NetworkConfig.PlayerPrefab != null }; ConnectionManager.HandleConnectionApproval(ServerClientId, response); } SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); OnServerStarted?.Invoke(); OnClientStarted?.Invoke(); // This assures that any in-scene placed NetworkObject is spawned and // any associated NetworkBehaviours' netcode related properties are // set prior to invoking OnClientConnected. ConnectionManager.InvokeOnClientConnectedCallback(LocalClientId); } /// /// Disconnects the remote client. /// /// The ClientId to disconnect public void DisconnectClient(ulong clientId) => ConnectionManager.DisconnectClient(clientId); /// /// Disconnects the remote client. /// /// The ClientId to disconnect /// Disconnection reason. If set, client will receive a DisconnectReasonMessage and have the /// reason available in the NetworkManager.DisconnectReason property public void DisconnectClient(ulong clientId, string reason = null) => ConnectionManager.DisconnectClient(clientId, reason); /// /// 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; MessageManager.StopProcessing = discardMessageQueue; } NetworkConfig.NetworkTransport.OnTransportEvent -= ConnectionManager.HandleNetworkEvent; } // 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 (gameObject != null && scene == gameObject.scene) { OnDestroy(); } } internal void ShutdownInternal() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(ShutdownInternal)); } this.UnregisterAllNetworkUpdates(); // Everything is shutdown in the order of their dependencies DeferredMessageManager?.CleanupAllTriggers(); CustomMessagingManager = null; BehaviourUpdater?.Shutdown(); BehaviourUpdater = null; // Shutdown connection manager last which shuts down transport ConnectionManager.Shutdown(); if (MessageManager != null) { MessageManager.Dispose(); MessageManager = null; } // We need to clean up NetworkObjects before we reset the IsServer // and IsClient properties. This provides consistency of these two // property values for NetworkObjects that are still spawned when // the shutdown cycle begins. SpawnManager?.DespawnAndDestroyNetworkObjects(); SpawnManager?.ServerResetShudownStateForSceneObjects(); SpawnManager = null; // Let the NetworkSceneManager clean up its two SceneEvenData instances SceneManager?.Dispose(); SceneManager = null; IsListening = false; m_ShuttingDown = false; if (ConnectionManager.LocalClient.IsClient) { // If we were a client, we want to know if we were a host // client or not. (why we pass in "IsServer") OnClientStopped?.Invoke(ConnectionManager.LocalClient.IsServer); } if (ConnectionManager.LocalClient.IsServer) { // If we were a server, we want to know if we were a host // or not. (why we pass in "IsClient") OnServerStopped?.Invoke(ConnectionManager.LocalClient.IsClient); } // Reset the client's roles ConnectionManager.LocalClient.SetRole(false, false); // This cleans up the internal prefabs list NetworkConfig?.Prefabs?.Shutdown(); // Reset the configuration hash for next session in the event // that the prefab list changes NetworkConfig?.ClearConfigHash(); // Time & tick systems should be the last system shutdown so other systems // can unsubscribe from tick updates and such. NetworkTimeSystem?.Shutdown(); NetworkTickSystem = null; } // 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; } } } }