using System; using System.Collections.Generic; using System.Linq; using kcp2k; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; namespace Mirror { public enum PlayerSpawnMethod { Random, RoundRobin } public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } [DisallowMultipleComponent] [AddComponentMenu("Network/Network Manager")] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager")] public class NetworkManager : MonoBehaviour { /// Enable to keep NetworkManager alive when changing scenes. // This should be set if your game has a single NetworkManager that exists for the lifetime of the process. If there is a NetworkManager in each scene, then this should not be set. [Header("Configuration")] [FormerlySerializedAs("m_DontDestroyOnLoad")] [Tooltip("Should the Network Manager object be persisted through scene changes?")] public bool dontDestroyOnLoad = true; /// Multiplayer games should always run in the background so the network doesn't time out. [FormerlySerializedAs("m_RunInBackground")] [Tooltip("Multiplayer games should always run in the background so the network doesn't time out.")] public bool runInBackground = true; /// Should the server auto-start when 'Server Build' is checked in build settings [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] [FormerlySerializedAs("startOnHeadless")] public bool autoStartServerBuild = true; /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. [Tooltip("Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] public int serverTickRate = 30; /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). [Header("Scene Management")] [Scene] [FormerlySerializedAs("m_OfflineScene")] [Tooltip("Scene that Mirror will switch to when the client or server is stopped")] public string offlineScene = ""; /// Automatically switch to this scene upon going online (after connect/startserver). [Scene] [FormerlySerializedAs("m_OnlineScene")] [Tooltip("Scene that Mirror will switch to when the server is started. Clients will recieve a Scene Message to load the server's current scene when they connect.")] public string onlineScene = ""; // transport layer [Header("Network Info")] [Tooltip("Transport component attached to this object that server and client will use to connect")] [SerializeField] protected Transport transport; /// Server's address for clients to connect to. [FormerlySerializedAs("m_NetworkAddress")] [Tooltip("Network Address where the client should connect to the server. Server does not use this for anything.")] public string networkAddress = "localhost"; /// The maximum number of concurrent network connections to support. [FormerlySerializedAs("m_MaxConnections")] [Tooltip("Maximum number of concurrent connections.")] public int maxConnections = 100; [Header("Authentication")] [Tooltip("Authentication component attached to this object")] public NetworkAuthenticator authenticator; /// The default prefab to be used to create player objects on the server. // Player objects are created in the default handler for AddPlayer() on // the server. Implementing OnServerAddPlayer overrides this behaviour. [Header("Player Object")] [FormerlySerializedAs("m_PlayerPrefab")] [Tooltip("Prefab of the player object. Prefab must have a Network Identity component. May be an empty game object or a full avatar.")] public GameObject playerPrefab; /// Enable to automatically create player objects on connect and on scene change. [FormerlySerializedAs("m_AutoCreatePlayer")] [Tooltip("Should Mirror automatically spawn the player after scene change?")] public bool autoCreatePlayer = true; /// Where to spawn players. [FormerlySerializedAs("m_PlayerSpawnMethod")] [Tooltip("Round Robin or Random order of Start Position selection")] public PlayerSpawnMethod playerSpawnMethod; /// Prefabs that can be spawned over the network need to be registered here. [FormerlySerializedAs("m_SpawnPrefabs"), HideInInspector] public List spawnPrefabs = new List(); /// List of transforms populated by NetworkStartPositions public static List startPositions = new List(); public static int startPositionIndex; /// The one and only NetworkManager public static NetworkManager singleton { get; internal set; } /// Number of active player objects across all connections on the server. public int numPlayers => NetworkServer.connections.Count(kv => kv.Value.identity != null); /// True if the server is running or client is connected/connecting. public bool isNetworkActive => NetworkServer.active || NetworkClient.active; // TODO remove this // internal for tests internal static NetworkConnection clientReadyConnection; /// True if the client loaded a new scene when connecting to the server. // This is set before OnClientConnect is called, so it can be checked // there to perform different logic if a scene load occurred. protected bool clientLoadedScene; // helper enum to know if we started the networkmanager as server/client/host. // -> this is necessary because when StartHost changes server scene to // online scene, FinishLoadScene is called and the host client isn't // connected yet (no need to connect it before server was fully set up). // in other words, we need this to know which mode we are running in // during FinishLoadScene. public NetworkManagerMode mode { get; private set; } // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too public virtual void OnValidate() { // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); if (playerPrefab != null && playerPrefab.GetComponent() == null) { Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); playerPrefab = null; } // This avoids the mysterious "Replacing existing prefab with assetId ... Old prefab 'Player', New prefab 'Player'" warning. if (playerPrefab != null && spawnPrefabs.Contains(playerPrefab)) { Debug.LogWarning("NetworkManager - Player Prefab should not be added to Registered Spawnable Prefabs list...removed it."); spawnPrefabs.Remove(playerPrefab); } } // virtual so that inheriting classes' Reset() can call base.Reset() too // Reset only gets called when the component is added or the user resets the component // Thats why we validate these things that only need to be validated on adding the NetworkManager here // If we would do it in OnValidate() then it would run this everytime a value changes public virtual void Reset() { // make sure someone doesn't accidentally add another NetworkManager // need transform.root because when adding to a child, the parent's // Reset isn't called. foreach (NetworkManager manager in transform.root.GetComponentsInChildren()) { if (manager != this) { Debug.LogError($"{name} detected another component of type {typeof(NetworkManager)} in its hierarchy on {manager.name}. There can only be one, please remove one of them."); // return early so that transport component isn't auto-added // to the duplicate NetworkManager. return; } } // add transport if there is none yet. makes upgrading easier. if (transport == null) { #if UNITY_EDITOR // RecordObject needs to be called before we make the change UnityEditor.Undo.RecordObject(gameObject, "Added default Transport"); #endif transport = GetComponent(); // was a transport added yet? if not, add one if (transport == null) { transport = gameObject.AddComponent(); Debug.Log("NetworkManager: added default Transport because there was none yet."); } } } // virtual so that inheriting classes' Awake() can call base.Awake() too public virtual void Awake() { // Don't allow collision-destroyed second instance to continue. if (!InitializeSingleton()) return; Debug.Log("Mirror | mirror-networking.com | discord.gg/N9QVxbM"); // Set the networkSceneName to prevent a scene reload // if client connection to server fails. networkSceneName = offlineScene; // setup OnSceneLoaded callback SceneManager.sceneLoaded += OnSceneLoaded; } // virtual so that inheriting classes' Start() can call base.Start() too public virtual void Start() { // headless mode? then start the server // can't do this in Awake because Awake is for initialization. // some transports might not be ready until Start. // // (tick rate is applied in StartServer!) #if UNITY_SERVER if (autoStartServerBuild) { StartServer(); } #endif } // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too public virtual void LateUpdate() { UpdateScene(); } // keep the online scene change check in a separate function bool IsServerOnlineSceneChangeNeeded() { // Only change scene if the requested online scene is not blank, and is not already loaded return !string.IsNullOrWhiteSpace(onlineScene) && !IsSceneActive(onlineScene) && onlineScene != offlineScene; } public static bool IsSceneActive(string scene) { Scene activeScene = SceneManager.GetActiveScene(); return activeScene.path == scene || activeScene.name == scene; } // full server setup code, without spawning objects yet void SetupServer() { // Debug.Log("NetworkManager SetupServer"); InitializeSingleton(); if (runInBackground) Application.runInBackground = true; if (authenticator != null) { authenticator.OnStartServer(); authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated); } ConfigureHeadlessFrameRate(); // start listening to network connections NetworkServer.Listen(maxConnections); // call OnStartServer AFTER Listen, so that NetworkServer.active is // true and we can call NetworkServer.Spawn in OnStartServer // overrides. // (useful for loading & spawning stuff from database etc.) // // note: there is no risk of someone connecting after Listen() and // before OnStartServer() because this all runs in one thread // and we don't start processing connects until Update. OnStartServer(); // this must be after Listen(), since that registers the default message handlers RegisterServerMessages(); } /// Starts the server, listening for incoming connections. public void StartServer() { if (NetworkServer.active) { Debug.LogWarning("Server already started."); return; } mode = NetworkManagerMode.ServerOnly; // StartServer is inherently ASYNCHRONOUS (=doesn't finish immediately) // // Here is what it does: // Listen // if onlineScene: // LoadSceneAsync // ... // FinishLoadSceneServerOnly // SpawnObjects // else: // SpawnObjects // // there is NO WAY to make it synchronous because both LoadSceneAsync // and LoadScene do not finish loading immediately. as long as we // have the onlineScene feature, it will be asynchronous! SetupServer(); // scene change needed? then change scene and spawn afterwards. if (IsServerOnlineSceneChangeNeeded()) { ServerChangeScene(onlineScene); } // otherwise spawn directly else { NetworkServer.SpawnObjects(); } } /// Starts the client, connects it to the server with networkAddress. public void StartClient() { if (NetworkClient.active) { Debug.LogWarning("Client already started."); return; } mode = NetworkManagerMode.ClientOnly; InitializeSingleton(); if (runInBackground) Application.runInBackground = true; if (authenticator != null) { authenticator.OnStartClient(); authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } // In case this is a headless client... ConfigureHeadlessFrameRate(); RegisterClientMessages(); if (string.IsNullOrWhiteSpace(networkAddress)) { Debug.LogError("Must set the Network Address field in the manager"); return; } // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); NetworkClient.Connect(networkAddress); OnStartClient(); } /// Starts the client, connects it to the server via Uri public void StartClient(Uri uri) { if (NetworkClient.active) { Debug.LogWarning("Client already started."); return; } mode = NetworkManagerMode.ClientOnly; InitializeSingleton(); if (runInBackground) Application.runInBackground = true; if (authenticator != null) { authenticator.OnStartClient(); authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } RegisterClientMessages(); // Debug.Log($"NetworkManager StartClient address:{uri}"); networkAddress = uri.Host; NetworkClient.Connect(uri); OnStartClient(); } /// Starts a network "host" - a server and client in the same application. public void StartHost() { if (NetworkServer.active || NetworkClient.active) { Debug.LogWarning("Server or Client already started."); return; } mode = NetworkManagerMode.Host; // StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately) // // Here is what it does: // Listen // ConnectHost // if onlineScene: // LoadSceneAsync // ... // FinishLoadSceneHost // FinishStartHost // SpawnObjects // StartHostClient <= not guaranteed to happen after SpawnObjects if onlineScene is set! // ClientAuth // success: server sends changescene msg to client // else: // FinishStartHost // // there is NO WAY to make it synchronous because both LoadSceneAsync // and LoadScene do not finish loading immediately. as long as we // have the onlineScene feature, it will be asynchronous! // setup server first SetupServer(); // call OnStartHost AFTER SetupServer. this way we can use // NetworkServer.Spawn etc. in there too. just like OnStartServer // is called after the server is actually properly started. OnStartHost(); // scene change needed? then change scene and spawn afterwards. // => BEFORE host client connects. if client auth succeeds then the // server tells it to load 'onlineScene'. we can't do that if // server is still in 'offlineScene'. so load on server first. if (IsServerOnlineSceneChangeNeeded()) { // call FinishStartHost after changing scene. finishStartHostPending = true; ServerChangeScene(onlineScene); } // otherwise call FinishStartHost directly else { FinishStartHost(); } } // This may be set true in StartHost and is evaluated in FinishStartHost bool finishStartHostPending; // FinishStartHost is guaranteed to be called after the host server was // fully started and all the asynchronous StartHost magic is finished // (= scene loading), or immediately if there was no asynchronous magic. // // note: we don't really need FinishStartClient/FinishStartServer. the // host version is enough. void FinishStartHost() { // ConnectHost needs to be called BEFORE SpawnObjects: // https://github.com/vis2k/Mirror/pull/1249/ // -> this sets NetworkServer.localConnection. // -> localConnection needs to be set before SpawnObjects because: // -> SpawnObjects calls OnStartServer in all NetworkBehaviours // -> OnStartServer might spawn an object and set [SyncVar(hook="OnColorChanged")] object.color = green; // -> this calls SyncVar.set (generated by Weaver), which has // a custom case for host mode (because host mode doesn't // get OnDeserialize calls, where SyncVar hooks are usually // called): // // if (!SyncVarEqual(value, ref color)) // { // if (NetworkServer.localClientActive && !getSyncVarHookGuard(1uL)) // { // setSyncVarHookGuard(1uL, value: true); // OnColorChangedHook(value); // setSyncVarHookGuard(1uL, value: false); // } // SetSyncVar(value, ref color, 1uL); // } // // -> localClientActive needs to be true, otherwise the hook // isn't called in host mode! // // TODO call this after spawnobjects and worry about the syncvar hook fix later? NetworkClient.ConnectHost(); // server scene was loaded. now spawn all the objects NetworkServer.SpawnObjects(); // connect client and call OnStartClient AFTER server scene was // loaded and all objects were spawned. // DO NOT do this earlier. it would cause race conditions where a // client will do things before the server is even fully started. //Debug.Log("StartHostClient called"); StartHostClient(); } void StartHostClient() { //Debug.Log("NetworkManager ConnectLocalClient"); if (authenticator != null) { authenticator.OnStartClient(); authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } networkAddress = "localhost"; NetworkServer.ActivateHostScene(); RegisterClientMessages(); // ConnectLocalServer needs to be called AFTER RegisterClientMessages // (https://github.com/vis2k/Mirror/pull/1249/) NetworkClient.ConnectLocalServer(); OnStartClient(); } /// This stops both the client and the server that the manager is using. public void StopHost() { OnStopHost(); // calling OnTransportDisconnected was needed to fix // https://github.com/vis2k/Mirror/issues/1515 // so that the host client receives a DisconnectMessage // TODO reevaluate if this is still needed after all the disconnect // fixes, and try to put this into LocalConnection.Disconnect! NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId); StopClient(); StopServer(); } /// Stops the server from listening and simulating the game. public void StopServer() { // return if already stopped to avoid recursion deadlock if (!NetworkServer.active) return; if (authenticator != null) { authenticator.OnServerAuthenticated.RemoveListener(OnServerAuthenticated); authenticator.OnStopServer(); } // Get Network Manager out of DDOL before going to offline scene // to avoid collision and let a fresh Network Manager be created. // IMPORTANT: .gameObject can be null if StopClient is called from // OnApplicationQuit or from tests! if (gameObject != null && gameObject.scene.name == "DontDestroyOnLoad" && !string.IsNullOrWhiteSpace(offlineScene) && SceneManager.GetActiveScene().path != offlineScene) SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); OnStopServer(); //Debug.Log("NetworkManager StopServer"); NetworkServer.Shutdown(); // set offline mode BEFORE changing scene so that FinishStartScene // doesn't think we need initialize anything. mode = NetworkManagerMode.Offline; if (!string.IsNullOrWhiteSpace(offlineScene)) { ServerChangeScene(offlineScene); } startPositionIndex = 0; networkSceneName = ""; } /// Stops and disconnects the client. public void StopClient() { if (mode == NetworkManagerMode.Offline) return; if (authenticator != null) { authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); authenticator.OnStopClient(); } // Get Network Manager out of DDOL before going to offline scene // to avoid collision and let a fresh Network Manager be created. // IMPORTANT: .gameObject can be null if StopClient is called from // OnApplicationQuit or from tests! if (gameObject != null && gameObject.scene.name == "DontDestroyOnLoad" && !string.IsNullOrWhiteSpace(offlineScene) && SceneManager.GetActiveScene().path != offlineScene) SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); OnStopClient(); //Debug.Log("NetworkManager StopClient"); // set offline mode BEFORE changing scene so that FinishStartScene // doesn't think we need initialize anything. // set offline mode BEFORE NetworkClient.Disconnect so StopClient // only runs once. mode = NetworkManagerMode.Offline; // shutdown client NetworkClient.Disconnect(); NetworkClient.Shutdown(); // If this is the host player, StopServer will already be changing scenes. // Check loadingSceneAsync to ensure we don't double-invoke the scene change. // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. if (!string.IsNullOrWhiteSpace(offlineScene) && !IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) { ClientChangeScene(offlineScene, SceneOperation.Normal); } networkSceneName = ""; } // called when quitting the application by closing the window / pressing // stop in the editor. virtual so that inheriting classes' // OnApplicationQuit() can call base.OnApplicationQuit() too public virtual void OnApplicationQuit() { // stop client first // (we want to send the quit packet to the server instead of waiting // for a timeout) if (NetworkClient.isConnected) { StopClient(); //Debug.Log("OnApplicationQuit: stopped client"); } // stop server after stopping client (for proper host mode stopping) if (NetworkServer.active) { StopServer(); //Debug.Log("OnApplicationQuit: stopped server"); } // Call ResetStatics to reset statics and singleton ResetStatics(); } /// Set the frame rate for a headless builds. Override to disable or modify. // useful for dedicated servers. // useful for headless benchmark clients. public virtual void ConfigureHeadlessFrameRate() { #if UNITY_SERVER Application.targetFrameRate = serverTickRate; // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); #endif } bool InitializeSingleton() { if (singleton != null && singleton == this) return true; if (dontDestroyOnLoad) { if (singleton != null) { Debug.LogWarning("Multiple NetworkManagers detected in the scene. Only one NetworkManager can exist at a time. The duplicate NetworkManager will be destroyed."); Destroy(gameObject); // Return false to not allow collision-destroyed second instance to continue. return false; } //Debug.Log("NetworkManager created singleton (DontDestroyOnLoad)"); singleton = this; if (Application.isPlaying) { // Force the object to scene root, in case user made it a child of something // in the scene since DDOL is only allowed for scene root objects transform.SetParent(null); DontDestroyOnLoad(gameObject); } } else { //Debug.Log("NetworkManager created singleton (ForScene)"); singleton = this; } // set active transport AFTER setting singleton. // so only if we didn't destroy ourselves. Transport.activeTransport = transport; return true; } void RegisterServerMessages() { NetworkServer.OnConnectedEvent = OnServerConnectInternal; NetworkServer.OnDisconnectedEvent = OnServerDisconnect; NetworkServer.OnErrorEvent = OnServerError; NetworkServer.RegisterHandler(OnServerAddPlayerInternal); // Network Server initially registers its own handler for this, so we replace it here. NetworkServer.ReplaceHandler(OnServerReadyMessageInternal); } void RegisterClientMessages() { NetworkClient.OnConnectedEvent = OnClientConnectInternal; NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; NetworkClient.OnErrorEvent = OnClientError; NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); NetworkClient.RegisterHandler(OnClientSceneInternal, false); if (playerPrefab != null) NetworkClient.RegisterPrefab(playerPrefab); foreach (GameObject prefab in spawnPrefabs.Where(t => t != null)) NetworkClient.RegisterPrefab(prefab); } // This is the only way to clear the singleton, so another instance can be created. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void ResetStatics() { // call StopHost if we have a singleton if (singleton) singleton.StopHost(); // reset all statics startPositions.Clear(); startPositionIndex = 0; clientReadyConnection = null; loadingSceneAsync = null; networkSceneName = string.Empty; // and finally (in case it isn't null already)... singleton = null; } // virtual so that inheriting classes' OnDestroy() can call base.OnDestroy() too public virtual void OnDestroy() { //Debug.Log("NetworkManager destroyed"); } /// The name of the current network scene. // set by NetworkManager when changing the scene. // new clients will automatically load this scene. // Loading a scene manually won't set it. public static string networkSceneName { get; protected set; } = ""; public static AsyncOperation loadingSceneAsync; /// Change the server scene and all client's scenes across the network. // Called automatically if onlineScene or offlineScene are set, but it // can be called from user code to switch scenes again while the game is // in progress. This automatically sets clients to be not-ready during // the change and ready again to participate in the new scene. public virtual void ServerChangeScene(string newSceneName) { if (string.IsNullOrWhiteSpace(newSceneName)) { Debug.LogError("ServerChangeScene empty scene name"); return; } if (NetworkServer.isLoadingScene && newSceneName == networkSceneName) { Debug.LogError($"Scene change is already in progress for {newSceneName}"); return; } // Debug.Log($"ServerChangeScene {newSceneName}"); NetworkServer.SetAllClientsNotReady(); networkSceneName = newSceneName; // Let server prepare for scene change OnServerChangeScene(newSceneName); // set server flag to stop processing messages while changing scenes // it will be re-enabled in FinishLoadScene. NetworkServer.isLoadingScene = true; loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); // ServerChangeScene can be called when stopping the server // when this happens the server is not active so does not need to tell clients about the change if (NetworkServer.active) { // notify all clients about the new scene NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName }); } startPositionIndex = 0; startPositions.Clear(); } // This is only set in ClientChangeScene below...never on server. // We need to check this in OnClientSceneChanged called from FinishLoadSceneClientOnly // to prevent AddPlayer message after loading/unloading additive scenes SceneOperation clientSceneOperation = SceneOperation.Normal; internal void ClientChangeScene(string newSceneName, SceneOperation sceneOperation = SceneOperation.Normal, bool customHandling = false) { if (string.IsNullOrWhiteSpace(newSceneName)) { Debug.LogError("ClientChangeScene empty scene name"); return; } //Debug.Log($"ClientChangeScene newSceneName: {newSceneName} networkSceneName{networkSceneName}"); // Let client prepare for scene change OnClientChangeScene(newSceneName, sceneOperation, customHandling); // After calling OnClientChangeScene, exit if server since server is already doing // the actual scene change, and we don't need to do it for the host client if (NetworkServer.active) return; // set client flag to stop processing messages while loading scenes. // otherwise we would process messages and then lose all the state // as soon as the load is finishing, causing all kinds of bugs // because of missing state. // (client may be null after StopClient etc.) // Debug.Log("ClientChangeScene: pausing handlers while scene is loading to avoid data loss after scene was loaded."); NetworkClient.isLoadingScene = true; // Cache sceneOperation so we know what was requested by the // Scene message in OnClientChangeScene and OnClientSceneChanged clientSceneOperation = sceneOperation; // scene handling will happen in overrides of OnClientChangeScene and/or OnClientSceneChanged // Do not call FinishLoadScene here. Custom handler will assign loadingSceneAsync and we need // to wait for that to finish. UpdateScene already checks for that to be not null and isDone. if (customHandling) return; switch (sceneOperation) { case SceneOperation.Normal: loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); break; case SceneOperation.LoadAdditive: // Ensure additive scene is not already loaded on client by name or path // since we don't know which was passed in the Scene message if (!SceneManager.GetSceneByName(newSceneName).IsValid() && !SceneManager.GetSceneByPath(newSceneName).IsValid()) loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, LoadSceneMode.Additive); else { Debug.LogWarning($"Scene {newSceneName} is already loaded"); // Reset the flag that we disabled before entering this switch NetworkClient.isLoadingScene = false; } break; case SceneOperation.UnloadAdditive: // Ensure additive scene is actually loaded on client by name or path // since we don't know which was passed in the Scene message if (SceneManager.GetSceneByName(newSceneName).IsValid() || SceneManager.GetSceneByPath(newSceneName).IsValid()) loadingSceneAsync = SceneManager.UnloadSceneAsync(newSceneName, UnloadSceneOptions.UnloadAllEmbeddedSceneObjects); else { Debug.LogWarning($"Cannot unload {newSceneName} with UnloadAdditive operation"); // Reset the flag that we disabled before entering this switch NetworkClient.isLoadingScene = false; } break; } // don't change the client's current networkSceneName when loading additive scene content if (sceneOperation == SceneOperation.Normal) networkSceneName = newSceneName; } // support additive scene loads: // NetworkScenePostProcess disables all scene objects on load, and // * NetworkServer.SpawnObjects enables them again on the server when // calling OnStartServer // * NetworkClient.PrepareToSpawnSceneObjects enables them again on the // client after the server sends ObjectSpawnStartedMessage to client // in SpawnObserversForConnection. this is only called when the // client joins, so we need to rebuild scene objects manually again // TODO merge this with FinishLoadScene()? void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (mode == LoadSceneMode.Additive) { if (NetworkServer.active) { // TODO only respawn the server objects from that scene later! NetworkServer.SpawnObjects(); // Debug.Log($"Respawned Server objects after additive scene load: {scene.name}"); } if (NetworkClient.active) { NetworkClient.PrepareToSpawnSceneObjects(); // Debug.Log($"Rebuild Client spawnableObjects after additive scene load: {scene.name}"); } } } void UpdateScene() { if (loadingSceneAsync != null && loadingSceneAsync.isDone) { //Debug.Log($"ClientChangeScene done readyConn {clientReadyConnection}"); // try-finally to guarantee loadingSceneAsync being cleared. // fixes https://github.com/vis2k/Mirror/issues/2517 where if // FinishLoadScene throws an exception, loadingSceneAsync would // never be cleared and this code would run every Update. try { FinishLoadScene(); } finally { loadingSceneAsync.allowSceneActivation = true; loadingSceneAsync = null; } } } protected void FinishLoadScene() { // NOTE: this cannot use NetworkClient.allClients[0] - that client may be for a completely different purpose. // process queued messages that we received while loading the scene //Debug.Log("FinishLoadScene: resuming handlers after scene was loading."); NetworkServer.isLoadingScene = false; NetworkClient.isLoadingScene = false; // host mode? if (mode == NetworkManagerMode.Host) { FinishLoadSceneHost(); } // server-only mode? else if (mode == NetworkManagerMode.ServerOnly) { FinishLoadSceneServerOnly(); } // client-only mode? else if (mode == NetworkManagerMode.ClientOnly) { FinishLoadSceneClientOnly(); } // otherwise we called it after stopping when loading offline scene. // do nothing then. } // finish load scene part for host mode. makes code easier and is // necessary for FinishStartHost later. // (the 3 things have to happen in that exact order) void FinishLoadSceneHost() { // debug message is very important. if we ever break anything then // it's very obvious to notice. //Debug.Log("Finished loading scene in host mode."); if (clientReadyConnection != null) { #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientConnect(clientReadyConnection); #pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } // do we need to finish a StartHost() call? // then call FinishStartHost and let it take care of spawning etc. if (finishStartHostPending) { finishStartHostPending = false; FinishStartHost(); // call OnServerSceneChanged OnServerSceneChanged(networkSceneName); // DO NOT call OnClientSceneChanged here. // the scene change happened because StartHost loaded the // server's online scene. it has nothing to do with the client. // this was not meant as a client scene load, so don't call it. // // otherwise AddPlayer would be called twice: // -> once for client OnConnected // -> once in OnClientSceneChanged } // otherwise we just changed a scene in host mode else { // spawn server objects NetworkServer.SpawnObjects(); // call OnServerSceneChanged OnServerSceneChanged(networkSceneName); if (NetworkClient.isConnected) { // let client know that we changed scene #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientSceneChanged(NetworkClient.connection); #pragma warning restore 618 } } } // finish load scene part for server-only. . makes code easier and is // necessary for FinishStartServer later. void FinishLoadSceneServerOnly() { // debug message is very important. if we ever break anything then // it's very obvious to notice. //Debug.Log("Finished loading scene in server-only mode."); NetworkServer.SpawnObjects(); OnServerSceneChanged(networkSceneName); } // finish load scene part for client-only. makes code easier and is // necessary for FinishStartClient later. void FinishLoadSceneClientOnly() { // debug message is very important. if we ever break anything then // it's very obvious to notice. //Debug.Log("Finished loading scene in client-only mode."); if (clientReadyConnection != null) { #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientConnect(clientReadyConnection); #pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } if (NetworkClient.isConnected) { #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientSceneChanged(NetworkClient.connection); #pragma warning restore 618 } } /// /// Registers the transform of a game object as a player spawn location. /// This is done automatically by NetworkStartPosition components, but can be done manually from user script code. /// /// Transform to register. // Static because it's called from NetworkStartPosition::Awake // and singleton may not exist yet public static void RegisterStartPosition(Transform start) { // Debug.Log($"RegisterStartPosition: {start.gameObject.name} {start.position}"); startPositions.Add(start); // reorder the list so that round-robin spawning uses the start positions // in hierarchy order. This assumes all objects with NetworkStartPosition // component are siblings, either in the scene root or together as children // under a single parent in the scene. startPositions = startPositions.OrderBy(transform => transform.GetSiblingIndex()).ToList(); } /// Unregister a Transform from start positions. // Static because it's called from NetworkStartPosition::OnDestroy // and singleton may not exist yet public static void UnRegisterStartPosition(Transform start) { //Debug.Log($"UnRegisterStartPosition: {start.name} {start.position}"); startPositions.Remove(start); } /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. public Transform GetStartPosition() { // first remove any dead transforms startPositions.RemoveAll(t => t == null); if (startPositions.Count == 0) return null; if (playerSpawnMethod == PlayerSpawnMethod.Random) { return startPositions[UnityEngine.Random.Range(0, startPositions.Count)]; } else { Transform startPosition = startPositions[startPositionIndex]; startPositionIndex = (startPositionIndex + 1) % startPositions.Count; return startPosition; } } void OnServerConnectInternal(NetworkConnectionToClient conn) { //Debug.Log("NetworkManager.OnServerConnectInternal"); if (authenticator != null) { // we have an authenticator - let it handle authentication authenticator.OnServerAuthenticate(conn); } else { // authenticate immediately OnServerAuthenticated(conn); } } // called after successful authentication // TODO do the NetworkServer.OnAuthenticated thing from x branch void OnServerAuthenticated(NetworkConnectionToClient conn) { //Debug.Log("NetworkManager.OnServerAuthenticated"); // set connection to authenticated conn.isAuthenticated = true; // proceed with the login handshake by calling OnServerConnect if (networkSceneName != "" && networkSceneName != offlineScene) { SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; conn.Send(msg); } OnServerConnect(conn); } void OnServerReadyMessageInternal(NetworkConnectionToClient conn, ReadyMessage msg) { //Debug.Log("NetworkManager.OnServerReadyMessageInternal"); OnServerReady(conn); } public void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage msg) { //Debug.Log("NetworkManager.OnServerAddPlayer"); if (autoCreatePlayer && playerPrefab == null) { Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); return; } if (autoCreatePlayer && playerPrefab.GetComponent() == null) { Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); return; } if (conn.identity != null) { Debug.LogWarning("There is already a player for this connection."); return; } OnServerAddPlayer(conn); } void OnClientConnectInternal() { //Debug.Log("NetworkManager.OnClientConnectInternal"); if (authenticator != null) { // we have an authenticator - let it handle authentication authenticator.OnClientAuthenticate(); } else { // authenticate immediately OnClientAuthenticated(); } } // called after successful authentication void OnClientAuthenticated() { //Debug.Log("NetworkManager.OnClientAuthenticated"); // set connection to authenticated NetworkClient.connection.isAuthenticated = true; // proceed with the login handshake by calling OnClientConnect if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || IsSceneActive(onlineScene)) { clientLoadedScene = false; #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientConnect(NetworkClient.connection); #pragma warning restore 618 } else { // will wait for scene id to come from the server. clientLoadedScene = true; clientReadyConnection = NetworkClient.connection; } } void OnClientDisconnectInternal() { //Debug.Log("NetworkManager.OnClientDisconnectInternal"); #pragma warning disable 618 // obsolete method calls new method because it's not empty OnClientDisconnect(NetworkClient.connection); #pragma warning restore 618 } void OnClientNotReadyMessageInternal(NotReadyMessage msg) { //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); NetworkClient.ready = false; #pragma warning disable 618 OnClientNotReady(NetworkClient.connection); #pragma warning restore 618 OnClientNotReady(); // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. } void OnClientSceneInternal(SceneMessage msg) { //Debug.Log("NetworkManager.OnClientSceneInternal"); // This needs to run for host client too. NetworkServer.active is checked there if (NetworkClient.isConnected) { ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling); } } /// Called on the server when a new client connects. public virtual void OnServerConnect(NetworkConnectionToClient conn) { } /// Called on the server when a client disconnects. // Called by NetworkServer.OnTransportDisconnect! public virtual void OnServerDisconnect(NetworkConnectionToClient conn) { // by default, this function destroys the connection's player. // can be overwritten for cases like delayed logouts in MMOs to // avoid players escaping from PvP situations by logging out. NetworkServer.DestroyPlayerForConnection(conn); //Debug.Log("OnServerDisconnect: Client disconnected."); } /// Called on the server when a client is ready (= loaded the scene) public virtual void OnServerReady(NetworkConnectionToClient conn) { if (conn.identity == null) { // this is now allowed (was not for a while) //Debug.Log("Ready with no player object"); } NetworkServer.SetClientReady(conn); } /// Called on server when a client requests to add the player. Adds playerPrefab by default. Can be overwritten. // The default implementation for this function creates a new player object from the playerPrefab. public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) { Transform startPos = GetStartPosition(); GameObject player = startPos != null ? Instantiate(playerPrefab, startPos.position, startPos.rotation) : Instantiate(playerPrefab); // instantiating a "Player" prefab gives it the name "Player(clone)" // => appending the connectionId is WAY more useful for debugging! player.name = $"{playerPrefab.name} [connId={conn.connectionId}]"; // PlayerPrefs.GetString("NickName") NetworkServer.AddPlayerForConnection(conn, player); } /// Called on server when transport raises an exception. NetworkConnection may be null. public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) { } /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed public virtual void OnServerChangeScene(string newSceneName) { } /// Called on server after a scene load with ServerChangeScene() is completed. public virtual void OnServerSceneChanged(string sceneName) { } /// Called on the client when connected to a server. By default it sets client as ready and adds a player. public virtual void OnClientConnect() { // OnClientConnect by default calls AddPlayer but it should not do // that when we have online/offline scenes. so we need the // clientLoadedScene flag to prevent it. if (!clientLoadedScene) { // Ready/AddPlayer is usually triggered by a scene load completing. // if no scene was loaded, then Ready/AddPlayer it here instead. if (!NetworkClient.ready) NetworkClient.Ready(); if (autoCreatePlayer) NetworkClient.AddPlayer(); } } // Deprecated 2021-12-11 [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] public virtual void OnClientConnect(NetworkConnection conn) => OnClientConnect(); /// Called on clients when disconnected from a server. public virtual void OnClientDisconnect() { if (mode == NetworkManagerMode.Offline) return; StopClient(); } // Deprecated 2021-12-11 [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] public virtual void OnClientDisconnect(NetworkConnection conn) => OnClientDisconnect(); /// Called on client when transport raises an exception. public virtual void OnClientError(Exception exception) { } /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. public virtual void OnClientNotReady() { } // Deprecated 2021-12-11 [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] public virtual void OnClientNotReady(NetworkConnection conn) { } /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed // customHandling: indicates if scene loading will be handled through overrides public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { } /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. // Scene changes can cause player objects to be destroyed. The default // implementation of OnClientSceneChanged in the NetworkManager is to // add a player object for the connection if no player object exists. public virtual void OnClientSceneChanged() { // always become ready. if (!NetworkClient.ready) NetworkClient.Ready(); // Only call AddPlayer for normal scene changes, not additive load/unload if (clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) { // add player if existing one is null NetworkClient.AddPlayer(); } } // Deprecated 2021-12-11 [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] public virtual void OnClientSceneChanged(NetworkConnection conn) => OnClientSceneChanged(); // Since there are multiple versions of StartServer, StartClient and // StartHost, to reliably customize their functionality, users would // need override all the versions. Instead these callbacks are invoked // from all versions, so users only need to implement this one case. /// This is invoked when a host is started. public virtual void OnStartHost() { } /// This is invoked when a server is started - including when a host is started. public virtual void OnStartServer() { } /// This is invoked when the client is started. public virtual void OnStartClient() { } /// This is called when a server is stopped - including when a host is stopped. public virtual void OnStopServer() { } /// This is called when a client is stopped. public virtual void OnStopClient() { } /// This is called when a host is stopped. public virtual void OnStopHost() { } } }