using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if MULTIPLAYER_TOOLS
using Unity.Multiplayer.Tools;
#endif
using Unity.Profiling;
using UnityEngine.SceneManagement;
using System.Runtime.CompilerServices;
using Debug = UnityEngine.Debug;
namespace Unity.Netcode
{
///
/// The main component of the library
///
[AddComponentMenu("Netcode/" + nameof(NetworkManager), -100)]
public class NetworkManager : MonoBehaviour, INetworkUpdateSystem
{
#pragma warning disable IDE1006 // disable naming rule violation check
// RuntimeAccessModifiersILPP will make this `public`
internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters);
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary __rpc_func_table = new Dictionary();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// RuntimeAccessModifiersILPP will make this `public`
internal static readonly Dictionary __rpc_name_table = new Dictionary();
#endif
#pragma warning restore IDE1006 // restore naming rule violation check
#if DEVELOPMENT_BUILD || UNITY_EDITOR
private static ProfilerMarker s_SyncTime = new ProfilerMarker($"{nameof(NetworkManager)}.SyncTime");
private static ProfilerMarker s_TransportPoll = new ProfilerMarker($"{nameof(NetworkManager)}.TransportPoll");
private static ProfilerMarker s_TransportConnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportConnect");
private static ProfilerMarker s_HandleIncomingData = new ProfilerMarker($"{nameof(NetworkManager)}.{nameof(HandleIncomingData)}");
private static ProfilerMarker s_TransportDisconnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportDisconnect");
#endif
private const double k_TimeSyncFrequency = 1.0d; // sync every second
private const float k_DefaultBufferSizeSec = 0.05f; // todo talk with UX/Product, find good default value for this
internal static string PrefabDebugHelper(NetworkPrefab networkPrefab)
{
return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\"";
}
internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; }
internal MessagingSystem MessagingSystem { get; private set; }
private NetworkPrefabHandler m_PrefabHandler;
public NetworkPrefabHandler PrefabHandler
{
get
{
if (m_PrefabHandler == null)
{
m_PrefabHandler = new NetworkPrefabHandler();
}
return m_PrefabHandler;
}
}
private bool m_ShuttingDown;
private bool m_StopProcessingMessages;
private class NetworkManagerHooks : INetworkHooks
{
private NetworkManager m_NetworkManager;
internal NetworkManagerHooks(NetworkManager manager)
{
m_NetworkManager = manager;
}
public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage
{
}
public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage
{
}
public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes)
{
}
public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery)
{
}
public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes)
{
}
public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery)
{
return !m_NetworkManager.m_StopProcessingMessages;
}
public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context)
{
if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) &&
(client.ConnectionState == PendingClient.State.PendingApproval || (client.ConnectionState == PendingClient.State.PendingConnection && messageType != typeof(ConnectionRequestMessage))))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted");
}
return false;
}
return !m_NetworkManager.m_StopProcessingMessages;
}
public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage
{
}
public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage
{
}
}
private class NetworkManagerMessageSender : IMessageSender
{
private NetworkManager m_NetworkManager;
public NetworkManagerMessageSender(NetworkManager manager)
{
m_NetworkManager = manager;
}
public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData)
{
var sendBuffer = batchData.ToTempByteArray();
m_NetworkManager.NetworkConfig.NetworkTransport.Send(m_NetworkManager.ClientIdToTransportId(clientId), sendBuffer, delivery);
}
}
///
/// Returns the to use as the override as could be defined within the NetworkPrefab list
/// Note: This should be used to create pools (with components)
/// under the scenario where you are using the Host model as it spawns everything locally. As such, the override
/// will not be applied when spawning locally on a Host.
/// Related Classes and Interfaces:
///
///
///
/// the to be checked for a defined NetworkPrefab override
/// a that is either the override or if no overrides exist it returns the same as the one passed in as a parameter
public GameObject GetNetworkPrefabOverride(GameObject gameObject)
{
var networkObject = gameObject.GetComponent();
if (networkObject != null)
{
if (NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(networkObject.GlobalObjectIdHash))
{
switch (NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].Override)
{
case NetworkPrefabOverride.Hash:
case NetworkPrefabOverride.Prefab:
{
return NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].OverridingTargetPrefab;
}
}
}
}
return gameObject;
}
public NetworkTimeSystem NetworkTimeSystem { get; private set; }
public NetworkTickSystem NetworkTickSystem { get; private set; }
public NetworkTime LocalTime => NetworkTickSystem?.LocalTime ?? default;
public NetworkTime ServerTime => NetworkTickSystem?.ServerTime ?? default;
///
/// Gets or sets if the application should be set to run in background
///
[HideInInspector] public bool RunInBackground = true;
///
/// The log level to use
///
[HideInInspector] public LogLevel LogLevel = LogLevel.Normal;
///
/// The singleton instance of the NetworkManager
///
public static NetworkManager Singleton { get; private set; }
///
/// Gets the SpawnManager for this NetworkManager
///
public NetworkSpawnManager SpawnManager { get; private set; }
internal IDeferredMessageManager DeferredMessageManager { get; private set; }
public CustomMessagingManager CustomMessagingManager { get; private set; }
public NetworkSceneManager SceneManager { get; private set; }
public const ulong ServerClientId = 0;
///
/// Gets the networkId of the server
///
private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ?? throw new NullReferenceException($"The transport in the active {nameof(NetworkConfig)} is null");
///
/// Returns ServerClientId if IsServer or LocalClientId if not
///
public ulong LocalClientId
{
get => IsServer ? NetworkConfig.NetworkTransport.ServerClientId : m_LocalClientId;
internal set => m_LocalClientId = value;
}
private ulong m_LocalClientId;
private Dictionary m_ConnectedClients = new Dictionary();
private ulong m_NextClientId = 1;
private Dictionary m_ClientIdToTransportIdMap = new Dictionary();
private Dictionary m_TransportIdToClientIdMap = new Dictionary();
private List m_ConnectedClientsList = new List();
private List m_ConnectedClientIds = new List();
///
/// Gets a dictionary of connected clients and their clientId keys. This is only accessible on the server.
///
public IReadOnlyDictionary ConnectedClients
{
get
{
if (IsServer == false)
{
throw new NotServerException($"{nameof(ConnectedClients)} should only be accessed on server.");
}
return m_ConnectedClients;
}
}
///
/// Gets a list of connected clients. This is only accessible on the server.
///
public IReadOnlyList ConnectedClientsList
{
get
{
if (IsServer == false)
{
throw new NotServerException($"{nameof(ConnectedClientsList)} should only be accessed on server.");
}
return m_ConnectedClientsList;
}
}
///
/// Gets a list of just the IDs of all connected clients. This is only accessible on the server.
///
public IReadOnlyList ConnectedClientsIds
{
get
{
if (IsServer == false)
{
throw new NotServerException($"{nameof(m_ConnectedClientIds)} should only be accessed on server.");
}
return m_ConnectedClientIds;
}
}
///
/// Gets the local for this client.
///
public NetworkClient LocalClient { get; internal set; }
///
/// Gets a dictionary of the clients that have been accepted by the transport but are still pending by the Netcode. This is only populated on the server.
///
public readonly Dictionary PendingClients = new Dictionary();
///
/// Gets Whether or not a server is running
///
public bool IsServer { get; internal set; }
///
/// Gets Whether or not a client is running
///
public bool IsClient { get; internal set; }
///
/// Gets if we are running as host
///
public bool IsHost => IsServer && IsClient;
///
/// Gets Whether or not we are listening for connections
///
public bool IsListening { get; internal set; }
///
/// Gets if we are connected as a client
///
public bool IsConnectedClient { get; internal set; }
public bool ShutdownInProgress { get { return m_ShuttingDown; } }
///
/// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects.
///
public event Action OnClientConnectedCallback = null;
internal void InvokeOnClientConnectedCallback(ulong clientId) => OnClientConnectedCallback?.Invoke(clientId);
///
/// The callback to invoke when a client disconnects. This callback is only ran on the server and on the local client that disconnects.
///
public event Action OnClientDisconnectCallback = null;
///
/// The callback to invoke once the server is ready
///
public event Action OnServerStarted = null;
///
/// Delegate type called when connection has been approved. This only has to be set on the server.
///
/// If true, a player object will be created. Otherwise the client will have no object.
/// The prefabHash to use for the client. If createPlayerObject is false, this is ignored. If playerPrefabHash is null, the default player prefab is used.
/// Whether or not the client was approved
/// The position to spawn the client at. If null, the prefab position is used.
/// The rotation to spawn the client with. If null, the prefab position is used.
public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation);
///
/// The callback to invoke during connection approval
///
public event Action ConnectionApprovalCallback = null;
internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) => ConnectionApprovalCallback?.Invoke(payload, clientId, action);
///
/// The current NetworkConfig
///
[HideInInspector] public NetworkConfig NetworkConfig;
///
/// The current host name we are connected to, used to validate certificate
///
public string ConnectedHostname { get; private set; }
internal INetworkMetrics NetworkMetrics { get; private set; }
internal static event Action OnSingletonReady;
#if UNITY_EDITOR
private void OnValidate()
{
if (NetworkConfig == null)
{
return; // May occur when the component is added
}
if (GetComponentInChildren() != null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}.");
}
}
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
// If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information
if (!activeScene.isDirty || EditorApplication.isUpdating)
{
return;
}
// During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it
NetworkConfig.NetworkPrefabOverrideLinks.Clear();
// Check network prefabs and assign to dictionary for quick look up
for (int i = 0; i < NetworkConfig.NetworkPrefabs.Count; i++)
{
var networkPrefab = NetworkConfig.NetworkPrefabs[i];
var networkPrefabGo = networkPrefab?.Prefab;
if (networkPrefabGo != null)
{
var networkObject = networkPrefabGo.GetComponent();
if (networkObject == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogError($"Cannot register {PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root");
}
}
else
{
{
var childNetworkObjects = new List();
networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects);
if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)");
}
}
}
// Default to the standard NetworkPrefab.Prefab's NetworkObject first
var globalObjectIdHash = networkObject.GlobalObjectIdHash;
// Now check to see if it has an override
switch (networkPrefab.Override)
{
case NetworkPrefabOverride.Prefab:
{
if (NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride == null &&
NetworkConfig.NetworkPrefabs[i].Prefab != null)
{
if (networkPrefab.SourcePrefabToOverride == null)
{
networkPrefab.SourcePrefabToOverride = networkPrefabGo;
}
globalObjectIdHash = networkPrefab.SourcePrefabToOverride.GetComponent().GlobalObjectIdHash;
}
break;
}
case NetworkPrefabOverride.Hash:
globalObjectIdHash = networkPrefab.SourceHashToOverride;
break;
}
// Add to the NetworkPrefabOverrideLinks or handle a new (blank) entries
if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash))
{
NetworkConfig.NetworkPrefabOverrideLinks.Add(globalObjectIdHash, networkPrefab);
}
else
{
// Duplicate entries can happen when adding a new entry into a list of existing entries
// Either this is user error or a new entry, either case we replace it with a new, blank, NetworkPrefab under this condition
NetworkConfig.NetworkPrefabs[i] = new NetworkPrefab();
}
}
}
}
}
#endif
///
/// Adds a new prefab to the network prefab list.
/// This can be any GameObject with a NetworkObject component, from any source (addressables, asset
/// bundles, Resource.Load, dynamically created, etc)
///
/// There are three limitations to this method:
/// - If you have NetworkConfig.ForceSamePrefabs enabled, you can only do this before starting
/// networking, and the server and all connected clients must all have the same exact set of prefabs
/// added via this method before connecting
/// - Adding a prefab on the server does not automatically add it on the client - it's up to you
/// to make sure the client and server are synchronized via whatever method makes sense for your game
/// (RPCs, configs, deterministic loading, etc)
/// - If the server sends a Spawn message to a client that has not yet added a prefab for, the spawn message
/// and any other relevant messages will be held for a configurable time (default 1 second, configured via
/// NetworkConfig.SpawnTimeout) before an error is logged. This is intented to enable the SDK to gracefully
/// handle unexpected conditions (slow disks, slow network, etc) that slow down asset loading. This timeout
/// should not be relied on and code shouldn't be written around it - your code should be written so that
/// the asset is expected to be loaded before it's needed.
///
///
///
public void AddNetworkPrefab(GameObject prefab)
{
if (IsListening && NetworkConfig.ForceSamePrefabs)
{
throw new Exception($"All prefabs must be registered before starting {nameof(NetworkManager)} when {nameof(NetworkConfig.ForceSamePrefabs)} is enabled.");
}
var networkObject = prefab.GetComponent();
if (!networkObject)
{
throw new Exception($"All {nameof(NetworkPrefab)}s must contain a {nameof(NetworkObject)} component.");
}
var networkPrefab = new NetworkPrefab { Prefab = prefab };
NetworkConfig.NetworkPrefabs.Add(networkPrefab);
if (IsListening)
{
var sourcePrefabGlobalObjectIdHash = (uint)0;
var targetPrefabGlobalObjectIdHash = (uint)0;
if (!ShouldAddPrefab(networkPrefab, out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash))
{
NetworkConfig.NetworkPrefabs.Remove(networkPrefab);
return;
}
if (!AddPrefabRegistration(networkPrefab, sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash))
{
NetworkConfig.NetworkPrefabs.Remove(networkPrefab);
return;
}
DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnAddPrefab, networkObject.GlobalObjectIdHash);
}
}
private bool ShouldAddPrefab(NetworkPrefab networkPrefab, out uint sourcePrefabGlobalObjectIdHash, out uint targetPrefabGlobalObjectIdHash, int index = -1)
{
sourcePrefabGlobalObjectIdHash = 0;
targetPrefabGlobalObjectIdHash = 0;
var networkObject = (NetworkObject)null;
if (networkPrefab == null || (networkPrefab.Prefab == null && networkPrefab.Override == NetworkPrefabOverride.None))
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning(
$"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {index})");
}
return false;
}
else if (networkPrefab.Override == NetworkPrefabOverride.None)
{
networkObject = networkPrefab.Prefab.GetComponent();
if (networkObject == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} is missing " +
$"a {nameof(NetworkObject)} component (entry will be ignored).");
}
return false;
}
// Otherwise get the GlobalObjectIdHash value
sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash;
}
else // Validate Overrides
{
// Validate source prefab override values first
switch (networkPrefab.Override)
{
case NetworkPrefabOverride.Hash:
{
if (networkPrefab.SourceHashToOverride == 0)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourceHashToOverride)} is zero " +
"(entry will be ignored).");
}
return false;
}
sourcePrefabGlobalObjectIdHash = networkPrefab.SourceHashToOverride;
break;
}
case NetworkPrefabOverride.Prefab:
{
if (networkPrefab.SourcePrefabToOverride == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourcePrefabToOverride)} is null (entry will be ignored).");
}
Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored.");
return false;
}
else
{
networkObject = networkPrefab.SourcePrefabToOverride.GetComponent();
if (networkObject == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({networkPrefab.SourcePrefabToOverride.name}) " +
$"is missing a {nameof(NetworkObject)} component (entry will be ignored).");
}
Debug.LogWarning($"{nameof(NetworkPrefab)} override entry (\"{networkPrefab.SourcePrefabToOverride.name}\") will be removed and ignored.");
return false;
}
sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash;
}
break;
}
}
// Validate target prefab override values next
if (networkPrefab.OverridingTargetPrefab == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.OverridingTargetPrefab)} is null!");
}
switch (networkPrefab.Override)
{
case NetworkPrefabOverride.Hash:
{
Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored.");
break;
}
case NetworkPrefabOverride.Prefab:
{
Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({networkPrefab.SourcePrefabToOverride.name}) will be removed and ignored.");
break;
}
}
return false;
}
else
{
targetPrefabGlobalObjectIdHash = networkPrefab.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash;
}
}
return true;
}
internal bool AddPrefabRegistration(NetworkPrefab networkPrefab, uint sourcePrefabGlobalObjectIdHash, uint targetPrefabGlobalObjectIdHash)
{
// Assign the appropriate GlobalObjectIdHash to the appropriate NetworkPrefab
if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(sourcePrefabGlobalObjectIdHash))
{
if (networkPrefab.Override == NetworkPrefabOverride.None)
{
NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab);
}
else
{
if (!NetworkConfig.OverrideToNetworkPrefab.ContainsKey(targetPrefabGlobalObjectIdHash))
{
switch (networkPrefab.Override)
{
case NetworkPrefabOverride.Prefab:
{
NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab);
NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash);
}
break;
case NetworkPrefabOverride.Hash:
{
NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab);
NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash);
}
break;
}
}
else
{
var networkObject = networkPrefab.Prefab.GetComponent();
// This can happen if a user tries to make several GlobalObjectIdHash values point to the same target
Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {targetPrefabGlobalObjectIdHash}! Removing entry from list!");
return false;
}
}
}
else
{
var networkObject = networkPrefab.Prefab.GetComponent();
// This should never happen, but in the case it somehow does log an error and remove the duplicate entry
Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {sourcePrefabGlobalObjectIdHash}! Removing entry from list!");
return false;
}
return true;
}
private void InitializePrefabs(int startIdx = 0)
{
// This is used to remove entries not needed or invalid
var removeEmptyPrefabs = new List();
// Build the NetworkPrefabOverrideLinks dictionary
for (int i = startIdx; i < NetworkConfig.NetworkPrefabs.Count; i++)
{
var sourcePrefabGlobalObjectIdHash = (uint)0;
var targetPrefabGlobalObjectIdHash = (uint)0;
if (!ShouldAddPrefab(NetworkConfig.NetworkPrefabs[i], out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash, i))
{
removeEmptyPrefabs.Add(i);
continue;
}
if (!AddPrefabRegistration(NetworkConfig.NetworkPrefabs[i], sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash))
{
removeEmptyPrefabs.Add(i);
continue;
}
}
// Clear out anything that is invalid or not used (for invalid entries we already logged warnings to the user earlier)
// Iterate backwards so indices don't shift as we remove
for (int i = removeEmptyPrefabs.Count - 1; i >= 0; i--)
{
NetworkConfig.NetworkPrefabs.RemoveAt(removeEmptyPrefabs[i]);
}
removeEmptyPrefabs.Clear();
}
private void Initialize(bool server)
{
// Don't allow the user to start a network session if the NetworkManager is
// still parented under another GameObject
if (NetworkManagerCheckForParent(true))
{
return;
}
ComponentFactory.SetDefaults();
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(Initialize));
}
this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate);
this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate);
MessagingSystem = new MessagingSystem(new NetworkManagerMessageSender(this), this);
MessagingSystem.Hook(new NetworkManagerHooks(this));
#if DEVELOPMENT_BUILD || UNITY_EDITOR
MessagingSystem.Hook(new ProfilingHooks());
#endif
#if MULTIPLAYER_TOOLS
MessagingSystem.Hook(new MetricHooks(this));
#endif
LocalClientId = ulong.MaxValue;
ClearClients();
// Create spawn manager instance
SpawnManager = new NetworkSpawnManager(this);
DeferredMessageManager = ComponentFactory.Create(this);
CustomMessagingManager = new CustomMessagingManager(this);
SceneManager = new NetworkSceneManager(this);
BehaviourUpdater = new NetworkBehaviourUpdater();
if (NetworkMetrics == null)
{
#if MULTIPLAYER_TOOLS
NetworkMetrics = new NetworkMetrics();
#else
NetworkMetrics = new NullNetworkMetrics();
#endif
}
#if MULTIPLAYER_TOOLS
NetworkSolutionInterface.SetInterface(new NetworkSolutionInterfaceParameters
{
NetworkObjectProvider = new NetworkObjectProvider(this),
});
#endif
if (NetworkConfig.NetworkTransport == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogError("No transport has been selected!");
}
return;
}
NetworkConfig.NetworkTransport.NetworkMetrics = NetworkMetrics;
if (server)
{
NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem();
}
else
{
NetworkTimeSystem = new NetworkTimeSystem(1.0 / NetworkConfig.TickRate, k_DefaultBufferSizeSec, 0.2);
}
NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0);
NetworkTickSystem.Tick += OnNetworkManagerTick;
this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate);
// Always clear our prefab override links before building
NetworkConfig.NetworkPrefabOverrideLinks.Clear();
NetworkConfig.OverrideToNetworkPrefab.Clear();
InitializePrefabs();
// If we have a player prefab, then we need to verify it is in the list of NetworkPrefabOverrideLinks for client side spawning.
if (NetworkConfig.PlayerPrefab != null)
{
var playerPrefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent();
if (playerPrefabNetworkObject != null)
{
//In the event there is no NetworkPrefab entry (i.e. no override for default player prefab)
if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(playerPrefabNetworkObject
.GlobalObjectIdHash))
{
//Then add a new entry for the player prefab
var playerNetworkPrefab = new NetworkPrefab();
playerNetworkPrefab.Prefab = NetworkConfig.PlayerPrefab;
NetworkConfig.NetworkPrefabs.Insert(0, playerNetworkPrefab);
NetworkConfig.NetworkPrefabOverrideLinks.Add(playerPrefabNetworkObject.GlobalObjectIdHash,
playerNetworkPrefab);
}
}
else
{
// Provide the name of the prefab with issues so the user can more easily find the prefab and fix it
Debug.LogError($"{nameof(NetworkConfig.PlayerPrefab)} (\"{NetworkConfig.PlayerPrefab.name}\") has no NetworkObject assigned to it!.");
}
}
NetworkConfig.NetworkTransport.OnTransportEvent += HandleRawTransportPoll;
NetworkConfig.NetworkTransport.Initialize(this);
}
private void ClearClients()
{
PendingClients.Clear();
m_ConnectedClients.Clear();
m_ConnectedClientsList.Clear();
m_ConnectedClientIds.Clear();
LocalClient = null;
NetworkObject.OrphanChildren.Clear();
}
///
/// Starts a server
///
public bool StartServer()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(StartServer));
}
if (!CanStart(StartType.Server))
{
return false;
}
Initialize(true);
// If we failed to start then shutdown and notify user that the transport failed to start
if (NetworkConfig.NetworkTransport.StartServer())
{
IsServer = true;
IsClient = false;
IsListening = true;
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
OnServerStarted?.Invoke();
return true;
}
else
{
Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
}
return false;
}
///
/// Starts a client
///
public bool StartClient()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(StartClient));
}
if (!CanStart(StartType.Client))
{
return false;
}
Initialize(false);
MessagingSystem.ClientConnected(ServerClientId);
if (!NetworkConfig.NetworkTransport.StartClient())
{
Debug.LogError($"Client is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
return false;
}
IsServer = false;
IsClient = true;
IsListening = true;
return true;
}
///
/// Starts a Host
///
public bool StartHost()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(StartHost));
}
if (!CanStart(StartType.Host))
{
return false;
}
Initialize(true);
// If we failed to start then shutdown and notify user that the transport failed to start
if (!NetworkConfig.NetworkTransport.StartServer())
{
Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!");
Shutdown();
return false;
}
MessagingSystem.ClientConnected(ServerClientId);
LocalClientId = ServerClientId;
NetworkMetrics.SetConnectionId(LocalClientId);
IsServer = true;
IsClient = true;
IsListening = true;
if (NetworkConfig.ConnectionApproval)
{
InvokeConnectionApproval(NetworkConfig.ConnectionData, ServerClientId,
(createPlayerObject, playerPrefabHash, approved, position, rotation) =>
{
// You cannot decline the local server. Force approved to true
if (!approved)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"You cannot decline the host connection. The connection was automatically approved.");
}
}
HandleApproval(ServerClientId, createPlayerObject, playerPrefabHash, true, position, rotation);
});
}
else
{
HandleApproval(ServerClientId, NetworkConfig.PlayerPrefab != null, null, true, null, null);
}
SpawnManager.ServerSpawnSceneObjectsOnStartSweep();
OnServerStarted?.Invoke();
return true;
}
private enum StartType
{
Server,
Host,
Client
}
private bool CanStart(StartType type)
{
if (IsListening)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning("Cannot start " + type + " while an instance is already running");
}
return false;
}
// Only if it is starting as a server or host do we need to check this
// Clients don't invoke the ConnectionApprovalCallback
if (NetworkConfig.ConnectionApproval && type != StartType.Client)
{
if (ConnectionApprovalCallback == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"No ConnectionApproval callback defined. Connection approval will timeout");
}
}
}
if (ConnectionApprovalCallback != null)
{
if (!NetworkConfig.ConnectionApproval)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning(
"A ConnectionApproval callback is defined but ConnectionApproval is disabled. In order to use ConnectionApproval it has to be explicitly enabled ");
}
}
}
return true;
}
public void SetSingleton()
{
Singleton = this;
OnSingletonReady?.Invoke();
}
private void OnEnable()
{
if (RunInBackground)
{
Application.runInBackground = true;
}
if (Singleton == null)
{
SetSingleton();
}
if (!NetworkManagerCheckForParent())
{
DontDestroyOnLoad(gameObject);
}
}
private void Awake()
{
UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded;
NetworkVariableHelper.InitializeAllBaseDelegates();
}
///
/// Handle runtime detection for parenting the NetworkManager's GameObject under another GameObject
///
private void OnTransformParentChanged()
{
NetworkManagerCheckForParent();
}
///
/// Determines if the NetworkManager's GameObject is parented under another GameObject and
/// notifies the user that this is not allowed for the NetworkManager.
///
internal bool NetworkManagerCheckForParent(bool ignoreNetworkManagerCache = false)
{
#if UNITY_EDITOR
var isParented = NetworkManagerHelper.NotifyUserOfNestedNetworkManager(this, ignoreNetworkManagerCache);
#else
var isParented = transform.root != transform;
if (isParented)
{
throw new Exception(GenerateNestedNetworkManagerMessage(transform));
}
#endif
return isParented;
}
static internal string GenerateNestedNetworkManagerMessage(Transform transform)
{
return $"{transform.name} is nested under {transform.root.name}. NetworkManager cannot be nested.\n";
}
#if UNITY_EDITOR
static internal INetworkManagerHelper NetworkManagerHelper;
///
/// Interface for NetworkManagerHelper
///
internal interface INetworkManagerHelper
{
bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false);
void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false);
}
#endif
// Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when unloading a scene with a NetworkManager
private void OnSceneUnloaded(Scene scene)
{
if (scene == gameObject.scene)
{
OnDestroy();
}
}
// Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application.
private void OnApplicationQuit()
{
OnDestroy();
}
// Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit
private void OnDestroy()
{
ShutdownInternal();
UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded;
if (Singleton == this)
{
Singleton = null;
}
}
private void DisconnectRemoteClient(ulong clientId)
{
var transportId = ClientIdToTransportId(clientId);
NetworkConfig.NetworkTransport.DisconnectRemoteClient(transportId);
}
///
/// Globally shuts down the library.
/// Disconnects clients if connected and stops server if running.
///
///
/// If false, any messages that are currently in the incoming queue will be handled,
/// and any messages in the outgoing queue will be sent, before the shutdown is processed.
/// If true, NetworkManager will shut down immediately, and any unprocessed or unsent messages
/// will be discarded.
///
public void Shutdown(bool discardMessageQueue = false)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(Shutdown));
}
// If we're not running, don't start shutting down, it would only cause an immediate
// shutdown the next time the manager is started.
if (IsServer || IsClient)
{
m_ShuttingDown = true;
m_StopProcessingMessages = discardMessageQueue;
}
NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll;
}
internal void ShutdownInternal()
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo(nameof(ShutdownInternal));
}
if (IsServer)
{
// make sure all messages are flushed before transport disconnect clients
if (MessagingSystem != null)
{
MessagingSystem.ProcessSendQueues();
}
var disconnectedIds = new HashSet();
//Don't know if I have to disconnect the clients. I'm assuming the NetworkTransport does all the cleaning on shutdown. But this way the clients get a disconnect message from server (so long it does't get lost)
foreach (KeyValuePair pair in ConnectedClients)
{
if (!disconnectedIds.Contains(pair.Key))
{
disconnectedIds.Add(pair.Key);
if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId)
{
continue;
}
DisconnectRemoteClient(pair.Key);
}
}
foreach (KeyValuePair pair in PendingClients)
{
if (!disconnectedIds.Contains(pair.Key))
{
disconnectedIds.Add(pair.Key);
if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId)
{
continue;
}
DisconnectRemoteClient(pair.Key);
}
}
}
if (IsClient && IsConnectedClient)
{
// Client only, send disconnect to server
NetworkConfig.NetworkTransport.DisconnectLocalClient();
}
IsConnectedClient = false;
IsServer = false;
IsClient = false;
this.UnregisterAllNetworkUpdates();
if (NetworkTickSystem != null)
{
NetworkTickSystem.Tick -= OnNetworkManagerTick;
NetworkTickSystem = null;
}
if (MessagingSystem != null)
{
MessagingSystem.Dispose();
MessagingSystem = null;
}
if (NetworkConfig?.NetworkTransport != null)
{
NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll;
}
if (SpawnManager != null)
{
SpawnManager.DespawnAndDestroyNetworkObjects();
SpawnManager.ServerResetShudownStateForSceneObjects();
SpawnManager = null;
}
if (DeferredMessageManager != null)
{
DeferredMessageManager.CleanupAllTriggers();
}
if (SceneManager != null)
{
// Let the NetworkSceneManager clean up its two SceneEvenData instances
SceneManager.Dispose();
SceneManager = null;
}
if (CustomMessagingManager != null)
{
CustomMessagingManager = null;
}
if (BehaviourUpdater != null)
{
BehaviourUpdater = null;
}
// This is required for handling the potential scenario where multiple NetworkManager instances are created.
// See MTT-860 for more information
if (IsListening)
{
//The Transport is set during initialization, thus it is possible for the Transport to be null
NetworkConfig?.NetworkTransport?.Shutdown();
}
m_ClientIdToTransportIdMap.Clear();
m_TransportIdToClientIdMap.Clear();
IsListening = false;
m_ShuttingDown = false;
m_StopProcessingMessages = false;
ClearClients();
}
// INetworkUpdateSystem
public void NetworkUpdate(NetworkUpdateStage updateStage)
{
switch (updateStage)
{
case NetworkUpdateStage.EarlyUpdate:
OnNetworkEarlyUpdate();
break;
case NetworkUpdateStage.PreUpdate:
OnNetworkPreUpdate();
break;
case NetworkUpdateStage.PostLateUpdate:
OnNetworkPostLateUpdate();
break;
}
}
private void OnNetworkEarlyUpdate()
{
if (!IsListening)
{
return;
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportPoll.Begin();
#endif
NetworkEvent networkEvent;
do
{
networkEvent = NetworkConfig.NetworkTransport.PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime);
HandleRawTransportPoll(networkEvent, clientId, payload, receiveTime);
// Only do another iteration if: there are no more messages AND (there is no limit to max events or we have processed less than the maximum)
} while (IsListening && networkEvent != NetworkEvent.Nothing);
MessagingSystem.ProcessIncomingMessageQueue();
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportPoll.End();
#endif
}
// TODO Once we have a way to subscribe to NetworkUpdateLoop with order we can move this out of NetworkManager but for now this needs to be here because we need strict ordering.
private void OnNetworkPreUpdate()
{
if (IsServer == false && IsConnectedClient == false)
{
// As a client wait to run the time system until we are connected.
return;
}
if (m_ShuttingDown && m_StopProcessingMessages)
{
return;
}
// Only update RTT here, server time is updated by time sync messages
var reset = NetworkTimeSystem.Advance(Time.deltaTime);
if (reset)
{
NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime);
}
NetworkTickSystem.UpdateTick(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime);
if (IsServer == false)
{
NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.deltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d);
}
}
private void OnNetworkPostLateUpdate()
{
if (!m_ShuttingDown || !m_StopProcessingMessages)
{
MessagingSystem.ProcessSendQueues();
NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count);
NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1);
NetworkMetrics.DispatchFrame();
NetworkObject.VerifyParentingStatus();
}
DeferredMessageManager.CleanupStaleTriggers();
if (m_ShuttingDown)
{
ShutdownInternal();
}
}
///
/// This function runs once whenever the local tick is incremented and is responsible for the following (in order):
/// - collect commands/inputs and send them to the server (TBD)
/// - call NetworkFixedUpdate on all NetworkBehaviours in prediction/client authority mode
///
private void OnNetworkManagerTick()
{
// Do NetworkVariable updates
BehaviourUpdater.NetworkBehaviourUpdate(this);
int timeSyncFrequencyTicks = (int)(k_TimeSyncFrequency * NetworkConfig.TickRate);
if (IsServer && NetworkTickSystem.ServerTime.Tick % timeSyncFrequencyTicks == 0)
{
SyncTime();
}
}
private void SendConnectionRequest()
{
var message = new ConnectionRequestMessage
{
ConfigHash = NetworkConfig.GetConfig(),
ShouldSendConnectionData = NetworkConfig.ConnectionApproval,
ConnectionData = NetworkConfig.ConnectionData
};
SendMessage(ref message, NetworkDelivery.ReliableSequenced, ServerClientId);
}
private IEnumerator ApprovalTimeout(ulong clientId)
{
NetworkTime timeStarted = LocalTime;
//We yield every frame incase a pending client disconnects and someone else gets its connection id
while ((LocalTime - timeStarted).Time < NetworkConfig.ClientConnectionBufferTimeout && PendingClients.ContainsKey(clientId))
{
yield return null;
}
if (PendingClients.ContainsKey(clientId) && !ConnectedClients.ContainsKey(clientId))
{
// Timeout
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo($"Client {clientId} Handshake Timed Out");
}
DisconnectClient(clientId);
}
}
internal ulong TransportIdToClientId(ulong transportId)
{
return transportId == m_ServerTransportId ? ServerClientId : m_TransportIdToClientIdMap[transportId];
}
internal ulong ClientIdToTransportId(ulong clientId)
{
return clientId == ServerClientId ? m_ServerTransportId : m_ClientIdToTransportIdMap[clientId];
}
private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, ArraySegment payload, float receiveTime)
{
var transportId = clientId;
switch (networkEvent)
{
case NetworkEvent.Connect:
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportConnect.Begin();
#endif
// Assumptions:
// - When server receives a connection, it *must be* a client
// - When client receives one, it *must be* the server
// Client's can't connect to or talk to other clients.
// Server is a sentinel so only one exists, if we are server, we can't be
// connecting to it.
if (IsServer)
{
clientId = m_NextClientId++;
}
else
{
clientId = ServerClientId;
}
m_ClientIdToTransportIdMap[clientId] = transportId;
m_TransportIdToClientIdMap[transportId] = clientId;
MessagingSystem.ClientConnected(clientId);
if (IsServer)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo("Client Connected");
}
PendingClients.Add(clientId, new PendingClient()
{
ClientId = clientId,
ConnectionState = PendingClient.State.PendingConnection
});
StartCoroutine(ApprovalTimeout(clientId));
}
else
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo("Connected");
}
SendConnectionRequest();
StartCoroutine(ApprovalTimeout(clientId));
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportConnect.End();
#endif
break;
case NetworkEvent.Data:
{
clientId = TransportIdToClientId(clientId);
HandleIncomingData(clientId, payload, receiveTime);
break;
}
case NetworkEvent.Disconnect:
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportDisconnect.Begin();
#endif
clientId = TransportIdCleanUp(clientId, transportId);
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo($"Disconnect Event From {clientId}");
}
OnClientDisconnectCallback?.Invoke(clientId);
if (IsServer)
{
OnClientDisconnectFromServer(clientId);
}
else
{
// We must pass true here and not process any sends messages
// as we are no longer connected and thus there is no one to
// send any messages to and this will cause an exception within
// UnityTransport as the client ID is no longer valid.
Shutdown(true);
}
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_TransportDisconnect.End();
#endif
break;
}
}
///
/// Handles cleaning up the transport id/client id tables after
/// receiving a disconnect event from transport
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ulong TransportIdCleanUp(ulong clientId, ulong transportId)
{
// This check is for clients that attempted to connect but failed.
// When this happens, the client will not have an entry within the
// m_TransportIdToClientIdMap or m_ClientIdToTransportIdMap lookup
// tables so we exit early and just return 0 to be used for the
// disconnect event.
if (!IsServer && !m_TransportIdToClientIdMap.ContainsKey(clientId))
{
return 0;
}
clientId = TransportIdToClientId(clientId);
m_TransportIdToClientIdMap.Remove(transportId);
m_ClientIdToTransportIdMap.Remove(clientId);
return clientId;
}
internal unsafe int SendMessage(ref TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds)
where TMessageType : INetworkMessage
where TClientIdListType : IReadOnlyList
{
// Prevent server sending to itself
if (IsServer)
{
ulong* nonServerIds = stackalloc ulong[clientIds.Count];
int newIdx = 0;
for (int idx = 0; idx < clientIds.Count; ++idx)
{
if (clientIds[idx] == ServerClientId)
{
continue;
}
nonServerIds[newIdx++] = clientIds[idx];
}
if (newIdx == 0)
{
return 0;
}
return MessagingSystem.SendMessage(ref message, delivery, nonServerIds, newIdx);
}
// else
if (clientIds.Count != 1 || clientIds[0] != ServerClientId)
{
throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}");
}
return MessagingSystem.SendMessage(ref message, delivery, clientIds);
}
internal unsafe int SendMessage(ref T message, NetworkDelivery delivery,
ulong* clientIds, int numClientIds)
where T : INetworkMessage
{
// Prevent server sending to itself
if (IsServer)
{
ulong* nonServerIds = stackalloc ulong[numClientIds];
int newIdx = 0;
for (int idx = 0; idx < numClientIds; ++idx)
{
if (clientIds[idx] == ServerClientId)
{
continue;
}
nonServerIds[newIdx++] = clientIds[idx];
}
if (newIdx == 0)
{
return 0;
}
return MessagingSystem.SendMessage(ref message, delivery, nonServerIds, newIdx);
}
// else
if (numClientIds != 1 || clientIds[0] != ServerClientId)
{
throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}");
}
return MessagingSystem.SendMessage(ref message, delivery, clientIds, numClientIds);
}
internal unsafe int SendMessage(ref T message, NetworkDelivery delivery, in NativeArray clientIds)
where T : INetworkMessage
{
return SendMessage(ref message, delivery, (ulong*)clientIds.GetUnsafePtr(), clientIds.Length);
}
internal int SendMessage(ref T message, NetworkDelivery delivery, ulong clientId)
where T : INetworkMessage
{
// Prevent server sending to itself
if (IsServer && clientId == ServerClientId)
{
return 0;
}
if (!IsServer && clientId != ServerClientId)
{
throw new ArgumentException($"Clients may only send messages to {nameof(ServerClientId)}");
}
return MessagingSystem.SendMessage(ref message, delivery, clientId);
}
internal int SendPreSerializedMessage(in FastBufferWriter writer, int maxSize, ref T message, NetworkDelivery delivery, ulong clientId)
where T : INetworkMessage
{
return MessagingSystem.SendPreSerializedMessage(writer, maxSize, ref message, delivery, clientId);
}
internal void HandleIncomingData(ulong clientId, ArraySegment payload, float receiveTime)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_HandleIncomingData.Begin();
#endif
MessagingSystem.HandleIncomingData(clientId, payload, receiveTime);
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_HandleIncomingData.End();
#endif
}
///
/// Disconnects the remote client.
///
/// The ClientId to disconnect
public void DisconnectClient(ulong clientId)
{
if (!IsServer)
{
throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead.");
}
OnClientDisconnectFromServer(clientId);
DisconnectRemoteClient(clientId);
}
private void OnClientDisconnectFromServer(ulong clientId)
{
PendingClients.Remove(clientId);
if (ConnectedClients.TryGetValue(clientId, out NetworkClient networkClient))
{
if (IsServer)
{
var playerObject = networkClient.PlayerObject;
if (playerObject != null)
{
if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash))
{
PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject);
}
else
{
Destroy(playerObject.gameObject);
}
}
// Get the NetworkObjects owned by the disconnected client
var clientOwnedObjects = SpawnManager.GetClientOwnedObjects(clientId);
if (clientOwnedObjects == null)
{
// This could happen if a client is never assigned a player object and is disconnected
// Only log this in verbose/developer mode
if (LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"ClientID {clientId} disconnected with (0) zero owned objects! Was a player prefab not assigned?");
}
}
else
{
// Handle changing ownership and prefab handlers
for (int i = clientOwnedObjects.Count - 1; i >= 0; i--)
{
var ownedObject = clientOwnedObjects[i];
if (ownedObject != null)
{
if (!ownedObject.DontDestroyWithOwner)
{
if (PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash))
{
PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]);
}
else
{
Destroy(ownedObject.gameObject);
}
}
else
{
ownedObject.RemoveOwnership();
}
}
}
}
// TODO: Could(should?) be replaced with more memory per client, by storing the visibility
foreach (var sobj in SpawnManager.SpawnedObjectsList)
{
sobj.Observers.Remove(clientId);
}
}
for (int i = 0; i < ConnectedClientsList.Count; i++)
{
if (ConnectedClientsList[i].ClientId == clientId)
{
m_ConnectedClientsList.RemoveAt(i);
break;
}
}
for (int i = 0; i < ConnectedClientsIds.Count; i++)
{
if (ConnectedClientsIds[i] == clientId)
{
m_ConnectedClientIds.RemoveAt(i);
break;
}
}
m_ConnectedClients.Remove(clientId);
}
MessagingSystem.ClientDisconnected(clientId);
}
private void SyncTime()
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_SyncTime.Begin();
#endif
if (NetworkLog.CurrentLogLevel <= LogLevel.Developer)
{
NetworkLog.LogInfo("Syncing Time To Clients");
}
var message = new TimeSyncMessage
{
Tick = NetworkTickSystem.ServerTime.Tick
};
SendMessage(ref message, NetworkDelivery.Unreliable, ConnectedClientsIds);
#if DEVELOPMENT_BUILD || UNITY_EDITOR
s_SyncTime.End();
#endif
}
///
/// Server Side: Handles the approval of a client
///
/// client being approved
/// whether we want to create a player or not
/// the GlobalObjectIdHash value for the Network Prefab to create as the player
/// Is the player approved or not?
/// Used when createPlayerObject is true, position of the player when spawned
/// Used when createPlayerObject is true, rotation of the player when spawned
internal void HandleApproval(ulong ownerClientId, bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation)
{
if (approved)
{
// Inform new client it got approved
PendingClients.Remove(ownerClientId);
var client = new NetworkClient { ClientId = ownerClientId, };
m_ConnectedClients.Add(ownerClientId, client);
m_ConnectedClientsList.Add(client);
m_ConnectedClientIds.Add(client.ClientId);
if (createPlayerObject)
{
var networkObject = SpawnManager.CreateLocalNetworkObject(false, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, null, position, rotation);
SpawnManager.SpawnNetworkObjectLocally(networkObject, SpawnManager.GetNetworkObjectId(), false, true, ownerClientId, false);
ConnectedClients[ownerClientId].PlayerObject = networkObject;
}
// Server doesn't send itself the connection approved message
if (ownerClientId != ServerClientId)
{
var message = new ConnectionApprovedMessage
{
OwnerClientId = ownerClientId,
NetworkTick = LocalTime.Tick
};
if (!NetworkConfig.EnableSceneManagement)
{
if (SpawnManager.SpawnedObjectsList.Count != 0)
{
message.SpawnedObjectsList = SpawnManager.SpawnedObjectsList;
}
}
SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
// If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization
if (!NetworkConfig.EnableSceneManagement)
{
InvokeOnClientConnectedCallback(ownerClientId);
}
else
{
SceneManager.SynchronizeNetworkObjects(ownerClientId);
}
}
else // Server just adds itself as an observer to all spawned NetworkObjects
{
LocalClient = client;
SpawnManager.UpdateObservedNetworkObjects(ownerClientId);
InvokeOnClientConnectedCallback(ownerClientId);
}
if (!createPlayerObject || (playerPrefabHash == null && NetworkConfig.PlayerPrefab == null))
{
return;
}
// Separating this into a contained function call for potential further future separation of when this notification is sent.
ApprovedPlayerSpawn(ownerClientId, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash);
}
else
{
PendingClients.Remove(ownerClientId);
DisconnectRemoteClient(ownerClientId);
}
}
///
/// Spawns the newly approved player
///
/// new player client identifier
/// the prefab GlobalObjectIdHash value for this player
internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash)
{
foreach (var clientPair in ConnectedClients)
{
if (clientPair.Key == clientId ||
clientPair.Key == ServerClientId || // Server already spawned it
ConnectedClients[clientId].PlayerObject == null ||
!ConnectedClients[clientId].PlayerObject.Observers.Contains(clientPair.Key))
{
continue; //The new client.
}
var message = new CreateObjectMessage
{
ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key)
};
message.ObjectInfo.Header.Hash = playerPrefabHash;
message.ObjectInfo.Header.IsSceneObject = false;
message.ObjectInfo.Header.HasParent = false;
message.ObjectInfo.Header.IsPlayerObject = true;
message.ObjectInfo.Header.OwnerClientId = clientId;
var size = SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientPair.Key);
NetworkMetrics.TrackObjectSpawnSent(clientPair.Key, ConnectedClients[clientId].PlayerObject, size);
}
}
}
}