using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.SceneManagement; namespace Unity.Netcode { /// /// A component used to identify that a GameObject in the network /// [AddComponentMenu("Netcode/Network Object", -99)] [DisallowMultipleComponent] public sealed class NetworkObject : MonoBehaviour { [HideInInspector] [SerializeField] internal uint GlobalObjectIdHash; #if UNITY_EDITOR private void OnValidate() { GenerateGlobalObjectIdHash(); } internal void GenerateGlobalObjectIdHash() { // do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode if (UnityEditor.EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name)) { return; } // do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) { return; } var globalObjectIdString = UnityEditor.GlobalObjectId.GetGlobalObjectIdSlow(this).ToString(); GlobalObjectIdHash = XXHash.Hash32(globalObjectIdString); } #endif // UNITY_EDITOR /// /// Gets the NetworkManager that owns this NetworkObject instance /// public NetworkManager NetworkManager => NetworkManagerOwner ?? NetworkManager.Singleton; /// /// The NetworkManager that owns this NetworkObject. /// This property controls where this NetworkObject belongs. /// This property is null by default currently, which means that the above NetworkManager getter will return the Singleton. /// In the future this is the path where alternative NetworkManagers should be injected for running multi NetworkManagers /// internal NetworkManager NetworkManagerOwner; /// /// Gets the unique Id of this object that is synced across the network /// public ulong NetworkObjectId { get; internal set; } /// /// Gets the ClientId of the owner of this NetworkObject /// public ulong OwnerClientId { get; internal set; } /// /// If true, the object will always be replicated as root on clients and the parent will be ignored. /// public bool AlwaysReplicateAsRoot; /// /// Gets if this object is a player object /// public bool IsPlayerObject { get; internal set; } /// /// Gets if the object is the personal clients player object /// public bool IsLocalPlayer => NetworkManager != null && IsPlayerObject && OwnerClientId == NetworkManager.LocalClientId; /// /// Gets if the object is owned by the local player or if the object is the local player object /// public bool IsOwner => NetworkManager != null && OwnerClientId == NetworkManager.LocalClientId; /// /// Gets Whether or not the object is owned by anyone /// public bool IsOwnedByServer => NetworkManager != null && OwnerClientId == NetworkManager.ServerClientId; /// /// Gets if the object has yet been spawned across the network /// public bool IsSpawned { get; internal set; } /// /// Gets if the object is a SceneObject, null if it's not yet spawned but is a scene object. /// public bool? IsSceneObject { get; internal set; } /// /// Gets whether or not the object should be automatically removed when the scene is unloaded. /// public bool DestroyWithScene { get; set; } /// /// Delegate type for checking visibility /// /// The clientId to check visibility for public delegate bool VisibilityDelegate(ulong clientId); /// /// Delegate invoked when the netcode needs to know if the object should be visible to a client, if null it will assume true /// public VisibilityDelegate CheckObjectVisibility = null; /// /// Delegate type for checking spawn options /// /// The clientId to check spawn options for public delegate bool SpawnDelegate(ulong clientId); /// /// Delegate invoked when the netcode needs to know if it should include the transform when spawning the object, if null it will assume true /// public SpawnDelegate IncludeTransformWhenSpawning = null; /// /// Whether or not to destroy this object if it's owner is destroyed. /// If true, the objects ownership will be given to the server. /// public bool DontDestroyWithOwner; /// /// Whether or not to enable automatic NetworkObject parent synchronization. /// public bool AutoObjectParentSync = true; internal readonly HashSet Observers = new HashSet(); #if MULTIPLAYER_TOOLS private string m_CachedNameForMetrics; #endif internal string GetNameForMetrics() { #if MULTIPLAYER_TOOLS return m_CachedNameForMetrics ??= name; #else return null; #endif } private readonly HashSet m_EmptyULongHashSet = new HashSet(); /// /// Returns Observers enumerator /// /// Observers enumerator public HashSet.Enumerator GetObservers() { if (!IsSpawned) { return m_EmptyULongHashSet.GetEnumerator(); } return Observers.GetEnumerator(); } /// /// Whether or not this object is visible to a specific client /// /// The clientId of the client /// True if the client knows about the object public bool IsNetworkVisibleTo(ulong clientId) { if (!IsSpawned) { return false; } return Observers.Contains(clientId); } /// /// In the event the scene of origin gets unloaded, we keep /// the most important part to uniquely identify in-scene /// placed NetworkObjects /// internal int SceneOriginHandle = 0; private Scene m_SceneOrigin; /// /// The scene where the NetworkObject was first instantiated /// Note: Primarily for in-scene placed NetworkObjects /// We need to keep track of the original scene of origin for /// the NetworkObject in order to be able to uniquely identify it /// using the scene of origin's handle. /// internal Scene SceneOrigin { get { return m_SceneOrigin; } set { // The scene origin should only be set once. // Once set, it should never change. if (SceneOriginHandle == 0 && value.IsValid() && value.isLoaded) { m_SceneOrigin = value; SceneOriginHandle = value.handle; } } } /// /// Helper method to return the correct scene handle /// Note: Do not use this within NetworkSpawnManager.SpawnNetworkObjectLocallyCommon /// internal int GetSceneOriginHandle() { if (SceneOriginHandle == 0 && IsSpawned && IsSceneObject != false) { throw new Exception($"{nameof(GetSceneOriginHandle)} called when {nameof(SceneOriginHandle)} is still zero but the {nameof(NetworkObject)} is already spawned!"); } return SceneOriginHandle != 0 ? SceneOriginHandle : gameObject.scene.handle; } private void Awake() { SetCachedParent(transform.parent); SceneOrigin = gameObject.scene; } /// /// Makes the previously hidden "netcode visible" to the targeted client. /// /// /// Usage: Use to start sending updates for a previously hidden to the targeted client.
///
/// Dynamically Spawned: s will be instantiated and spawned on the targeted client side.
/// In-Scene Placed: The instantiated but despawned s will be spawned on the targeted client side.
///
/// See Also:
///
/// or
///
/// The targeted client public void NetworkShow(ulong clientId) { if (!IsSpawned) { throw new SpawnStateException("Object is not spawned"); } if (!NetworkManager.IsServer) { throw new NotServerException("Only server can change visibility"); } if (Observers.Contains(clientId)) { throw new VisibilityChangeException("The object is already visible"); } Observers.Add(clientId); NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this); } /// /// Makes a list of previously hidden s "netcode visible" for the client specified. /// /// /// Usage: Use to start sending updates for previously hidden s to the targeted client.
///
/// Dynamically Spawned: s will be instantiated and spawned on the targeted client's side.
/// In-Scene Placed: Already instantiated but despawned s will be spawned on the targeted client's side.
///
/// See Also:
///
/// or
///
/// The objects to become "netcode visible" to the targeted client /// The targeted client public static void NetworkShow(List networkObjects, ulong clientId) { if (networkObjects == null || networkObjects.Count == 0) { throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided"); } NetworkManager networkManager = networkObjects[0].NetworkManager; if (!networkManager.IsServer) { throw new NotServerException("Only server can change visibility"); } // Do the safety loop first to prevent putting the netcode in an invalid state. for (int i = 0; i < networkObjects.Count; i++) { if (!networkObjects[i].IsSpawned) { throw new SpawnStateException("Object is not spawned"); } if (networkObjects[i].Observers.Contains(clientId)) { throw new VisibilityChangeException($"{nameof(NetworkObject)} with NetworkId: {networkObjects[i].NetworkObjectId} is already visible"); } if (networkObjects[i].NetworkManager != networkManager) { throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager)); } } foreach (var networkObject in networkObjects) { networkObject.NetworkShow(clientId); } } /// /// Hides the from the targeted client. /// /// /// Usage: Use to stop sending updates to the targeted client, "netcode invisible", for a currently visible .
///
/// Dynamically Spawned: s will be despawned and destroyed on the targeted client's side.
/// In-Scene Placed: s will only be despawned on the targeted client's side.
///
/// See Also:
///
/// or
///
/// The targeted client public void NetworkHide(ulong clientId) { if (!IsSpawned) { throw new SpawnStateException("Object is not spawned"); } if (!NetworkManager.IsServer) { throw new NotServerException("Only server can change visibility"); } if (!Observers.Contains(clientId)) { throw new VisibilityChangeException("The object is already hidden"); } if (clientId == NetworkManager.ServerClientId) { throw new VisibilityChangeException("Cannot hide an object from the server"); } Observers.Remove(clientId); var message = new DestroyObjectMessage { NetworkObjectId = NetworkObjectId, DestroyGameObject = !IsSceneObject.Value }; // Send destroy call var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size); } /// /// Hides a list of s from the targeted client. /// /// /// Usage: Use to stop sending updates to the targeted client, "netcode invisible", for the currently visible s.
///
/// Dynamically Spawned: s will be despawned and destroyed on the targeted client's side.
/// In-Scene Placed: s will only be despawned on the targeted client's side.
///
/// See Also:
///
/// or
///
/// The s that will become "netcode invisible" to the targeted client /// The targeted client public static void NetworkHide(List networkObjects, ulong clientId) { if (networkObjects == null || networkObjects.Count == 0) { throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided"); } var networkManager = networkObjects[0].NetworkManager; if (!networkManager.IsServer) { throw new NotServerException("Only server can change visibility"); } if (clientId == NetworkManager.ServerClientId) { throw new VisibilityChangeException("Cannot hide an object from the server"); } // Do the safety loop first to prevent putting the netcode in an invalid state. for (int i = 0; i < networkObjects.Count; i++) { if (!networkObjects[i].IsSpawned) { throw new SpawnStateException("Object is not spawned"); } if (!networkObjects[i].Observers.Contains(clientId)) { throw new VisibilityChangeException($"{nameof(NetworkObject)} with {nameof(NetworkObjectId)}: {networkObjects[i].NetworkObjectId} is already hidden"); } if (networkObjects[i].NetworkManager != networkManager) { throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager)); } } foreach (var networkObject in networkObjects) { networkObject.NetworkHide(clientId); } } private void OnDestroy() { if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned && (IsSceneObject == null || (IsSceneObject.Value != true))) { throw new NotServerException($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); } if (NetworkManager != null && NetworkManager.SpawnManager != null && NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) { if (this == networkObject) { NetworkManager.SpawnManager.OnDespawnObject(networkObject, false); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (!NetworkManager.IsListening) { throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects"); } if (!NetworkManager.IsServer) { throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); } NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) { if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) { NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); } } } /// /// Spawns this across the network. Can only be called from the Server /// /// Should the object be destroyed when the scene is changed public void Spawn(bool destroyWithScene = false) { SpawnInternal(destroyWithScene, NetworkManager.ServerClientId, false); } /// /// Spawns a across the network with a given owner. Can only be called from server /// /// The clientId to own the object /// Should the object be destroyed when the scene is changed public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) { SpawnInternal(destroyWithScene, clientId, false); } /// /// Spawns a across the network and makes it the player object for the given client /// /// The clientId who's player object this is /// Should the object be destroyed when the scene is changed public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) { SpawnInternal(destroyWithScene, clientId, true); } /// /// Despawns the of this and sends a destroy message for it to all connected clients. /// /// (true) the will be destroyed (false) the will persist after being despawned public void Despawn(bool destroy = true) { MarkVariablesDirty(false); NetworkManager.SpawnManager.DespawnObject(this, destroy); } /// /// Removes all ownership of an object from any client. Can only be called from server /// public void RemoveOwnership() { NetworkManager.SpawnManager.RemoveOwnership(this); } /// /// Changes the owner of the object. Can only be called from server /// /// The new owner clientId public void ChangeOwnership(ulong newOwnerClientId) { NetworkManager.SpawnManager.ChangeOwnership(this, newOwnerClientId); } internal void InvokeBehaviourOnLostOwnership() { // Server already handles this earlier, hosts should ignore, all clients should update if (!NetworkManager.IsServer) { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true); } for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].InternalOnLostOwnership(); } } internal void InvokeBehaviourOnGainedOwnership() { // Server already handles this earlier, hosts should ignore and only client owners should update if (!NetworkManager.IsServer && NetworkManager.LocalClientId == OwnerClientId) { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); } for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) { ChildNetworkBehaviours[i].InternalOnGainedOwnership(); } else { Debug.LogWarning($"{ChildNetworkBehaviours[i].gameObject.name} is disabled! Netcode for GameObjects does not support disabled NetworkBehaviours! The {ChildNetworkBehaviours[i].GetType().Name} component was skipped during ownership assignment!"); } } } internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].OnNetworkObjectParentChanged(parentNetworkObject); } } private ulong? m_LatestParent; // What is our last set parent NetworkObject's ID? private Transform m_CachedParent; // What is our last set parent Transform reference? private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent internal void SetCachedParent(Transform parentTransform) { m_CachedParent = parentTransform; } internal ulong? GetNetworkParenting() => m_LatestParent; internal void SetNetworkParenting(ulong? latestParent, bool worldPositionStays) { m_LatestParent = latestParent; m_CachedWorldPositionStays = worldPositionStays; } /// /// Set the parent of the NetworkObject transform. /// /// The new parent for this NetworkObject transform will be the child of. /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. /// Whether or not reparenting was successful. public bool TrySetParent(Transform parent, bool worldPositionStays = true) { var networkObject = parent.GetComponent(); // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); } /// /// Set the parent of the NetworkObject transform. /// /// The new parent for this NetworkObject transform will be the child of. /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. /// Whether or not reparenting was successful. public bool TrySetParent(GameObject parent, bool worldPositionStays = true) { // If we are removing ourself from a parent if (parent == null) { return TrySetParent((NetworkObject)null, worldPositionStays); } var networkObject = parent.GetComponent(); // If the parent doesn't have a NetworkObjet then return false, otherwise continue trying to parent return networkObject == null ? false : TrySetParent(networkObject, worldPositionStays); } /// /// Used when despawning the parent, we want to preserve the cached WorldPositionStays value /// internal bool TryRemoveParentCachedWorldPositionStays() { return TrySetParent((NetworkObject)null, m_CachedWorldPositionStays); } /// /// Removes the parent of the NetworkObject's transform /// /// /// This is a more convenient way to remove the parent without having to cast the null value to either or /// /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. /// public bool TryRemoveParent(bool worldPositionStays = true) { return TrySetParent((NetworkObject)null, worldPositionStays); } /// /// Set the parent of the NetworkObject transform. /// /// The new parent for this NetworkObject transform will be the child of. /// If true, the parent-relative position, scale and rotation are modified such that the object keeps the same world space position, rotation and scale as before. /// Whether or not reparenting was successful. public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) { if (!AutoObjectParentSync) { return false; } if (NetworkManager == null || !NetworkManager.IsListening) { return false; } if (!NetworkManager.IsServer) { return false; } if (!IsSpawned) { return false; } if (parent != null && !parent.IsSpawned) { return false; } m_CachedWorldPositionStays = worldPositionStays; if (parent == null) { transform.SetParent(null, worldPositionStays); } else { transform.SetParent(parent.transform, worldPositionStays); } return true; } private void OnTransformParentChanged() { if (!AutoObjectParentSync) { return; } if (transform.parent == m_CachedParent) { return; } if (NetworkManager == null || !NetworkManager.IsListening) { transform.parent = m_CachedParent; Debug.LogException(new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before reparenting")); return; } if (!NetworkManager.IsServer) { transform.parent = m_CachedParent; Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s")); return; } if (!IsSpawned) { transform.parent = m_CachedParent; Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); return; } var removeParent = false; var parentTransform = transform.parent; if (parentTransform != null) { if (!transform.parent.TryGetComponent(out var parentObject)) { transform.parent = m_CachedParent; Debug.LogException(new InvalidParentException($"Invalid parenting, {nameof(NetworkObject)} moved under a non-{nameof(NetworkObject)} parent")); return; } if (!parentObject.IsSpawned) { transform.parent = m_CachedParent; Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented under another spawned {nameof(NetworkObject)}")); return; } m_LatestParent = parentObject.NetworkObjectId; } else { m_LatestParent = null; removeParent = m_CachedParent != null; } ApplyNetworkParenting(removeParent); var message = new ParentSyncMessage { NetworkObjectId = NetworkObjectId, IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue, LatestParent = m_LatestParent, RemoveParent = removeParent, WorldPositionStays = m_CachedWorldPositionStays, Position = m_CachedWorldPositionStays ? transform.position : transform.localPosition, Rotation = m_CachedWorldPositionStays ? transform.rotation : transform.localRotation, Scale = transform.localScale, }; // We need to preserve the m_CachedWorldPositionStays value until after we create the message // in order to assure any local space values changed/reset get applied properly. If our // parent is null then go ahead and reset the m_CachedWorldPositionStays the default value. if (parentTransform == null) { m_CachedWorldPositionStays = true; } unsafe { var maxCount = NetworkManager.ConnectedClientsIds.Count; ulong* clientIds = stackalloc ulong[maxCount]; int idx = 0; foreach (var clientId in NetworkManager.ConnectedClientsIds) { if (Observers.Contains(clientId)) { clientIds[idx++] = clientId; } } NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientIds, idx); } } // We're keeping this set called OrphanChildren which contains NetworkObjects // because at the time we initialize/spawn NetworkObject locally, we might not have its parent replicated from the other side // // For instance, if we're spawning NetworkObject 5 and its parent is 10, what should happen if we do not have 10 yet? // let's say 10 is on the way to be replicated in a few frames and we could fix that parent-child relationship later. // // If you couldn't find your parent, we put you into OrphanChildren set and every time we spawn another NetworkObject locally due to replication, // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false) { if (!AutoObjectParentSync) { return false; } // SPECIAL CASE: // The ignoreNotSpawned 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. This is the only scenario where the ignoreNotSpawned parameter is used. if (!IsSpawned && !ignoreNotSpawned) { return false; } // Handle the first in-scene placed NetworkObject parenting scenarios. Once the m_LatestParent // has been set, this will not be entered into again (i.e. the later code will be invoked and // users will get notifications when the parent changes). var isInScenePlaced = IsSceneObject.HasValue && IsSceneObject.Value; if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && isInScenePlaced) { var parentNetworkObject = transform.parent.GetComponent(); // If parentNetworkObject is null then the parent is a GameObject without a NetworkObject component // attached. Under this case, we preserve the hierarchy but we don't keep track of the parenting. // Note: We only start tracking parenting if the user removes the child from the standard GameObject // parent and then re-parents the child under a GameObject with a NetworkObject component attached. if (parentNetworkObject == null) { // If we are parented under a GameObject, go ahead and mark the world position stays as false // so clients synchronize their transform in local space. (only for in-scene placed NetworkObjects) m_CachedWorldPositionStays = false; return true; } else // If the parent still isn't spawned add this to the orphaned children and return false if (!parentNetworkObject.IsSpawned) { OrphanChildren.Add(this); return false; } else { // If we made it this far, go ahead and set the network parenting values // with the WorldPoisitonSays value set to false // Note: Since in-scene placed NetworkObjects are parented in the scene // the default "assumption" is that children are parenting local space // relative. SetNetworkParenting(parentNetworkObject.NetworkObjectId, false); // Set the cached parent m_CachedParent = parentNetworkObject.transform; return true; } } // If we are removing the parent or our latest parent is not set, then remove the parent // removeParent is only set when: // - The server-side NetworkObject.OnTransformParentChanged is invoked and the parent is being removed // - The client-side when handling a ParentSyncMessage // When clients are synchronizing only the m_LatestParent.HasValue will not have a value if there is no parent // or a parent was removed prior to the client connecting (i.e. in-scene placed NetworkObjects) if (removeParent || !m_LatestParent.HasValue) { m_CachedParent = null; // We must use Transform.SetParent when taking WorldPositionStays into // consideration, otherwise just setting transform.parent = null defaults // to WorldPositionStays which can cause scaling issues if the parent's // scale is not the default (Vetctor3.one) value. transform.SetParent(null, m_CachedWorldPositionStays); InvokeBehaviourOnNetworkObjectParentChanged(null); return true; } // If we have a latest parent id but it hasn't been spawned yet, then add this instance to the orphanChildren // HashSet and return false (i.e. parenting not applied yet) if (m_LatestParent.HasValue && !NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) { OrphanChildren.Add(this); return false; } // If we made it here, then parent this instance under the parentObject var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value]; m_CachedParent = parentObject.transform; transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); InvokeBehaviourOnNetworkObjectParentChanged(parentObject); return true; } internal static void CheckOrphanChildren() { var objectsToRemove = new List(); foreach (var orphanObject in OrphanChildren) { if (orphanObject.ApplyNetworkParenting()) { objectsToRemove.Add(orphanObject); } } foreach (var networkObject in objectsToRemove) { OrphanChildren.Remove(networkObject); } } internal void InvokeBehaviourNetworkSpawn() { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) { ChildNetworkBehaviours[i].InternalOnNetworkSpawn(); } else { Debug.LogWarning($"{ChildNetworkBehaviours[i].gameObject.name} is disabled! Netcode for GameObjects does not support spawning disabled NetworkBehaviours! The {ChildNetworkBehaviours[i].GetType().Name} component was skipped during spawn!"); } } for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) { ChildNetworkBehaviours[i].VisibleOnNetworkSpawn(); } } } internal void InvokeBehaviourNetworkDespawn() { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true); for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].InternalOnNetworkDespawn(); } } private List m_ChildNetworkBehaviours; internal List ChildNetworkBehaviours { get { if (m_ChildNetworkBehaviours != null) { return m_ChildNetworkBehaviours; } m_ChildNetworkBehaviours = new List(); var networkBehaviours = GetComponentsInChildren(true); for (int i = 0; i < networkBehaviours.Length; i++) { if (networkBehaviours[i].NetworkObject == this) { m_ChildNetworkBehaviours.Add(networkBehaviours[i]); } } return m_ChildNetworkBehaviours; } } internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behavior = ChildNetworkBehaviours[i]; behavior.InitializeVariables(); behavior.WriteNetworkVariableData(writer, targetClientId); } } internal void MarkVariablesDirty(bool dirty) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behavior = ChildNetworkBehaviours[i]; behavior.MarkVariablesDirty(dirty); } } // NGO currently guarantees that the client will receive spawn data for all objects in one network tick. // Children may arrive before their parents; when they do they are stored in OrphanedChildren and then // resolved when their parents arrived. Because we don't send a partial list of spawns (yet), something // has gone wrong if by the end of an update we still have unresolved orphans // // if and when we have different systems for where it is expected that orphans survive across ticks, // then this warning will remind us that we need to revamp the system because then we can no longer simply // spawn the orphan without its parent (at least, not when its transform is set to local coords mode) // - because then you'll have children popping at the wrong location not having their parent's global position to root them // - and then they'll pop to the correct location after they get the parent, and that would be not good internal static void VerifyParentingStatus() { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { if (OrphanChildren.Count > 0) { NetworkLog.LogWarning($"{nameof(NetworkObject)} ({OrphanChildren.Count}) children not resolved to parents by the end of frame"); } } } /// /// Only invoked during first synchronization of a NetworkObject (late join or newly spawned) /// internal void SetNetworkVariableData(FastBufferReader reader, ulong clientId) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behaviour = ChildNetworkBehaviours[i]; behaviour.InitializeVariables(); behaviour.SetNetworkVariableData(reader, clientId); } } internal ushort GetNetworkBehaviourOrderIndex(NetworkBehaviour instance) { // read the cached index, and verify it first if (instance.NetworkBehaviourIdCache < ChildNetworkBehaviours.Count) { if (ChildNetworkBehaviours[instance.NetworkBehaviourIdCache] == instance) { return instance.NetworkBehaviourIdCache; } // invalid cached id reset instance.NetworkBehaviourIdCache = default; } for (ushort i = 0; i < ChildNetworkBehaviours.Count; i++) { if (ChildNetworkBehaviours[i] == instance) { // cache the id, for next query instance.NetworkBehaviourIdCache = i; return i; } } return 0; } internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) { if (index >= ChildNetworkBehaviours.Count) { if (NetworkLog.CurrentLogLevel <= LogLevel.Error) { NetworkLog.LogError($"{nameof(NetworkBehaviour)} index {index} was out of bounds for {name}. NetworkBehaviours must be the same, and in the same order, between server and client."); } if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { var currentKnownChildren = new System.Text.StringBuilder(); currentKnownChildren.Append($"Known child {nameof(NetworkBehaviour)}s:"); for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var childNetworkBehaviour = ChildNetworkBehaviours[i]; currentKnownChildren.Append($" [{i}] {childNetworkBehaviour.__getTypeName()}"); currentKnownChildren.Append(i < ChildNetworkBehaviours.Count - 1 ? "," : "."); } NetworkLog.LogInfo(currentKnownChildren.ToString()); } return null; } return ChildNetworkBehaviours[index]; } internal struct SceneObject { private byte m_BitField; public uint Hash; public ulong NetworkObjectId; public ulong OwnerClientId; public bool IsPlayerObject { get => ByteUtility.GetBit(m_BitField, 0); set => ByteUtility.SetBit(ref m_BitField, 0, value); } public bool HasParent { get => ByteUtility.GetBit(m_BitField, 1); set => ByteUtility.SetBit(ref m_BitField, 1, value); } public bool IsSceneObject { get => ByteUtility.GetBit(m_BitField, 2); set => ByteUtility.SetBit(ref m_BitField, 2, value); } public bool HasTransform { get => ByteUtility.GetBit(m_BitField, 3); set => ByteUtility.SetBit(ref m_BitField, 3, value); } public bool IsLatestParentSet { get => ByteUtility.GetBit(m_BitField, 4); set => ByteUtility.SetBit(ref m_BitField, 4, value); } public bool WorldPositionStays { get => ByteUtility.GetBit(m_BitField, 5); set => ByteUtility.SetBit(ref m_BitField, 5, value); } //If(Metadata.HasParent) public ulong ParentObjectId; //If(Metadata.HasTransform) public struct TransformData : INetworkSerializeByMemcpy { public Vector3 Position; public Quaternion Rotation; public Vector3 Scale; } public TransformData Transform; //If(Metadata.IsReparented) //If(IsLatestParentSet) public ulong? LatestParent; public NetworkObject OwnerObject; public ulong TargetClientId; public int NetworkSceneHandle; public void Serialize(FastBufferWriter writer) { writer.WriteValueSafe(m_BitField); writer.WriteValueSafe(Hash); BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, OwnerClientId); if (HasParent) { BytePacker.WriteValueBitPacked(writer, ParentObjectId); if (IsLatestParentSet) { BytePacker.WriteValueBitPacked(writer, LatestParent.Value); } } var writeSize = 0; writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; if (!writer.TryBeginWrite(writeSize)) { throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); } if (HasTransform) { writer.WriteValue(Transform); } // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use // this to locate their local instance of the in-scene placed NetworkObject instance. // Only written for in-scene placed NetworkObjects. if (IsSceneObject) { writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); } public void Deserialize(FastBufferReader reader) { reader.ReadValueSafe(out m_BitField); reader.ReadValueSafe(out Hash); ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId); ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId); if (HasParent) { ByteUnpacker.ReadValueBitPacked(reader, out ParentObjectId); if (IsLatestParentSet) { ByteUnpacker.ReadValueBitPacked(reader, out ulong latestParent); LatestParent = latestParent; } } var readSize = 0; readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += IsSceneObject ? FastBufferWriter.GetWriteSize() : 0; // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) { throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } if (HasTransform) { reader.ReadValue(out Transform); } // In-Scene NetworkObjects are uniquely identified NetworkPrefabs defined by their // NetworkSceneHandle and GlobalObjectIdHash. Client-side NetworkSceneManagers use // this to locate their local instance of the in-scene placed NetworkObject instance. // Only read for in-scene placed NetworkObjects if (IsSceneObject) { reader.ReadValue(out NetworkSceneHandle); } } } internal void PostNetworkVariableWrite() { for (int k = 0; k < ChildNetworkBehaviours.Count; k++) { ChildNetworkBehaviours[k].PostNetworkVariableWrite(); } } /// /// Handles synchronizing NetworkVariables and custom synchronization data for NetworkBehaviours. /// /// /// This is where we determine how much data is written after the associated NetworkObject in order to recover /// from a failed instantiated NetworkObject without completely disrupting client synchronization. /// internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer, ulong targetClientId = 0) where T : IReaderWriter { if (serializer.IsWriter) { var writer = serializer.GetFastBufferWriter(); var positionBeforeSynchronizing = writer.Position; writer.WriteValueSafe((ushort)0); var sizeToSkipCalculationPosition = writer.Position; // Synchronize NetworkVariables WriteNetworkVariableData(writer, targetClientId); // Reserve the NetworkBehaviour synchronization count position var networkBehaviourCountPosition = writer.Position; writer.WriteValueSafe((byte)0); // Parse through all NetworkBehaviours and any that return true // had additional synchronization data written. // (See notes for reading/deserialization below) var synchronizationCount = (byte)0; foreach (var childBehaviour in ChildNetworkBehaviours) { if (childBehaviour.Synchronize(ref serializer)) { synchronizationCount++; } } var currentPosition = writer.Position; // Write the total number of bytes written for NetworkVariable and NetworkBehaviour // synchronization. writer.Seek(positionBeforeSynchronizing); // We want the size of everything after our size to skip calculation position var size = (ushort)(currentPosition - sizeToSkipCalculationPosition); writer.WriteValueSafe(size); // Write the number of NetworkBehaviours synchronized writer.Seek(networkBehaviourCountPosition); writer.WriteValueSafe(synchronizationCount); // seek back to the position after writing NetworkVariable and NetworkBehaviour // synchronization data. writer.Seek(currentPosition); } else { var reader = serializer.GetFastBufferReader(); reader.ReadValueSafe(out ushort sizeOfSynchronizationData); var seekToEndOfSynchData = reader.Position + sizeOfSynchronizationData; // Apply the network variable synchronization data SetNetworkVariableData(reader, targetClientId); // Read the number of NetworkBehaviours to synchronize reader.ReadValueSafe(out byte numberSynchronized); var networkBehaviourId = (ushort)0; // If a NetworkBehaviour writes synchronization data, it will first // write its NetworkBehaviourId so when deserializing the client-side // can find the right NetworkBehaviour to deserialize the synchronization data. for (int i = 0; i < numberSynchronized; i++) { serializer.SerializeValue(ref networkBehaviourId); var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId); networkBehaviour.Synchronize(ref serializer); } } } internal SceneObject GetMessageSceneObject(ulong targetClientId) { var obj = new SceneObject { NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, IsPlayerObject = IsPlayerObject, IsSceneObject = IsSceneObject ?? true, Hash = HostCheckForGlobalObjectIdHashOverride(), OwnerObject = this, TargetClientId = targetClientId }; NetworkObject parentNetworkObject = null; if (!AlwaysReplicateAsRoot && transform.parent != null) { parentNetworkObject = transform.parent.GetComponent(); // In-scene placed NetworkObjects parented under GameObjects with no NetworkObject // should set the has parent flag and preserve the world position stays value if (parentNetworkObject == null && obj.IsSceneObject) { obj.HasParent = true; obj.WorldPositionStays = m_CachedWorldPositionStays; } } if (parentNetworkObject != null) { obj.HasParent = true; obj.ParentObjectId = parentNetworkObject.NetworkObjectId; obj.WorldPositionStays = m_CachedWorldPositionStays; var latestParent = GetNetworkParenting(); var isLatestParentSet = latestParent != null && latestParent.HasValue; obj.IsLatestParentSet = isLatestParentSet; if (isLatestParentSet) { obj.LatestParent = latestParent.Value; } } if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId)) { obj.HasTransform = true; // We start with the default AutoObjectParentSync values to determine which transform space we will // be synchronizing clients with. var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; // If auto object synchronization is turned off if (!AutoObjectParentSync) { // We always synchronize position and rotation world space relative syncRotationPositionLocalSpaceRelative = false; // Scale is special, it synchronizes local space relative if it has a // parent since applying the world space scale under a parent with scale // will result in the improper scale for the child syncScaleLocalSpaceRelative = obj.HasParent; } obj.Transform = new SceneObject.TransformData { // If we are parented and we have the m_CachedWorldPositionStays disabled, then use local space // values as opposed world space values. Position = syncRotationPositionLocalSpaceRelative ? transform.localPosition : transform.position, Rotation = syncRotationPositionLocalSpaceRelative ? transform.localRotation : transform.rotation, // We only use the lossyScale if the NetworkObject has a parent. Multi-generation nested children scales can // impact the final scale of the child NetworkObject in question. The solution is to use the lossy scale // which can be thought of as "world space scale". // More information: // https://docs.unity3d.com/ScriptReference/Transform-lossyScale.html Scale = syncScaleLocalSpaceRelative ? transform.localScale : transform.lossyScale, }; } return obj; } /// /// Used to deserialize a serialized scene object which occurs /// when the client is approved or during a scene transition /// /// Deserialized scene object data /// FastBufferReader for the NetworkVariable data /// NetworkManager instance /// optional to use NetworkObject deserialized internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager) { //Attempt to create a local NetworkObject var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); if (networkObject == null) { // Log the error that the NetworkObject failed to construct if (networkManager.LogLevel <= LogLevel.Normal) { NetworkLog.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {sceneObject.Hash}."); } try { // If we failed to load this NetworkObject, then skip past the Network Variable and (if any) synchronization data reader.ReadValueSafe(out ushort networkBehaviourSynchronizationDataLength); reader.Seek(reader.Position + networkBehaviourSynchronizationDataLength); } catch (Exception ex) { Debug.LogException(ex); } // We have nothing left to do here. return null; } // This will get set again when the NetworkObject is spawned locally, but we set it here ahead of spawning // in order to be able to determine which NetworkVariables the client will be allowed to read. networkObject.OwnerClientId = sceneObject.OwnerClientId; // Synchronize NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); // Spawn the NetworkObject networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, false); return networkObject; } /// /// Only applies to Host mode. /// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists. /// Server and Clients will always return the NetworkObject's GlobalObjectIdHash. /// /// internal uint HostCheckForGlobalObjectIdHashOverride() { if (NetworkManager.IsHost) { if (NetworkManager.PrefabHandler.ContainsHandler(this)) { var globalObjectIdHash = NetworkManager.PrefabHandler.GetSourceGlobalObjectIdHash(GlobalObjectIdHash); return globalObjectIdHash == 0 ? GlobalObjectIdHash : globalObjectIdHash; } else if (NetworkManager.NetworkConfig.OverrideToNetworkPrefab.ContainsKey(GlobalObjectIdHash)) { return NetworkManager.NetworkConfig.OverrideToNetworkPrefab[GlobalObjectIdHash]; } } return GlobalObjectIdHash; } /// /// Removes a NetworkBehaviour from the ChildNetworkBehaviours list when destroyed /// while the NetworkObject is still spawned. /// internal void OnNetworkBehaviourDestroyed(NetworkBehaviour networkBehaviour) { if (networkBehaviour.IsSpawned && IsSpawned) { if (NetworkManager.LogLevel == LogLevel.Developer) { NetworkLog.LogWarning($"{nameof(NetworkBehaviour)}-{networkBehaviour.name} is being destroyed while {nameof(NetworkObject)}-{name} is still spawned! (could break state synchronization)"); } ChildNetworkBehaviours.Remove(networkBehaviour); } } } }