using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using AsyncOperation = UnityEngine.AsyncOperation; namespace Unity.Netcode { /// /// Used by to determine if a server invoked scene event has started. /// The returned status is stored in the property.
/// Note: This was formally known as SwitchSceneProgress which contained the . /// All s are now delivered by the event handler /// via the parameter. ///
public enum SceneEventProgressStatus { /// /// No scene event progress status can be used to initialize a variable that will be checked over time. /// None, /// /// The scene event was successfully started. /// Started, /// /// Returned if you try to unload a scene that was not yet loaded. /// SceneNotLoaded, /// /// Returned if you try to start a new scene event before a previous one is finished. /// SceneEventInProgress, /// /// Returned if the scene name used with /// or is invalid. /// InvalidSceneName, /// /// Server side: Returned if the delegate handler returns false /// (i.e. scene is considered not valid/safe to load). /// SceneFailedVerification, /// /// This is used for internal error notifications.
/// If you receive this event then it is most likely due to a bug (please open a GitHub issue with steps to replicate).
///
InternalNetcodeError, } /// /// Server side only: /// This tracks the progress of clients during a load or unload scene event /// internal class SceneEventProgress { /// /// List of clientIds of those clients that is done loading the scene. /// internal Dictionary ClientsProcessingSceneEvent { get; } = new Dictionary(); internal List ClientsThatDisconnected = new List(); /// /// This is when the current scene event will have timed out /// internal float WhenSceneEventHasTimedOut; /// /// Delegate type for when the switch scene progress is completed. Either by all clients done loading the scene or by time out. /// internal delegate bool OnCompletedDelegate(SceneEventProgress sceneEventProgress); /// /// The callback invoked when the switch scene progress is completed. Either by all clients done loading the scene or by time out. /// internal OnCompletedDelegate OnComplete; internal Action OnSceneEventCompleted; /// /// This will make sure that we only have timed out if we never completed /// internal bool HasTimedOut() { return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup; } /// /// The hash value generated from the full scene path /// internal uint SceneHash { get; set; } internal Guid Guid { get; } = Guid.NewGuid(); internal uint SceneEventId; private Coroutine m_TimeOutCoroutine; private AsyncOperation m_AsyncOperation; private NetworkManager m_NetworkManager { get; } internal SceneEventProgressStatus Status { get; set; } internal SceneEventType SceneEventType { get; set; } internal LoadSceneMode LoadSceneMode; internal List GetClientsWithStatus(bool completedSceneEvent) { var clients = new List(); if (completedSceneEvent) { // If we are the host, then add the host-client to the list // of clients that completed if the AsyncOperation is done. if (m_NetworkManager.IsHost && m_AsyncOperation.isDone) { clients.Add(m_NetworkManager.LocalClientId); } // Add all clients that completed the scene event foreach (var clientStatus in ClientsProcessingSceneEvent) { if (clientStatus.Value == completedSceneEvent) { clients.Add(clientStatus.Key); } } } else { // If we are the host, then add the host-client to the list // of clients that did not complete if the AsyncOperation is // not done. if (m_NetworkManager.IsHost && !m_AsyncOperation.isDone) { clients.Add(m_NetworkManager.LocalClientId); } // If we are getting the list of clients that have not completed the // scene event, then add any clients that disconnected during this // scene event. clients.AddRange(ClientsThatDisconnected); } return clients; } internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressStatus status = SceneEventProgressStatus.Started) { if (status == SceneEventProgressStatus.Started) { m_NetworkManager = networkManager; if (networkManager.IsServer) { m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback; // Track the clients that were connected when we started this event foreach (var connectedClientId in networkManager.ConnectedClientsIds) { // Ignore the host client if (NetworkManager.ServerClientId == connectedClientId) { continue; } ClientsProcessingSceneEvent.Add(connectedClientId, false); } WhenSceneEventHasTimedOut = Time.realtimeSinceStartup + networkManager.NetworkConfig.LoadSceneTimeOut; m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress()); } } Status = status; } /// /// Remove the client from the clients processing the current scene event /// Add this client to the clients that disconnected list /// private void OnClientDisconnectCallback(ulong clientId) { if (ClientsProcessingSceneEvent.ContainsKey(clientId)) { ClientsThatDisconnected.Add(clientId); ClientsProcessingSceneEvent.Remove(clientId); } } /// /// Coroutine that checks to see if the scene event is complete every network tick period. /// This will handle completing the scene event when one or more client(s) disconnect(s) /// during a scene event and if it does not complete within the scene loading time out period /// it will time out the scene event. /// internal IEnumerator TimeOutSceneEventProgress() { var waitForNetworkTick = new WaitForSeconds(1.0f / m_NetworkManager.NetworkConfig.TickRate); while (!HasTimedOut()) { yield return waitForNetworkTick; TryFinishingSceneEventProgress(); } } /// /// Sets the client's scene event progress to finished/true /// internal void ClientFinishedSceneEvent(ulong clientId) { if (ClientsProcessingSceneEvent.ContainsKey(clientId)) { ClientsProcessingSceneEvent[clientId] = true; TryFinishingSceneEventProgress(); } } /// /// Determines if the scene event has finished for both /// client(s) and server. /// /// /// The server checks if all known clients processing this scene event /// have finished and then it returns its local AsyncOperation status. /// Clients finish when their AsyncOperation finishes. /// private bool HasFinished() { // If the network session is terminated/terminating then finish tracking // this scene event if (!IsNetworkSessionActive()) { return true; } // Clients skip over this foreach (var clientStatus in ClientsProcessingSceneEvent) { if (!clientStatus.Value) { return false; } } // Return the local scene event's AsyncOperation status // Note: Integration tests process scene loading through a queue // and the AsyncOperation could not be assigned for several // network tick periods. Return false if that is the case. return m_AsyncOperation == null ? false : m_AsyncOperation.isDone; } /// /// Sets the AsyncOperation for the scene load/unload event /// internal void SetAsyncOperation(AsyncOperation asyncOperation) { m_AsyncOperation = asyncOperation; m_AsyncOperation.completed += new Action(asyncOp2 => { // Don't invoke the callback if the network session is disconnected // during a SceneEventProgress if (IsNetworkSessionActive()) { OnSceneEventCompleted?.Invoke(SceneEventId); } // Go ahead and try finishing even if the network session is terminated/terminating // as we might need to stop the coroutine TryFinishingSceneEventProgress(); }); } internal bool IsNetworkSessionActive() { return m_NetworkManager != null && m_NetworkManager.IsListening && !m_NetworkManager.ShutdownInProgress; } /// /// Will try to finish the current scene event in progress as long as /// all conditions are met. /// internal void TryFinishingSceneEventProgress() { if (HasFinished() || HasTimedOut()) { // Don't attempt to finalize this scene event if we are no longer listening or a shutdown is in progress if (IsNetworkSessionActive()) { OnComplete?.Invoke(this); m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid); m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnectCallback; } if (m_TimeOutCoroutine != null) { m_NetworkManager.StopCoroutine(m_TimeOutCoroutine); } } } } }