using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Unity.Netcode { /// /// Class that handles object spawning /// public class NetworkSpawnManager { /// /// The currently spawned objects /// public readonly Dictionary SpawnedObjects = new Dictionary(); /// /// A list of the spawned objects /// public readonly HashSet SpawnedObjectsList = new HashSet(); /// /// 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 /// public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); /// /// Object to Ownership Table: /// [NetworkObjectId][ClientId] /// Used internally to find the client Id that currently owns /// the NetworkObject /// private Dictionary m_ObjectToOwnershipTable = new Dictionary(); /// /// Used to update a NetworkObject's ownership /// 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()); } // 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}!"); } } /// /// Returns a list of all NetworkObjects that belong to a client. /// /// the client's id public List GetClientOwnedObjects(ulong clientId) { if (!OwnershipToObjectsTable.ContainsKey(clientId)) { OwnershipToObjectsTable.Add(clientId, new Dictionary()); } return OwnershipToObjectsTable[clientId].Values.ToList(); } /// /// Gets the NetworkManager associated with this SpawnManager. /// public NetworkManager NetworkManager { get; } internal NetworkSpawnManager(NetworkManager networkManager) { NetworkManager = networkManager; } internal readonly Queue ReleasedNetworkObjectIds = new Queue(); private ulong m_NetworkObjectIdCounter; // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations private List m_TargetClientIds = new List(); internal ulong GetNetworkObjectId() { if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (Time.unscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) { return ReleasedNetworkObjectIds.Dequeue().NetworkId; } m_NetworkObjectIdCounter++; return m_NetworkObjectIdCounter; } /// /// Returns the local player object or null if one does not exist /// /// The local player object or null if one does not exist public NetworkObject GetLocalPlayerObject() { return GetPlayerNetworkObject(NetworkManager.LocalClientId); } /// /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. /// /// The player object with a given clientId or null if one does not exist 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; } // Server removes the entry and takes over ownership before notifying UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true); networkObject.OwnerClientId = NetworkManager.ServerClientId; 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); } } /// /// Helper function to get a network client for a clientId from the NetworkManager. /// On the server this will check the list. /// On a non-server this will check the only. /// /// The clientId for which to try getting the NetworkClient for. /// The found NetworkClient. Null if no client was found. /// True if a NetworkClient with a matching id was found else false. 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; // Server adds entries for all client ownership UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); 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); } } internal bool HasPrefab(bool isSceneObject, uint globalObjectIdHash) { if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) { if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { return true; } if (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks.TryGetValue(globalObjectIdHash, 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(globalObjectIdHash); return networkObject != null; } /// /// Should only run on the client /// internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalObjectIdHash, ulong ownerClientId, ulong? parentNetworkId, Vector3? position, Quaternion? rotation, bool isReparented = false) { NetworkObject parentNetworkObject = null; if (parentNetworkId != null && !isReparented) { if (SpawnedObjects.TryGetValue(parentNetworkId.Value, out NetworkObject networkObject)) { parentNetworkObject = networkObject; } else { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { NetworkLog.LogWarning("Cannot find parent. Parent objects always have to be spawned and replicated BEFORE the child"); } } } if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) { // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject var networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerClientId, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity)); networkObject.NetworkManagerOwner = NetworkManager; if (parentNetworkObject != null) { networkObject.transform.SetParent(parentNetworkObject.transform, true); } if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) { UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } return networkObject; } else { // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash GameObject networkPrefabReference = null; if (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) { switch (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks[globalObjectIdHash].Override) { default: case NetworkPrefabOverride.None: networkPrefabReference = NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks[globalObjectIdHash].Prefab; break; case NetworkPrefabOverride.Hash: case NetworkPrefabOverride.Prefab: networkPrefabReference = NetworkManager.NetworkConfig.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)}?"); } return null; } // Otherwise, instantiate an instance of the NetworkPrefab linked to the prefabHash var networkObject = ((position == null && rotation == null) ? UnityEngine.Object.Instantiate(networkPrefabReference) : UnityEngine.Object.Instantiate(networkPrefabReference, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity))).GetComponent(); networkObject.NetworkManagerOwner = NetworkManager; if (parentNetworkObject != null) { networkObject.transform.SetParent(parentNetworkObject.transform, true); } if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) { UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } return networkObject; } } else { var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash); 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}!"); } return null; } if (parentNetworkObject != null) { networkObject.transform.SetParent(parentNetworkObject.transform, true); } 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(); 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, FastBufferReader variableData, bool destroyWithScene) { if (networkObject == null) { throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); } if (networkObject.IsSpawned) { throw new SpawnStateException("Object is already spawned"); } networkObject.SetNetworkVariableData(variableData); SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.Header.NetworkObjectId, sceneObject.Header.IsSceneObject, sceneObject.Header.IsPlayerObject, sceneObject.Header.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; } // this initialization really should be at the bottom of the function networkObject.IsSpawned = true; // this initialization really should be at the top of this function. If and when we break the // NetworkVariable dependency on NetworkBehaviour, this otherwise creates problems because // SetNetworkVariableData above calls InitializeVariables, and the 'baked out' data isn't ready there; // the current design banks on getting the network behaviour set and then only reading from it after the // below initialization code. However cowardice compels me to hold off on moving this until that commit networkObject.IsSceneObject = sceneObject; 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.SetCachedParent(networkObject.transform.parent); networkObject.ApplyNetworkParenting(); NetworkObject.CheckOrphanChildren(); networkObject.InvokeBehaviourNetworkSpawn(); NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnSpawn, networkId); // propagate the IsSceneObject setting to child NetworkObjects var children = networkObject.GetComponentsInChildren(); foreach (var childObject in children) { childObject.IsSceneObject = sceneObject; } } internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) { //Currently, if this is called and the clientId (destination) is the server's client Id, this case will be checked // within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer placing this check here. [NSS] if (NetworkManager.IsServer && 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); networkObject.MarkVariablesDirty(); } internal ulong? GetSpawnParentId(NetworkObject networkObject) { NetworkObject parentNetworkObject = null; if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) { parentNetworkObject = networkObject.transform.parent.GetComponent(); } 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() { var networkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); foreach (var sobj in networkObjects) { sobj.IsSpawned = false; sobj.DestroyWithScene = false; sobj.IsSceneObject = null; } } /// /// Gets called only by NetworkSceneManager.SwitchScene /// 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() { var networkObjects = UnityEngine.Object.FindObjectsOfType(); 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() { var networkObjects = UnityEngine.Object.FindObjectsOfType(); 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() { var networkObjects = UnityEngine.Object.FindObjectsOfType(); var networkObjectsToSpawn = new List(); 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 if (!NetworkManager.ShutdownInProgress) { // Move child NetworkObjects to the root when parent NetworkObject is destroyed foreach (var spawnedNetObj in SpawnedObjectsList) { var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); if (isReparented && latestParent == networkObject.NetworkObjectId) { spawnedNetObj.gameObject.transform.parent = null; 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 = Time.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 }; 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); } var gobj = networkObject.gameObject; if (destroyGameObject && gobj != null) { if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) { NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); } else { UnityEngine.Object.Destroy(gobj); } } } /// /// Updates all spawned for the specified client /// Note: if the clientId is the server then it is observable to all spawned 's /// 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); } } } } } }