using FishNet.Managing; using FishNet.Connection; using UnityEngine; using FishNet.Serializing; using FishNet.Transporting; using System.Collections.Generic; using System.Runtime.CompilerServices; using FishNet.Utility.Performance; using System; using FishNet.Managing.Object; using FishNet.Component.Ownership; #if UNITY_EDITOR using UnityEditor; #endif namespace FishNet.Object { [DisallowMultipleComponent] public sealed partial class NetworkObject : MonoBehaviour { #region Public. /// /// True if was nested during scene setup or within a prefab. /// [field: SerializeField, HideInInspector] public bool IsNested { get; private set; } /// /// NetworkConnection which predicted spawned this object. /// public NetworkConnection PredictedSpawner { get; private set; } = NetworkManager.EmptyConnection; /// /// True if this NetworkObject was active during edit. Will be true if placed in scene during edit, and was in active state on run. /// [System.NonSerialized] internal bool ActiveDuringEdit; /// /// Returns if this object was placed in the scene during edit-time. /// /// public bool IsSceneObject => (SceneId > 0); /// /// ComponentIndex for this NetworkBehaviour. /// [field: SerializeField, HideInInspector] public byte ComponentIndex { get; private set; } /// /// Unique Id for this NetworkObject. This does not represent the object owner. /// public int ObjectId { get; private set; } /// /// True if this NetworkObject is deinitializing. Will also be true until Initialize is called. May be false until the object is cleaned up if object is destroyed without using Despawn. /// internal bool IsDeinitializing { get; private set; } = true; /// /// PredictedSpawn component on this object. Will be null if not added manually. /// [field: SerializeField, HideInInspector] public PredictedSpawn PredictedSpawn { get; private set; } /// /// /// [field: SerializeField, HideInInspector] private NetworkBehaviour[] _networkBehaviours; /// /// NetworkBehaviours within the root and children of this object. /// public NetworkBehaviour[] NetworkBehaviours { get => _networkBehaviours; private set => _networkBehaviours = value; } /// /// NetworkObject parenting this instance. The parent NetworkObject will be null if there was no parent during serialization. /// [field: SerializeField, HideInInspector] public NetworkObject ParentNetworkObject { get; private set; } /// /// NetworkObjects nested beneath this one. Recursive NetworkObjects may exist within each entry of this field. /// [field: SerializeField, HideInInspector] public List ChildNetworkObjects { get; private set; } = new List(); /// /// /// [SerializeField, HideInInspector] internal TransformProperties SerializedTransformProperties = new TransformProperties(); /// /// Current state of the NetworkObject. /// [System.NonSerialized] internal NetworkObjectState State = NetworkObjectState.Unset; #endregion #region Serialized. /// /// True if the object will always initialize as a networked object. When false the object will not automatically initialize over the network. Using Spawn() on an object will always set that instance as networked. /// public bool IsNetworked { get => _isNetworked; private set => _isNetworked = value; } /// /// Sets IsNetworked value. This method must be called before Start. /// /// New IsNetworked value. public void SetIsNetworked(bool value) { IsNetworked = value; } [Tooltip("True if the object will always initialize as a networked object. When false the object will not automatically initialize over the network. Using Spawn() on an object will always set that instance as networked.")] [SerializeField] private bool _isNetworked = true; /// /// True to make this object global, and added to the DontDestroyOnLoad scene. This value may only be set for instantiated objects, and can be changed if done immediately after instantiating. /// public bool IsGlobal { get => _isGlobal; private set => _isGlobal = value; } /// /// Sets IsGlobal value. /// /// New global value. public void SetIsGlobal(bool value) { if (IsNested) { NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot change IsGlobal because it is nested. Only root objects may be set global."); return; } if (!IsDeinitializing) { NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot change IsGlobal as it's already initialized. IsGlobal may only be changed immediately after instantiating."); return; } if (IsSceneObject) { NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot have be global because it is a scene object. Only instantiated objects may be global."); return; } _networkObserverInitiliazed = false; IsGlobal = value; } [Tooltip("True to make this object global, and added to the DontDestroyOnLoad scene. This value may only be set for instantiated objects, and can be changed if done immediately after instantiating.")] [SerializeField] private bool _isGlobal; /// /// Order to initialize this object's callbacks when spawned with other NetworkObjects in the same tick. Default value is 0, negative values will execute callbacks first. /// public sbyte GetInitializeOrder() => _initializeOrder; [Tooltip("Order to initialize this object's callbacks when spawned with other NetworkObjects in the same tick. Default value is 0, negative values will execute callbacks first.")] [SerializeField] private sbyte _initializeOrder = 0; /// /// How to handle this object when it despawns. Scene objects are never destroyed when despawning. /// [SerializeField] [Tooltip("How to handle this object when it despawns. Scene objects are never destroyed when despawning.")] private DespawnType _defaultDespawnType = DespawnType.Destroy; /// /// True to use configured ObjectPool rather than destroy this NetworkObject when being despawned. Scene objects are never destroyed. /// public DespawnType GetDefaultDespawnType() => _defaultDespawnType; /// /// Sets DespawnType value. /// /// Default despawn type for this NetworkObject. public void SetDefaultDespawnType(DespawnType despawnType) { _defaultDespawnType = despawnType; } #endregion #region Private. /// /// True if disabled NetworkBehaviours have been initialized. /// private bool _disabledNetworkBehavioursInitialized; #endregion #region Const. /// /// Value used when the ObjectId has not been set. /// public const int UNSET_OBJECTID_VALUE = ushort.MaxValue; /// /// Value used when the PrefabId has not been set. /// public const int UNSET_PREFABID_VALUE = ushort.MaxValue; #endregion #region Editor Debug. #if UNITY_EDITOR private int _editorOwnerId; #endif #endregion private void Awake() { SetChildDespawnedState(); } private void Start() { TryStartDeactivation(); } /// /// Initializes NetworkBehaviours if they are disabled. /// private void InitializeNetworkBehavioursIfDisabled() { if (_disabledNetworkBehavioursInitialized) return; _disabledNetworkBehavioursInitialized = true; for (int i = 0; i < NetworkBehaviours.Length; i++) NetworkBehaviours[i].InitializeIfDisabled(); } /// /// Sets Despawned on child NetworkObjects if they are not enabled. /// private void SetChildDespawnedState() { NetworkObject nob; for (int i = 0; i < ChildNetworkObjects.Count; i++) { nob = ChildNetworkObjects[i]; if (!nob.gameObject.activeSelf) nob.State = NetworkObjectState.Despawned; } } /// /// Deactivates this NetworkObject during it's start cycle if conditions are met. /// internal void TryStartDeactivation() { if (!IsNetworked) return; //Global. if (IsGlobal && !IsSceneObject) DontDestroyOnLoad(gameObject); if (NetworkManager == null || (!NetworkManager.IsClient && !NetworkManager.IsServer)) { //ActiveDuringEdit is only used for scene objects. if (IsSceneObject) ActiveDuringEdit = true; gameObject.SetActive(false); } } private void OnDisable() { /* If deinitializing and an owner exist * then remove object from owner. */ if (IsDeinitializing && Owner.IsValid) Owner.RemoveObject(this); /* If not nested then check to despawn this OnDisable. * A nob may become disabled without being despawned if it's * beneath another deinitializing nob. This can be true even while * not nested because users may move a nob under another at runtime. * * This object must also be activeSelf, meaning that it became disabled * because a parent was. If not activeSelf then it's possible the * user simply deactivated the object themselves. */ else if (IsServer && !IsNested && gameObject.activeSelf) { bool canDespawn = false; Transform nextParent = transform.parent; while (nextParent != null) { if (nextParent.TryGetComponent(out NetworkObject pNob)) { /* If pNob is not the same as ParentNetworkObject * then that means this object was moved around. It could be * that this was previously a child of something else * or that was given a parent later on in it's life cycle. ^ * When this occurs do not send a despawn for this object. * Rather, let it destroy from unity callbacks which will force * the proper destroy/stop cycle. */ if (pNob != ParentNetworkObject) break; //If nob is deinitialized then this one cannot exist. if (pNob.IsDeinitializing) { canDespawn = true; break; } } nextParent = nextParent.parent; } if (canDespawn) Despawn(); } } private void OnDestroy() { //Does this need to be here? I'm thinking no, remove it and examine later. //todo if (Owner.IsValid) Owner.RemoveObject(this); //Already being deinitialized by FishNet. if (IsDeinitializing) return; if (NetworkManager != null) { //Was destroyed without going through the proper methods. if (NetworkManager.IsServer) NetworkManager.ServerManager.Objects.NetworkObjectUnexpectedlyDestroyed(this, true); if (NetworkManager.IsClient) NetworkManager.ClientManager.Objects.NetworkObjectUnexpectedlyDestroyed(this, false); } /* When destroyed unexpectedly it's * impossible to know if this occurred on * the server or client side, so send callbacks * for both. */ if (IsServer) InvokeStopCallbacks(true); if (IsClient) InvokeStopCallbacks(false); /* If owner exist then remove object from owner. * This has to be called here as well OnDisable because * the OnDisable will only remove the object if * deinitializing. This is because the object shouldn't * be removed from owner if the object is simply being * disabled, but not deinitialized. But in the scenario * the object is unexpectedly destroyed, which is how we * arrive here, the object needs to be removed from owner. */ if (Owner.IsValid) Owner.RemoveObject(this); Observers.Clear(); IsDeinitializing = true; SetActiveStatus(false); //Do not need to set state if being destroyed. //Don't need to reset sync types if object is being destroyed. } /// /// Sets IsClient or IsServer to isActive. /// private void SetActiveStatus(bool isActive, bool server) { if (server) IsServer = isActive; else IsClient = isActive; } /// /// Sets IsClient and IsServer to isActive. /// private void SetActiveStatus(bool isActive) { IsServer = isActive; IsClient = isActive; } /// /// Initializes this script. This is only called once even when as host. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Preinitialize_Internal(NetworkManager networkManager, int objectId, NetworkConnection owner, bool asServer) { State = NetworkObjectState.Spawned; InitializeNetworkBehavioursIfDisabled(); IsDeinitializing = false; //QOL references. NetworkManager = networkManager; ServerManager = networkManager.ServerManager; ClientManager = networkManager.ClientManager; ObserverManager = networkManager.ObserverManager; TransportManager = networkManager.TransportManager; TimeManager = networkManager.TimeManager; SceneManager = networkManager.SceneManager; PredictionManager = networkManager.PredictionManager; RollbackManager = networkManager.RollbackManager; SetOwner(owner); ObjectId = objectId; /* This must be called at the beginning * so that all conditions are handled by the observer * manager prior to the preinitialize call on networkobserver. * The method called is dependent on NetworkManager being set. */ AddDefaultNetworkObserverConditions(); for (int i = 0; i < NetworkBehaviours.Length; i++) NetworkBehaviours[i].InitializeOnce_Internal(); /* NetworkObserver uses some information from * NetworkBehaviour so it must be preinitialized * after NetworkBehaviours are. */ if (asServer) NetworkObserver.PreInitialize(this); _networkObserverInitiliazed = true; //Add to connection objects if owner exist. if (owner != null) owner.AddObject(this); } /// /// Adds a NetworkBehaviour and serializes it's components. /// internal T AddAndSerialize() where T : NetworkBehaviour //runtimeNB make public. { int startingLength = NetworkBehaviours.Length; T result = gameObject.AddComponent(); //Add to network behaviours. Array.Resize(ref _networkBehaviours, startingLength + 1); _networkBehaviours[startingLength] = result; //Serialize values and return. result.SerializeComponents(this, (byte)startingLength); return result; } /// /// Updates NetworkBehaviours and initializes them with serialized values. /// /// True if this call originated from a prefab collection, such as during it's initialization. internal void UpdateNetworkBehaviours(NetworkObject parentNob, ref byte componentIndex) //runtimeNB make public. { /* This method can be called by the developer initializing prefabs, the prefab collection doing it automatically, * or when the networkobject is modified or added to an object. * * Prefab collections generally contain all prefabs, meaning they will not only call this on the topmost * networkobject but also each child, as the child would be it's own prefab in the collection. This assumes * that is, the child is a nested prefab. * * Because of this potential a check must be done where if the componentIndex is 0 we must look * for a networkobject above this one. If there is a networkObject above this one then we know the prefab * is being initialized individually, not part of a recursive check. In this case exit early * as the parent would have already resolved the needed information. */ //If first componentIndex make sure there's no more than maximum allowed nested nobs. if (componentIndex == 0) { //Not possible for index to be 0 and nested. if (IsNested) return; byte maxNobs = 255; if (GetComponentsInChildren(true).Length > maxNobs) { Debug.LogError($"The number of child NetworkObjects on {gameObject.name} exceeds the maximum of {maxNobs}."); return; } } PredictedSpawn = GetComponent(); ComponentIndex = componentIndex; ParentNetworkObject = parentNob; //Transforms which can be searched for networkbehaviours. ListCache transformCache = ListCaches.GetTransformCache(); transformCache.Reset(); ChildNetworkObjects.Clear(); transformCache.AddValue(transform); for (int z = 0; z < transformCache.Written; z++) { Transform currentT = transformCache.Collection[z]; for (int i = 0; i < currentT.childCount; i++) { Transform t = currentT.GetChild(i); /* If contains a nob then do not add to transformsCache. * Do add to ChildNetworkObjects so it can be initialized when * parent is. */ if (t.TryGetComponent(out NetworkObject childNob)) { /* Make sure both objects have the same value for * IsSceneObject. It's possible the user instantiated * an object and placed it beneath a scene object * before the scene initialized. They may also * add a scene object under an instantiated, even though * this almost certainly will break things. */ if (IsSceneObject == childNob.IsSceneObject) ChildNetworkObjects.Add(childNob); } else { transformCache.AddValue(t); } } } int written; //Iterate all cached transforms and get networkbehaviours. ListCache nbCache = ListCaches.GetNetworkBehaviourCache(); nbCache.Reset(); written = transformCache.Written; List ts = transformCache.Collection; // for (int i = 0; i < written; i++) nbCache.AddValues(ts[i].GetNetworkBehaviours()); //Copy to array. written = nbCache.Written; List nbs = nbCache.Collection; NetworkBehaviours = new NetworkBehaviour[written]; // for (int i = 0; i < written; i++) { NetworkBehaviours[i] = nbs[i]; NetworkBehaviours[i].SerializeComponents(this, (byte)i); } ListCaches.StoreCache(transformCache); ListCaches.StoreCache(nbCache); //Tell children nobs to update their NetworkBehaviours. foreach (NetworkObject item in ChildNetworkObjects) { componentIndex++; item.UpdateNetworkBehaviours(this, ref componentIndex); } } /// /// Called after all data is synchronized with this NetworkObject. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Initialize(bool asServer, bool invokeSyncTypeCallbacks) { InitializeCallbacks(asServer, invokeSyncTypeCallbacks); } /// /// Called to prepare this object to be destroyed or disabled. /// internal void Deinitialize(bool asServer) { InvokeStopCallbacks(asServer); if (asServer) { IsDeinitializing = true; } else { ClientManager.Connection.LevelOfDetails.Remove(this); //Client only. if (!NetworkManager.IsServer) IsDeinitializing = true; RemoveClientRpcLinkIndexes(); } SetActiveStatus(false, asServer); if (asServer) Observers.Clear(); } /// /// Resets states for object to be pooled. /// /// True if performing as server. public void ResetForObjectPool() { int count = NetworkBehaviours.Length; for (int i = 0; i < count; i++) NetworkBehaviours[i].ResetForObjectPool(); State = NetworkObjectState.Unset; SetOwner(NetworkManager.EmptyConnection); NetworkObserver.Deinitialize(); //QOL references. NetworkManager = null; ServerManager = null; ClientManager = null; ObserverManager = null; TransportManager = null; TimeManager = null; SceneManager = null; RollbackManager = null; //Misc sets. ObjectId = 0; ClientInitialized = false; } /// /// Removes ownership from all clients. /// public void RemoveOwnership() { GiveOwnership(null, true); } /// /// Gives ownership to newOwner. /// /// public void GiveOwnership(NetworkConnection newOwner) { GiveOwnership(newOwner, true); } /// /// Gives ownership to newOwner. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void GiveOwnership(NetworkConnection newOwner, bool asServer) { /* Additional asServer checks. */ if (asServer) { if (!NetworkManager.IsServer) { NetworkManager.LogWarning($"Ownership cannot be given for object {gameObject.name}. Only server may give ownership."); return; } //If the same owner don't bother sending a message, just ignore request. if (newOwner == Owner && asServer) return; if (newOwner != null && newOwner.IsActive && !newOwner.LoadedStartScenes(true)) { NetworkManager.LogWarning($"Ownership has been transfered to ConnectionId {newOwner.ClientId} but this is not recommended until after they have loaded start scenes. You can be notified when a connection loads start scenes by using connection.OnLoadedStartScenes on the connection, or SceneManager.OnClientLoadStartScenes."); } } bool activeNewOwner = (newOwner != null && newOwner.IsActive); //Set prevOwner, disallowing null. NetworkConnection prevOwner = Owner; if (prevOwner == null) prevOwner = NetworkManager.EmptyConnection; SetOwner(newOwner); /* Only modify objects if asServer or not * host. When host, server would * have already modified objects * collection so there is no need * for client to as well. */ if (asServer || !NetworkManager.IsHost) { if (activeNewOwner) newOwner.AddObject(this); if (prevOwner.IsValid && prevOwner != newOwner) prevOwner.RemoveObject(this); } //After changing owners invoke callbacks. InvokeOwnership(prevOwner, asServer); //If asServer send updates to clients as needed. if (asServer) { if (activeNewOwner) ServerManager.Objects.RebuildObservers(this, newOwner); using (PooledWriter writer = WriterPool.GetWriter()) { writer.WritePacketId(PacketId.OwnershipChange); writer.WriteNetworkObject(this); writer.WriteNetworkConnection(Owner); //If sharing then send to all observers. if (NetworkManager.ServerManager.ShareIds) { NetworkManager.TransportManager.SendToClients((byte)Channel.Reliable, writer.GetArraySegment(), this); } //Only sending to old / new. else { if (prevOwner.IsActive) NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), prevOwner); if (activeNewOwner) NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), newOwner); } } if (prevOwner.IsActive) ServerManager.Objects.RebuildObservers(prevOwner); } } /// /// Initializes a predicted object for client. /// internal void InitializePredictedObject_Server(NetworkManager manager, NetworkConnection predictedSpawner) { NetworkManager = manager; PredictedSpawner = predictedSpawner; } /// /// Initializes a predicted object for client. /// internal void PreinitializePredictedObject_Client(NetworkManager manager, int objectId, NetworkConnection owner, NetworkConnection predictedSpawner) { PredictedSpawner = predictedSpawner; Preinitialize_Internal(manager, objectId, owner, false); } /// /// Deinitializes this predicted spawned object. /// internal void DeinitializePredictedObject_Client() { /* For the time being we're just going to disable the object because * deinitializing instead could present a lot of problems. * For example: if client deinitializes rpc links are unregistered, * and if server had a rpc on the way already the link would * not be found. This would cause the reader length to be wrong * resulting in packet corruption. */ gameObject.SetActive(false); } /// /// Sets the owner of this object. /// /// /// private void SetOwner(NetworkConnection owner) { Owner = owner; } /// /// Returns if this NetworkObject is a scene object, and has changed. /// /// internal ChangedTransformProperties GetTransformChanges(TransformProperties stp) { ChangedTransformProperties ctp = ChangedTransformProperties.Unset; if (transform.localPosition != stp.Position) ctp |= ChangedTransformProperties.LocalPosition; if (transform.localRotation != stp.Rotation) ctp |= ChangedTransformProperties.LocalRotation; if (transform.localScale != stp.LocalScale) ctp |= ChangedTransformProperties.LocalScale; return ctp; } /// /// Returns if this NetworkObject is a scene object, and has changed. /// /// internal ChangedTransformProperties GetTransformChanges(GameObject prefab) { Transform t = prefab.transform; ChangedTransformProperties ctp = ChangedTransformProperties.Unset; if (transform.position != t.position) ctp |= ChangedTransformProperties.LocalPosition; if (transform.rotation != t.rotation) ctp |= ChangedTransformProperties.LocalRotation; if (transform.localScale != t.localScale) ctp |= ChangedTransformProperties.LocalScale; return ctp; } #region Editor. #if UNITY_EDITOR /// /// Removes duplicate NetworkObject components on this object returning the removed count. /// /// internal int RemoveDuplicateNetworkObjects() { NetworkObject[] nobs = GetComponents(); for (int i = 1; i < nobs.Length; i++) DestroyImmediate(nobs[i]); return (nobs.Length - 1); } /// /// Sets IsNested and returns the result. /// /// private bool SetIsNestedThroughTraversal() { Transform parent = transform.parent; //Iterate long as parent isn't null, and isnt self. while (parent != null && parent != transform) { if (parent.TryGetComponent(out _)) { IsNested = true; return IsNested; } parent = parent.parent; } //No NetworkObject found in parents, meaning this is not nested. IsNested = false; return IsNested; } private void OnValidate() { SetIsNestedThroughTraversal(); SceneUpdateNetworkBehaviours(); ReferenceIds_OnValidate(); if (IsGlobal && IsSceneObject) Debug.LogWarning($"Object {gameObject.name} will have it's IsGlobal state ignored because it is a scene object. Instantiated copies will still be global. This warning is informative only."); } private void Reset() { SetIsNestedThroughTraversal(); SerializeTransformProperties(); SceneUpdateNetworkBehaviours(); ReferenceIds_Reset(); } private void SceneUpdateNetworkBehaviours() { //In a scene. if (!string.IsNullOrEmpty(gameObject.scene.name)) { if (IsNested) return; byte componentIndex = 0; UpdateNetworkBehaviours(null, ref componentIndex); } } private void OnDrawGizmosSelected() { _editorOwnerId = (Owner == null) ? -1 : Owner.ClientId; SerializeTransformProperties(); } /// /// Serializes TransformProperties to current transform properties. /// private void SerializeTransformProperties() { /* Use this method to set scene data since it doesn't need to exist outside * the editor and because its updated regularly while selected. */ //If a scene object. if (!EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name)) { SerializedTransformProperties = new TransformProperties( transform.localPosition, transform.localRotation, transform.localScale); } } #endif #endregion } }