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);
}
}
}
}
}
}