using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; #if UNITY_EDITOR using UnityEditor; #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; #else using UnityEditor.Experimental.SceneManagement; #endif #endif 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; /// /// Used to track the source GlobalObjectIdHash value of the associated network prefab. /// When an override exists or it is in-scene placed, GlobalObjectIdHash and PrefabGlobalObjectIdHash /// will be different. The PrefabGlobalObjectIdHash value is what is used when sending a . /// internal uint PrefabGlobalObjectIdHash; /// /// This is the source prefab of an in-scene placed NetworkObject. This is not set for in-scene /// placd NetworkObjects that are not prefab instances, dynamically spawned prefab instances, /// or for network prefab assets. /// [HideInInspector] [SerializeField] internal uint InScenePlacedSourceGlobalObjectIdHash; /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 /// [HideInInspector] public uint PrefabIdHash { get { return GlobalObjectIdHash; } } #if UNITY_EDITOR private const string k_GlobalIdTemplate = "GlobalObjectId_V1-{0}-{1}-{2}-{3}"; /// /// Object Types /// Parameter 0 of /// // 0 = Null (when considered a null object type we can ignore) // 1 = Imported Asset // 2 = Scene Object // 3 = Source Asset. private const int k_NullObjectType = 0; private const int k_ImportedAssetObjectType = 1; private const int k_SceneObjectType = 2; private const int k_SourceAssetObjectType = 3; [ContextMenu("Refresh In-Scene Prefab Instances")] internal void RefreshAllPrefabInstances() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); if (!PrefabUtility.IsPartOfAnyPrefab(this) || instanceGlobalId.identifierType != k_ImportedAssetObjectType) { EditorUtility.DisplayDialog("Network Prefab Assets Only", "This action can only be performed on a network prefab asset.", "Ok"); return; } // Handle updating the currently active scene var networkObjects = FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var networkObject in networkObjects) { networkObject.OnValidate(); } NetworkObjectRefreshTool.ProcessActiveScene(); // Refresh all build settings scenes var activeScene = SceneManager.GetActiveScene(); foreach (var editorScene in EditorBuildSettings.scenes) { // skip disabled scenes and the currently active scene if (!editorScene.enabled || activeScene.path == editorScene.path) { continue; } // Add the scene to be processed NetworkObjectRefreshTool.ProcessScene(editorScene.path, false); } // Process all added scenes NetworkObjectRefreshTool.ProcessScenes(); } private void OnValidate() { // do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode if (EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name)) { return; } // do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode if (!EditorApplication.isPlaying && EditorApplication.isPlayingOrWillChangePlaymode) { return; } // Get a global object identifier for this network prefab var globalId = GetGlobalId(); // if the identifier type is 0, then don't update the GlobalObjectIdHash if (globalId.identifierType == k_NullObjectType) { return; } var oldValue = GlobalObjectIdHash; GlobalObjectIdHash = globalId.ToString().Hash32(); // If the GlobalObjectIdHash value changed, then mark the asset dirty if (GlobalObjectIdHash != oldValue) { // Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed) if (!IsEditingPrefab() && gameObject.scene.name != null && gameObject.scene.name != gameObject.name) { // Sanity check to make sure this is a scene placed object if (globalId.identifierType != k_SceneObjectType) { // This should never happen, but in the event it does throw and error Debug.LogError($"[{gameObject.name}] is detected as an in-scene placed object but its identifier is of type {globalId.identifierType}! **Report this error**"); } // If this is a prefab instance if (PrefabUtility.IsPartOfAnyPrefab(this)) { // We must invoke this in order for the modifications to get saved with the scene (does not mark scene as dirty) PrefabUtility.RecordPrefabInstancePropertyModifications(this); } } else // Otherwise, this is a standard network prefab asset so we just mark it dirty for the AssetDatabase to update it { EditorUtility.SetDirty(this); } } // Always check for in-scene placed to assure any previous version scene assets with in-scene place NetworkObjects gets updated CheckForInScenePlaced(); } private bool IsEditingPrefab() { // Check if we are directly editing the prefab var stage = PrefabStageUtility.GetPrefabStage(gameObject); // if we are not editing the prefab directly (or a sub-prefab), then return the object identifier if (stage == null || stage.assetPath == null) { return false; } return true; } /// /// This checks to see if this NetworkObject is an in-scene placed prefab instance. If so it will /// automatically find the source prefab asset's GlobalObjectIdHash value, assign it to /// InScenePlacedSourceGlobalObjectIdHash and mark this as being in-scene placed. /// /// /// This NetworkObject is considered an in-scene placed prefab asset instance if it is: /// - Part of a prefab /// - Not being directly edited /// - Within a valid scene that is part of the scenes in build list /// (In-scene defined NetworkObjects that are not part of a prefab instance are excluded.) /// private void CheckForInScenePlaced() { if (PrefabUtility.IsPartOfAnyPrefab(this) && !IsEditingPrefab() && gameObject.scene.IsValid() && gameObject.scene.isLoaded && gameObject.scene.buildIndex >= 0) { var prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); var assetPath = AssetDatabase.GetAssetPath(prefab); var sourceAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (sourceAsset != null && sourceAsset.GlobalObjectIdHash != 0 && InScenePlacedSourceGlobalObjectIdHash != sourceAsset.GlobalObjectIdHash) { InScenePlacedSourceGlobalObjectIdHash = sourceAsset.GlobalObjectIdHash; } IsSceneObject = true; } } private GlobalObjectId GetGlobalId() { var instanceGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(this); // If not editing a prefab, then just use the generated id if (!IsEditingPrefab()) { return instanceGlobalId; } // If the asset doesn't exist at the given path, then return the object identifier var prefabStageAssetPath = PrefabStageUtility.GetPrefabStage(gameObject).assetPath; // If (for some reason) the asset path is null return the generated id if (prefabStageAssetPath == null) { return instanceGlobalId; } var theAsset = AssetDatabase.LoadAssetAtPath(prefabStageAssetPath); // If there is no asset at that path (for some odd/edge case reason), return the generated id if (theAsset == null) { return instanceGlobalId; } // If we can't get the asset GUID and/or the file identifier, then return the object identifier if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(theAsset, out var guid, out long localFileId)) { return instanceGlobalId; } // Note: If we reached this point, then we are most likely opening a prefab to edit. // The instanceGlobalId will be constructed as if it is a scene object, however when it // is serialized its value will be treated as a file asset (the "why" to the below code). // Construct an imported asset identifier with the type being a source asset object type var prefabGlobalIdText = string.Format(k_GlobalIdTemplate, k_SourceAssetObjectType, guid, (ulong)localFileId, 0); // If we can't parse the result log an error and return the instanceGlobalId if (!GlobalObjectId.TryParse(prefabGlobalIdText, out var prefabGlobalId)) { Debug.LogError($"[GlobalObjectId Gen] Failed to parse ({prefabGlobalIdText}) returning default ({instanceGlobalId})! ** Please Report This Error **"); return instanceGlobalId; } // Otherwise, return the constructed identifier for the source prefab asset return prefabGlobalId; } #endif // UNITY_EDITOR /// /// Gets the NetworkManager that owns this NetworkObject instance /// public NetworkManager NetworkManager => NetworkManagerOwner ? NetworkManagerOwner : NetworkManager.Singleton; /// /// Useful to know if we should or should not send a message /// internal bool HasRemoteObservers => !(Observers.Count() == 0 || (Observers.Contains(NetworkManager.LocalClientId) && Observers.Count() == 1)); /// /// Distributed Authority Mode Only /// When set, NetworkObjects despawned remotely will be delayed until the tick count specified is reached on all non-owner instances. /// It will still despawn immediately on the owner-local side. /// [HideInInspector] public int DeferredDespawnTick; /// /// Distributed Authority Mode Only /// The delegate handler declaration for . /// /// true (despawn) or false (do not despawn) public delegate bool OnDeferedDespawnCompleteDelegateHandler(); /// /// If assigned, this callback will be invoked each frame update to determine if a that has had its despawn deferred /// should despawn. Use this callback to handle scenarios where you might have additional changes in state that could vindicate despawning earlier /// than the deferred despawn targeted future network tick. /// public OnDeferedDespawnCompleteDelegateHandler OnDeferredDespawnComplete; /// /// Distributed Authority Mode Only /// When invoked by the authority of the , this will locally despawn the while /// sending a delayed despawn to all non-authority instances. The tick offset + the authority's current known network tick (ServerTime.Tick) /// is when non-authority instances will despawn this instance. /// /// The number of ticks from the authority's currently known to delay the despawn. /// Defaults to true, determines whether the will be destroyed. public void DeferDespawn(int tickOffset, bool destroy = true) { if (!NetworkManager.DistributedAuthorityMode) { NetworkLog.LogError($"This method is only available in distributed authority mode."); return; } if (!IsSpawned) { NetworkLog.LogError($"Cannot defer despawning {name} because it is not spawned!"); return; } if (!HasAuthority) { NetworkLog.LogError($"Only the authoirty can invoke {nameof(DeferDespawn)} and local Client-{NetworkManager.LocalClientId} is not the authority of {name}!"); return; } // Apply the relative tick offset for when this NetworkObject should be despawned on // non-authoritative instances. DeferredDespawnTick = NetworkManager.ServerTime.Tick + tickOffset; var connectionManager = NetworkManager.ConnectionManager; for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].PreVariableUpdate(); // Notify all NetworkBehaviours that the authority is performing a deferred despawn. // This is when user script would update NetworkVariable states that might be needed // for the deferred despawn sequence on non-authoritative instances. ChildNetworkBehaviours[i].OnDeferringDespawn(DeferredDespawnTick); } // DAHost handles sending updates to all clients if (NetworkManager.DAHost) { for (int i = 0; i < connectionManager.ConnectedClientsList.Count; i++) { var client = connectionManager.ConnectedClientsList[i]; if (IsNetworkVisibleTo(client.ClientId)) { // Sync just the variables for just the objects this client sees for (int k = 0; k < ChildNetworkBehaviours.Count; k++) { ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId); } } } } else // Clients just send their deltas to the service or DAHost { for (int k = 0; k < ChildNetworkBehaviours.Count; k++) { ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId); } } // Now despawn the local authority instance Despawn(destroy); } /// /// When enabled, NetworkObject ownership is distributed amongst clients. /// To set during runtime, use /// /// /// Scenarios of interest: /// - If the is locked and the current owner is still connected, then it will not be redistributed upon a new client joining. /// - If the has an ownership request in progress, then it will not be redistributed upon a new client joining. /// - If the is locked but the owner is not longer connected, then it will be redistributed. /// - If the has an ownership request in progress but the target client is no longer connected, then it will be redistributed. /// public bool IsOwnershipDistributable => Ownership.HasFlag(OwnershipStatus.Distributable); /// /// Returns true if the is has ownership locked. /// When locked, the cannot be redistributed nor can it be transferred by another client. /// To toggle the ownership loked status during runtime, use . /// public bool IsOwnershipLocked => ((OwnershipStatusExtended)Ownership).HasFlag(OwnershipStatusExtended.Locked); /// /// When true, the 's ownership can be acquired by any non-owner client. /// To set during runtime, use . /// public bool IsOwnershipTransferable => Ownership.HasFlag(OwnershipStatus.Transferable); /// /// When true, the 's ownership can be acquired through non-owner client requesting ownership. /// To set during runtime, use /// To request ownership, use . /// public bool IsOwnershipRequestRequired => Ownership.HasFlag(OwnershipStatus.RequestRequired); /// /// When true, the 's ownership cannot be acquired because an ownership request is underway. /// In order for this status to be applied, the the must have the /// flag set and a non-owner client must have sent a request via . /// public bool IsRequestInProgress => ((OwnershipStatusExtended)Ownership).HasFlag(OwnershipStatusExtended.Requested); /// /// Determines whether a NetworkObject can be distributed to other clients during /// a session. /// [SerializeField] internal OwnershipStatus Ownership = OwnershipStatus.Distributable; /// /// Ownership status flags: /// : If nothing is set, then ownership is considered "static" and cannot be redistributed, requested, or transferred (i.e. a Player would have this). /// : When set, this instance will be automatically redistributed when a client joins (if not locked or no request is pending) or leaves. /// : When set, a non-owner can obtain ownership immediately (without requesting and as long as it is not locked). /// : When set, When set, a non-owner must request ownership from the owner (will always get locked once ownership is transferred). /// // Ranges from 1 to 8 bits [Flags] public enum OwnershipStatus { None = 0, Distributable = 1 << 0, Transferable = 1 << 1, RequestRequired = 1 << 2, } /// /// Intentionally internal /// // Ranges from 9 to 16 bits [Flags] internal enum OwnershipStatusExtended { // When locked and CanRequest is set, a non-owner can request ownership. If the owner responds by removing the Locked status, then ownership is transferred. // If the owner responds by removing the Requested status only, then ownership is denied. Requested = (1 << 8), Locked = (1 << 9), } internal bool HasExtendedOwnershipStatus(OwnershipStatusExtended extended) { var extendedOwnership = (OwnershipStatusExtended)Ownership; return extendedOwnership.HasFlag(extended); } internal void AddOwnershipExtended(OwnershipStatusExtended extended) { var extendedOwnership = (OwnershipStatusExtended)Ownership; extendedOwnership |= extended; Ownership = (OwnershipStatus)extendedOwnership; } internal void RemoveOwnershipExtended(OwnershipStatusExtended extended) { var extendedOwnership = (OwnershipStatusExtended)Ownership; extendedOwnership &= ~extended; Ownership = (OwnershipStatus)extendedOwnership; } /// /// Distributed Authority Only /// Locks ownership of a NetworkObject by the current owner. /// /// defaults to lock (true) or unlock (false) /// true or false depending upon lock operation's success public bool SetOwnershipLock(bool lockOwnership = true) { // If we are not in distributed autority mode, then exit early if (!NetworkManager.DistributedAuthorityMode) { Debug.LogError($"[Feature Not Allowed In Client-Server Mode] Ownership flags are a distributed authority feature only!"); return false; } // If we don't have authority exit early if (!HasAuthority) { NetworkLog.LogWarningServer($"[Attempted Lock Without Authority] Client-{NetworkManager.LocalClientId} is trying to lock ownership but does not have authority!"); return false; } // If we don't have the Transferable flag set and it is not a player object, then it is the same as having a static lock on ownership if (!IsOwnershipTransferable && !IsPlayerObject) { NetworkLog.LogWarning($"Trying to add or remove ownership lock on [{name}] which does not have the {nameof(OwnershipStatus.Transferable)} flag set!"); return false; } // If we are locking and are already locked or we are unlocking and are already unlocked exit early and return true if (!(IsOwnershipLocked ^ lockOwnership)) { return true; } if (lockOwnership) { AddOwnershipExtended(OwnershipStatusExtended.Locked); } else { RemoveOwnershipExtended(OwnershipStatusExtended.Locked); } SendOwnershipStatusUpdate(); return true; } /// /// In the event of an immediate (local instance) failure to change ownership, the following ownership /// permission failure status codes will be returned via . /// : The is locked and ownership cannot be acquired. /// : The requires an ownership request via . /// : The already is processing an ownership request and ownership cannot be acquired at this time. /// does not have the flag set and ownership cannot be acquired. /// public enum OwnershipPermissionsFailureStatus { Locked, RequestRequired, RequestInProgress, NotTransferrable } /// /// /// /// public delegate void OnOwnershipPermissionsFailureDelegateHandler(OwnershipPermissionsFailureStatus changeOwnershipFailure); /// /// If there is any callback assigned or subscriptions to this handler, then upon any ownership permissions failure that occurs during /// the invocation of will trigger this notification containing an . /// public OnOwnershipPermissionsFailureDelegateHandler OnOwnershipPermissionsFailure; /// /// Returned by to signify w /// : The request for ownership was sent (does not mean it will be granted, but the request was sent). /// : The current client is already the owner (no need to request ownership). /// : The flag is not set on this /// : The current owner has locked ownership which means requests are not available at this time. /// : There is already a known request in progress. You can scan for ownership changes and try upon /// a change in ownership or just try again after a specific period of time or no longer attempt to request ownership. /// public enum OwnershipRequestStatus { RequestSent, AlreadyOwner, RequestRequiredNotSet, Locked, RequestInProgress, } /// /// Invoke this from a non-authority client to request ownership. /// /// /// The results of requesting ownership: /// : The request for ownership was sent (does not mean it will be granted, but the request was sent). /// : The current client is already the owner (no need to request ownership). /// : The flag is not set on this /// : The current owner has locked ownership which means requests are not available at this time. /// : There is already a known request in progress. You can scan for ownership changes and try upon /// a change in ownership or just try again after a specific period of time or no longer attempt to request ownership. /// /// public OwnershipRequestStatus RequestOwnership() { // Exit early the local client is already the owner if (OwnerClientId == NetworkManager.LocalClientId) { return OwnershipRequestStatus.AlreadyOwner; } // Exit early if it doesn't have the RequestRequired flag if (!IsOwnershipRequestRequired) { return OwnershipRequestStatus.RequestRequiredNotSet; } // Exit early if it is locked if (IsOwnershipLocked) { return OwnershipRequestStatus.Locked; } // Exit early if there is already a request in progress if (IsRequestInProgress) { return OwnershipRequestStatus.RequestInProgress; } // Otherwise, send the request ownership message var changeOwnership = new ChangeOwnershipMessage { NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, ClientIdCount = 1, RequestClientId = NetworkManager.LocalClientId, ClientIds = new ulong[1] { OwnerClientId }, DistributedAuthorityMode = true, RequestOwnership = true, OwnershipFlags = (ushort)Ownership, }; var sendTarget = NetworkManager.DAHost ? OwnerClientId : NetworkManager.ServerClientId; NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, sendTarget); return OwnershipRequestStatus.RequestSent; } /// /// The delegate handler declaration used by . /// /// /// public delegate bool OnOwnershipRequestedDelegateHandler(ulong clientRequesting); /// /// The callback that can be used /// to control when ownership can be transferred to a non-authority client. /// /// /// Requesting ownership requires the flags to have the flag set. /// public OnOwnershipRequestedDelegateHandler OnOwnershipRequested; /// /// Invoked by ChangeOwnershipMessage /// /// the client requesting ownership /// internal void OwnershipRequest(ulong clientRequestingOwnership) { var response = OwnershipRequestResponseStatus.Approved; // Do a last check to make sure this NetworkObject can be requested // CMB-DANGO-TODO: We could help optimize this process and check the below flags on the service side. // It wouldn't cover the scenario were an update was in-bound to the service from the owner, but it would // handle the case where something had already changed and the service was already "aware" of the change. if (IsOwnershipLocked) { response = OwnershipRequestResponseStatus.Locked; } else if (IsRequestInProgress) { response = OwnershipRequestResponseStatus.RequestInProgress; } else if (!IsOwnershipRequestRequired && !IsOwnershipTransferable) { response = OwnershipRequestResponseStatus.CannotRequest; } // Finally, check to see if OnOwnershipRequested is registered and if user script is allowing // this transfer of ownership if (OnOwnershipRequested != null && !OnOwnershipRequested.Invoke(clientRequestingOwnership)) { response = OwnershipRequestResponseStatus.Denied; } // If we made it here and the response is still approved, then change ownership if (response == OwnershipRequestResponseStatus.Approved) { // When requested and approved, the owner immediately sets the Requested flag **prior to** // changing the ownership. This prevents race conditions from happening. // Until the ownership change has propagated out, requests can still flow through this owner, // but by that time this owner's instance will have the extended Requested flag and will // respond to any additional ownership request with OwnershipRequestResponseStatus.RequestInProgress. AddOwnershipExtended(OwnershipStatusExtended.Requested); // This action is always authorized as long as the client still has authority. // We need to pass in that this is a request approval ownership change. NetworkManager.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, HasAuthority, true); } else { // Otherwise, send back the reason why the ownership request was denied for the clientRequestingOwnership /// Notes: /// We always apply the as opposed to to the /// value as ownership could have changed and the denied requests /// targeting this instance are because there is a request pending. /// DANGO-TODO: What happens if the client requesting disconnects prior to responding with the update in request pending? var changeOwnership = new ChangeOwnershipMessage { NetworkObjectId = NetworkObjectId, OwnerClientId = NetworkManager.LocalClientId, // Always use the local clientId (see above notes) RequestClientId = clientRequestingOwnership, DistributedAuthorityMode = true, RequestDenied = true, OwnershipRequestResponseStatus = (byte)response, OwnershipFlags = (ushort)Ownership, }; var sendTarget = NetworkManager.DAHost ? clientRequestingOwnership : NetworkManager.ServerClientId; NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, sendTarget); } } /// /// What is returned via after an ownership request has been sent via /// /// /// Approved: Granted ownership, and returned after the requesting client has gained ownership on the local instance. /// Locked: Was locked after request was sent. /// RequestInProgress: A request started before this request was received. /// CannotRequest: The RequestRequired status changed while the request was in flight. /// Denied: General denied message that is only set if returns false by the authority instance. /// public enum OwnershipRequestResponseStatus { Approved, Locked, RequestInProgress, CannotRequest, Denied, } /// /// The delegate handler declaration used by . /// /// public delegate void OnOwnershipRequestResponseDelegateHandler(OwnershipRequestResponseStatus ownershipRequestResponse); /// /// The callback that can be used /// to control when ownership can be transferred to a non-authority client. /// /// /// Requesting ownership requires the flags to have the flag set. /// public OnOwnershipRequestResponseDelegateHandler OnOwnershipRequestResponse; /// /// Invoked when a request is denied /// internal void OwnershipRequestResponse(OwnershipRequestResponseStatus ownershipRequestResponse) { OnOwnershipRequestResponse?.Invoke(ownershipRequestResponse); } /// /// When passed as a parameter in , the following additional locking actions will occur: /// - : (default) No locking action /// - : Will set the passed in flags and then lock the /// - : Will set the passed in flags and then unlock the /// public enum OwnershipLockActions { None, SetAndLock, SetAndUnlock } /// /// Adds an flag to the flags /// /// flag(s) to update /// defaults to false, but when true will clear the permissions and then set the permissions flags /// defaults to , but when set it to anther action type it will either lock or unlock ownership after setting the flags /// true (applied)/false (not applied) /// /// If it returns false, then this means the flag(s) you are attempting to /// set were already set on the instance. /// If it returns true, then the flags were set and an ownership update message /// was sent to all observers of the instance. /// public bool SetOwnershipStatus(OwnershipStatus status, bool clearAndSet = false, OwnershipLockActions lockAction = OwnershipLockActions.None) { // If it already has the flag do nothing if (!clearAndSet && Ownership.HasFlag(status)) { return false; } if (clearAndSet || status == OwnershipStatus.None) { Ownership = OwnershipStatus.None; } // Faster to just OR a None status than to check // if it is !None before "OR'ing". Ownership |= status; if (lockAction != OwnershipLockActions.None) { SetOwnershipLock(lockAction == OwnershipLockActions.SetAndLock); } SendOwnershipStatusUpdate(); return true; } /// /// Use this method to remove one or more ownership flags from the NetworkObject. /// If you want to clear and then set, use . /// /// the flag(s) to remove /// true/false /// /// If it returns false, then this means the flag(s) you are attempting to /// remove were not already set on the instance. /// If it returns true, then the flags were removed and an ownership update message /// was sent to all observers of the instance. /// public bool RemoveOwnershipStatus(OwnershipStatus status) { // If it doesn't have the ownership flag or we are trying to remove the None permission, then return false if (!Ownership.HasFlag(status) || status == OwnershipStatus.None) { return false; } Ownership &= ~status; SendOwnershipStatusUpdate(); return true; } /// /// Sends an update ownership status to all non-owner clients /// internal void SendOwnershipStatusUpdate() { // If there are no remote observers, then exit early if (!HasRemoteObservers) { return; } var changeOwnership = new ChangeOwnershipMessage { NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, DistributedAuthorityMode = true, OwnershipFlagsUpdate = true, OwnershipFlags = (ushort)Ownership, }; if (NetworkManager.DAHost) { foreach (var clientId in Observers) { if (clientId == NetworkManager.LocalClientId) { continue; } NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, clientId); } } else { changeOwnership.ClientIdCount = Observers.Count(); changeOwnership.ClientIds = Observers.ToArray(); NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, NetworkManager.ServerClientId); } } /// /// Use this method to determine if a has one or more ownership flags set. /// /// one or more flags /// true if the flag(s) are set and false if the flag or any one of the flags are not set public bool HasOwnershipStatus(OwnershipStatus status) { return Ownership.HasFlag(status); } /// /// This property can be used in client-server or distributed authority modes to determine if the local instance has authority. /// When in client-server mode, the server will always have authority over the NetworkObject and associated NetworkBehaviours. /// When in distributed authority mode, the owner is always the authority. /// /// /// When in client-server mode, authority should is not considered the same as ownership. /// public bool HasAuthority => InternalHasAuthority(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool InternalHasAuthority() { var networkManager = NetworkManager; return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; } /// /// 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; } internal ulong PreviousOwnerId; /// /// 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; } /// /// Determines if the associated NetworkObject's transform will get /// synchronized when spawned. /// /// /// For things like in-scene placed NetworkObjects that have no visual /// components can help reduce the instance's initial synchronization /// bandwidth cost. This can also be useful for UI elements that have /// a predetermined fixed position. /// public bool SynchronizeTransform = true; /// /// 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; } //DANGOEXP TODO: Determine if we want to keep this public void SetSceneObjectStatus(bool isSceneObject = false) { IsSceneObject = isSceneObject; } /// /// Gets whether or not the object should be automatically removed when the scene is unloaded. /// public bool DestroyWithScene { get; set; } /// /// When set to true and the active scene is changed, this will automatically migrate the /// into the new active scene on both the server and client instances. /// /// /// - This only applies to dynamically spawned s. /// - This only works when using integrated scene management (). /// /// If there are more than one scenes loaded and the currently active scene is unloaded, then typically /// the will automatically assign a new active scene. Similar to /// being set to , this prevents any from being destroyed /// with the unloaded active scene by migrating it into the automatically assigned active scene. /// Additionally, this is can be useful in some seamless scene streaming implementations. /// Note: /// Only having set to true will *not* synchronize clients when /// changing a 's scene via . /// To synchronize clients of a 's scene being changed via , /// make sure is enabled (it is by default). /// public bool ActiveSceneSynchronization; /// /// When enabled (the default), if a is migrated to a different scene (active or not) /// via on the server side all client /// instances will be synchronized and the migrated into the newly assigned scene. /// The updated scene migration will get synchronized with late joining clients as well. /// /// /// - This only applies to dynamically spawned s. /// - This only works when using integrated scene management (). /// Note: /// You can have both and enabled. /// The primary difference between the two is that only synchronizes clients /// when the server migrates a to a new scene. If the scene is unloaded and /// is and is and the scene is not the currently /// active scene, then the will be destroyed. /// public bool SceneMigrationSynchronization = true; /// /// Notifies when the NetworkObject is migrated into a new scene /// /// /// - or (or both) need to be enabled /// - This only applies to dynamically spawned s. /// - This only works when using integrated scene management (). /// public Action OnMigratedToNewScene; /// /// When set to false, the NetworkObject will be spawned with no observers initially (other than the server) /// [Tooltip("When false, the NetworkObject will spawn with no observers initially. (default is true)")] public bool SpawnWithObservers = true; /// /// 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 [MethodImpl(MethodImplOptions.AggressiveInlining)] 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; /// /// The server-side scene origin handle /// internal int NetworkSceneHandle = 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; } /// /// 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 (!HasAuthority) { if (NetworkManager.DistributedAuthorityMode) { throw new NotServerException($"Only the owner-authority can change visibility when distributed authority mode is enabled!"); } else { throw new NotServerException("Only the authority can change visibility"); } } if (Observers.Contains(clientId)) { if (NetworkManager.DistributedAuthorityMode) { Debug.LogError($"The object {name} is already visible to Client-{clientId}!"); return; } else { throw new NotServerException("Only server can change visibility"); } } if (CheckObjectVisibility != null && !CheckObjectVisibility(clientId)) { if (NetworkManager.LogLevel <= LogLevel.Normal) { NetworkLog.LogWarning($"[NetworkShow] Trying to make {nameof(NetworkObject)} {gameObject.name} visible to client ({clientId}) but {nameof(CheckObjectVisibility)} returned false!"); } return; } NetworkManager.SpawnManager.MarkObjectForShowingTo(this, clientId); Observers.Add(clientId); } /// /// 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) { NetworkLog.LogErrorServer($"At least one {nameof(NetworkObject)} has to be provided when showing a list of {nameof(NetworkObject)}s!"); return; } // Do the safety loop first to prevent putting the netcode in an invalid state. for (int i = 0; i < networkObjects.Count; i++) { var networkObject = networkObjects[i]; var networkManager = networkObject.NetworkManager; if (networkManager.DistributedAuthorityMode && clientId == networkObject.OwnerClientId) { NetworkLog.LogErrorServer($"Cannot hide an object from the owner when distributed authority mode is enabled! (Skipping {networkObject.gameObject.name})"); } else if (!networkManager.DistributedAuthorityMode && clientId == NetworkManager.ServerClientId) { NetworkLog.LogErrorServer("Cannot hide an object from the server!"); continue; } // Distributed authority mode adjustments to log a network error and continue when trying to show a NetworkObject // that the local instance does not own if (!networkObjects[i].HasAuthority) { if (networkObjects[i].NetworkManager.DistributedAuthorityMode) { // It will log locally and to the "master-host". NetworkLog.LogErrorServer("Only the owner-authority can change visibility when distributed authority mode is enabled!"); continue; } else { throw new NotServerException("Only server can change visibility"); } } 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 (!HasAuthority && !NetworkManager.DAHost) { if (NetworkManager.DistributedAuthorityMode) { throw new NotServerException($"Only the owner-authority can change visibility when distributed authority mode is enabled!"); } else { throw new NotServerException("Only the authority can change visibility"); } } if (!NetworkManager.SpawnManager.RemoveObjectFromShowingTo(this, clientId)) { if (!Observers.Contains(clientId)) { if (NetworkManager.LogLevel <= LogLevel.Developer) { Debug.LogWarning($"{name} is already hidden from Client-{clientId}! (ignoring)"); return; } } Observers.Remove(clientId); var message = new DestroyObjectMessage { NetworkObjectId = NetworkObjectId, DestroyGameObject = !IsSceneObject.Value, IsDistributedAuthority = NetworkManager.DistributedAuthorityMode, IsTargetedDestroy = NetworkManager.DistributedAuthorityMode, TargetClientId = clientId, // Just always populate this value whether we write it or not DeferredDespawnTick = DeferredDespawnTick, }; var size = 0; if (NetworkManager.DistributedAuthorityMode) { if (!NetworkManager.DAHost) { // Send destroy call to service or DAHost size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); } else // DAHost mocking service { // Send destroy call size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); // Broadcast the destroy to all clients so they can update their observers list foreach (var client in NetworkManager.ConnectedClientsIds) { if (client == clientId || client == NetworkManager.LocalClientId) { continue; } size += NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client); } } } else { // Send destroy call size = NetworkManager.ConnectionManager.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) { NetworkLog.LogErrorServer($"At least one {nameof(NetworkObject)} has to be provided when hiding a list of {nameof(NetworkObject)}s!"); return; } // Do the safety loop first to prevent putting the netcode in an invalid state. for (int i = 0; i < networkObjects.Count; i++) { var networkObject = networkObjects[i]; var networkManager = networkObject.NetworkManager; if (networkManager.DistributedAuthorityMode && clientId == networkObject.OwnerClientId) { NetworkLog.LogErrorServer($"Cannot hide an object from the owner when distributed authority mode is enabled! (Skipping {networkObject.gameObject.name})"); } else if (!networkManager.DistributedAuthorityMode && clientId == NetworkManager.ServerClientId) { NetworkLog.LogErrorServer("Cannot hide an object from the server!"); continue; } // Distributed authority mode adjustments to log a network error and continue when trying to show a NetworkObject // that the local instance does not own if (!networkObjects[i].HasAuthority) { if (networkObjects[i].NetworkManager.DistributedAuthorityMode) { // It will log locally and to the "master-host". NetworkLog.LogErrorServer($"Only the owner-authority can change hide a {nameof(NetworkObject)} when distributed authority mode is enabled!"); continue; } else { throw new NotServerException("Only server can change visibility!"); } } // CLIENT SPAWNING TODO: Log error and continue as opposed to throwing an exception 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 no NetworkManager is assigned, then just exit early if (!NetworkManager) { return; } // Authority is the server (client-server) and the owner or DAHost (distributed authority) when destroying a NetworkObject var isAuthority = HasAuthority || NetworkManager.DAHost; if (NetworkManager.IsListening && !isAuthority && IsSpawned && (IsSceneObject == null || (IsSceneObject.Value != true))) { // Clients should not despawn NetworkObjects while connected to a session, but we don't want to destroy the current call stack // if this happens. Instead, we should just generate a network log error and exit early (as long as we are not shutting down). if (!NetworkManager.ShutdownInProgress) { // Since we still have a session connection, log locally and on the server to inform user of this issue. if (NetworkManager.LogLevel <= LogLevel.Error) { if (NetworkManager.DistributedAuthorityMode) { NetworkLog.LogError($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-owner client is not valid during a distributed authority session. Call {nameof(Destroy)} or {nameof(Despawn)} on the client-owner instead."); } else { NetworkLog.LogErrorServer($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); } } return; } // Otherwise, clients can despawn NetworkObjects while shutting down and should not generate any messages when this happens } if (NetworkManager.SpawnManager != null && NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) { if (this == networkObject) { NetworkManager.SpawnManager.OnDespawnObject(networkObject, false); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (NetworkManagerOwner == null) { NetworkManagerOwner = NetworkManager.Singleton; } if (!NetworkManager.IsListening) { throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects"); } if ((!NetworkManager.IsServer && !NetworkManager.DistributedAuthorityMode) || (NetworkManager.DistributedAuthorityMode && !NetworkManager.LocalClient.IsSessionOwner && NetworkManager.LocalClientId != ownerClientId)) { if (NetworkManager.DistributedAuthorityMode) { throw new NotServerException($"When distributed authority mode is enabled, you can only spawn NetworkObjects that belong to the local instance! Local instance id {NetworkManager.LocalClientId} is not the same as the assigned owner id: {ownerClientId}!"); } else { throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); } } if (NetworkManager.DistributedAuthorityMode) { if (NetworkManager.NetworkConfig.EnableSceneManagement) { NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[gameObject.scene.handle]; } if (DontDestroyWithOwner && !IsOwnershipDistributable) { //Ownership |= OwnershipStatus.Distributable; // DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set if (NetworkManager.LogLevel == LogLevel.Developer) { NetworkLog.LogWarning("DANGO-TODO: Review over don't destroy with owner being set but DistributeOwnership not being set. For now, if the NetworkObject does not destroy with the owner it will automatically set DistributeOwnership."); } } } NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); if ((NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost) || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer)) { for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) { if (NetworkManager.ConnectedClientsList[i].ClientId == NetworkManager.ServerClientId) { continue; } if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) { NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); } } } else if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) { NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ServerClientId, this); } else { NetworkLog.LogWarningServer($"Ran into unknown conditional check during spawn when determining distributed authority mode or not"); } } /// /// This invokes . /// /// The NetworkPrefab to instantiate and spawn. /// The local instance of the NetworkManager connected to an session in progress. /// The owner of the instance (defaults to server). /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). /// Whether the instance is a player object or not (default is false). /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). /// The starting poisiton of the instance. /// The starting rotation of the instance. /// The newly instantiated and spawned prefab instance. public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { var networkObject = networkPrefab.GetComponent(); if (networkObject == null) { Debug.LogError($"The {nameof(NetworkPrefab)} {networkPrefab.name} does not have a {nameof(NetworkObject)} component!"); return null; } return networkObject.InstantiateAndSpawn(networkManager, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); } /// /// This invokes . /// /// The local instance of the NetworkManager connected to an session in progress. /// The owner of the instance (defaults to server). /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). /// Whether the instance is a player object or not (default is false). /// Whether you want to force spawning the override when running as a host or server or if you want it to spawn the override for host mode and /// the source prefab for server. If there is an override, clients always spawn that as opposed to the source prefab (defaults to false). /// The starting poisiton of the instance. /// The starting rotation of the instance. /// The newly instantiated and spawned prefab instance. public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default) { if (networkManager == null) { Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NetworkManagerNull]); return null; } if (!networkManager.IsListening) { Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NoActiveSession]); return null; } ownerClientId = networkManager.DistributedAuthorityMode ? networkManager.LocalClientId : NetworkManager.ServerClientId; // We only need to check for authority when running in client-server mode if (!networkManager.IsServer && !networkManager.DistributedAuthorityMode) { Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NotAuthority]); return null; } if (networkManager.ShutdownInProgress) { Debug.LogWarning(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.InvokedWhenShuttingDown]); return null; } // Verify it is actually a valid prefab if (!networkManager.NetworkConfig.Prefabs.Contains(gameObject)) { Debug.LogError(NetworkSpawnManager.InstantiateAndSpawnErrors[NetworkSpawnManager.InstantiateAndSpawnErrorTypes.NotRegisteredNetworkPrefab]); return null; } return networkManager.SpawnManager.InstantiateAndSpawnNoParameterChecks(this, ownerClientId, destroyWithScene, isPlayerObject, forceOverride, position, rotation); } /// /// 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) { var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; SpawnInternal(destroyWithScene, clientId, 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, HasAuthority); } internal void InvokeBehaviourOnLostOwnership() { // Always update the ownership table in distributed authority mode if (NetworkManager.DistributedAuthorityMode) { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true); } else // Server already handles this earlier, hosts should ignore and only client owners 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() { // Always update the ownership table in distributed authority mode if (NetworkManager.DistributedAuthorityMode) { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); } else // 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 InvokeOwnershipChanged(ulong previous, ulong next) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { if (ChildNetworkBehaviours[i].gameObject.activeInHierarchy) { ChildNetworkBehaviours[i].InternalOnOwnershipChanged(previous, next); } 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 /// /// With distributed authority, we need to have a way to determine if the parenting action is authorized. /// This is set when handling an incoming ParentSyncMessage and when running as a DAHost and a client has disconnected. /// internal bool AuthorityAppliedParenting = false; /// /// Returns the last known cached WorldPositionStays value for this NetworkObject /// /// /// When parenting NetworkObjects, the optional WorldPositionStays value is cached and synchronized with clients. /// This method provides access to the instance relative cached value. /// /// /// /// /// or public bool WorldPositionStays() { return m_CachedWorldPositionStays; } internal void SetCachedParent(Transform parentTransform) { AuthorityAppliedParenting = false; m_CachedParent = parentTransform; } internal Transform GetCachedParent() { return m_CachedParent; } 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) { // 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); } /// /// 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 InternalTrySetParent(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; } // DANGO-TODO: Do we want to worry about ownership permissions here? // It wouldn't make sense to not allow parenting, but keeping this note here as a reminder. var isAuthority = HasAuthority; // If we don't have authority and we are not shutting down, then don't allow any parenting. // If we are shutting down and don't have authority then allow it. if (!isAuthority && !NetworkManager.ShutdownInProgress) { return false; } return InternalTrySetParent(parent, worldPositionStays); } internal bool InternalTrySetParent(NetworkObject parent, bool worldPositionStays = true) { if (parent != null && (IsSpawned ^ parent.IsSpawned)) { if (NetworkManager != null && !NetworkManager.ShutdownInProgress) { 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 || NetworkManager.ShutdownInProgress) { return; } if (transform.parent == m_CachedParent) { return; } if (NetworkManager == null || !NetworkManager.IsListening) { // DANGO-TODO: Review as to whether we want to provide a better way to handle changing parenting of objects when the // object is not spawned. Really, we shouldn't care about these types of changes. if (NetworkManager.DistributedAuthorityMode && m_CachedParent != null && transform.parent == null) { m_CachedParent = null; return; } transform.parent = m_CachedParent; Debug.LogException(new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before reparenting")); return; } var isAuthority = false; // With distributed authority, we need to track "valid authoritative" parenting changes. // So, either the authority or AuthorityAppliedParenting is considered a "valid parenting change". isAuthority = HasAuthority || AuthorityAppliedParenting; var distributedAuthority = NetworkManager.DistributedAuthorityMode; // If we do not have authority and we are spawned if (!isAuthority && IsSpawned) { // If the cached parent has not already been set and we are in distributed authority mode, then log an exception and exit early as a non-authority instance // is trying to set the parent. if (distributedAuthority) { transform.parent = m_CachedParent; NetworkLog.LogError($"[Not Owner] Only the owner-authority of child {gameObject.name}'s {nameof(NetworkObject)} component can reparent it!"); } else { transform.parent = m_CachedParent; Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s")); } return; } if (!IsSpawned) { AuthorityAppliedParenting = false; // and we are removing the parent, then go ahead and allow parenting to occur if (transform.parent == null) { m_LatestParent = null; SetCachedParent(null); InvokeBehaviourOnNetworkObjectParentChanged(null); } else { 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; AuthorityAppliedParenting = false; Debug.LogException(new InvalidParentException($"Invalid parenting, {nameof(NetworkObject)} moved under a non-{nameof(NetworkObject)} parent")); return; } else if (!parentObject.IsSpawned) { transform.parent = m_CachedParent; AuthorityAppliedParenting = false; 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; } // This can be reset within ApplyNetworkParenting var authorityApplied = AuthorityAppliedParenting; ApplyNetworkParenting(removeParent); var message = new ParentSyncMessage { NetworkObjectId = NetworkObjectId, IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue, LatestParent = m_LatestParent, RemoveParent = removeParent, AuthorityApplied = authorityApplied, 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; } // If we are connected to a CMB service or we are running a mock CMB service then send to the "server" identifier if (distributedAuthority) { if (!NetworkManager.DAHost) { NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, 0); return; } else { foreach (var clientId in NetworkManager.ConnectedClientsIds) { if (clientId == NetworkManager.ServerClientId) { continue; } NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); } } } else { // Otherwise we are running in client-server =or= this has to be a DAHost instance. // Send to all connected clients. unsafe { var maxCount = NetworkManager.ConnectedClientsIds.Count; ulong* clientIds = stackalloc ulong[maxCount]; int idx = 0; foreach (var clientId in NetworkManager.ConnectedClientsIds) { if (clientId == NetworkManager.ServerClientId) { continue; } if (Observers.Contains(clientId)) { clientIds[idx++] = clientId; } } NetworkManager.ConnectionManager.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, bool orphanedChildPass = 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 SetCachedParent(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) { SetCachedParent(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]; // If we are handling an orphaned child and its parent is orphaned too, then don't parent yet. if (orphanedChildPass) { if (OrphanChildren.Contains(parentObject)) { return false; } } SetCachedParent(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(orphanedChildPass: true)) { objectsToRemove.Add(orphanObject); } } foreach (var networkObject in objectsToRemove) { OrphanChildren.Remove(networkObject); } } internal void InvokeBehaviourNetworkSpawn() { NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); if (SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) { AddNetworkObjectToSceneChangedUpdates(this); } 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(); } if (SceneMigrationSynchronization && NetworkManager.NetworkConfig.EnableSceneManagement) { RemoveNetworkObjectFromSceneChangedUpdates(this); } } 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) { if (NetworkManager.DistributedAuthorityMode) { writer.WriteValueSafe((ushort)ChildNetworkBehaviours.Count); if (ChildNetworkBehaviours.Count == 0) { return; } } 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 bool SetNetworkVariableData(FastBufferReader reader, ulong clientId) { if (NetworkManager.DistributedAuthorityMode) { var readerPosition = reader.Position; reader.ReadValueSafe(out ushort behaviorCount); if (behaviorCount != ChildNetworkBehaviours.Count) { Debug.LogError($"Network Behavior Count Mismatch! [{readerPosition}][{reader.Position}]"); return false; } } for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behaviour = ChildNetworkBehaviours[i]; behaviour.InitializeVariables(); behaviour.SetNetworkVariableData(reader, clientId); } return true; } public 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 ushort m_BitField; public uint Hash; public ulong NetworkObjectId; public ulong OwnerClientId; public ushort OwnershipFlags; 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); } /// /// Even though the server sends notifications for NetworkObjects that get /// destroyed when a scene is unloaded, we want to synchronize this so /// the client side can use it as part of a filter for automatically migrating /// to the current active scene when its scene is unloaded. (only for dynamically spawned) /// public bool DestroyWithScene { get => ByteUtility.GetBit(m_BitField, 6); set => ByteUtility.SetBit(ref m_BitField, 6, value); } public bool DontDestroyWithOwner { get => ByteUtility.GetBit(m_BitField, 7); set => ByteUtility.SetBit(ref m_BitField, 7, value); } public bool HasOwnershipFlags { get => ByteUtility.GetBit(m_BitField, 8); set => ByteUtility.SetBit(ref m_BitField, 8, value); } public bool SyncObservers { get => ByteUtility.GetBit(m_BitField, 9); set => ByteUtility.SetBit(ref m_BitField, 9, value); } public bool SpawnWithObservers { get => ByteUtility.GetBit(m_BitField, 10); set => ByteUtility.SetBit(ref m_BitField, 10, value); } // When handling the initial synchronization of NetworkObjects, // this will be populated with the known observers. public ulong[] Observers; //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) { if (OwnerObject.NetworkManager.DistributedAuthorityMode) { HasOwnershipFlags = true; SpawnWithObservers = OwnerObject.SpawnWithObservers; } 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); } } if (HasOwnershipFlags) { writer.WriteValueSafe(OwnershipFlags); } if (SyncObservers) { BytePacker.WriteValuePacked(writer, Observers.Length); foreach (var observer in Observers) { BytePacker.WriteValuePacked(writer, observer); } } var writeSize = 0; writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; writeSize += FastBufferWriter.GetWriteSize(); if (!writer.TryBeginWrite(writeSize)) { throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); } if (HasTransform) { writer.WriteValue(Transform); } // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. if (OwnerObject.NetworkManager.DistributedAuthorityMode) { writer.WriteValue(OwnerObject.NetworkSceneHandle); } else { 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; } } if (HasOwnershipFlags) { reader.ReadValueSafe(out OwnershipFlags); } if (SyncObservers) { var observerCount = 0; var observerId = (ulong)0; ByteUnpacker.ReadValuePacked(reader, out observerCount); Observers = new ulong[observerCount]; for (int i = 0; i < observerCount; i++) { ByteUnpacker.ReadValuePacked(reader, out observerId); Observers[i] = observerId; } } var readSize = 0; readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += FastBufferWriter.GetWriteSize(); // 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); } // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. 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, targetClientId)) { 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 seekToEndOfSynchData = 0; var reader = serializer.GetFastBufferReader(); try { reader.ReadValueSafe(out ushort sizeOfSynchronizationData); seekToEndOfSynchData = reader.Position + sizeOfSynchronizationData; // Apply the network variable synchronization data if (!SetNetworkVariableData(reader, targetClientId)) { reader.Seek(seekToEndOfSynchData); return; } // 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++) { reader.ReadValueSafe(out networkBehaviourId); var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId); networkBehaviour.Synchronize(ref serializer, targetClientId); } if (seekToEndOfSynchData != reader.Position) { Debug.LogWarning($"[Size mismatch] Expected: {seekToEndOfSynchData} Currently At: {reader.Position}!"); } } catch { reader.Seek(seekToEndOfSynchData); } } } internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) { var obj = new SceneObject { NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, IsPlayerObject = IsPlayerObject, IsSceneObject = IsSceneObject ?? true, DestroyWithScene = DestroyWithScene, DontDestroyWithOwner = DontDestroyWithOwner, HasOwnershipFlags = NetworkManager.DistributedAuthorityMode, OwnershipFlags = (ushort)Ownership, SyncObservers = syncObservers, Observers = syncObservers ? Observers.ToArray() : null, NetworkSceneHandle = NetworkSceneHandle, 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 = SynchronizeTransform; // 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; // Always synchronize in-scene placed object's scale using local space if (obj.IsSceneObject) { syncScaleLocalSpaceRelative = obj.HasParent; } // 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 /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) { //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, sceneObject.DestroyWithScene); if (sceneObject.SyncObservers) { foreach (var observer in sceneObject.Observers) { networkObject.Observers.Add(observer); } } if (networkManager.DistributedAuthorityMode) { networkObject.SpawnWithObservers = sceneObject.SpawnWithObservers; } // If this was not invoked by a message handler, we are in distributed authority mode, and we are spawning with observers or // we are an observer (in case SpawnWithObservers is false) if (networkManager.DistributedAuthorityMode && (!invokedByMessage || networkObject.IsPlayerObject) && (networkObject.SpawnWithObservers || networkObject.Observers.Contains(networkManager.LocalClientId))) { if (networkManager.LocalClient != null && networkManager.LocalClient.PlayerObject != null) { var playerObject = networkManager.LocalClient.PlayerObject; if (networkObject.IsPlayerObject) { // If it is another player, then make sure the local player is aware of the player playerObject.Observers.Add(networkObject.OwnerClientId); } // Assure the local player has observability networkObject.Observers.Add(playerObject.OwnerClientId); // If it is a player object, then add it to all known spawned NetworkObjects that spawn with observers if (networkObject.IsPlayerObject) { foreach (var netObject in networkManager.SpawnManager.SpawnedObjects) { if (netObject.Value.SpawnWithObservers) { netObject.Value.Observers.Add(networkObject.OwnerClientId); } } } // Add all known players to the observers list if they don't already exist foreach (var player in networkManager.SpawnManager.PlayerObjects) { networkObject.Observers.Add(player.OwnerClientId); } } } return networkObject; } /// /// Subscribes to changes in the currently active scene /// /// /// Only for dynamically spawned NetworkObjects /// internal void SubscribeToActiveSceneForSynch() { if (ActiveSceneSynchronization) { if (IsSceneObject.HasValue && !IsSceneObject.Value) { // Just in case it is a recycled NetworkObject, unsubscribe first SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged; } } } /// /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating /// a NetworkObject's scene information. /// private void CurrentlyActiveSceneChanged(Scene current, Scene next) { // Early exit if there is no NetworkManager assigned, the NetworkManager is shutting down, the NetworkObject // is not spawned, or an in-scene placed NetworkObject if (NetworkManager == null || NetworkManager.ShutdownInProgress || !IsSpawned || IsSceneObject != false) { return; } // This check is here in the event a user wants to disable this for some reason but also wants // the NetworkObject to synchronize to changes in the currently active scene at some later time. if (ActiveSceneSynchronization) { // Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate // and update their scene handles if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null) { SceneManager.MoveGameObjectToScene(gameObject, next); SceneChangedUpdate(next); } } } /// /// Handles updating the NetworkObject's tracked scene handles /// internal void SceneChangedUpdate(Scene scene, bool notify = false) { // Avoiding edge case scenarios, if no NetworkSceneManager exit early if (NetworkManager.SceneManager == null || !IsSpawned) { return; } if (NetworkManager.SceneManager.IsSceneEventInProgress()) { return; } var isAuthority = HasAuthority; SceneOriginHandle = scene.handle; // non-authority needs to update the NetworkSceneHandle if (!isAuthority && NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle)) { NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle]; } else if (isAuthority) { // Since the authority is the source of truth for the NetworkSceneHandle, // the NetworkSceneHandle is the same as the SceneOriginHandle. if (NetworkManager.DistributedAuthorityMode) { NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle]; } else { NetworkSceneHandle = SceneOriginHandle; } } else // Otherwise, the client did not find the client to server scene handle if (NetworkManager.LogLevel == LogLevel.Developer) { // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject // into, but that scenario seemed very edge case and under most instances a user should be notified that this // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to // the server-side too. NetworkLog.LogWarningServer($"[Client-{NetworkManager.LocalClientId}][{gameObject.name}] Server - " + $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" + $"has no associated server side (network) scene handle!"); } OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes if (isAuthority && notify && transform.parent == null) { NetworkManager.SceneManager.NotifyNetworkObjectSceneChanged(this); } } internal static Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); internal static void AddNetworkObjectToSceneChangedUpdates(NetworkObject networkObject) { if (!NetworkObjectsToSynchronizeSceneChanges.ContainsKey(networkObject.NetworkObjectId)) { NetworkObjectsToSynchronizeSceneChanges.Add(networkObject.NetworkObjectId, networkObject); } networkObject.UpdateForSceneChanges(); } internal static void RemoveNetworkObjectFromSceneChangedUpdates(NetworkObject networkObject) { NetworkObjectsToSynchronizeSceneChanges.Remove(networkObject.NetworkObjectId); } internal static void UpdateNetworkObjectSceneChanges() { foreach (var entry in NetworkObjectsToSynchronizeSceneChanges) { entry.Value.UpdateForSceneChanges(); } } private void Awake() { SetCachedParent(transform.parent); SceneOrigin = gameObject.scene; } /// /// Update /// Detects if a NetworkObject's scene has changed for both server and client instances /// /// /// About In-Scene Placed NetworkObjects: /// Since the same scene can be loaded more than once and in-scene placed NetworkObjects GlobalObjectIdHash /// values are only unique to the scene asset itself (and not per scene instance loaded), we will not be able /// to add this same functionality to in-scene placed NetworkObjects until we have a way to generate /// per-NetworkObject-instance unique GlobalObjectIdHash values for in-scene placed NetworkObjects. /// internal void UpdateForSceneChanges() { // Early exit if SceneMigrationSynchronization is disabled, there is no NetworkManager assigned, // the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed // NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle if (!SceneMigrationSynchronization || !IsSpawned || NetworkManager == null || NetworkManager.ShutdownInProgress || !NetworkManager.NetworkConfig.EnableSceneManagement || IsSceneObject != false || gameObject.scene.handle == SceneOriginHandle) { return; } // Otherwise, this has to be a dynamically spawned NetworkObject that has been // migrated to a new scene. SceneChangedUpdate(gameObject.scene, true); } /// /// 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.IsServer) { if (NetworkManager.PrefabHandler.ContainsHandler(this)) { var globalObjectIdHash = NetworkManager.PrefabHandler.GetSourceGlobalObjectIdHash(GlobalObjectIdHash); return globalObjectIdHash == 0 ? GlobalObjectIdHash : globalObjectIdHash; } // If scene management is disabled and this is an in-scene placed NetworkObject then go ahead // and send the InScenePlacedSourcePrefab's GlobalObjectIdHash value (i.e. what to dynamically spawn) if (!NetworkManager.NetworkConfig.EnableSceneManagement && IsSceneObject.Value && InScenePlacedSourceGlobalObjectIdHash != 0) { return InScenePlacedSourceGlobalObjectIdHash; } // If the PrefabGlobalObjectIdHash is a non-zero value and the GlobalObjectIdHash value is // different from the PrefabGlobalObjectIdHash value, then the NetworkObject instance is // an override for the original network prefab (i.e. PrefabGlobalObjectIdHash) if (!IsSceneObject.Value && GlobalObjectIdHash != PrefabGlobalObjectIdHash) { // If the PrefabGlobalObjectIdHash is already populated (i.e. InstantiateAndSpawn used), then return this if (PrefabGlobalObjectIdHash != 0) { return PrefabGlobalObjectIdHash; } else { // For legacy manual instantiation and spawning, check the OverrideToNetworkPrefab for a possible match if (NetworkManager.NetworkConfig.Prefabs.OverrideToNetworkPrefab.ContainsKey(GlobalObjectIdHash)) { return NetworkManager.NetworkConfig.Prefabs.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); } } } }