using System.Collections.Generic; using System; using System.IO; using MLAPI.Configuration; using MLAPI.Exceptions; using MLAPI.Logging; using MLAPI.Messaging; using MLAPI.Serialization.Pooled; using MLAPI.Spawning; using UnityEngine; using UnityEngine.SceneManagement; using MLAPI.Messaging.Buffering; using MLAPI.Transports; namespace MLAPI.SceneManagement { /// /// Main class for managing network scenes /// public static class NetworkSceneManager { /// /// Delegate for when the scene has been switched /// public delegate void SceneSwitchedDelegate(); /// /// Delegate for when a scene switch has been initiated /// public delegate void SceneSwitchStartedDelegate(AsyncOperation operation); /// /// Event that is invoked when the scene is switched /// public static event SceneSwitchedDelegate OnSceneSwitched; /// /// Event that is invoked when a local scene switch has started /// public static event SceneSwitchStartedDelegate OnSceneSwitchStarted; internal static readonly HashSet RegisteredSceneNames = new HashSet(); internal static readonly Dictionary SceneNameToIndex = new Dictionary(); internal static readonly Dictionary SceneIndexToString = new Dictionary(); internal static readonly Dictionary SceneSwitchProgresses = new Dictionary(); private static Scene s_LastScene; private static string s_NextSceneName; private static bool s_IsSwitching = false; internal static uint CurrentSceneIndex = 0; internal static Guid CurrentSceneSwitchProgressGuid = new Guid(); internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad = false; internal static void SetCurrentSceneIndex() { if (!SceneNameToIndex.ContainsKey(SceneManager.GetActiveScene().name)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"The current scene ({SceneManager.GetActiveScene().name}) is not regisered as a network scene."); } return; } CurrentSceneIndex = SceneNameToIndex[SceneManager.GetActiveScene().name]; CurrentActiveSceneIndex = CurrentSceneIndex; } internal static uint CurrentActiveSceneIndex { get; private set; } = 0; /// /// Adds a scene during runtime. /// The index is REQUIRED to be unique AND the same across all instances. /// /// Scene name. /// Index. public static void AddRuntimeSceneName(string sceneName, uint index) { if (!NetworkManager.Singleton.NetworkConfig.AllowRuntimeSceneChanges) { throw new NetworkConfigurationException($"Cannot change the scene configuration when {nameof(NetworkConfig.AllowRuntimeSceneChanges)} is false"); } RegisteredSceneNames.Add(sceneName); SceneIndexToString.Add(index, sceneName); SceneNameToIndex.Add(sceneName, index); } /// /// Switches to a scene with a given name. Can only be called from Server /// /// The name of the scene to switch to public static SceneSwitchProgress SwitchScene(string sceneName) { if (!NetworkManager.Singleton.IsServer) { throw new NotServerException("Only server can start a scene switch"); } if (s_IsSwitching) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("Scene switch already in progress"); } return null; } if (!RegisteredSceneNames.Contains(sceneName)) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"The scene {sceneName} is not registered as a switchable scene."); } return null; } NetworkSpawnManager.ServerDestroySpawnedSceneObjects(); //Destroy current scene objects before switching. s_IsSwitching = true; s_LastScene = SceneManager.GetActiveScene(); var switchSceneProgress = new SceneSwitchProgress(); SceneSwitchProgresses.Add(switchSceneProgress.Guid, switchSceneProgress); CurrentSceneSwitchProgressGuid = switchSceneProgress.Guid; // Move ALL NetworkObjects to the temp scene MoveObjectsToDontDestroyOnLoad(); IsSpawnedObjectsPendingInDontDestroyOnLoad = true; // Switch scene AsyncOperation sceneLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single); s_NextSceneName = sceneName; sceneLoad.completed += (AsyncOperation asyncOp2) => { OnSceneLoaded(switchSceneProgress.Guid, null); }; switchSceneProgress.SetSceneLoadOperation(sceneLoad); OnSceneSwitchStarted?.Invoke(sceneLoad); return switchSceneProgress; } // Called on client internal static void OnSceneSwitch(uint sceneIndex, Guid switchSceneGuid, Stream objectStream) { if (!SceneIndexToString.ContainsKey(sceneIndex) || !RegisteredSceneNames.Contains(SceneIndexToString[sceneIndex])) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("Server requested a scene switch to a non-registered scene"); } return; } s_LastScene = SceneManager.GetActiveScene(); // Move ALL NetworkObjects to the temp scene MoveObjectsToDontDestroyOnLoad(); IsSpawnedObjectsPendingInDontDestroyOnLoad = true; string sceneName = SceneIndexToString[sceneIndex]; var sceneLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single); s_NextSceneName = sceneName; sceneLoad.completed += asyncOp2 => OnSceneLoaded(switchSceneGuid, objectStream); OnSceneSwitchStarted?.Invoke(sceneLoad); } internal static void OnFirstSceneSwitchSync(uint sceneIndex, Guid switchSceneGuid) { if (!SceneIndexToString.ContainsKey(sceneIndex) || !RegisteredSceneNames.Contains(SceneIndexToString[sceneIndex])) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("Server requested a scene switch to a non-registered scene"); } return; } if (SceneManager.GetActiveScene().name == SceneIndexToString[sceneIndex]) { return; //This scene is already loaded. This usually happends at first load } s_LastScene = SceneManager.GetActiveScene(); string sceneName = SceneIndexToString[sceneIndex]; s_NextSceneName = sceneName; CurrentActiveSceneIndex = SceneNameToIndex[sceneName]; IsSpawnedObjectsPendingInDontDestroyOnLoad = true; SceneManager.LoadScene(sceneName); using (var buffer = PooledNetworkBuffer.Get()) using (var writer = PooledNetworkWriter.Get(buffer)) { writer.WriteByteArray(switchSceneGuid.ToByteArray()); InternalMessageSender.Send(NetworkManager.Singleton.ServerClientId, NetworkConstants.CLIENT_SWITCH_SCENE_COMPLETED, NetworkChannel.Internal, buffer); } s_IsSwitching = false; } private static void OnSceneLoaded(Guid switchSceneGuid, Stream objectStream) { CurrentActiveSceneIndex = SceneNameToIndex[s_NextSceneName]; var nextScene = SceneManager.GetSceneByName(s_NextSceneName); SceneManager.SetActiveScene(nextScene); // Move all objects to the new scene MoveObjectsToScene(nextScene); IsSpawnedObjectsPendingInDontDestroyOnLoad = false; CurrentSceneIndex = CurrentActiveSceneIndex; if (NetworkManager.Singleton.IsServer) { OnSceneUnloadServer(switchSceneGuid); } else { OnSceneUnloadClient(switchSceneGuid, objectStream); } } private static void OnSceneUnloadServer(Guid switchSceneGuid) { // Justification: Rare alloc, could(should?) reuse var newSceneObjects = new List(); { var networkObjects = MonoBehaviour.FindObjectsOfType(); for (int i = 0; i < networkObjects.Length; i++) { if (networkObjects[i].IsSceneObject == null) { NetworkSpawnManager.SpawnNetworkObjectLocally(networkObjects[i], NetworkSpawnManager.GetNetworkObjectId(), true, false, null, null, false, 0, false, true); newSceneObjects.Add(networkObjects[i]); } } } for (int j = 0; j < NetworkManager.Singleton.ConnectedClientsList.Count; j++) { if (NetworkManager.Singleton.ConnectedClientsList[j].ClientId != NetworkManager.Singleton.ServerClientId) { using (var buffer = PooledNetworkBuffer.Get()) using (var writer = PooledNetworkWriter.Get(buffer)) { writer.WriteUInt32Packed(CurrentActiveSceneIndex); writer.WriteByteArray(switchSceneGuid.ToByteArray()); uint sceneObjectsToSpawn = 0; for (int i = 0; i < newSceneObjects.Count; i++) { if (newSceneObjects[i].m_Observers.Contains(NetworkManager.Singleton.ConnectedClientsList[j].ClientId)) { sceneObjectsToSpawn++; } } writer.WriteUInt32Packed(sceneObjectsToSpawn); for (int i = 0; i < newSceneObjects.Count; i++) { if (newSceneObjects[i].m_Observers.Contains(NetworkManager.Singleton.ConnectedClientsList[j].ClientId)) { writer.WriteBool(newSceneObjects[i].IsPlayerObject); writer.WriteUInt64Packed(newSceneObjects[i].NetworkObjectId); writer.WriteUInt64Packed(newSceneObjects[i].OwnerClientId); NetworkObject parentNetworkObject = null; if (!newSceneObjects[i].AlwaysReplicateAsRoot && newSceneObjects[i].transform.parent != null) { parentNetworkObject = newSceneObjects[i].transform.parent.GetComponent(); } if (parentNetworkObject == null) { writer.WriteBool(false); } else { writer.WriteBool(true); writer.WriteUInt64Packed(parentNetworkObject.NetworkObjectId); } if (!NetworkManager.Singleton.NetworkConfig.EnableSceneManagement || NetworkManager.Singleton.NetworkConfig.UsePrefabSync) { writer.WriteUInt64Packed(newSceneObjects[i].PrefabHash); writer.WriteSinglePacked(newSceneObjects[i].transform.position.x); writer.WriteSinglePacked(newSceneObjects[i].transform.position.y); writer.WriteSinglePacked(newSceneObjects[i].transform.position.z); writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.x); writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.y); writer.WriteSinglePacked(newSceneObjects[i].transform.rotation.eulerAngles.z); } else { writer.WriteUInt64Packed(newSceneObjects[i].NetworkInstanceId); } if (NetworkManager.Singleton.NetworkConfig.EnableNetworkVariable) { newSceneObjects[i].WriteNetworkVariableData(buffer, NetworkManager.Singleton.ConnectedClientsList[j].ClientId); } } } InternalMessageSender.Send(NetworkManager.Singleton.ConnectedClientsList[j].ClientId, NetworkConstants.SWITCH_SCENE, NetworkChannel.Internal, buffer); } } } //Tell server that scene load is completed if (NetworkManager.Singleton.IsHost) { OnClientSwitchSceneCompleted(NetworkManager.Singleton.LocalClientId, switchSceneGuid); } s_IsSwitching = false; OnSceneSwitched?.Invoke(); } private static void OnSceneUnloadClient(Guid switchSceneGuid, Stream objectStream) { if (!NetworkManager.Singleton.NetworkConfig.EnableSceneManagement || NetworkManager.Singleton.NetworkConfig.UsePrefabSync) { NetworkSpawnManager.DestroySceneObjects(); using (var reader = PooledNetworkReader.Get(objectStream)) { uint newObjectsCount = reader.ReadUInt32Packed(); for (int i = 0; i < newObjectsCount; i++) { bool isPlayerObject = reader.ReadBool(); ulong networkId = reader.ReadUInt64Packed(); ulong owner = reader.ReadUInt64Packed(); bool hasParent = reader.ReadBool(); ulong? parentNetworkId = null; if (hasParent) { parentNetworkId = reader.ReadUInt64Packed(); } ulong prefabHash = reader.ReadUInt64Packed(); Vector3? position = null; Quaternion? rotation = null; if (reader.ReadBool()) { position = new Vector3(reader.ReadSinglePacked(), reader.ReadSinglePacked(), reader.ReadSinglePacked()); rotation = Quaternion.Euler(reader.ReadSinglePacked(), reader.ReadSinglePacked(), reader.ReadSinglePacked()); } var networkObject = NetworkSpawnManager.CreateLocalNetworkObject(false, 0, prefabHash, parentNetworkId, position, rotation); NetworkSpawnManager.SpawnNetworkObjectLocally(networkObject, networkId, true, isPlayerObject, owner, objectStream, false, 0, true, false); var bufferQueue = BufferManager.ConsumeBuffersForNetworkId(networkId); // Apply buffered messages if (bufferQueue != null) { while (bufferQueue.Count > 0) { BufferManager.BufferedMessage message = bufferQueue.Dequeue(); NetworkManager.Singleton.HandleIncomingData(message.SenderClientId, message.NetworkChannel, new ArraySegment(message.NetworkBuffer.GetBuffer(), (int)message.NetworkBuffer.Position, (int)message.NetworkBuffer.Length), message.ReceiveTime, false); BufferManager.RecycleConsumedBufferedMessage(message); } } } } } else { var networkObjects = MonoBehaviour.FindObjectsOfType(); NetworkSpawnManager.ClientCollectSoftSyncSceneObjectSweep(networkObjects); using (var reader = PooledNetworkReader.Get(objectStream)) { uint newObjectsCount = reader.ReadUInt32Packed(); for (int i = 0; i < newObjectsCount; i++) { bool isPlayerObject = reader.ReadBool(); ulong networkId = reader.ReadUInt64Packed(); ulong owner = reader.ReadUInt64Packed(); bool hasParent = reader.ReadBool(); ulong? parentNetworkId = null; if (hasParent) { parentNetworkId = reader.ReadUInt64Packed(); } ulong instanceId = reader.ReadUInt64Packed(); var networkObject = NetworkSpawnManager.CreateLocalNetworkObject(true, instanceId, 0, parentNetworkId, null, null); NetworkSpawnManager.SpawnNetworkObjectLocally(networkObject, networkId, true, isPlayerObject, owner, objectStream, false, 0, true, false); var bufferQueue = BufferManager.ConsumeBuffersForNetworkId(networkId); // Apply buffered messages if (bufferQueue != null) { while (bufferQueue.Count > 0) { BufferManager.BufferedMessage message = bufferQueue.Dequeue(); NetworkManager.Singleton.HandleIncomingData(message.SenderClientId, message.NetworkChannel, new ArraySegment(message.NetworkBuffer.GetBuffer(), (int)message.NetworkBuffer.Position, (int)message.NetworkBuffer.Length), message.ReceiveTime, false); BufferManager.RecycleConsumedBufferedMessage(message); } } } } } using (var buffer = PooledNetworkBuffer.Get()) using (var writer = PooledNetworkWriter.Get(buffer)) { writer.WriteByteArray(switchSceneGuid.ToByteArray()); InternalMessageSender.Send(NetworkManager.Singleton.ServerClientId, NetworkConstants.CLIENT_SWITCH_SCENE_COMPLETED, NetworkChannel.Internal, buffer); } s_IsSwitching = false; OnSceneSwitched?.Invoke(); } internal static bool HasSceneMismatch(uint sceneIndex) => SceneManager.GetActiveScene().name != SceneIndexToString[sceneIndex]; // Called on server internal static void OnClientSwitchSceneCompleted(ulong clientId, Guid switchSceneGuid) { if (switchSceneGuid == Guid.Empty) { //If Guid is empty it means the client has loaded the start scene of the server and the server would never have a switchSceneProgresses created for the start scene. return; } if (!SceneSwitchProgresses.ContainsKey(switchSceneGuid)) { return; } SceneSwitchProgresses[switchSceneGuid].AddClientAsDone(clientId); } internal static void RemoveClientFromSceneSwitchProgresses(ulong clientId) { foreach (var switchSceneProgress in SceneSwitchProgresses.Values) { switchSceneProgress.RemoveClientAsDone(clientId); } } private static void MoveObjectsToDontDestroyOnLoad() { // Move ALL NetworkObjects to the temp scene var objectsToKeep = NetworkSpawnManager.SpawnedObjectsList; foreach (var sobj in objectsToKeep) { //In case an object has been set as a child of another object it has to be unchilded in order to be moved from one scene to another. if (sobj.gameObject.transform.parent != null) { sobj.gameObject.transform.parent = null; } MonoBehaviour.DontDestroyOnLoad(sobj.gameObject); } } private static void MoveObjectsToScene(Scene scene) { // Move ALL NetworkObjects to the temp scene var objectsToKeep = NetworkSpawnManager.SpawnedObjectsList; foreach (var sobj in objectsToKeep) { //In case an object has been set as a child of another object it has to be unchilded in order to be moved from one scene to another. if (sobj.gameObject.transform.parent != null) { sobj.gameObject.transform.parent = null; } SceneManager.MoveGameObjectToScene(sobj.gameObject, scene); } } } }