The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [1.0.0-pre.7] - 2022-04-01 ### Added - Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828) - Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823) - Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762) - `UnityTransport` settings can now be set programmatically. (#1845) - `FastBufferWriter` and Reader IsInitialized property. (#1859) ### Changed - Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849) ### Removed - Removed `SnapshotSystem` (#1852) - Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812) - Removed `com.unity.collections` dependency from the package (#1849) ### Fixed - Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850) - Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847) - Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841) - Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838) - Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836) - Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828) - Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821) - Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811) - Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809) - Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808) - Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801) - Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780) - Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779) - Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777) - Fixed parenting warning printing for false positives (#1855)
1069 lines
40 KiB
C#
1069 lines
40 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.CompilerServices;
|
||
using UnityEngine;
|
||
|
||
namespace Unity.Netcode
|
||
{
|
||
/// <summary>
|
||
/// A component used to identify that a GameObject in the network
|
||
/// </summary>
|
||
[AddComponentMenu("Netcode/" + nameof(NetworkObject), -99)]
|
||
[DisallowMultipleComponent]
|
||
public sealed class NetworkObject : MonoBehaviour
|
||
{
|
||
[HideInInspector]
|
||
[SerializeField]
|
||
internal uint GlobalObjectIdHash;
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnValidate()
|
||
{
|
||
GenerateGlobalObjectIdHash();
|
||
}
|
||
|
||
internal void GenerateGlobalObjectIdHash()
|
||
{
|
||
// do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode
|
||
if (UnityEditor.EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode
|
||
if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var globalObjectIdString = UnityEditor.GlobalObjectId.GetGlobalObjectIdSlow(this).ToString();
|
||
GlobalObjectIdHash = XXHash.Hash32(globalObjectIdString);
|
||
}
|
||
#endif // UNITY_EDITOR
|
||
|
||
/// <summary>
|
||
/// Gets the NetworkManager that owns this NetworkObject instance
|
||
/// </summary>
|
||
public NetworkManager NetworkManager => NetworkManagerOwner ?? NetworkManager.Singleton;
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
internal NetworkManager NetworkManagerOwner;
|
||
|
||
/// <summary>
|
||
/// Gets the unique Id of this object that is synced across the network
|
||
/// </summary>
|
||
public ulong NetworkObjectId { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Gets the ClientId of the owner of this NetworkObject
|
||
/// </summary>
|
||
public ulong OwnerClientId { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// If true, the object will always be replicated as root on clients and the parent will be ignored.
|
||
/// </summary>
|
||
public bool AlwaysReplicateAsRoot;
|
||
|
||
/// <summary>
|
||
/// Gets if this object is a player object
|
||
/// </summary>
|
||
public bool IsPlayerObject { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Gets if the object is the the personal clients player object
|
||
/// </summary>
|
||
public bool IsLocalPlayer => NetworkManager != null && IsPlayerObject && OwnerClientId == NetworkManager.LocalClientId;
|
||
|
||
/// <summary>
|
||
/// Gets if the object is owned by the local player or if the object is the local player object
|
||
/// </summary>
|
||
public bool IsOwner => NetworkManager != null && OwnerClientId == NetworkManager.LocalClientId;
|
||
|
||
/// <summary>
|
||
/// Gets Whether or not the object is owned by anyone
|
||
/// </summary>
|
||
public bool IsOwnedByServer => NetworkManager != null && OwnerClientId == NetworkManager.ServerClientId;
|
||
|
||
/// <summary>
|
||
/// Gets if the object has yet been spawned across the network
|
||
/// </summary>
|
||
public bool IsSpawned { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Gets if the object is a SceneObject, null if it's not yet spawned but is a scene object.
|
||
/// </summary>
|
||
public bool? IsSceneObject { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Gets whether or not the object should be automatically removed when the scene is unloaded.
|
||
/// </summary>
|
||
public bool DestroyWithScene { get; set; }
|
||
|
||
/// <summary>
|
||
/// Delegate type for checking visibility
|
||
/// </summary>
|
||
/// <param name="clientId">The clientId to check visibility for</param>
|
||
public delegate bool VisibilityDelegate(ulong clientId);
|
||
|
||
/// <summary>
|
||
/// Delegate invoked when the netcode needs to know if the object should be visible to a client, if null it will assume true
|
||
/// </summary>
|
||
public VisibilityDelegate CheckObjectVisibility = null;
|
||
|
||
/// <summary>
|
||
/// Delegate type for checking spawn options
|
||
/// </summary>
|
||
/// <param name="clientId">The clientId to check spawn options for</param>
|
||
public delegate bool SpawnDelegate(ulong clientId);
|
||
|
||
/// <summary>
|
||
/// Delegate invoked when the netcode needs to know if it should include the transform when spawning the object, if null it will assume true
|
||
/// </summary>
|
||
public SpawnDelegate IncludeTransformWhenSpawning = null;
|
||
|
||
/// <summary>
|
||
/// Whether or not to destroy this object if it's owner is destroyed.
|
||
/// If false, the objects ownership will be given to the server.
|
||
/// </summary>
|
||
public bool DontDestroyWithOwner;
|
||
|
||
/// <summary>
|
||
/// Whether or not to enable automatic NetworkObject parent synchronization.
|
||
/// </summary>
|
||
public bool AutoObjectParentSync = true;
|
||
|
||
internal readonly HashSet<ulong> Observers = new HashSet<ulong>();
|
||
|
||
#if MULTIPLAYER_TOOLS
|
||
private string m_CachedNameForMetrics;
|
||
#endif
|
||
internal string GetNameForMetrics()
|
||
{
|
||
#if MULTIPLAYER_TOOLS
|
||
return m_CachedNameForMetrics ??= name;
|
||
#else
|
||
return null;
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns Observers enumerator
|
||
/// </summary>
|
||
/// <returns>Observers enumerator</returns>
|
||
public HashSet<ulong>.Enumerator GetObservers()
|
||
{
|
||
if (!IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
return Observers.GetEnumerator();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Whether or not this object is visible to a specific client
|
||
/// </summary>
|
||
/// <param name="clientId">The clientId of the client</param>
|
||
/// <returns>True if the client knows about the object</returns>
|
||
public bool IsNetworkVisibleTo(ulong clientId)
|
||
{
|
||
if (!IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
return Observers.Contains(clientId);
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
SetCachedParent(transform.parent);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Shows a previously hidden <see cref="NetworkObject"/> to a client
|
||
/// </summary>
|
||
/// <param name="clientId">The client to show the <see cref="NetworkObject"/> to</param>
|
||
public void NetworkShow(ulong clientId)
|
||
{
|
||
if (!IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
throw new NotServerException("Only server can change visibility");
|
||
}
|
||
|
||
if (Observers.Contains(clientId))
|
||
{
|
||
throw new VisibilityChangeException("The object is already visible");
|
||
}
|
||
|
||
Observers.Add(clientId);
|
||
|
||
NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Shows a list of previously hidden <see cref="NetworkObject"/>s to a client
|
||
/// </summary>
|
||
/// <param name="networkObjects">The <see cref="NetworkObject"/>s to show</param>
|
||
/// <param name="clientId">The client to show the objects to</param>
|
||
public static void NetworkShow(List<NetworkObject> networkObjects, ulong clientId)
|
||
{
|
||
if (networkObjects == null || networkObjects.Count == 0)
|
||
{
|
||
throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided");
|
||
}
|
||
|
||
NetworkManager networkManager = networkObjects[0].NetworkManager;
|
||
|
||
if (!networkManager.IsServer)
|
||
{
|
||
throw new NotServerException("Only server can change visibility");
|
||
}
|
||
|
||
// Do the safety loop first to prevent putting the netcode in an invalid state.
|
||
for (int i = 0; i < networkObjects.Count; i++)
|
||
{
|
||
if (!networkObjects[i].IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
if (networkObjects[i].Observers.Contains(clientId))
|
||
{
|
||
throw new VisibilityChangeException($"{nameof(NetworkObject)} with NetworkId: {networkObjects[i].NetworkObjectId} is already visible");
|
||
}
|
||
|
||
if (networkObjects[i].NetworkManager != networkManager)
|
||
{
|
||
throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager));
|
||
}
|
||
}
|
||
|
||
foreach (var networkObject in networkObjects)
|
||
{
|
||
networkObject.NetworkShow(clientId);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Hides a object from a specific client
|
||
/// </summary>
|
||
/// <param name="clientId">The client to hide the object for</param>
|
||
public void NetworkHide(ulong clientId)
|
||
{
|
||
if (!IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
throw new NotServerException("Only server can change visibility");
|
||
}
|
||
|
||
if (!Observers.Contains(clientId))
|
||
{
|
||
throw new VisibilityChangeException("The object is already hidden");
|
||
}
|
||
|
||
if (clientId == NetworkManager.ServerClientId)
|
||
{
|
||
throw new VisibilityChangeException("Cannot hide an object from the server");
|
||
}
|
||
|
||
Observers.Remove(clientId);
|
||
|
||
var message = new DestroyObjectMessage
|
||
{
|
||
NetworkObjectId = NetworkObjectId
|
||
};
|
||
// Send destroy call
|
||
var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId);
|
||
NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Hides a list of objects from a client
|
||
/// </summary>
|
||
/// <param name="networkObjects">The objects to hide</param>
|
||
/// <param name="clientId">The client to hide the objects from</param>
|
||
public static void NetworkHide(List<NetworkObject> networkObjects, ulong clientId)
|
||
{
|
||
if (networkObjects == null || networkObjects.Count == 0)
|
||
{
|
||
throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided");
|
||
}
|
||
|
||
var networkManager = networkObjects[0].NetworkManager;
|
||
|
||
if (!networkManager.IsServer)
|
||
{
|
||
throw new NotServerException("Only server can change visibility");
|
||
}
|
||
|
||
if (clientId == NetworkManager.ServerClientId)
|
||
{
|
||
throw new VisibilityChangeException("Cannot hide an object from the server");
|
||
}
|
||
|
||
// Do the safety loop first to prevent putting the netcode in an invalid state.
|
||
for (int i = 0; i < networkObjects.Count; i++)
|
||
{
|
||
if (!networkObjects[i].IsSpawned)
|
||
{
|
||
throw new SpawnStateException("Object is not spawned");
|
||
}
|
||
|
||
if (!networkObjects[i].Observers.Contains(clientId))
|
||
{
|
||
throw new VisibilityChangeException($"{nameof(NetworkObject)} with {nameof(NetworkObjectId)}: {networkObjects[i].NetworkObjectId} is already hidden");
|
||
}
|
||
|
||
if (networkObjects[i].NetworkManager != networkManager)
|
||
{
|
||
throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager));
|
||
}
|
||
}
|
||
|
||
foreach (var networkObject in networkObjects)
|
||
{
|
||
networkObject.NetworkHide(clientId);
|
||
}
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned &&
|
||
(IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true)))
|
||
{
|
||
throw new NotServerException($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead.");
|
||
}
|
||
|
||
if (NetworkManager != null && NetworkManager.SpawnManager != null &&
|
||
NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject))
|
||
{
|
||
NetworkManager.SpawnManager.OnDespawnObject(networkObject, false);
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject)
|
||
{
|
||
if (!NetworkManager.IsListening)
|
||
{
|
||
throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects");
|
||
}
|
||
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s");
|
||
}
|
||
|
||
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), false, playerObject, ownerClientId, destroyWithScene);
|
||
|
||
for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++)
|
||
{
|
||
if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId))
|
||
{
|
||
NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Spawns this <see cref="NetworkObject"/> across the network. Can only be called from the Server
|
||
/// </summary>
|
||
/// <param name="destroyWithScene">Should the object be destroyed when the scene is changed</param>
|
||
public void Spawn(bool destroyWithScene = false)
|
||
{
|
||
SpawnInternal(destroyWithScene, NetworkManager.ServerClientId, false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Spawns a <see cref="NetworkObject"/> across the network with a given owner. Can only be called from server
|
||
/// </summary>
|
||
/// <param name="clientId">The clientId to own the object</param>
|
||
/// <param name="destroyWithScene">Should the object be destroyed when the scene is changed</param>
|
||
public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false)
|
||
{
|
||
SpawnInternal(destroyWithScene, clientId, false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Spawns a <see cref="NetworkObject"/> across the network and makes it the player object for the given client
|
||
/// </summary>
|
||
/// <param name="clientId">The clientId whos player object this is</param>
|
||
/// <param name="destroyWithScene">Should the object be destroyd when the scene is changed</param>
|
||
public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false)
|
||
{
|
||
SpawnInternal(destroyWithScene, clientId, true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Despawns the <see cref="GameObject"/> of this <see cref="NetworkObject"/> and sends a destroy message for it to all connected clients.
|
||
/// </summary>
|
||
/// <param name="destroy">(true) the <see cref="GameObject"/> will be destroyed (false) the <see cref="GameObject"/> will persist after being despawned</param>
|
||
public void Despawn(bool destroy = true)
|
||
{
|
||
NetworkManager.SpawnManager.DespawnObject(this, destroy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes all ownership of an object from any client. Can only be called from server
|
||
/// </summary>
|
||
public void RemoveOwnership()
|
||
{
|
||
NetworkManager.SpawnManager.RemoveOwnership(this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Changes the owner of the object. Can only be called from server
|
||
/// </summary>
|
||
/// <param name="newOwnerClientId">The new owner clientId</param>
|
||
public void ChangeOwnership(ulong newOwnerClientId)
|
||
{
|
||
NetworkManager.SpawnManager.ChangeOwnership(this, newOwnerClientId);
|
||
}
|
||
|
||
internal void InvokeBehaviourOnLostOwnership()
|
||
{
|
||
// Server already handles this earlier, hosts should ignore, all clients should update
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true);
|
||
}
|
||
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
ChildNetworkBehaviours[i].InternalOnLostOwnership();
|
||
}
|
||
}
|
||
|
||
internal void InvokeBehaviourOnGainedOwnership()
|
||
{
|
||
// Server already handles this earlier, hosts should ignore and only client owners should update
|
||
if (!NetworkManager.IsServer && NetworkManager.LocalClientId == OwnerClientId)
|
||
{
|
||
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId);
|
||
}
|
||
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
ChildNetworkBehaviours[i].InternalOnGainedOwnership();
|
||
}
|
||
}
|
||
|
||
internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNetworkObject)
|
||
{
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
ChildNetworkBehaviours[i].OnNetworkObjectParentChanged(parentNetworkObject);
|
||
}
|
||
}
|
||
|
||
private bool m_IsReparented; // Did initial parent (came from the scene hierarchy) change at runtime?
|
||
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?
|
||
|
||
internal void SetCachedParent(Transform parentTransform)
|
||
{
|
||
m_CachedParent = parentTransform;
|
||
}
|
||
|
||
internal (bool IsReparented, ulong? LatestParent) GetNetworkParenting() => (m_IsReparented, m_LatestParent);
|
||
|
||
internal void SetNetworkParenting(bool isReparented, ulong? latestParent)
|
||
{
|
||
m_IsReparented = isReparented;
|
||
m_LatestParent = latestParent;
|
||
}
|
||
|
||
public bool TrySetParent(Transform parent, bool worldPositionStays = true)
|
||
{
|
||
return TrySetParent(parent.GetComponent<NetworkObject>(), worldPositionStays);
|
||
}
|
||
|
||
public bool TrySetParent(GameObject parent, bool worldPositionStays = true)
|
||
{
|
||
return TrySetParent(parent.GetComponent<NetworkObject>(), worldPositionStays);
|
||
}
|
||
|
||
public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true)
|
||
{
|
||
if (!AutoObjectParentSync)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (NetworkManager == null || !NetworkManager.IsListening)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!IsSpawned)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (parent == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!parent.IsSpawned)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
transform.SetParent(parent.transform, worldPositionStays);
|
||
return true;
|
||
}
|
||
|
||
private void OnTransformParentChanged()
|
||
{
|
||
if (!AutoObjectParentSync)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (transform.parent == m_CachedParent)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (NetworkManager == null || !NetworkManager.IsListening)
|
||
{
|
||
transform.parent = m_CachedParent;
|
||
Debug.LogException(new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before reparenting"));
|
||
return;
|
||
}
|
||
|
||
if (!NetworkManager.IsServer)
|
||
{
|
||
transform.parent = m_CachedParent;
|
||
Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s"));
|
||
return;
|
||
}
|
||
|
||
if (!IsSpawned)
|
||
{
|
||
transform.parent = m_CachedParent;
|
||
Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned"));
|
||
return;
|
||
}
|
||
|
||
var parentTransform = transform.parent;
|
||
if (parentTransform != null)
|
||
{
|
||
var parentObject = transform.parent.GetComponent<NetworkObject>();
|
||
if (parentObject == null)
|
||
{
|
||
transform.parent = m_CachedParent;
|
||
Debug.LogException(new InvalidParentException($"Invalid parenting, {nameof(NetworkObject)} moved under a non-{nameof(NetworkObject)} parent"));
|
||
return;
|
||
}
|
||
|
||
if (!parentObject.IsSpawned)
|
||
{
|
||
transform.parent = m_CachedParent;
|
||
Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented under another spawned {nameof(NetworkObject)}"));
|
||
return;
|
||
}
|
||
|
||
m_LatestParent = parentObject.NetworkObjectId;
|
||
}
|
||
else
|
||
{
|
||
m_LatestParent = null;
|
||
}
|
||
|
||
m_IsReparented = true;
|
||
ApplyNetworkParenting();
|
||
|
||
var message = new ParentSyncMessage
|
||
{
|
||
NetworkObjectId = NetworkObjectId,
|
||
IsReparented = m_IsReparented,
|
||
IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue,
|
||
LatestParent = m_LatestParent
|
||
};
|
||
|
||
unsafe
|
||
{
|
||
var maxCount = NetworkManager.ConnectedClientsIds.Count;
|
||
ulong* clientIds = stackalloc ulong[maxCount];
|
||
int idx = 0;
|
||
foreach (var clientId in NetworkManager.ConnectedClientsIds)
|
||
{
|
||
if (Observers.Contains(clientId))
|
||
{
|
||
clientIds[idx++] = clientId;
|
||
}
|
||
}
|
||
|
||
NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientIds, idx);
|
||
}
|
||
}
|
||
|
||
// We're keeping this set called OrphanChildren which contains NetworkObjects
|
||
// because at the time we initialize/spawn NetworkObject locally, we might not have its parent replicated from the other side
|
||
//
|
||
// For instance, if we're spawning NetworkObject 5 and its parent is 10, what should happen if we do not have 10 yet?
|
||
// let's say 10 is on the way to be replicated in a few frames and we could fix that parent-child relationship later.
|
||
//
|
||
// If you couldn't find your parent, we put you into OrphanChildren set and everytime 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<NetworkObject> OrphanChildren = new HashSet<NetworkObject>();
|
||
|
||
internal bool ApplyNetworkParenting()
|
||
{
|
||
if (!AutoObjectParentSync)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!IsSpawned)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!m_IsReparented)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (m_LatestParent == null || !m_LatestParent.HasValue)
|
||
{
|
||
m_CachedParent = null;
|
||
transform.parent = null;
|
||
|
||
InvokeBehaviourOnNetworkObjectParentChanged(null);
|
||
return true;
|
||
}
|
||
|
||
if (!NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value))
|
||
{
|
||
OrphanChildren.Add(this);
|
||
return false;
|
||
}
|
||
|
||
var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value];
|
||
|
||
m_CachedParent = parentObject.transform;
|
||
transform.parent = parentObject.transform;
|
||
|
||
InvokeBehaviourOnNetworkObjectParentChanged(parentObject);
|
||
return true;
|
||
}
|
||
|
||
internal static void CheckOrphanChildren()
|
||
{
|
||
var objectsToRemove = new List<NetworkObject>();
|
||
foreach (var orphanObject in OrphanChildren)
|
||
{
|
||
if (orphanObject.ApplyNetworkParenting())
|
||
{
|
||
objectsToRemove.Add(orphanObject);
|
||
}
|
||
}
|
||
foreach (var networkObject in objectsToRemove)
|
||
{
|
||
OrphanChildren.Remove(networkObject);
|
||
}
|
||
}
|
||
|
||
internal void InvokeBehaviourNetworkSpawn()
|
||
{
|
||
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId);
|
||
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
ChildNetworkBehaviours[i].InternalOnNetworkSpawn();
|
||
}
|
||
}
|
||
|
||
internal void InvokeBehaviourNetworkDespawn()
|
||
{
|
||
NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true);
|
||
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
ChildNetworkBehaviours[i].InternalOnNetworkDespawn();
|
||
}
|
||
}
|
||
|
||
private List<NetworkBehaviour> m_ChildNetworkBehaviours;
|
||
|
||
internal List<NetworkBehaviour> ChildNetworkBehaviours
|
||
{
|
||
get
|
||
{
|
||
if (m_ChildNetworkBehaviours != null)
|
||
{
|
||
return m_ChildNetworkBehaviours;
|
||
}
|
||
|
||
m_ChildNetworkBehaviours = new List<NetworkBehaviour>();
|
||
var networkBehaviours = GetComponentsInChildren<NetworkBehaviour>(true);
|
||
for (int i = 0; i < networkBehaviours.Length; i++)
|
||
{
|
||
if (networkBehaviours[i].NetworkObject == this)
|
||
{
|
||
m_ChildNetworkBehaviours.Add(networkBehaviours[i]);
|
||
}
|
||
}
|
||
|
||
return m_ChildNetworkBehaviours;
|
||
}
|
||
}
|
||
|
||
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId)
|
||
{
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
var behavior = ChildNetworkBehaviours[i];
|
||
behavior.InitializeVariables();
|
||
behavior.WriteNetworkVariableData(writer, targetClientId);
|
||
}
|
||
}
|
||
|
||
internal void MarkVariablesDirty()
|
||
{
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
var behavior = ChildNetworkBehaviours[i];
|
||
behavior.MarkVariablesDirty();
|
||
}
|
||
}
|
||
|
||
// 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");
|
||
}
|
||
}
|
||
}
|
||
internal void SetNetworkVariableData(FastBufferReader reader)
|
||
{
|
||
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
var behaviour = ChildNetworkBehaviours[i];
|
||
behaviour.InitializeVariables();
|
||
behaviour.SetNetworkVariableData(reader);
|
||
}
|
||
}
|
||
|
||
internal ushort GetNetworkBehaviourOrderIndex(NetworkBehaviour instance)
|
||
{
|
||
// read the cached index, and verify it first
|
||
if (instance.NetworkBehaviourIdCache < ChildNetworkBehaviours.Count)
|
||
{
|
||
if (ChildNetworkBehaviours[instance.NetworkBehaviourIdCache] == instance)
|
||
{
|
||
return instance.NetworkBehaviourIdCache;
|
||
}
|
||
|
||
// invalid cached id reset
|
||
instance.NetworkBehaviourIdCache = default;
|
||
}
|
||
|
||
for (ushort i = 0; i < ChildNetworkBehaviours.Count; i++)
|
||
{
|
||
if (ChildNetworkBehaviours[i] == instance)
|
||
{
|
||
// cache the id, for next query
|
||
instance.NetworkBehaviourIdCache = i;
|
||
return i;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index)
|
||
{
|
||
if (index >= ChildNetworkBehaviours.Count)
|
||
{
|
||
if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
|
||
{
|
||
NetworkLog.LogError($"Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?");
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
return ChildNetworkBehaviours[index];
|
||
}
|
||
|
||
internal struct SceneObject
|
||
{
|
||
public struct HeaderData
|
||
{
|
||
public ulong NetworkObjectId;
|
||
public ulong OwnerClientId;
|
||
public uint Hash;
|
||
|
||
public bool IsPlayerObject;
|
||
public bool HasParent;
|
||
public bool IsSceneObject;
|
||
public bool HasTransform;
|
||
public bool IsReparented;
|
||
}
|
||
|
||
public HeaderData Header;
|
||
|
||
//If(Metadata.HasParent)
|
||
public ulong ParentObjectId;
|
||
|
||
//If(Metadata.HasTransform)
|
||
public struct TransformData
|
||
{
|
||
public Vector3 Position;
|
||
public Quaternion Rotation;
|
||
}
|
||
|
||
public TransformData Transform;
|
||
|
||
//If(Metadata.IsReparented)
|
||
public bool IsLatestParentSet;
|
||
|
||
//If(IsLatestParentSet)
|
||
public ulong? LatestParent;
|
||
|
||
public NetworkObject OwnerObject;
|
||
public ulong TargetClientId;
|
||
|
||
public unsafe void Serialize(FastBufferWriter writer)
|
||
{
|
||
if (!writer.TryBeginWrite(
|
||
sizeof(HeaderData) +
|
||
(Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0) +
|
||
(Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0) +
|
||
(Header.IsReparented
|
||
? FastBufferWriter.GetWriteSize(IsLatestParentSet) +
|
||
(IsLatestParentSet ? FastBufferWriter.GetWriteSize<ulong>() : 0)
|
||
: 0)))
|
||
{
|
||
throw new OverflowException("Could not serialize SceneObject: Out of buffer space.");
|
||
}
|
||
|
||
writer.WriteValue(Header);
|
||
|
||
if (Header.HasParent)
|
||
{
|
||
writer.WriteValue(ParentObjectId);
|
||
}
|
||
|
||
if (Header.HasTransform)
|
||
{
|
||
writer.WriteValue(Transform);
|
||
}
|
||
|
||
if (Header.IsReparented)
|
||
{
|
||
writer.WriteValue(IsLatestParentSet);
|
||
if (IsLatestParentSet)
|
||
{
|
||
writer.WriteValue((ulong)LatestParent);
|
||
}
|
||
}
|
||
|
||
OwnerObject.WriteNetworkVariableData(writer, TargetClientId);
|
||
}
|
||
|
||
public unsafe void Deserialize(FastBufferReader reader)
|
||
{
|
||
if (!reader.TryBeginRead(sizeof(HeaderData)))
|
||
{
|
||
throw new OverflowException("Could not deserialize SceneObject: Out of buffer space.");
|
||
}
|
||
reader.ReadValue(out Header);
|
||
if (!reader.TryBeginRead(
|
||
(Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0) +
|
||
(Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0) +
|
||
(Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) : 0)))
|
||
{
|
||
throw new OverflowException("Could not deserialize SceneObject: Out of buffer space.");
|
||
}
|
||
|
||
if (Header.HasParent)
|
||
{
|
||
reader.ReadValue(out ParentObjectId);
|
||
}
|
||
|
||
if (Header.HasTransform)
|
||
{
|
||
reader.ReadValue(out Transform);
|
||
}
|
||
|
||
if (Header.IsReparented)
|
||
{
|
||
reader.ReadValue(out IsLatestParentSet);
|
||
if (IsLatestParentSet)
|
||
{
|
||
reader.ReadValueSafe(out ulong latestParent);
|
||
LatestParent = latestParent;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
internal SceneObject GetMessageSceneObject(ulong targetClientId)
|
||
{
|
||
var obj = new SceneObject
|
||
{
|
||
Header = new SceneObject.HeaderData
|
||
{
|
||
IsPlayerObject = IsPlayerObject,
|
||
NetworkObjectId = NetworkObjectId,
|
||
OwnerClientId = OwnerClientId,
|
||
IsSceneObject = IsSceneObject ?? true,
|
||
Hash = HostCheckForGlobalObjectIdHashOverride(),
|
||
},
|
||
OwnerObject = this,
|
||
TargetClientId = targetClientId
|
||
};
|
||
|
||
NetworkObject parentNetworkObject = null;
|
||
|
||
if (!AlwaysReplicateAsRoot && transform.parent != null)
|
||
{
|
||
parentNetworkObject = transform.parent.GetComponent<NetworkObject>();
|
||
}
|
||
|
||
if (parentNetworkObject)
|
||
{
|
||
obj.Header.HasParent = true;
|
||
obj.ParentObjectId = parentNetworkObject.NetworkObjectId;
|
||
}
|
||
if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId))
|
||
{
|
||
obj.Header.HasTransform = true;
|
||
obj.Transform = new SceneObject.TransformData
|
||
{
|
||
Position = transform.position,
|
||
Rotation = transform.rotation
|
||
};
|
||
}
|
||
|
||
var (isReparented, latestParent) = GetNetworkParenting();
|
||
obj.Header.IsReparented = isReparented;
|
||
if (isReparented)
|
||
{
|
||
var isLatestParentSet = latestParent != null && latestParent.HasValue;
|
||
obj.IsLatestParentSet = isLatestParentSet;
|
||
if (isLatestParentSet)
|
||
{
|
||
obj.LatestParent = latestParent.Value;
|
||
}
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to deserialize a serialized scene object which occurs
|
||
/// when the client is approved or during a scene transition
|
||
/// </summary>
|
||
/// <param name="sceneObject">Deserialized scene object data</param>
|
||
/// <param name="variableData">reader for the NetworkVariable data</param>
|
||
/// <param name="networkManager">NetworkManager instance</param>
|
||
/// <returns>optional to use NetworkObject deserialized</returns>
|
||
internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader variableData, NetworkManager networkManager)
|
||
{
|
||
Vector3? position = null;
|
||
Quaternion? rotation = null;
|
||
ulong? parentNetworkId = null;
|
||
|
||
if (sceneObject.Header.HasTransform)
|
||
{
|
||
position = sceneObject.Transform.Position;
|
||
rotation = sceneObject.Transform.Rotation;
|
||
}
|
||
|
||
if (sceneObject.Header.HasParent)
|
||
{
|
||
parentNetworkId = sceneObject.ParentObjectId;
|
||
}
|
||
|
||
//Attempt to create a local NetworkObject
|
||
var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(
|
||
sceneObject.Header.IsSceneObject, sceneObject.Header.Hash,
|
||
sceneObject.Header.OwnerClientId, parentNetworkId, position, rotation, sceneObject.Header.IsReparented);
|
||
|
||
networkObject?.SetNetworkParenting(sceneObject.Header.IsReparented, sceneObject.LatestParent);
|
||
|
||
if (networkObject == null)
|
||
{
|
||
// Log the error that the NetworkObject failed to construct
|
||
Debug.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {sceneObject.Header.Hash}.");
|
||
|
||
// If we failed to load this NetworkObject, then skip past the network variable data
|
||
variableData.ReadValueSafe(out ushort varSize);
|
||
variableData.Seek(variableData.Position + varSize);
|
||
|
||
// We have nothing left to do here.
|
||
return null;
|
||
}
|
||
|
||
// Spawn the NetworkObject(
|
||
networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, variableData, false);
|
||
|
||
return networkObject;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
internal uint HostCheckForGlobalObjectIdHashOverride()
|
||
{
|
||
if (NetworkManager.IsHost)
|
||
{
|
||
if (NetworkManager.PrefabHandler.ContainsHandler(this))
|
||
{
|
||
var globalObjectIdHash = NetworkManager.PrefabHandler.GetSourceGlobalObjectIdHash(GlobalObjectIdHash);
|
||
return globalObjectIdHash == 0 ? GlobalObjectIdHash : globalObjectIdHash;
|
||
}
|
||
else if (NetworkManager.NetworkConfig.OverrideToNetworkPrefab.ContainsKey(GlobalObjectIdHash))
|
||
{
|
||
return NetworkManager.NetworkConfig.OverrideToNetworkPrefab[GlobalObjectIdHash];
|
||
}
|
||
}
|
||
|
||
return GlobalObjectIdHash;
|
||
}
|
||
}
|
||
}
|