using System.Collections.Generic; using System; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; namespace Unity.Netcode { /// /// Used for local notifications of various scene events. The of /// delegate type uses this class to provide /// scene event status.
/// Note: This is only when is enabled.
/// *** Do not start new scene events within scene event notification callbacks.
/// See also:
/// ///
public class SceneEvent { /// /// The returned by
/// This is set for the following s: /// /// /// /// ///
public AsyncOperation AsyncOperation; /// /// Will always be set to the current /// public SceneEventType SceneEventType; /// /// If applicable, this reflects the type of scene loading or unloading that is occurring.
/// This is set for the following s: /// /// /// /// /// /// /// /// ///
public LoadSceneMode LoadSceneMode; /// /// This will be set to the scene name that the event pertains to.
/// This is set for the following s: /// /// /// /// /// /// /// /// ///
public string SceneName; /// /// When a scene is loaded, the Scene structure is returned.
/// This is set for the following s: /// /// /// ///
public Scene Scene; /// /// The client identifier can vary depending upon the following conditions:
/// /// s that always set the /// to the local client identifier, are initiated (and processed locally) by the /// server-host, and sent to all clients to be processed.
/// /// /// /// /// /// ///
/// Events that always set the to the local client identifier, /// are initiated (and processed locally) by a client or server-host, and if initiated /// by a client will always be sent to and processed on the server-host: /// /// /// /// /// /// /// /// Events that always set the to the ServerId: /// /// /// /// /// ///
///
public ulong ClientId; /// /// List of clients that completed a loading or unloading event.
/// This is set for the following s: /// /// /// /// ///
public List ClientsThatCompleted; /// /// List of clients that timed out during a loading or unloading event.
/// This is set for the following s: /// /// /// /// ///
public List ClientsThatTimedOut; } /// /// Main class for managing network scenes when is enabled. /// Uses the message to communicate between the server and client(s) /// public class NetworkSceneManager : IDisposable { private const NetworkDelivery k_DeliveryType = NetworkDelivery.ReliableFragmentedSequenced; internal const int InvalidSceneNameOrPath = -1; // Used to be able to turn re-synchronization off internal static bool DisableReSynchronization; /// /// Used to detect if a scene event is underway /// Only 1 scene event can occur on the server at a time for now. /// private bool m_IsSceneEventActive = false; /// /// The delegate callback definition for scene event notifications.
/// See also:
///
/// ///
/// public delegate void SceneEventDelegate(SceneEvent sceneEvent); /// /// Subscribe to this event to receive all notifications.
/// For more details review over and .
/// Alternate Single Event Type Notification Registration Options
/// To receive only a specific event type notification or a limited set of notifications you can alternately subscribe to /// each notification type individually via the following events:
/// /// Invoked only when a event is being processed /// Invoked only when an event is being processed /// Invoked only when a event is being processed /// Invoked only when a event is being processed /// Invoked only when an event is being processed /// Invoked only when a event is being processed /// Invoked only when an event is being processed /// Invoked only when a event is being processed /// /// Note: Do not start new scene events within NetworkSceneManager scene event notification callbacks.
///
public event SceneEventDelegate OnSceneEvent; /// /// Delegate declaration for the OnLoad event.
/// See also:
/// for more information ///
/// the client that is processing this event (the server will receive all of these events for every client and itself) /// name of the scene being processed /// the LoadSceneMode mode for the scene being loaded /// the associated that can be used for scene loading progress public delegate void OnLoadDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation); /// /// Delegate declaration for the OnUnload event.
/// See also:
/// for more information ///
/// the client that is processing this event (the server will receive all of these events for every client and itself) /// name of the scene being processed /// the associated that can be used for scene unloading progress public delegate void OnUnloadDelegateHandler(ulong clientId, string sceneName, AsyncOperation asyncOperation); /// /// Delegate declaration for the OnSynchronize event.
/// See also:
/// for more information ///
/// the client that is processing this event (the server will receive all of these events for every client and itself) public delegate void OnSynchronizeDelegateHandler(ulong clientId); /// /// Delegate declaration for the OnLoadEventCompleted and OnUnloadEventCompleted events.
/// See also:
///
/// ///
/// scene pertaining to this event /// of the associated event completed /// the clients that completed the loading event /// the clients (if any) that timed out during the loading event public delegate void OnEventCompletedDelegateHandler(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut); /// /// Delegate declaration for the OnLoadComplete event.
/// See also:
/// for more information ///
/// the client that is processing this event (the server will receive all of these events for every client and itself) /// the scene name pertaining to this event /// the mode the scene was loaded in public delegate void OnLoadCompleteDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode); /// /// Delegate declaration for the OnUnloadComplete event.
/// See also:
/// for more information ///
/// the client that is processing this event (the server will receive all of these events for every client and itself) /// the scene name pertaining to this event public delegate void OnUnloadCompleteDelegateHandler(ulong clientId, string sceneName); /// /// Delegate declaration for the OnSynchronizeComplete event.
/// See also:
/// for more information ///
/// the client that completed this event public delegate void OnSynchronizeCompleteDelegateHandler(ulong clientId); /// /// Invoked when a event is started by the server.
/// Note: The server and connected client(s) will always receive this notification.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadDelegateHandler OnLoad; /// /// Invoked when a event is started by the server.
/// Note: The server and connected client(s) will always receive this notification.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadDelegateHandler OnUnload; /// /// Invoked when a event is started by the server /// after a client is approved for connection in order to synchronize the client with the currently loaded /// scenes and NetworkObjects. This event signifies the beginning of the synchronization event.
/// Note: The server and connected client(s) will always receive this notification. /// This event is generated on a per newly connected and approved client basis.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeDelegateHandler OnSynchronize; /// /// Invoked when a event is generated by the server. /// This event signifies the end of an existing event as it pertains /// to all clients connected when the event was started. This event signifies that all clients (and server) have /// finished the event.
/// Note: this is useful to know when all clients have loaded the same scene (single or additive mode)
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnLoadEventCompleted; /// /// Invoked when a event is generated by the server. /// This event signifies the end of an existing event as it pertains /// to all clients connected when the event was started. This event signifies that all clients (and server) have /// finished the event.
/// Note: this is useful to know when all clients have unloaded a specific scene. The will /// always be for this event.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnEventCompletedDelegateHandler OnUnloadEventCompleted; /// /// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself). /// Each client receives their own notification sent to the server.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnLoadCompleteDelegateHandler OnLoadComplete; /// /// Invoked when a event is generated by a client or server.
/// Note: The server receives this message from all clients (including itself). /// Each client receives their own notification sent to the server.
/// *** Do not start new scene events within scene event notification callbacks.
///
public event OnUnloadCompleteDelegateHandler OnUnloadComplete; /// /// Invoked when a event is generated by a client.
/// Note: The server receives this message from the client, but will never generate this event for itself. /// Each client receives their own notification sent to the server. This is useful to know that a client has /// completed the entire connection sequence, loaded all scenes, and synchronized all NetworkObjects. /// *** Do not start new scene events within scene event notification callbacks.
///
public event OnSynchronizeCompleteDelegateHandler OnSynchronizeComplete; /// /// Delegate declaration for the handler that provides /// an additional level of scene loading security and/or validation to assure the scene being loaded /// is valid scene to be loaded in the LoadSceneMode specified. /// /// Build Settings Scenes in Build List index of the scene /// Name of the scene /// LoadSceneMode the scene is going to be loaded /// true (valid) or false (not valid) public delegate bool VerifySceneBeforeLoadingDelegateHandler(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode); /// /// Delegate handler defined by that is invoked before the /// server or client loads a scene during an active netcode game session.
/// Client Side: In order for clients to be notified of this condition you must assign the delegate handler.
/// Server Side: will return . ///
public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading; /// /// The SceneManagerHandler implementation /// internal ISceneManagerHandler SceneManagerHandler = new DefaultSceneManagerHandler(); /// /// The default SceneManagerHandler that interfaces between the SceneManager and NetworkSceneManager /// private class DefaultSceneManagerHandler : ISceneManagerHandler { public AsyncOperation LoadSceneAsync(string sceneName, LoadSceneMode loadSceneMode, SceneEventProgress sceneEventProgress) { var operation = SceneManager.LoadSceneAsync(sceneName, loadSceneMode); sceneEventProgress.SetAsyncOperation(operation); return operation; } public AsyncOperation UnloadSceneAsync(Scene scene, SceneEventProgress sceneEventProgress) { var operation = SceneManager.UnloadSceneAsync(scene); sceneEventProgress.SetAsyncOperation(operation); return operation; } } internal readonly Dictionary SceneEventProgressTracking = new Dictionary(); /// /// Used to track in-scene placed NetworkObjects /// We store them by: /// [GlobalObjectIdHash][Scene.Handle][NetworkObject] /// The Scene.Handle aspect allows us to distinguish duplicated in-scene placed NetworkObjects created by the loading /// of the same additive scene multiple times. /// internal readonly Dictionary> ScenePlacedObjects = new Dictionary>(); /// /// This is used for the deserialization of in-scene placed NetworkObjects in order to distinguish duplicated in-scene /// placed NetworkObjects created by the loading of the same additive scene multiple times. /// internal Scene SceneBeingSynchronized; /// /// Used to track which scenes are currently loaded /// We store the scenes as [SceneHandle][Scene] in order to handle the loading and unloading of the same scene additively /// Scene handle is only unique locally. So, clients depend upon the in order /// to be able to know which specific scene instance the server is instructing the client to unload. /// The client links the server scene handle to the client local scene handle upon a scene being loaded /// /// internal Dictionary ScenesLoaded = new Dictionary(); /// /// Since Scene.handle is unique per client, we create a look-up table between the client and server to associate server unique scene /// instances with client unique scene instances /// internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary(); /// /// Hash to build index lookup table /// internal Dictionary HashToBuildIndex = new Dictionary(); /// /// Build index to hash lookup table /// internal Dictionary BuildIndexToHash = new Dictionary(); /// /// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned /// they need to be moved into the do not destroy temporary scene /// When it is set: Just before starting the asynchronous loading call /// When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do /// not destroy temporary scene are moved into the active scene /// internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad; /// /// Client and Server: /// Used for all scene event processing /// internal Dictionary SceneEventDataStore; private NetworkManager m_NetworkManager { get; } internal Scene DontDestroyOnLoadScene; /// /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and /// the server's currently active scene will be loaded in single mode on the client /// unless it was already loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the /// method. ///
public LoadSceneMode ClientSynchronizationMode { get; internal set; } /// /// When true, the messages will be turned off /// private bool m_DisableValidationWarningMessages; /// /// Handle NetworkSeneManager clean up /// public void Dispose() { SceneUnloadEventHandler.Shutdown(); foreach (var keypair in SceneEventDataStore) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'."); } keypair.Value.Dispose(); } SceneEventDataStore.Clear(); SceneEventDataStore = null; } /// /// Creates a new SceneEventData object for a new scene event /// /// SceneEventData instance internal SceneEventData BeginSceneEvent() { var sceneEventData = new SceneEventData(m_NetworkManager); SceneEventDataStore.Add(sceneEventData.SceneEventId, sceneEventData); return sceneEventData; } /// /// Disposes and removes SceneEventData object for the scene event /// /// SceneEventId to end internal void EndSceneEvent(uint sceneEventId) { if (SceneEventDataStore.ContainsKey(sceneEventId)) { SceneEventDataStore[sceneEventId].Dispose(); SceneEventDataStore.Remove(sceneEventId); } else { Debug.LogWarning($"Trying to dispose and remove SceneEventData Id '{sceneEventId}' that no longer exists!"); } } /// /// Gets the scene name from full path to the scene /// internal string GetSceneNameFromPath(string scenePath) { var begin = scenePath.LastIndexOf("/", StringComparison.Ordinal) + 1; var end = scenePath.LastIndexOf(".", StringComparison.Ordinal); return scenePath.Substring(begin, end - begin); } /// /// Generates the hash values and associated tables /// for the scenes in build list /// internal void GenerateScenesInBuild() { HashToBuildIndex.Clear(); BuildIndexToHash.Clear(); for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++) { var scenePath = SceneUtility.GetScenePathByBuildIndex(i); var hash = XXHash.Hash32(scenePath); var buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath); // In the rare-case scenario where a programmatically generated build has duplicate // scene entries, we will log an error and skip the entry if (!HashToBuildIndex.ContainsKey(hash)) { HashToBuildIndex.Add(hash, buildIndex); BuildIndexToHash.Add(buildIndex, hash); } else { Debug.LogError($"{nameof(NetworkSceneManager)} is skipping duplicate scene path entry {scenePath}. Make sure your scenes in build list does not contain duplicates!"); } } } /// /// Gets the scene name from a hash value generated from the full scene path /// internal string SceneNameFromHash(uint sceneHash) { // In the event there is no scene associated with the scene event then just return "No Scene" // This can happen during unit tests when clients first connect and the only scene loaded is the // unit test scene (which is ignored by default) that results in a scene event that has no associated // scene. Under this specific special case, we just return "No Scene". if (sceneHash == 0) { return "No Scene"; } return GetSceneNameFromPath(ScenePathFromHash(sceneHash)); } /// /// Gets the full scene path from a hash value /// internal string ScenePathFromHash(uint sceneHash) { if (HashToBuildIndex.ContainsKey(sceneHash)) { return SceneUtility.GetScenePathByBuildIndex(HashToBuildIndex[sceneHash]); } else { throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table! Verify that all scenes requiring" + $" server to client synchronization are in the scenes in build list."); } } /// /// Gets the associated hash value for the scene name or path /// internal uint SceneHashFromNameOrPath(string sceneNameOrPath) { var buildIndex = SceneUtility.GetBuildIndexByScenePath(sceneNameOrPath); if (buildIndex >= 0) { if (BuildIndexToHash.ContainsKey(buildIndex)) { return BuildIndexToHash[buildIndex]; } else { throw new Exception($"Scene '{sceneNameOrPath}' has a build index of {buildIndex} that does not exist in the {nameof(BuildIndexToHash)} table!"); } } else { throw new Exception($"Scene '{sceneNameOrPath}' couldn't be loaded because it has not been added to the build settings scenes in build list."); } } /// /// When set to true, this will disable the console warnings about /// a scene being invalidated. /// /// true/false public void DisableValidationWarnings(bool disabled) { m_DisableValidationWarningMessages = disabled; } /// /// This will change how clients are initially synchronized.
/// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and /// the server's currently active scene will be loaded in single mode on the client /// unless it was already loaded.
/// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the /// method. ///
/// for initial client synchronization public void SetClientSynchronizationMode(LoadSceneMode mode) { ClientSynchronizationMode = mode; } /// /// Constructor /// /// one instance per instance /// maximum pool size internal NetworkSceneManager(NetworkManager networkManager) { m_NetworkManager = networkManager; SceneEventDataStore = new Dictionary(); GenerateScenesInBuild(); // Since NetworkManager is now always migrated to the DDOL we will use this to get the DDOL scene DontDestroyOnLoadScene = networkManager.gameObject.scene; ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle); ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene); } /// /// If the VerifySceneBeforeLoading delegate handler has been set by the user, this will provide /// an additional level of security and/or validation that the scene being loaded in the specified /// loading mode is "a valid scene to be loaded in the LoadSceneMode specified". /// /// index into ScenesInBuild /// LoadSceneMode the scene is going to be loaded /// true (Valid) or false (Invalid) internal bool ValidateSceneBeforeLoading(uint sceneHash, LoadSceneMode loadSceneMode) { var validated = true; var sceneName = SceneNameFromHash(sceneHash); var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName); if (VerifySceneBeforeLoading != null) { validated = VerifySceneBeforeLoading.Invoke((int)sceneIndex, sceneName, loadSceneMode); } if (!validated && !m_DisableValidationWarningMessages) { var serverHostorClient = "Client"; if (m_NetworkManager.IsServer) { serverHostorClient = m_NetworkManager.IsHost ? "Host" : "Server"; } Debug.LogWarning($"Scene {sceneName} of Scenes in Build Index {sceneIndex} being loaded in {loadSceneMode} mode failed validation on the {serverHostorClient}!"); } return validated; } /// /// Used for NetcodeIntegrationTest testing in order to properly /// assign the right loaded scene to the right client's ScenesLoaded list /// internal Func OverrideGetAndAddNewlyLoadedSceneByName; /// /// Since SceneManager.GetSceneByName only returns the first scene that matches the name /// we must "find" a newly added scene by looking through all loaded scenes and determining /// which scene with the same name has not yet been loaded. /// In order to support loading the same additive scene within in-scene placed NetworkObjects, /// we must do this to be able to soft synchronize the "right version" of the NetworkObject. /// /// /// internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName) { if (OverrideGetAndAddNewlyLoadedSceneByName != null) { return OverrideGetAndAddNewlyLoadedSceneByName.Invoke(sceneName); } else { for (int i = 0; i < SceneManager.sceneCount; i++) { var sceneLoaded = SceneManager.GetSceneAt(i); if (sceneLoaded.name == sceneName) { if (!ScenesLoaded.ContainsKey(sceneLoaded.handle)) { ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded); return sceneLoaded; } } } throw new Exception($"Failed to find any loaded scene named {sceneName}!"); } } /// /// Client Side Only: /// This takes a server scene handle that is written by the server before the scene relative /// NetworkObject is serialized and converts the server scene handle to a local client handle /// so it can set the appropriate SceneBeingSynchronized. /// Note: This is now part of the soft synchronization process and is needed for the scenario /// where a user loads the same scene additively that has an in-scene placed NetworkObject /// which means each scene relative in-scene placed NetworkObject will have the identical GlobalObjectIdHash /// value. Scene handles are used to distinguish between in-scene placed NetworkObjects under this situation. /// /// internal void SetTheSceneBeingSynchronized(int serverSceneHandle) { var clientSceneHandle = serverSceneHandle; if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverSceneHandle)) { clientSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle]; // If we were already set, then ignore if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == clientSceneHandle) { return; } // Get the scene currently being synchronized SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene(); if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded) { // Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of SceneBeingSynchronized = SceneManager.GetActiveScene(); // Keeping the warning here in the event we cannot find the scene being synchronized Debug.LogWarning($"[{nameof(NetworkSceneManager)}- {nameof(ScenesLoaded)}] Could not find the appropriate scene to set as being synchronized! Using the currently active scene."); } } else { // Most common scenario for DontDestroyOnLoad is when NetworkManager is set to not be destroyed if (serverSceneHandle == DontDestroyOnLoadScene.handle) { SceneBeingSynchronized = m_NetworkManager.gameObject.scene; return; } else { // Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of // or the NetworkObject has yet to be moved to that specific scene (i.e. no DontDestroyOnLoad scene exists yet). SceneBeingSynchronized = SceneManager.GetActiveScene(); // This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene // Otherwise, this is some other specific scenario that we might not be handling currently. Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene."); } } } /// /// During soft synchronization of in-scene placed NetworkObjects, this is now used by NetworkSpawnManager.CreateLocalNetworkObject /// /// /// internal NetworkObject GetSceneRelativeInSceneNetworkObject(uint globalObjectIdHash, int? networkSceneHandle) { if (ScenePlacedObjects.ContainsKey(globalObjectIdHash)) { var sceneHandle = SceneBeingSynchronized.handle; if (networkSceneHandle.HasValue && networkSceneHandle.Value != 0) { sceneHandle = ServerSceneHandleToClientSceneHandle[networkSceneHandle.Value]; } if (ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle)) { return ScenePlacedObjects[globalObjectIdHash][sceneHandle]; } } return null; } /// /// Generic sending of scene event data /// /// array of client identifiers to receive the scene event message private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds) { if (targetClientIds.Length == 0) { // This would be the Host/Server with no clients connected // Silently return as there is nothing to be done return; } var message = new SceneEventMessage { EventData = SceneEventDataStore[sceneEventId] }; var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, targetClientIds); m_NetworkManager.NetworkMetrics.TrackSceneEventSent(targetClientIds, (uint)SceneEventDataStore[sceneEventId].SceneEventType, SceneNameFromHash(SceneEventDataStore[sceneEventId].SceneHash), size); } /// /// Entry method for scene unloading validation /// /// the scene to be unloaded /// private SceneEventProgress ValidateSceneEventUnLoading(Scene scene) { if (!m_NetworkManager.IsServer) { throw new NotServerException("Only server can start a scene event!"); } if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) { //Log message about enabling SceneManagement throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); } if (!scene.isLoaded) { Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); return new SceneEventProgress(null, SceneEventProgressStatus.SceneNotLoaded); } return ValidateSceneEvent(scene.name, true); } /// /// Entry method for scene loading validation /// /// scene name to load /// private SceneEventProgress ValidateSceneEventLoading(string sceneName) { if (!m_NetworkManager.IsServer) { throw new NotServerException("Only server can start a scene event!"); } if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) { //Log message about enabling SceneManagement throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); } return ValidateSceneEvent(sceneName); } /// /// Validates the new scene event request by the server-side code. /// This also initializes some commonly shared values as well as SceneEventProgress /// /// /// that should have a of otherwise it failed. private SceneEventProgress ValidateSceneEvent(string sceneName, bool isUnloading = false) { // Return scene event already in progress if one is already in progress if (m_IsSceneEventActive) { return new SceneEventProgress(null, SceneEventProgressStatus.SceneEventInProgress); } // Return invalid scene name status if the scene name is invalid if (SceneUtility.GetBuildIndexByScenePath(sceneName) == InvalidSceneNameOrPath) { Debug.LogError($"Scene '{sceneName}' couldn't be loaded because it has not been added to the build settings scenes in build list."); return new SceneEventProgress(null, SceneEventProgressStatus.InvalidSceneName); } var sceneEventProgress = new SceneEventProgress(m_NetworkManager) { SceneHash = SceneHashFromNameOrPath(sceneName) }; SceneEventProgressTracking.Add(sceneEventProgress.Guid, sceneEventProgress); if (!isUnloading) { // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned // they need to be moved into the do not destroy temporary scene // When it is set: Just before starting the asynchronous loading call // When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do // not destroy temporary scene are moved into the active scene IsSpawnedObjectsPendingInDontDestroyOnLoad = true; } m_IsSceneEventActive = true; // Set our callback delegate handler for completion sceneEventProgress.OnComplete = OnSceneEventProgressCompleted; return sceneEventProgress; } /// /// Callback for the handler /// /// private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress) { var sceneEventData = BeginSceneEvent(); var clientsThatCompleted = sceneEventProgress.GetClientsWithStatus(true); var clientsThatTimedOut = sceneEventProgress.GetClientsWithStatus(false); sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; sceneEventData.SceneHash = sceneEventProgress.SceneHash; sceneEventData.SceneEventType = sceneEventProgress.SceneEventType; sceneEventData.ClientsCompleted = clientsThatCompleted; sceneEventData.LoadSceneMode = sceneEventProgress.LoadSceneMode; sceneEventData.ClientsTimedOut = clientsThatTimedOut; var message = new SceneEventMessage { EventData = sceneEventData }; var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, m_NetworkManager.ConnectedClientsIds); m_NetworkManager.NetworkMetrics.TrackSceneEventSent( m_NetworkManager.ConnectedClientsIds, (uint)sceneEventProgress.SceneEventType, SceneNameFromHash(sceneEventProgress.SceneHash), size); // Send a local notification to the server that all clients are done loading or unloading OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventProgress.SceneEventType, SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), ClientId = NetworkManager.ServerClientId, LoadSceneMode = sceneEventProgress.LoadSceneMode, ClientsThatCompleted = clientsThatCompleted, ClientsThatTimedOut = clientsThatTimedOut, }); if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) { OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); } else { OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); } EndSceneEvent(sceneEventData.SceneEventId); return true; } /// /// Server Side: /// Unloads an additively loaded scene. If you want to unload a mode loaded scene load another scene. /// When applicable, the is delivered within the via the /// /// /// ( means it was successful) public SceneEventProgressStatus UnloadScene(Scene scene) { var sceneName = scene.name; var sceneHandle = scene.handle; if (!scene.isLoaded) { Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); return SceneEventProgressStatus.SceneNotLoaded; } var sceneEventProgress = ValidateSceneEventUnLoading(scene); if (sceneEventProgress.Status != SceneEventProgressStatus.Started) { return sceneEventProgress.Status; } if (!ScenesLoaded.ContainsKey(sceneHandle)) { Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!"); return SceneEventProgressStatus.InternalNetcodeError; } var sceneEventData = BeginSceneEvent(); sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; sceneEventData.SceneEventType = SceneEventType.Unload; sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName); sceneEventData.LoadSceneMode = LoadSceneMode.Additive; // The only scenes unloaded are scenes that were additively loaded sceneEventData.SceneHandle = sceneHandle; // This will be the message we send to everyone when this scene event sceneEventProgress is complete sceneEventProgress.SceneEventType = SceneEventType.UnloadEventCompleted; ScenesLoaded.Remove(scene.handle); sceneEventProgress.SceneEventId = sceneEventData.SceneEventId; sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; var sceneUnload = SceneManagerHandler.UnloadSceneAsync(scene, sceneEventProgress); // Notify local server that a scene is going to be unloaded OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = sceneUnload, SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = sceneName, ClientId = NetworkManager.ServerClientId // Server can only invoke this }); OnUnload?.Invoke(NetworkManager.ServerClientId, sceneName, sceneUnload); //Return the status return sceneEventProgress.Status; } /// /// Client Side: /// Handles scene events. /// private void OnClientUnloadScene(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneName = SceneNameFromHash(sceneEventData.SceneHash); if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle)) { Debug.Log($"Client failed to unload scene {sceneName} " + $"because we are missing the client scene handle due to the server scene handle {sceneEventData.SceneHandle} not being found."); EndSceneEvent(sceneEventId); return; } var sceneHandle = ServerSceneHandleToClientSceneHandle[sceneEventData.SceneHandle]; if (!ScenesLoaded.ContainsKey(sceneHandle)) { // Error scene handle not found! throw new Exception($"Client failed to unload scene {sceneName} " + $"because the client scene handle {sceneHandle} was not found in ScenesLoaded!"); } m_IsSceneEventActive = true; var sceneEventProgress = new SceneEventProgress(m_NetworkManager); sceneEventProgress.SceneEventId = sceneEventData.SceneEventId; sceneEventProgress.OnSceneEventCompleted = OnSceneUnloaded; var sceneUnload = SceneManagerHandler.UnloadSceneAsync(ScenesLoaded[sceneHandle], sceneEventProgress); ScenesLoaded.Remove(sceneHandle); // Remove our server to scene handle lookup ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle); // Notify the local client that a scene is going to be unloaded OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = sceneUnload, SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded SceneName = sceneName, ClientId = m_NetworkManager.LocalClientId // Server sent this message to the client, but client is executing it }); OnUnload?.Invoke(m_NetworkManager.LocalClientId, sceneName, sceneUnload); } /// /// Server and Client: /// Invoked when an additively loaded scene is unloaded /// private void OnSceneUnloaded(uint sceneEventId) { // If we are shutdown or about to shutdown, then ignore this event if (!m_NetworkManager.IsListening || m_NetworkManager.ShutdownInProgress) { return; } var sceneEventData = SceneEventDataStore[sceneEventId]; // First thing we do, if we are a server, is to send the unload scene event. if (m_NetworkManager.IsServer) { // Server sends the unload scene notification after unloading because it will despawn all scene relative in-scene NetworkObjects // If we send this event to all clients before the server is finished unloading they will get warning about an object being // despawned that no longer exists SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray()); //Only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(NetworkManager.ServerClientId); } } // Next we prepare to send local notifications for unload complete sceneEventData.SceneEventType = SceneEventType.UnloadComplete; //Notify the client or server that a scene was unloaded OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = m_NetworkManager.IsServer ? NetworkManager.ServerClientId : m_NetworkManager.LocalClientId }); OnUnloadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash)); // Clients send a notification back to the server they have completed the unload scene event if (!m_NetworkManager.IsServer) { SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); } EndSceneEvent(sceneEventId); // This scene event is now considered "complete" m_IsSceneEventActive = false; } private void EmptySceneUnloadedOperation(uint sceneEventId) { // Do nothing (this is a stub call since it is only used to flush all additively loaded scenes) } /// /// Clears all scenes when loading in single mode /// Since we assume a single mode loaded scene will be considered the "currently active scene", /// we only unload any additively loaded scenes. /// internal void UnloadAdditivelyLoadedScenes(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; // Unload all additive scenes while making sure we don't try to unload the base scene ( loaded in single mode ). var currentActiveScene = SceneManager.GetActiveScene(); foreach (var keyHandleEntry in ScenesLoaded) { // Validate the scene as well as ignore the DDOL (which will have a negative buildIndex) if (currentActiveScene.name != keyHandleEntry.Value.name && keyHandleEntry.Value.buildIndex >= 0) { var sceneEventProgress = new SceneEventProgress(m_NetworkManager); sceneEventProgress.SceneEventId = sceneEventId; sceneEventProgress.OnSceneEventCompleted = EmptySceneUnloadedOperation; var sceneUnload = SceneManagerHandler.UnloadSceneAsync(keyHandleEntry.Value, sceneEventProgress); SceneUnloadEventHandler.RegisterScene(this, keyHandleEntry.Value, LoadSceneMode.Additive, sceneUnload); } } // clear out our scenes loaded list ScenesLoaded.Clear(); } /// /// Server side: /// Loads the scene name in either additive or single loading mode. /// When applicable, the is delivered within the via /// /// the name of the scene to be loaded /// how the scene will be loaded (single or additive mode) /// ( means it was successful) public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSceneMode) { var sceneEventProgress = ValidateSceneEventLoading(sceneName); if (sceneEventProgress.Status != SceneEventProgressStatus.Started) { return sceneEventProgress.Status; } // This will be the message we send to everyone when this scene event sceneEventProgress is complete sceneEventProgress.SceneEventType = SceneEventType.LoadEventCompleted; sceneEventProgress.LoadSceneMode = loadSceneMode; var sceneEventData = BeginSceneEvent(); // Now set up the current scene event sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; sceneEventData.SceneEventType = SceneEventType.Load; sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName); sceneEventData.LoadSceneMode = loadSceneMode; var sceneEventId = sceneEventData.SceneEventId; // This both checks to make sure the scene is valid and if not resets the active scene event m_IsSceneEventActive = ValidateSceneBeforeLoading(sceneEventData.SceneHash, loadSceneMode); if (!m_IsSceneEventActive) { EndSceneEvent(sceneEventId); return SceneEventProgressStatus.SceneFailedVerification; } if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { // Destroy current scene objects before switching. m_NetworkManager.SpawnManager.ServerDestroySpawnedSceneObjects(); // Preserve the objects that should not be destroyed during the scene event MoveObjectsToDontDestroyOnLoad(); // Now Unload all currently additively loaded scenes UnloadAdditivelyLoadedScenes(sceneEventId); // Register the active scene for unload scene event notifications SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single); } // Now start loading the scene sceneEventProgress.SceneEventId = sceneEventId; sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify the local server that a scene loading event has begun OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = sceneLoad, SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = sceneName, ClientId = NetworkManager.ServerClientId }); OnLoad?.Invoke(NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); //Return our scene progress instance return sceneEventProgress.Status; } /// /// Helper class used to handle "odd ball" scene unload event notification scenarios /// when scene switching. /// internal class SceneUnloadEventHandler { private static Dictionary> s_Instances = new Dictionary>(); internal static void RegisterScene(NetworkSceneManager networkSceneManager, Scene scene, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation = null) { var networkManager = networkSceneManager.m_NetworkManager; if (!s_Instances.ContainsKey(networkManager)) { s_Instances.Add(networkManager, new List()); } var clientId = networkManager.IsServer ? NetworkManager.ServerClientId : networkManager.LocalClientId; s_Instances[networkManager].Add(new SceneUnloadEventHandler(networkSceneManager, scene, clientId, loadSceneMode, asyncOperation)); } private static void SceneUnloadComplete(SceneUnloadEventHandler sceneUnloadEventHandler) { if (sceneUnloadEventHandler == null || sceneUnloadEventHandler.m_NetworkSceneManager == null || sceneUnloadEventHandler.m_NetworkSceneManager.m_NetworkManager == null) { return; } var networkManager = sceneUnloadEventHandler.m_NetworkSceneManager.m_NetworkManager; if (s_Instances.ContainsKey(networkManager)) { s_Instances[networkManager].Remove(sceneUnloadEventHandler); if (s_Instances[networkManager].Count == 0) { s_Instances.Remove(networkManager); } } } /// /// Called by NetworkSceneManager when it is disposing /// internal static void Shutdown() { foreach (var instanceEntry in s_Instances) { foreach (var instance in instanceEntry.Value) { instance.OnShutdown(); } instanceEntry.Value.Clear(); } s_Instances.Clear(); } private NetworkSceneManager m_NetworkSceneManager; private AsyncOperation m_AsyncOperation; private LoadSceneMode m_LoadSceneMode; private ulong m_ClientId; private Scene m_Scene; private bool m_ShuttingDown; private void OnShutdown() { m_ShuttingDown = true; SceneManager.sceneUnloaded -= SceneUnloaded; } private void SceneUnloaded(Scene scene) { if (m_Scene.handle == scene.handle && !m_ShuttingDown) { if (m_NetworkSceneManager != null && m_NetworkSceneManager.m_NetworkManager != null) { m_NetworkSceneManager.OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = m_AsyncOperation, SceneEventType = SceneEventType.UnloadComplete, SceneName = m_Scene.name, LoadSceneMode = m_LoadSceneMode, ClientId = m_ClientId }); m_NetworkSceneManager.OnUnloadComplete?.Invoke(m_ClientId, m_Scene.name); } SceneManager.sceneUnloaded -= SceneUnloaded; SceneUnloadComplete(this); } } private SceneUnloadEventHandler(NetworkSceneManager networkSceneManager, Scene scene, ulong clientId, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation = null) { m_LoadSceneMode = loadSceneMode; m_AsyncOperation = asyncOperation; m_NetworkSceneManager = networkSceneManager; m_ClientId = clientId; m_Scene = scene; SceneManager.sceneUnloaded += SceneUnloaded; // Send the initial unload event notification m_NetworkSceneManager.OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = m_AsyncOperation, SceneEventType = SceneEventType.Unload, SceneName = m_Scene.name, LoadSceneMode = m_LoadSceneMode, ClientId = clientId }); m_NetworkSceneManager.OnUnload?.Invoke(networkSceneManager.m_NetworkManager.LocalClientId, m_Scene.name, null); } } /// /// Client Side: /// Handles both forms of scene loading /// /// Stream data associated with the event private void OnClientSceneLoadingEvent(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneName = SceneNameFromHash(sceneEventData.SceneHash); // Run scene validation before loading a scene if (!ValidateSceneBeforeLoading(sceneEventData.SceneHash, sceneEventData.LoadSceneMode)) { EndSceneEvent(sceneEventId); return; } if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { // Move ALL NetworkObjects to the temp scene MoveObjectsToDontDestroyOnLoad(); // Now Unload all currently additively loaded scenes UnloadAdditivelyLoadedScenes(sceneEventData.SceneEventId); } // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned // they need to be moved into the do not destroy temporary scene // When it is set: Just before starting the asynchronous loading call // When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do // not destroy temporary scene are moved into the active scene if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { IsSpawnedObjectsPendingInDontDestroyOnLoad = true; // Register the active scene for unload scene event notifications SceneUnloadEventHandler.RegisterScene(this, SceneManager.GetActiveScene(), LoadSceneMode.Single); } var sceneEventProgress = new SceneEventProgress(m_NetworkManager); sceneEventProgress.SceneEventId = sceneEventId; sceneEventProgress.OnSceneEventCompleted = OnSceneLoaded; var sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode, sceneEventProgress); OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = sceneLoad, SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = sceneName, ClientId = m_NetworkManager.LocalClientId }); OnLoad?.Invoke(m_NetworkManager.LocalClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); } /// /// Client and Server: /// Generic on scene loaded callback method to be called upon a scene loading /// private void OnSceneLoaded(uint sceneEventId) { // If we are shutdown or about to shutdown, then ignore this event if (!m_NetworkManager.IsListening || m_NetworkManager.ShutdownInProgress) { return; } var sceneEventData = SceneEventDataStore[sceneEventId]; var nextScene = GetAndAddNewlyLoadedSceneByName(SceneNameFromHash(sceneEventData.SceneHash)); if (!nextScene.isLoaded || !nextScene.IsValid()) { throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!"); } if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { SceneManager.SetActiveScene(nextScene); } //Get all NetworkObjects loaded by the scene PopulateScenePlacedObjects(nextScene); if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { // Move all objects to the new scene MoveObjectsFromDontDestroyOnLoadToScene(nextScene); } // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned // they need to be moved into the do not destroy temporary scene // When it is set: Just before starting the asynchronous loading call // When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do // not destroy temporary scene are moved into the active scene IsSpawnedObjectsPendingInDontDestroyOnLoad = false; if (m_NetworkManager.IsServer) { OnServerLoadedScene(sceneEventId, nextScene); } else { // For the client, we make a server scene handle to client scene handle look up table if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle)) { ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle); } else { // If the exact same handle exists then there are problems with using handles throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})"); } OnClientLoadedScene(sceneEventId, nextScene); } } /// /// Server side: /// On scene loaded callback method invoked by OnSceneLoading only /// private void OnServerLoadedScene(uint sceneEventId, Scene scene) { var sceneEventData = SceneEventDataStore[sceneEventId]; // Register in-scene placed NetworkObjects with spawn manager foreach (var keyValuePairByGlobalObjectIdHash in ScenePlacedObjects) { foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) { if (!keyValuePairBySceneHandle.Value.IsPlayerObject) { // All in-scene placed NetworkObjects default to being owned by the server m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, NetworkManager.ServerClientId, true); } } } // Add any despawned when spawned in-scene placed NetworkObjects to the scene event data sceneEventData.AddDespawnedInSceneNetworkObjects(); // Set the server's scene's handle so the client can build a look up table sceneEventData.SceneHandle = scene.handle; // Send all clients the scene load event for (int j = 0; j < m_NetworkManager.ConnectedClientsList.Count; j++) { var clientId = m_NetworkManager.ConnectedClientsList[j].ClientId; if (clientId != NetworkManager.ServerClientId) { sceneEventData.TargetClientId = clientId; var message = new SceneEventMessage { EventData = sceneEventData }; var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, clientId); m_NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEventData.SceneEventType, scene.name, size); } } m_IsSceneEventActive = false; //First, notify local server that the scene was loaded OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = SceneEventType.LoadComplete, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = NetworkManager.ServerClientId, Scene = scene, }); OnLoadComplete?.Invoke(NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); //Second, only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(NetworkManager.ServerClientId); } EndSceneEvent(sceneEventId); } /// /// Client side: /// On scene loaded callback method invoked by OnSceneLoading only /// private void OnClientLoadedScene(uint sceneEventId, Scene scene) { var sceneEventData = SceneEventDataStore[sceneEventId]; sceneEventData.DeserializeScenePlacedObjects(); sceneEventData.SceneEventType = SceneEventType.LoadComplete; SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); m_IsSceneEventActive = false; // Notify local client that the scene was loaded OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = SceneEventType.LoadComplete, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = m_NetworkManager.LocalClientId, Scene = scene, }); OnLoadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); EndSceneEvent(sceneEventId); } /// /// Used for integration testing, due to the complexities of having all clients loading scenes /// this is needed to "filter" out the scenes not loaded by NetworkSceneManager /// (i.e. we don't want a late joining player to load all of the other client scenes) /// internal Func ExcludeSceneFromSychronization; /// /// Server Side: /// This is used for players that have just had their connection approved and will assure they are synchronized /// properly if they are late joining /// Note: We write out all of the scenes to be loaded first and then all of the NetworkObjects that need to be /// synchronized. /// /// newly joined client identifier internal void SynchronizeNetworkObjects(ulong clientId) { // Update the clients m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId); var sceneEventData = BeginSceneEvent(); sceneEventData.InitializeForSynch(); sceneEventData.TargetClientId = clientId; sceneEventData.LoadSceneMode = ClientSynchronizationMode; var activeScene = SceneManager.GetActiveScene(); sceneEventData.SceneEventType = SceneEventType.Synchronize; // Organize how (and when) we serialize our NetworkObjects for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); // NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager // unless the scene in question is the currently active scene. if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene)) { continue; } var sceneHash = SceneHashFromNameOrPath(scene.path); // This would depend upon whether we are additive or not // If we are the base scene, then we set the root scene index; if (activeScene == scene) { if (!ValidateSceneBeforeLoading(sceneHash, sceneEventData.LoadSceneMode)) { continue; } sceneEventData.SceneHash = sceneHash; sceneEventData.SceneHandle = scene.handle; } else if (!ValidateSceneBeforeLoading(sceneHash, LoadSceneMode.Additive)) { continue; } sceneEventData.AddSceneToSynchronize(sceneHash, scene.handle); } sceneEventData.AddSpawnedNetworkObjects(); sceneEventData.AddDespawnedInSceneNetworkObjects(); var message = new SceneEventMessage { EventData = sceneEventData }; var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, clientId); m_NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEventData.SceneEventType, "", size); // Notify the local server that the client has been sent the synchronize event OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, ClientId = clientId }); OnSynchronize?.Invoke(clientId); EndSceneEvent(sceneEventData.SceneEventId); } /// /// This is called when the client receives the event /// Note: This can recurse one additional time by the client if the current scene loaded by the client /// is already loaded. /// private void OnClientBeginSync(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneHash = sceneEventData.GetNextSceneSynchronizationHash(); var sceneHandle = sceneEventData.GetNextSceneSynchronizationHandle(); var sceneName = SceneNameFromHash(sceneHash); var activeScene = SceneManager.GetActiveScene(); var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; // Store the sceneHandle and hash sceneEventData.NetworkSceneHandle = sceneHandle; sceneEventData.ClientSceneHash = sceneHash; // If this is the beginning of the synchronization event, then send client a notification that synchronization has begun if (sceneHash == sceneEventData.SceneHash) { OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = SceneEventType.Synchronize, ClientId = m_NetworkManager.LocalClientId, }); OnSynchronize?.Invoke(m_NetworkManager.LocalClientId); // Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process ScenePlacedObjects.Clear(); } // Always check to see if the scene needs to be validated if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode)) { HandleClientSceneEvent(sceneEventId); if (m_NetworkManager.LogLevel == LogLevel.Developer) { NetworkLog.LogInfo($"Client declined to load the scene {sceneName}, continuing with synchronization."); } return; } var shouldPassThrough = false; var sceneLoad = (AsyncOperation)null; // Check to see if the client already has loaded the scene to be loaded if (sceneName == activeScene.name) { // If the client is already in the same scene, then pass through and // don't try to reload it. shouldPassThrough = true; } if (!shouldPassThrough) { // If not, then load the scene var sceneEventProgress = new SceneEventProgress(m_NetworkManager); sceneEventProgress.SceneEventId = sceneEventId; sceneEventProgress.OnSceneEventCompleted = ClientLoadedSynchronization; sceneLoad = SceneManagerHandler.LoadSceneAsync(sceneName, loadSceneMode, sceneEventProgress); // Notify local client that a scene load has begun OnSceneEvent?.Invoke(new SceneEvent() { AsyncOperation = sceneLoad, SceneEventType = SceneEventType.Load, LoadSceneMode = loadSceneMode, SceneName = sceneName, ClientId = m_NetworkManager.LocalClientId, }); OnLoad?.Invoke(m_NetworkManager.LocalClientId, sceneName, loadSceneMode, sceneLoad); } else { // If so, then pass through ClientLoadedSynchronization(sceneEventId); } } /// /// Once a scene is loaded ( or if it was already loaded) this gets called. /// This handles all of the in-scene and dynamically spawned NetworkObject synchronization /// /// Netcode scene index that was loaded private void ClientLoadedSynchronization(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash); var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName); if (!nextScene.isLoaded || !nextScene.IsValid()) { throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!"); } var loadSceneMode = (sceneEventData.ClientSceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive); // For now, during a synchronization event, we will make the first scene the "base/master" scene that denotes a "complete scene switch" if (loadSceneMode == LoadSceneMode.Single) { SceneManager.SetActiveScene(nextScene); } if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle)) { ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle); } else { // If the exact same handle exists then there are problems with using handles throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})"); } // Apply all in-scene placed NetworkObjects loaded by the scene PopulateScenePlacedObjects(nextScene, false); // Send notification back to server that we finished loading this scene var responseSceneEventData = BeginSceneEvent(); responseSceneEventData.LoadSceneMode = loadSceneMode; responseSceneEventData.SceneEventType = SceneEventType.LoadComplete; responseSceneEventData.SceneHash = sceneEventData.ClientSceneHash; var message = new SceneEventMessage { EventData = responseSceneEventData }; var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); m_NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size); EndSceneEvent(responseSceneEventData.SceneEventId); // Send notification to local client that the scene has finished loading OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = SceneEventType.LoadComplete, LoadSceneMode = loadSceneMode, SceneName = sceneName, Scene = nextScene, ClientId = m_NetworkManager.LocalClientId, }); OnLoadComplete?.Invoke(m_NetworkManager.LocalClientId, sceneName, loadSceneMode); // Check to see if we still have scenes to load and synchronize with HandleClientSceneEvent(sceneEventId); } /// /// Client Side: /// Handles incoming Scene_Event messages for clients /// /// data associated with the event private void HandleClientSceneEvent(uint sceneEventId) { var sceneEventData = SceneEventDataStore[sceneEventId]; switch (sceneEventData.SceneEventType) { case SceneEventType.Load: { OnClientSceneLoadingEvent(sceneEventId); break; } case SceneEventType.Unload: { OnClientUnloadScene(sceneEventId); break; } case SceneEventType.Synchronize: { if (!sceneEventData.IsDoneWithSynchronization()) { OnClientBeginSync(sceneEventId); } else { // Include anything in the DDOL scene PopulateScenePlacedObjects(DontDestroyOnLoadScene, false); // Synchronize the NetworkObjects for this scene sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager); sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete; SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); // All scenes are synchronized, let the server know we are done synchronizing m_NetworkManager.IsConnectedClient = true; // Client is now synchronized and fully "connected". This also means the client can send "RPCs" at this time m_NetworkManager.InvokeOnClientConnectedCallback(m_NetworkManager.LocalClientId); // Notify the client that they have finished synchronizing OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, ClientId = m_NetworkManager.LocalClientId, // Client sent this to the server }); OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId); EndSceneEvent(sceneEventId); } break; } case SceneEventType.ReSynchronize: { // Notify the local client that they have been re-synchronized after being synchronized with an in progress game session OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, ClientId = NetworkManager.ServerClientId, // Server sent this to client }); EndSceneEvent(sceneEventId); break; } case SceneEventType.LoadEventCompleted: case SceneEventType.UnloadEventCompleted: { // Notify the local client that all clients have finished loading or unloading OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = NetworkManager.ServerClientId, ClientsThatCompleted = sceneEventData.ClientsCompleted, ClientsThatTimedOut = sceneEventData.ClientsTimedOut, }); if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) { OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); } else { OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); } EndSceneEvent(sceneEventId); break; } default: { Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!"); break; } } } /// /// Server Side: /// Handles incoming Scene_Event messages for host or server /// private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) { var sceneEventData = SceneEventDataStore[sceneEventId]; switch (sceneEventData.SceneEventType) { case SceneEventType.LoadComplete: { // Notify the local server that the client has finished loading a scene OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = clientId }); OnLoadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); } EndSceneEvent(sceneEventId); break; } case SceneEventType.UnloadComplete: { if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].ClientFinishedSceneEvent(clientId); } // Notify the local server that the client has finished unloading a scene OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), ClientId = clientId }); OnUnloadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash)); EndSceneEvent(sceneEventId); break; } case SceneEventType.SynchronizeComplete: { // Notify the local server that a client has finished synchronizing OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, SceneName = string.Empty, ClientId = clientId }); OnSynchronizeComplete?.Invoke(clientId); // We now can call the client connected callback on the server at this time // This assures the client is fully synchronized with all loaded scenes and // NetworkObjects m_NetworkManager.InvokeOnClientConnectedCallback(clientId); // Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid // a potential crash within the MessageSystem (i.e. sending to a client that no longer exists) if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization && m_NetworkManager.ConnectedClients.ContainsKey(clientId)) { sceneEventData.SceneEventType = SceneEventType.ReSynchronize; SendSceneEventData(sceneEventId, new ulong[] { clientId }); OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, SceneName = string.Empty, ClientId = clientId }); } EndSceneEvent(sceneEventId); break; } default: { Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!"); break; } } } /// /// Both Client and Server: Incoming scene event entry point /// /// client who sent the scene event /// data associated with the scene event internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) { if (m_NetworkManager != null) { var sceneEventData = BeginSceneEvent(); sceneEventData.Deserialize(reader); m_NetworkManager.NetworkMetrics.TrackSceneEventReceived( clientId, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), reader.Length); if (sceneEventData.IsSceneEventClientSide()) { HandleClientSceneEvent(sceneEventData.SceneEventId); } else { HandleServerSceneEvent(sceneEventData.SceneEventId, clientId); } } else { Debug.LogError($"{nameof(NetworkSceneManager.HandleSceneEvent)} was invoked but {nameof(NetworkManager)} reference was null!"); } } /// /// Moves all NetworkObjects that don't have the set to /// the "Do not destroy on load" scene. /// internal void MoveObjectsToDontDestroyOnLoad() { // Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList); foreach (var sobj in objectsToKeep) { if (sobj == null) { continue; } if (!sobj.DestroyWithScene || sobj.gameObject.scene == DontDestroyOnLoadScene) { // Only move dynamically spawned network objects with no parent as child objects will follow if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value) { UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject); } } else if (m_NetworkManager.IsServer) { sobj.Despawn(); } } } /// /// Should be invoked on both the client and server side after: /// -- A new scene has been loaded /// -- Before any "DontDestroyOnLoad" NetworkObjects have been added back into the scene. /// Added the ability to choose not to clear the scene placed objects for additive scene loading. /// We organize our ScenePlacedObjects by: /// [GlobalObjectIdHash][SceneHandle][NetworkObject] /// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to /// distinguish between duplicate in-scene placed NetworkObjects /// internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true) { if (clearScenePlacedObjects) { ScenePlacedObjects.Clear(); } #if UNITY_2023_1_OR_NEWER var networkObjects = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.InstanceID); #else var networkObjects = UnityEngine.Object.FindObjectsOfType(); #endif // Just add every NetworkObject found that isn't already in the list // With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value // During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then // at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects foreach (var networkObjectInstance in networkObjects) { var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash; var sceneHandle = networkObjectInstance.GetSceneOriginHandle(); // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes) if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && sceneHandle == sceneToFilterBy.handle) { if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash)) { ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); } if (!ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle)) { ScenePlacedObjects[globalObjectIdHash].Add(sceneHandle, networkObjectInstance); } else { var exitingEntryName = ScenePlacedObjects[globalObjectIdHash][sceneHandle] != null ? ScenePlacedObjects[globalObjectIdHash][sceneHandle].name : "Null Entry"; throw new Exception($"{networkObjectInstance.name} tried to registered with {nameof(ScenePlacedObjects)} which already contains " + $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!"); } } } } /// /// Moves all spawned NetworkObjects (from do not destroy on load) to the scene specified /// /// scene to move the NetworkObjects to internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) { // Move ALL NetworkObjects to the temp scene var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList; foreach (var sobj in objectsToKeep) { if (sobj == null) { continue; } // If it is in the DDOL then if (sobj.gameObject.scene == DontDestroyOnLoadScene) { // only move dynamically spawned network objects, with no parent as child objects will follow, // back into the currently active scene if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value) { SceneManager.MoveGameObjectToScene(sobj.gameObject, scene); } } } } } }