您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

300 行
12 KiB

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using AsyncOperation = UnityEngine.AsyncOperation;
namespace Unity.Netcode
{
/// <summary>
/// Used by <see cref="NetworkSceneManager"/> to determine if a server invoked scene event has started.
/// The returned status is stored in the <see cref="SceneEventProgress.Status"/> property.<br/>
/// <em>Note: This was formally known as SwitchSceneProgress which contained the <see cref="AsyncOperation"/>.
/// All <see cref="AsyncOperation"/>s are now delivered by the <see cref="NetworkSceneManager.OnSceneEvent"/> event handler
/// via the <see cref="SceneEvent"/> parameter.</em>
/// </summary>
public enum SceneEventProgressStatus
{
/// <summary>
/// No scene event progress status can be used to initialize a variable that will be checked over time.
/// </summary>
None,
/// <summary>
/// The scene event was successfully started.
/// </summary>
Started,
/// <summary>
/// Returned if you try to unload a scene that was not yet loaded.
/// </summary>
SceneNotLoaded,
/// <summary>
/// Returned if you try to start a new scene event before a previous one is finished.
/// </summary>
SceneEventInProgress,
/// <summary>
/// Returned if the scene name used with <see cref="NetworkSceneManager.LoadScene(string, LoadSceneMode)"/>
/// or <see cref="NetworkSceneManager.UnloadScene(Scene)"/>is invalid.
/// </summary>
InvalidSceneName,
/// <summary>
/// Server side: Returned if the <see cref="NetworkSceneManager.VerifySceneBeforeLoading"/> delegate handler returns false
/// (<em>i.e. scene is considered not valid/safe to load</em>).
/// </summary>
SceneFailedVerification,
/// <summary>
/// This is used for internal error notifications.<br/>
/// If you receive this event then it is most likely due to a bug (<em>please open a GitHub issue with steps to replicate</em>).<br/>
/// </summary>
InternalNetcodeError,
}
/// <summary>
/// Server side only:
/// This tracks the progress of clients during a load or unload scene event
/// </summary>
internal class SceneEventProgress
{
/// <summary>
/// List of clientIds of those clients that is done loading the scene.
/// </summary>
internal Dictionary<ulong, bool> ClientsProcessingSceneEvent { get; } = new Dictionary<ulong, bool>();
internal List<ulong> ClientsThatDisconnected = new List<ulong>();
/// <summary>
/// This is when the current scene event will have timed out
/// </summary>
internal float WhenSceneEventHasTimedOut;
/// <summary>
/// Delegate type for when the switch scene progress is completed. Either by all clients done loading the scene or by time out.
/// </summary>
internal delegate bool OnCompletedDelegate(SceneEventProgress sceneEventProgress);
/// <summary>
/// The callback invoked when the switch scene progress is completed. Either by all clients done loading the scene or by time out.
/// </summary>
internal OnCompletedDelegate OnComplete;
internal Action<uint> OnSceneEventCompleted;
/// <summary>
/// This will make sure that we only have timed out if we never completed
/// </summary>
internal bool HasTimedOut()
{
return WhenSceneEventHasTimedOut <= Time.realtimeSinceStartup;
}
/// <summary>
/// The hash value generated from the full scene path
/// </summary>
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<ulong> GetClientsWithStatus(bool completedSceneEvent)
{
var clients = new List<ulong>();
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;
}
/// <summary>
/// Remove the client from the clients processing the current scene event
/// Add this client to the clients that disconnected list
/// </summary>
private void OnClientDisconnectCallback(ulong clientId)
{
if (ClientsProcessingSceneEvent.ContainsKey(clientId))
{
ClientsThatDisconnected.Add(clientId);
ClientsProcessingSceneEvent.Remove(clientId);
}
}
/// <summary>
/// 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.
/// </summary>
internal IEnumerator TimeOutSceneEventProgress()
{
var waitForNetworkTick = new WaitForSeconds(1.0f / m_NetworkManager.NetworkConfig.TickRate);
while (!HasTimedOut())
{
yield return waitForNetworkTick;
TryFinishingSceneEventProgress();
}
}
/// <summary>
/// Sets the client's scene event progress to finished/true
/// </summary>
internal void ClientFinishedSceneEvent(ulong clientId)
{
if (ClientsProcessingSceneEvent.ContainsKey(clientId))
{
ClientsProcessingSceneEvent[clientId] = true;
TryFinishingSceneEventProgress();
}
}
/// <summary>
/// Determines if the scene event has finished for both
/// client(s) and server.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>
/// Sets the AsyncOperation for the scene load/unload event
/// </summary>
internal void SetAsyncOperation(AsyncOperation asyncOperation)
{
m_AsyncOperation = asyncOperation;
m_AsyncOperation.completed += new Action<AsyncOperation>(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;
}
/// <summary>
/// Will try to finish the current scene event in progress as long as
/// all conditions are met.
/// </summary>
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);
}
}
}
}
}