This repository has been archived on 2025-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
Unity Technologies b5abc3ff7c com.unity.netcode.gameobjects@1.4.0
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).

## [1.4.0] - 2023-04-10

### Added

- Added a way to access the GlobalObjectIdHash via PrefabIdHash for use in the Connection Approval Callback. (#2437)
- Added `OnServerStarted` and `OnServerStopped` events that will trigger only on the server (or host player) to notify that the server just started or is no longer active (#2420)
- Added `OnClientStarted` and `OnClientStopped` events that will trigger only on the client (or host player) to notify that the client just started or is no longer active (#2420)
- Added `NetworkTransform.UseHalfFloatPrecision` property that, when enabled, will use half float values for position, rotation, and scale. This yields a 50% bandwidth savings a the cost of precision. (#2388)
- Added `NetworkTransform.UseQuaternionSynchronization` property that, when enabled, will synchronize the entire quaternion. (#2388)
- Added `NetworkTransform.UseQuaternionCompression` property that, when enabled, will use a smallest three implementation reducing a full quaternion synchronization update to the size of an unsigned integer. (#2388)
- Added `NetworkTransform.SlerpPosition` property that, when enabled along with interpolation being enabled, will interpolate using `Vector3.Slerp`. (#2388)
- Added `BufferedLinearInterpolatorVector3` that replaces the float version, is now used by `NetworkTransform`, and provides the ability to enable or disable `Slerp`. (#2388)
- Added `HalfVector3` used for scale when half float precision is enabled. (#2388)
- Added `HalfVector4` used for rotation when half float precision and quaternion synchronization is enabled. (#2388)
- Added `HalfVector3DeltaPosition` used for position when half float precision is enabled. This handles loss in position precision by updating only the delta position as opposed to the full position. (#2388)
- Added `NetworkTransform.GetSpaceRelativePosition` and `NetworkTransform.GetSpaceRelativeRotation` helper methods to return the proper values depending upon whether local or world space. (#2388)
- Added `NetworkTransform.OnAuthorityPushTransformState` virtual method that is invoked just prior to sending the `NetworkTransformState` to non-authoritative instances. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
- Added `NetworkTransform.OnNetworkTransformStateUpdated` virtual method that is invoked just after the authoritative `NetworkTransformState` is applied. This provides users with the ability to obtain more precise delta values for prediction related calculations. (#2388)
- Added `NetworkTransform.OnInitialize`virtual method that is invoked after the `NetworkTransform` has been initialized or re-initialized when ownership changes. This provides for a way to make adjustments when `NetworkTransform` is initialized (i.e. resetting client prediction etc) (#2388)
- Added `NetworkObject.SynchronizeTransform` property (default is true) that provides users with another way to help with bandwidth optimizations where, when set to false, the `NetworkObject`'s associated transform will not be included when spawning and/or synchronizing late joining players. (#2388)
- Added `NetworkSceneManager.ActiveSceneSynchronizationEnabled` property, disabled by default, that enables client synchronization of server-side active scene changes. (#2383)
- Added `NetworkObject.ActiveSceneSynchronization`, disabled by default, that will automatically migrate a `NetworkObject` to a newly assigned active scene. (#2383)
- Added `NetworkObject.SceneMigrationSynchronization`, enabled by default, that will synchronize client(s) when a `NetworkObject` is migrated into a new scene on the server side via `SceneManager.MoveGameObjectToScene`. (#2383)

### Changed

- Made sure the `CheckObjectVisibility` delegate is checked and applied, upon `NetworkShow` attempt. Found while supporting (#2454), although this is not a fix for this (already fixed) issue. (#2463)
- Changed `NetworkTransform` authority handles delta checks on each new network tick and no longer consumes processing cycles checking for deltas for all frames in-between ticks. (#2388)
- Changed the `NetworkTransformState` structure is now public and now has public methods that provide access to key properties of the `NetworkTransformState` structure. (#2388)
- Changed `NetworkTransform` interpolation adjusts its interpolation "ticks ago" to be 2 ticks latent if it is owner authoritative and the instance is not the server or 1 tick latent if the instance is the server and/or is server authoritative. (#2388)
- Updated `NetworkSceneManager` to migrate dynamically spawned `NetworkObject`s with `DestroyWithScene` set to false into the active scene if their current scene is unloaded. (#2383)
- Updated the server to synchronize its local `NetworkSceneManager.ClientSynchronizationMode` during the initial client synchronization. (#2383)

### Fixed

- Fixed issue where during client synchronization the synchronizing client could receive a ObjectSceneChanged message before the client-side NetworkObject instance had been instantiated and spawned. (#2502)
- Fixed issue where `NetworkAnimator` was building client RPC parameters to exclude the host from sending itself messages but was not including it in the ClientRpc parameters. (#2492)
- Fixed issue where `NetworkAnimator` was not properly detecting and synchronizing cross fade initiated transitions. (#2481)
- Fixed issue where `NetworkAnimator` was not properly synchronizing animation state updates. (#2481)
- Fixed float NetworkVariables not being rendered properly in the inspector of NetworkObjects. (#2441)
- Fixed an issue where Named Message Handlers could remove themselves causing an exception when the metrics tried to access the name of the message.(#2426)
- Fixed registry of public `NetworkVariable`s in derived `NetworkBehaviour`s (#2423)
- Fixed issue where runtime association of `Animator` properties to `AnimationCurve`s would cause `NetworkAnimator` to attempt to update those changes. (#2416)
- Fixed issue where `NetworkAnimator` would not check if its associated `Animator` was valid during serialization and would spam exceptions in the editor console. (#2416)
- Fixed issue with a child's rotation rolling over when interpolation is enabled on a `NetworkTransform`. Now using half precision or full quaternion synchronization will always update all axis. (#2388)
- Fixed issue where `NetworkTransform` was not setting the teleport flag when the `NetworkTransform.InLocalSpace` value changed. This issue only impacted `NetworkTransform` when interpolation was enabled. (#2388)
- Fixed issue when the `NetworkSceneManager.ClientSynchronizationMode` is `LoadSceneMode.Additive` and the server changes the currently active scene prior to a client connecting then upon a client connecting and being synchronized the NetworkSceneManager would clear its internal ScenePlacedObjects list that could already be populated. (#2383)
- Fixed issue where a client would load duplicate scenes of already preloaded scenes during the initial client synchronization and `NetworkSceneManager.ClientSynchronizationMode` was set to `LoadSceneMode.Additive`. (#2383)
2023-04-10 00:00:00 +00:00

949 lines
42 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Unity.Netcode
{
/// <summary>
/// Class that handles object spawning
/// </summary>
public class NetworkSpawnManager
{
/// <summary>
/// The currently spawned objects
/// </summary>
public readonly Dictionary<ulong, NetworkObject> SpawnedObjects = new Dictionary<ulong, NetworkObject>();
/// <summary>
/// A list of the spawned objects
/// </summary>
public readonly HashSet<NetworkObject> SpawnedObjectsList = new HashSet<NetworkObject>();
/// <summary>
/// Use to get all NetworkObjects owned by a client
/// Ownership to Objects Table Format:
/// [ClientId][NetworkObjectId][NetworkObject]
/// Server: Keeps track of all clients' ownership
/// Client: Keeps track of only its ownership
/// </summary>
public readonly Dictionary<ulong, Dictionary<ulong, NetworkObject>> OwnershipToObjectsTable = new Dictionary<ulong, Dictionary<ulong, NetworkObject>>();
/// <summary>
/// Object to Ownership Table:
/// [NetworkObjectId][ClientId]
/// Used internally to find the client Id that currently owns
/// the NetworkObject
/// </summary>
private Dictionary<ulong, ulong> m_ObjectToOwnershipTable = new Dictionary<ulong, ulong>();
/// <summary>
/// Used to update a NetworkObject's ownership
/// </summary>
internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false)
{
var previousOwner = newOwner;
// Use internal lookup table to see if the NetworkObject has a previous owner
if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId))
{
// Keep track of the previous owner's ClientId
previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId];
// We are either despawning (remove) or changing ownership (assign)
if (isRemoving)
{
m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId);
}
else
{
m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner;
}
}
else
{
// Otherwise, just add a new lookup entry
m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner);
}
// Check to see if we had a previous owner
if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner))
{
// Before updating the previous owner, assure this entry exists
if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId))
{
// Remove the previous owner's entry
OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId);
// Server or Host alway invokes the lost ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnLostOwnership();
}
// If we are removing the entry (i.e. despawning or client lost ownership)
if (isRemoving)
{
return;
}
}
else
{
// Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen
throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?");
}
}
// If the owner doesn't have an entry then create one
if (!OwnershipToObjectsTable.ContainsKey(newOwner))
{
OwnershipToObjectsTable.Add(newOwner, new Dictionary<ulong, NetworkObject>());
}
// Sanity check to make sure we don't already have this entry (we shouldn't)
if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId))
{
// Add the new ownership entry
OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject);
// Server or Host always invokes the gained ownership notification locally
if (NetworkManager.IsServer)
{
networkObject.InvokeBehaviourOnGainedOwnership();
}
}
else if (isRemoving)
{
OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId);
}
else if (NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!");
}
}
/// <summary>
/// Returns a list of all NetworkObjects that belong to a client.
/// </summary>
/// <param name="clientId">the client's id <see cref="NetworkManager.LocalClientId"/></param>
/// <returns>returns the list of <see cref="NetworkObject"/>s owned by the client</returns>
public List<NetworkObject> GetClientOwnedObjects(ulong clientId)
{
if (!OwnershipToObjectsTable.ContainsKey(clientId))
{
OwnershipToObjectsTable.Add(clientId, new Dictionary<ulong, NetworkObject>());
}
return OwnershipToObjectsTable[clientId].Values.ToList();
}
/// <summary>
/// Gets the NetworkManager associated with this SpawnManager.
/// </summary>
public NetworkManager NetworkManager { get; }
internal NetworkSpawnManager(NetworkManager networkManager)
{
NetworkManager = networkManager;
}
internal readonly Queue<ReleasedNetworkId> ReleasedNetworkObjectIds = new Queue<ReleasedNetworkId>();
private ulong m_NetworkObjectIdCounter;
// A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations
private List<ulong> m_TargetClientIds = new List<ulong>();
internal ulong GetNetworkObjectId()
{
if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (NetworkManager.RealTimeProvider.UnscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay)
{
return ReleasedNetworkObjectIds.Dequeue().NetworkId;
}
m_NetworkObjectIdCounter++;
return m_NetworkObjectIdCounter;
}
/// <summary>
/// Returns the local player object or null if one does not exist
/// </summary>
/// <returns>The local player object or null if one does not exist</returns>
public NetworkObject GetLocalPlayerObject()
{
return GetPlayerNetworkObject(NetworkManager.LocalClientId);
}
/// <summary>
/// Returns the player object with a given clientId or null if one does not exist. This is only valid server side.
/// </summary>
/// <param name="clientId">the client identifier of the player</param>
/// <returns>The player object with a given clientId or null if one does not exist</returns>
public NetworkObject GetPlayerNetworkObject(ulong clientId)
{
if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId)
{
throw new NotServerException("Only the server can find player objects from other clients.");
}
if (TryGetNetworkClient(clientId, out NetworkClient networkClient))
{
return networkClient.PlayerObject;
}
return null;
}
internal void RemoveOwnership(NetworkObject networkObject)
{
if (!NetworkManager.IsServer)
{
throw new NotServerException("Only the server can change ownership");
}
if (!networkObject.IsSpawned)
{
throw new SpawnStateException("Object is not spawned");
}
// If we made it here then we are the server and if the server is determined to already be the owner
// then ignore the RemoveOwnership invocation.
if (networkObject.OwnerClientId == NetworkManager.ServerClientId)
{
return;
}
networkObject.OwnerClientId = NetworkManager.ServerClientId;
// Server removes the entry and takes over ownership before notifying
UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true);
var message = new ChangeOwnershipMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
OwnerClientId = networkObject.OwnerClientId
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds);
foreach (var client in NetworkManager.ConnectedClients)
{
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
/// <summary>
/// Helper function to get a network client for a clientId from the NetworkManager.
/// On the server this will check the <see cref="NetworkManager.ConnectedClients"/> list.
/// On a non-server this will check the <see cref="NetworkManager.LocalClient"/> only.
/// </summary>
/// <param name="clientId">The clientId for which to try getting the NetworkClient for.</param>
/// <param name="networkClient">The found NetworkClient. Null if no client was found.</param>
/// <returns>True if a NetworkClient with a matching id was found else false.</returns>
private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient)
{
if (NetworkManager.IsServer)
{
return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient);
}
if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId)
{
networkClient = NetworkManager.LocalClient;
return true;
}
networkClient = null;
return false;
}
internal void ChangeOwnership(NetworkObject networkObject, ulong clientId)
{
if (!NetworkManager.IsServer)
{
throw new NotServerException("Only the server can change ownership");
}
if (!networkObject.IsSpawned)
{
throw new SpawnStateException("Object is not spawned");
}
networkObject.OwnerClientId = clientId;
networkObject.MarkVariablesDirty(true);
NetworkManager.BehaviourUpdater.AddForUpdate(networkObject);
// Server adds entries for all client ownership
UpdateOwnershipTable(networkObject, networkObject.OwnerClientId);
var message = new ChangeOwnershipMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
OwnerClientId = networkObject.OwnerClientId
};
foreach (var client in NetworkManager.ConnectedClients)
{
if (networkObject.IsNetworkVisibleTo(client.Value.ClientId))
{
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client.Value.ClientId);
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size);
}
}
}
internal bool HasPrefab(NetworkObject.SceneObject sceneObject)
{
if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject)
{
if (NetworkManager.PrefabHandler.ContainsHandler(sceneObject.Hash))
{
return true;
}
if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.TryGetValue(sceneObject.Hash, out var networkPrefab))
{
switch (networkPrefab.Override)
{
default:
case NetworkPrefabOverride.None:
return networkPrefab.Prefab != null;
case NetworkPrefabOverride.Hash:
case NetworkPrefabOverride.Prefab:
return networkPrefab.OverridingTargetPrefab != null;
}
}
return false;
}
var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(sceneObject.Hash, sceneObject.NetworkSceneHandle);
return networkObject != null;
}
/// <summary>
/// Creates a local NetowrkObject to be spawned.
/// </summary>
/// <remarks>
/// For most cases this is client-side only, with the exception of when the server
/// is spawning a player.
/// </remarks>
internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject)
{
NetworkObject networkObject = null;
var globalObjectIdHash = sceneObject.Hash;
var position = sceneObject.HasTransform ? sceneObject.Transform.Position : default;
var rotation = sceneObject.HasTransform ? sceneObject.Transform.Rotation : default;
var scale = sceneObject.HasTransform ? sceneObject.Transform.Scale : default;
var parentNetworkId = sceneObject.HasParent ? sceneObject.ParentObjectId : default;
var worldPositionStays = (!sceneObject.HasParent) || sceneObject.WorldPositionStays;
var isSpawnedByPrefabHandler = false;
// If scene management is disabled or the NetworkObject was dynamically spawned
if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject)
{
// If the prefab hash has a registered INetworkPrefabInstanceHandler derived class
if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash))
{
// Let the handler spawn the NetworkObject
networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, sceneObject.OwnerClientId, position, rotation);
networkObject.NetworkManagerOwner = NetworkManager;
isSpawnedByPrefabHandler = true;
}
else
{
// See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash
GameObject networkPrefabReference = null;
if (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash))
{
switch (NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].Override)
{
default:
case NetworkPrefabOverride.None:
networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].Prefab;
break;
case NetworkPrefabOverride.Hash:
case NetworkPrefabOverride.Prefab:
networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab;
break;
}
}
// If not, then there is an issue (user possibly didn't register the prefab properly?)
if (networkPrefabReference == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?");
}
}
else
{
// Create prefab instance
networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent<NetworkObject>();
networkObject.NetworkManagerOwner = NetworkManager;
}
}
}
else // Get the in-scene placed NetworkObject
{
networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash, sceneObject.NetworkSceneHandle);
if (networkObject == null)
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
{
NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!");
}
}
// Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so
// NetworkBehaviours will have their OnNetworkSpawn method invoked
if (networkObject != null && !networkObject.gameObject.activeInHierarchy)
{
networkObject.gameObject.SetActive(true);
}
}
if (networkObject != null)
{
networkObject.DestroyWithScene = sceneObject.DestroyWithScene;
networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;
// SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject)
// This is a special case scenario where a late joining client has joined and loaded one or
// more scenes that contain nested in-scene placed NetworkObject children yet the server's
// synchronization information does not indicate the NetworkObject in question has a parent.
// Under this scenario, we want to remove the parent before spawning and setting the transform values.
if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.transform.parent != null)
{
// if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not
// include parenting, then we need to force the removal of that parent
if (networkObject.transform.parent.GetComponent<NetworkObject>() != null)
{
// remove the parent
networkObject.ApplyNetworkParenting(true, true);
}
}
// Set the transform unless we were spawned by a prefab handler
// Note: prefab handlers are provided the position and rotation
// but it is up to the user to set those values
if (sceneObject.HasTransform && !isSpawnedByPrefabHandler)
{
// If world position stays is true or we have auto object parent synchronization disabled
// then we want to apply the position and rotation values world space relative
if (worldPositionStays || !networkObject.AutoObjectParentSync)
{
networkObject.transform.position = position;
networkObject.transform.rotation = rotation;
}
else
{
networkObject.transform.localPosition = position;
networkObject.transform.localRotation = rotation;
}
// SPECIAL CASE:
// Since players are created uniquely we don't apply scale because
// the ConnectionApprovalResponse does not currently provide the
// ability to specify scale. So, we just use the default scale of
// the network prefab used to represent the player.
// Note: not doing this would set the player's scale to zero since
// that is the default value of Vector3.
if (!sceneObject.IsPlayerObject)
{
// Since scale is always applied to local space scale, we do the transform
// space logic during serialization such that it works out whether AutoObjectParentSync
// is enabled or not (see NetworkObject.SceneObject)
networkObject.transform.localScale = scale;
}
}
if (sceneObject.HasParent)
{
// Go ahead and set network parenting properties, if the latest parent is not set then pass in null
// (we always want to set worldPositionStays)
ulong? parentId = null;
if (sceneObject.IsLatestParentSet)
{
parentId = parentNetworkId;
}
networkObject.SetNetworkParenting(parentId, worldPositionStays);
}
// Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL
// until the scene is loaded. They are then migrated back into the newly loaded and currently active scene.
if (!sceneObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad)
{
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
}
}
return networkObject;
}
// Ran on both server and client
internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene)
{
if (networkObject == null)
{
throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object");
}
if (networkObject.IsSpawned)
{
throw new SpawnStateException("Object is already spawned");
}
if (!sceneObject)
{
var networkObjectChildren = networkObject.GetComponentsInChildren<NetworkObject>();
if (networkObjectChildren.Length > 1)
{
Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!");
}
}
SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene);
}
// Ran on both server and client
internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene)
{
if (networkObject == null)
{
throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object");
}
if (networkObject.IsSpawned)
{
throw new SpawnStateException("Object is already spawned");
}
SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene);
}
private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene)
{
if (SpawnedObjects.ContainsKey(networkId))
{
Debug.LogWarning($"Trying to spawn {nameof(NetworkObject.NetworkObjectId)} {networkId} that already exists!");
return;
}
networkObject.IsSpawned = true;
networkObject.IsSceneObject = sceneObject;
// Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects
// Note: Always check SceneOriginHandle directly at this specific location.
if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle == 0)
{
networkObject.SceneOrigin = networkObject.gameObject.scene;
}
// For integration testing, this makes sure that the appropriate NetworkManager is assigned to
// the NetworkObject since it uses the NetworkManager.Singleton when not set
if (networkObject.NetworkManagerOwner != NetworkManager)
{
networkObject.NetworkManagerOwner = NetworkManager;
}
networkObject.NetworkObjectId = networkId;
networkObject.DestroyWithScene = sceneObject || destroyWithScene;
networkObject.OwnerClientId = ownerClientId;
networkObject.IsPlayerObject = playerObject;
SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject);
SpawnedObjectsList.Add(networkObject);
if (NetworkManager.IsServer)
{
if (playerObject)
{
// If there was an already existing player object for this player, then mark it as no longer
// a player object.
if (NetworkManager.ConnectedClients[ownerClientId].PlayerObject != null)
{
NetworkManager.ConnectedClients[ownerClientId].PlayerObject.IsPlayerObject = false;
}
NetworkManager.ConnectedClients[ownerClientId].PlayerObject = networkObject;
}
}
else if (ownerClientId == NetworkManager.LocalClientId)
{
if (playerObject)
{
// If there was an already existing player object for this player, then mark it as no longer a player object.
if (NetworkManager.LocalClient.PlayerObject != null)
{
NetworkManager.LocalClient.PlayerObject.IsPlayerObject = false;
}
NetworkManager.LocalClient.PlayerObject = networkObject;
}
}
if (NetworkManager.IsServer)
{
for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++)
{
if (networkObject.CheckObjectVisibility == null || networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsList[i].ClientId))
{
networkObject.Observers.Add(NetworkManager.ConnectedClientsList[i].ClientId);
}
}
}
networkObject.ApplyNetworkParenting();
NetworkObject.CheckOrphanChildren();
networkObject.InvokeBehaviourNetworkSpawn();
NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnSpawn, networkId);
// propagate the IsSceneObject setting to child NetworkObjects
var children = networkObject.GetComponentsInChildren<NetworkObject>();
foreach (var childObject in children)
{
// Do not propagate the in-scene object setting if a child was dynamically spawned.
if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value)
{
continue;
}
childObject.IsSceneObject = sceneObject;
}
// Only dynamically spawned NetworkObjects are allowed
if (!sceneObject)
{
networkObject.SubscribeToActiveSceneForSynch();
}
}
internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject)
{
// If we are a host and sending to the host's client id, then we can skip sending ourselves the spawn message.
if (clientId == NetworkManager.ServerClientId)
{
return;
}
var message = new CreateObjectMessage
{
ObjectInfo = networkObject.GetMessageSceneObject(clientId)
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId);
NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size);
}
internal ulong? GetSpawnParentId(NetworkObject networkObject)
{
NetworkObject parentNetworkObject = null;
if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null)
{
parentNetworkObject = networkObject.transform.parent.GetComponent<NetworkObject>();
}
if (parentNetworkObject == null)
{
return null;
}
return parentNetworkObject.NetworkObjectId;
}
internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false)
{
if (!networkObject.IsSpawned)
{
throw new SpawnStateException("Object is not spawned");
}
if (!NetworkManager.IsServer)
{
throw new NotServerException("Only server can despawn objects");
}
OnDespawnObject(networkObject, destroyObject);
}
// Makes scene objects ready to be reused
internal void ServerResetShudownStateForSceneObjects()
{
#if UNITY_2023_1_OR_NEWER
var networkObjects = UnityEngine.Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true);
#else
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true);
#endif
foreach (var sobj in networkObjects)
{
sobj.IsSpawned = false;
sobj.DestroyWithScene = false;
sobj.IsSceneObject = null;
}
}
/// <summary>
/// Gets called only by NetworkSceneManager.SwitchScene
/// </summary>
internal void ServerDestroySpawnedSceneObjects()
{
// This Allocation is "OK" for now because this code only executes when a new scene is switched to
// We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove
// objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur.
var spawnedObjects = SpawnedObjectsList.ToList();
foreach (var sobj in spawnedObjects)
{
if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene)
{
SpawnedObjectsList.Remove(sobj);
UnityEngine.Object.Destroy(sobj.gameObject);
}
}
}
internal void DespawnAndDestroyNetworkObjects()
{
#if UNITY_2023_1_OR_NEWER
var networkObjects = UnityEngine.Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID);
#else
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>();
#endif
for (int i = 0; i < networkObjects.Length; i++)
{
if (networkObjects[i].NetworkManager == NetworkManager)
{
if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i]))
{
OnDespawnObject(networkObjects[i], false);
// Leave destruction up to the handler
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]);
}
else if (networkObjects[i].IsSpawned)
{
// If it is an in-scene placed NetworkObject then just despawn
// and let it be destroyed when the scene is unloaded. Otherwise, despawn and destroy it.
var shouldDestroy = !(networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value);
OnDespawnObject(networkObjects[i], shouldDestroy);
}
else if (networkObjects[i].IsSceneObject != null && !networkObjects[i].IsSceneObject.Value)
{
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
}
}
}
}
internal void DestroySceneObjects()
{
#if UNITY_2023_1_OR_NEWER
var networkObjects = UnityEngine.Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID);
#else
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>();
#endif
for (int i = 0; i < networkObjects.Length; i++)
{
if (networkObjects[i].NetworkManager == NetworkManager)
{
if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true)
{
if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i]))
{
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]);
if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId))
{
OnDespawnObject(networkObjects[i], false);
}
}
else
{
UnityEngine.Object.Destroy(networkObjects[i].gameObject);
}
}
}
}
}
internal void ServerSpawnSceneObjectsOnStartSweep()
{
#if UNITY_2023_1_OR_NEWER
var networkObjects = UnityEngine.Object.FindObjectsByType<NetworkObject>(FindObjectsSortMode.InstanceID);
#else
var networkObjects = UnityEngine.Object.FindObjectsOfType<NetworkObject>();
#endif
var networkObjectsToSpawn = new List<NetworkObject>();
for (int i = 0; i < networkObjects.Length; i++)
{
if (networkObjects[i].NetworkManager == NetworkManager)
{
if (networkObjects[i].IsSceneObject == null)
{
networkObjectsToSpawn.Add(networkObjects[i]);
}
}
}
foreach (var networkObject in networkObjectsToSpawn)
{
SpawnNetworkObjectLocally(networkObject, GetNetworkObjectId(), true, false, networkObject.OwnerClientId, true);
}
}
internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject)
{
if (NetworkManager == null)
{
return;
}
// We have to do this check first as subsequent checks assume we can access NetworkObjectId.
if (networkObject == null)
{
Debug.LogWarning($"Trying to destroy network object but it is null");
return;
}
// Removal of spawned object
if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId))
{
Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!");
return;
}
// If we are shutting down the NetworkManager, then ignore resetting the parent
// and only attempt to remove the child's parent on the server-side
if (!NetworkManager.ShutdownInProgress && NetworkManager.IsServer)
{
// Move child NetworkObjects to the root when parent NetworkObject is destroyed
foreach (var spawnedNetObj in SpawnedObjectsList)
{
var latestParent = spawnedNetObj.GetNetworkParenting();
if (latestParent.HasValue && latestParent.Value == networkObject.NetworkObjectId)
{
// Try to remove the parent using the cached WorldPositioNStays value
// Note: WorldPositionStays will still default to true if this was an
// in-scene placed NetworkObject and parenting was predefined in the
// scene via the editor.
if (!spawnedNetObj.TryRemoveParentCachedWorldPositionStays())
{
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogError($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} could not be moved to the root when its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} was being destroyed");
}
}
if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed");
}
}
}
}
networkObject.InvokeBehaviourNetworkDespawn();
if (NetworkManager != null && NetworkManager.IsServer)
{
if (NetworkManager.NetworkConfig.RecycleNetworkIds)
{
ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId()
{
NetworkId = networkObject.NetworkObjectId,
ReleaseTime = NetworkManager.RealTimeProvider.UnscaledTime
});
}
if (networkObject != null)
{
// As long as we have any remaining clients, then notify of the object being destroy.
if (NetworkManager.ConnectedClientsList.Count > 0)
{
m_TargetClientIds.Clear();
// We keep only the client for which the object is visible
// as the other clients have them already despawned
foreach (var clientId in NetworkManager.ConnectedClientsIds)
{
if (networkObject.IsNetworkVisibleTo(clientId))
{
m_TargetClientIds.Add(clientId);
}
}
var message = new DestroyObjectMessage
{
NetworkObjectId = networkObject.NetworkObjectId,
DestroyGameObject = networkObject.IsSceneObject != false ? destroyGameObject : true
};
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, m_TargetClientIds);
foreach (var targetClientId in m_TargetClientIds)
{
NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size);
}
}
}
}
networkObject.IsSpawned = false;
if (SpawnedObjects.Remove(networkObject.NetworkObjectId))
{
SpawnedObjectsList.Remove(networkObject);
}
// Always clear out the observers list when despawned
networkObject.Observers.Clear();
var gobj = networkObject.gameObject;
if (destroyGameObject && gobj != null)
{
if (NetworkManager.PrefabHandler.ContainsHandler(networkObject))
{
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject);
}
else
{
UnityEngine.Object.Destroy(gobj);
}
}
}
/// <summary>
/// Updates all spawned <see cref="NetworkObject.Observers"/> for the specified client
/// Note: if the clientId is the server then it is observable to all spawned <see cref="NetworkObject"/>'s
/// </summary>
internal void UpdateObservedNetworkObjects(ulong clientId)
{
foreach (var sobj in SpawnedObjectsList)
{
if (sobj.CheckObjectVisibility == null)
{
if (!sobj.Observers.Contains(clientId))
{
sobj.Observers.Add(clientId);
}
}
else
{
if (sobj.CheckObjectVisibility(clientId))
{
sobj.Observers.Add(clientId);
}
else if (sobj.Observers.Contains(clientId))
{
sobj.Observers.Remove(clientId);
}
}
}
}
}
}