diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 51fe4a90f6..1d4b065e9b 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -39,6 +39,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Added `SinglePlayerTransport` that provides the ability to start as a host for a single player network session. (#3473) - When using UnityTransport >=2.4 and Unity >= 6000.1.0a1, SetConnectionData will accept a fully qualified hostname instead of an IP as a connect address on the client side. (#3441) +- Added `NetworkPrefabInstanceHandlerWithData`, a variant of `INetworkPrefabInstanceHandler` that provides access to custom instantiation data directly within the `Instantiate()` method. (#3430) ### Fixed diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 9a85534637..c8cb98637b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1593,8 +1593,9 @@ internal void ShutdownInternal() // Completely reset the NetworkClient ConnectionManager.LocalClient = new NetworkClient(); - // This cleans up the internal prefabs list + // Clean up the internal prefabs data NetworkConfig?.Prefabs?.Shutdown(); + PrefabHandler.Shutdown(); // Reset the configuration hash for next session in the event // that the prefab list changes diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 9be3e7e599..9afcdc7fd2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -58,6 +58,12 @@ public uint PrefabIdHash } } + /// + /// InstantiationData sent during the instantiation process. + /// Available to read as T parameter to for custom handling by user code. + /// + internal byte[] InstantiationData; + /// /// All component instances associated with a component instance. /// @@ -2857,6 +2863,12 @@ public bool SpawnWithObservers set => ByteUtility.SetBit(ref m_BitField, 10, value); } + public bool HasInstantiationData + { + get => ByteUtility.GetBit(m_BitField, 11); + set => ByteUtility.SetBit(ref m_BitField, 11, value); + } + // When handling the initial synchronization of NetworkObjects, // this will be populated with the known observers. public ulong[] Observers; @@ -2884,6 +2896,7 @@ public struct TransformData : INetworkSerializeByMemcpy public int NetworkSceneHandle; + internal int SynchronizationDataSize; public void Serialize(FastBufferWriter writer) { @@ -2945,9 +2958,29 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } + // write placeholder for serialized data size. + // Can't be bitpacked because we don't know the value until we calculate it later + var positionBeforeSynchronizing = writer.Position; + writer.WriteValueSafe(0); + var sizeToSkipCalculationPosition = writer.Position; + + if (HasInstantiationData) + { + writer.WriteValueSafe(OwnerObject.InstantiationData); + } + // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + + var currentPosition = writer.Position; + // Write the total number of bytes written for synchronization data. + writer.Seek(positionBeforeSynchronizing); + // We want the size of everything after our size to skip calculation position + var size = currentPosition - sizeToSkipCalculationPosition; + writer.WriteValueSafe(size); + // seek back to the head of the writer. + writer.Seek(currentPosition); } public void Deserialize(FastBufferReader reader) @@ -3003,6 +3036,10 @@ public void Deserialize(FastBufferReader reader) // The NetworkSceneHandle is the server-side relative // scene handle that the NetworkObject resides in. reader.ReadValue(out NetworkSceneHandle); + + // Read the size of the remaining synchronization data + // This data will be read in AddSceneObject() + reader.ReadValueSafe(out SynchronizationDataSize); } } @@ -3017,12 +3054,7 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer { if (serializer.IsWriter) { - // write placeholder int. - // Can't be bitpacked because we don't know the value until we calculate it later var writer = serializer.GetFastBufferWriter(); - var positionBeforeSynchronizing = writer.Position; - writer.WriteValueSafe(0); - var sizeToSkipCalculationPosition = writer.Position; // Synchronize NetworkVariables foreach (var behavior in ChildNetworkBehaviours) @@ -3048,12 +3080,6 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer } 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 = currentPosition - sizeToSkipCalculationPosition; - writer.WriteValueSafe(size); // Write the number of NetworkBehaviours synchronized writer.Seek(networkBehaviourCountPosition); writer.WriteValueSafe(synchronizationCount); @@ -3063,41 +3089,26 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer } else { - var seekToEndOfSynchData = 0; var reader = serializer.GetFastBufferReader(); - try - { - reader.ReadValueSafe(out int sizeOfSynchronizationData); - seekToEndOfSynchData = reader.Position + sizeOfSynchronizationData; - - // Apply the network variable synchronization data - foreach (var behaviour in ChildNetworkBehaviours) - { - behaviour.InitializeVariables(); - behaviour.SetNetworkVariableData(reader, targetClientId); - } - // Read the number of NetworkBehaviours to synchronize - reader.ReadValueSafe(out byte numberSynchronized); + // Apply the network variable synchronization data + foreach (var behaviour in ChildNetworkBehaviours) + { + behaviour.InitializeVariables(); + behaviour.SetNetworkVariableData(reader, targetClientId); + } - // 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 ushort networkBehaviourId); - var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId); - networkBehaviour.Synchronize(ref serializer, targetClientId); - } + // Read the number of NetworkBehaviours to synchronize + reader.ReadValueSafe(out byte numberSynchronized); - if (seekToEndOfSynchData != reader.Position) - { - Debug.LogWarning($"[Size mismatch] Expected: {seekToEndOfSynchData} Currently At: {reader.Position}!"); - } - } - catch + // 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.Seek(seekToEndOfSynchData); + reader.ReadValueSafe(out ushort networkBehaviourId); + var networkBehaviour = GetNetworkBehaviourAtOrderIndex(networkBehaviourId); + networkBehaviour.Synchronize(ref serializer, targetClientId); } } } @@ -3121,7 +3132,8 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager NetworkSceneHandle = NetworkSceneHandle, Hash = CheckForGlobalObjectIdHashOverride(), OwnerObject = this, - TargetClientId = targetClientId + TargetClientId = targetClientId, + HasInstantiationData = InstantiationData != null && InstantiationData.Length > 0 }; // Handle Parenting @@ -3186,8 +3198,18 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager /// 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); + var endOfSynchronizationData = reader.Position + sceneObject.SynchronizationDataSize; + + byte[] instantiationData = null; + if (sceneObject.HasInstantiationData) + { + reader.ReadValueSafe(out instantiationData); + } + + + // Attempt to create a local NetworkObject + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, instantiationData); + if (networkObject == null) { @@ -3200,8 +3222,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf try { // If we failed to load this NetworkObject, then skip past the Network Variable and (if any) synchronization data - reader.ReadValueSafe(out int networkBehaviourSynchronizationDataLength); - reader.Seek(reader.Position + networkBehaviourSynchronizationDataLength); + reader.Seek(endOfSynchronizationData); } catch (Exception ex) { @@ -3219,9 +3240,24 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours networkObject.InvokeBehaviourNetworkPreSpawn(); - // Synchronize NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); - networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + // Process the remaining synchronization data from the buffer + try + { + // Synchronize NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + + // Ensure that the buffer is completely reset + if (reader.Position != endOfSynchronizationData) + { + Debug.LogWarning($"[Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); + reader.Seek(endOfSynchronizationData); + } + } + catch + { + reader.Seek(endOfSynchronizationData); + } // If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are // being told we do not have a parent, then we want to clear the latest parent so it is not automatically diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs new file mode 100644 index 0000000000..4191e0f758 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Interface for customizing, overriding, spawning, and destroying Network Prefabs + /// Used by + /// + public interface INetworkPrefabInstanceHandler + { + /// + /// Client Side Only + /// Once an implementation is registered with the , this method will be called every time + /// a Network Prefab associated is spawned on clients + /// + /// Note On Hosts: Use the + /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client. + /// + /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active + /// via the method. + /// + /// + /// If you need to pass custom data at instantiation time (e.g., selecting a variant, setting initialization parameters, or choosing a pre-instantiated object), + /// implement instead. + /// + /// the owner for the to be instantiated + /// the initial/default position for the to be instantiated + /// the initial/default rotation for the to be instantiated + /// The instantiated NetworkObject instance. Returns null if instantiation fails. + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); + + /// + /// Invoked on Client and Server + /// Once an implementation is registered with the , this method will be called when + /// a Network Prefab associated is: + /// + /// Server Side: destroyed or despawned with the destroy parameter equal to true + /// If is invoked with the default destroy parameter (i.e. false) then this method will NOT be invoked! + /// + /// Client Side: destroyed when the client receives a destroy object message from the server or host. + /// + /// Note on Pooling: When this method is invoked, you do not need to destroy the NetworkObject as long as you want your pool to persist. + /// The most common approach is to make the inactive by calling . + /// + /// The being destroyed + public void Destroy(NetworkObject networkObject); + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs.meta b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs.meta new file mode 100644 index 0000000000..2dd371625f --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8745d8824dc248c1b8d43e0bc6fe5aba +timeCreated: 1749586536 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index fe0dd270e9..7b8632d3c3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -4,46 +4,6 @@ namespace Unity.Netcode { - /// - /// Interface for customizing, overriding, spawning, and destroying Network Prefabs - /// Used by - /// - public interface INetworkPrefabInstanceHandler - { - /// - /// Client Side Only - /// Once an implementation is registered with the , this method will be called every time - /// a Network Prefab associated is spawned on clients - /// - /// Note On Hosts: Use the - /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client. - /// - /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active - /// via the method. - /// - /// the owner for the to be instantiated - /// the initial/default position for the to be instantiated - /// the initial/default rotation for the to be instantiated - /// The instantiated NetworkObject instance. Returns null if instantiation fails. - NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); - - /// - /// Invoked on Client and Server - /// Once an implementation is registered with the , this method will be called when - /// a Network Prefab associated is: - /// - /// Server Side: destroyed or despawned with the destroy parameter equal to true - /// If is invoked with the default destroy parameter (i.e. false) then this method will NOT be invoked! - /// - /// Client Side: destroyed when the client receives a destroy object message from the server or host. - /// - /// Note on Pooling: When this method is invoked, you do not need to destroy the NetworkObject as long as you want your pool to persist. - /// The most common approach is to make the inactive by calling . - /// - /// The being destroyed - void Destroy(NetworkObject networkObject); - } - /// /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) /// Register custom prefab handlers by implementing the interface. @@ -57,6 +17,12 @@ public class NetworkPrefabHandler /// private readonly Dictionary m_PrefabAssetToPrefabHandler = new Dictionary(); + /// + /// Links a network prefab asset to a class with the INetworkPrefabInstanceHandlerWithData interface, + /// used to keep a smaller lookup table than for faster instantiation data injection into NetworkObject + /// + private readonly Dictionary m_PrefabAssetToPrefabHandlerWithData = new Dictionary(); + /// /// Links the custom prefab instance's GlobalNetworkObjectId to the original prefab asset's GlobalNetworkObjectId. (Needed for HandleNetworkPrefabDestroy) /// [PrefabInstance][PrefabAsset] @@ -98,12 +64,61 @@ public bool AddHandler(uint globalObjectIdHash, INetworkPrefabInstanceHandler in if (!m_PrefabAssetToPrefabHandler.ContainsKey(globalObjectIdHash)) { m_PrefabAssetToPrefabHandler.Add(globalObjectIdHash, instanceHandler); + if (instanceHandler is INetworkPrefabInstanceHandlerWithData instanceHandlerWithData) + { + m_PrefabAssetToPrefabHandlerWithData.Add(globalObjectIdHash, instanceHandlerWithData); + } return true; } return false; } + + /// + /// + /// The containing a to which the instantiation data will be assigned. The must match a handler that was previously registered. + /// + /// The custom instantiation data to serialize and assign. + public void SetInstantiationData(GameObject gameObject, T instantiationData) where T : struct, INetworkSerializable + { + if (gameObject.TryGetComponent(out var networkObject)) + { + SetInstantiationData(networkObject, instantiationData); + } + } + + /// + /// Serializes and assigns custom instantiation data to a for use during network spawning. + /// The must have a registered prefab handler that implements . + /// + /// The type of instantiation data, which must be a struct implementing . + /// + /// The to which the instantiation data will be assigned. The must match a handler that was previously registered. + /// + /// The custom instantiation data to serialize and assign. + public void SetInstantiationData(NetworkObject networkObject, T instantiationData) where T : struct, INetworkSerializable + { + if (!TryGetHandlerWithData(networkObject.GlobalObjectIdHash, out var prefabHandler) || !prefabHandler.HandlesDataType()) + { + Debug.LogError("[InstantiationData] Cannot inject data: no compatible handler found for the specified data type."); + return; + } + + using var writer = new FastBufferWriter(4, Collections.Allocator.Temp, int.MaxValue); + var serializer = new BufferSerializer(new BufferSerializerWriter(writer)); + + try + { + instantiationData.NetworkSerialize(serializer); + networkObject.InstantiationData = writer.ToArray(); + } + catch (Exception ex) + { + NetworkLog.LogError($"[InstantiationData] Failed to serialize instantiation data for {nameof(NetworkObject)} '{networkObject.name}': {ex}"); + } + } + /// /// HOST ONLY! /// Since a host is unique and is considered both a client and a server, for each source NetworkPrefab you must manually @@ -199,6 +214,7 @@ public bool RemoveHandler(uint globalObjectIdHash) m_PrefabInstanceToPrefabAsset.Remove(networkPrefabHashKey); } + m_PrefabAssetToPrefabHandlerWithData.Remove(globalObjectIdHash); return m_PrefabAssetToPrefabHandler.Remove(globalObjectIdHash); } @@ -223,6 +239,17 @@ public bool RemoveHandler(uint globalObjectIdHash) /// true or false internal bool ContainsHandler(uint networkPrefabHash) => m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabHash) || m_PrefabInstanceToPrefabAsset.ContainsKey(networkPrefabHash); + /// + /// Returns the implementation for a given + /// + /// + /// + /// + internal bool TryGetHandlerWithData(uint objectHash, out INetworkPrefabInstanceHandlerWithData handler) + { + return m_PrefabAssetToPrefabHandlerWithData.TryGetValue(objectHash, out handler); + } + /// /// Returns the source NetworkPrefab's /// @@ -251,24 +278,35 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) /// /// /// + /// Instantiation data sent from the authority if set /// - internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation) + internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] instantiationData = null) { - if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) + NetworkObject networkObjectInstance = null; + if (instantiationData != null) { - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); - - //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) - //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. - if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + if (m_PrefabAssetToPrefabHandlerWithData.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) { - m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); + networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation, instantiationData); + } + else + { + Debug.LogError($"[InstantiationData] Failed instantiate with data: no compatible data handler found for object hash {networkPrefabAssetHash}. Instantiation data will be dropped."); + return null; } - - return networkObjectInstance; + } + else if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) + { + networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); } - return null; + // Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) + // is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. + if (networkObjectInstance != null) + { + m_PrefabInstanceToPrefabAsset.TryAdd(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); + } + return networkObjectInstance; } /// @@ -422,5 +460,10 @@ internal void Initialize(NetworkManager networkManager) { m_NetworkManager = networkManager; } + + internal void Shutdown() + { + m_PrefabInstanceToPrefabAsset.Clear(); + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs new file mode 100644 index 0000000000..2477644a5d --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Specialized version of that receives + /// custom instantiation data injected by the authority before spawning. + /// + /// The type of the instantiation data. Must be a struct implementing . + /// + /// Use or + /// on the authority side to set instantiation data before spawning an object or synchronizing a client. The data set on the authority will then be passed into the call. + /// + public abstract class NetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandlerWithData where T : struct, INetworkSerializable + { + /// + /// The client ID that will own the instantiated object. + /// The world position where the object should be spawned. + /// The world rotation for the spawned object. + /// Custom data of type provided by the server to be used during instantiation. + public abstract NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, T instantiationData); + + /// + public abstract void Destroy(NetworkObject networkObject); + + bool INetworkPrefabInstanceHandlerWithData.HandlesDataType() => typeof(T) == typeof(TK); + + NetworkObject INetworkPrefabInstanceHandlerWithData.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] instantiationData) + { + using var reader = new FastBufferReader(instantiationData, Collections.Allocator.Temp); + reader.ReadValueSafe(out T payload); + + var networkObject = Instantiate(ownerClientId, position, rotation, payload); + + if (networkObject != null) + { + networkObject.InstantiationData = instantiationData; + } + + return networkObject; + } + + NetworkObject INetworkPrefabInstanceHandler.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) => Instantiate(ownerClientId, position, rotation, default); + } + + internal interface INetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandler + { + public bool HandlesDataType(); + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, byte[] instantiationData); + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs.meta b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs.meta new file mode 100644 index 0000000000..3895ba444c --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9bf5119a47f8d3247aaa4cd13c1ee96b \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 2a2f2ceee8..8a39fcfa06 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -639,9 +639,9 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool } // After we have sent the change ownership message to all client observers, invoke the ownership changed notification. - /// !!Important!! - /// This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership - /// change can be sent from NetworkBehaviours that override the + // !!Important!! + // This gets called specifically *after* sending the ownership message so any additional messages that need to proceed an ownership + // change can be sent from NetworkBehaviours that override the networkObject.InvokeOwnershipChanged(networkObject.PreviousOwnerId, clientId); // Keep track of the ownership change frequency to assure a user is not exceeding changes faster than 2x the current Tick Rate. @@ -817,14 +817,14 @@ internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networ /// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the /// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned. /// - internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false) + internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false, byte[] instantiationData = null) { NetworkObject networkObject = null; // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) { // Let the handler spawn the NetworkObject - networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default); + networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerId, position ?? default, rotation ?? default, instantiationData); networkObject.NetworkManagerOwner = NetworkManager; } else @@ -913,7 +913,7 @@ internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint p /// For most cases this is client-side only, with the exception of when the server /// is spawning a player. /// - internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject) + internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject, byte[] instantiationData = null) { NetworkObject networkObject = null; var globalObjectIdHash = sceneObject.Hash; @@ -926,7 +926,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO // If scene management is disabled or the NetworkObject was dynamically spawned if (!NetworkManager.NetworkConfig.EnableSceneManagement || !sceneObject.IsSceneObject) { - networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject); + networkObject = GetNetworkObjectToSpawn(sceneObject.Hash, sceneObject.OwnerClientId, position, rotation, sceneObject.IsSceneObject, instantiationData); } else // Get the in-scene placed NetworkObject { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs new file mode 100644 index 0000000000..7b06e49576 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(NetworkTopologyTypes.ClientServer)] + [TestFixture(NetworkTopologyTypes.DistributedAuthority)] + internal class NetworkPrefabHandlerWithDataTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 4; + private const string k_TestPrefabObjectName = "NetworkPrefabTestObject"; + private GameObject m_Prefab; + private PrefabInstanceHandlerWithData[] m_ClientHandlers; + + public NetworkPrefabHandlerWithDataTests(NetworkTopologyTypes topology) : base(topology) + { + } + + protected override void OnServerAndClientsCreated() + { + // Creates a network object prefab and registers it to all clients. + m_Prefab = CreateNetworkObjectPrefab(k_TestPrefabObjectName).gameObject; + var authority = GetAuthorityNetworkManager(); + + m_ClientHandlers = new PrefabInstanceHandlerWithData[NumberOfClients]; + var idx = 0; + foreach (var manager in m_NetworkManagers) + { + RegisterPrefabHandler(manager, out var handler); + if (manager != authority) + { + m_ClientHandlers[idx] = handler; + idx++; + } + } + } + + private PrefabInstanceHandlerWithData m_LateJoinPrefabHandler; + protected override void OnNewClientCreated(NetworkManager networkManager) + { + // This will register all prefabs from the authority to the newly created client. + base.OnNewClientCreated(networkManager); + + RegisterPrefabHandler(networkManager, out var lateJoinPrefabHandler); + m_LateJoinPrefabHandler = lateJoinPrefabHandler; + } + + [UnityTest] + public IEnumerator InstantiationPayload_SyncsCorrectly() + { + var data = new NetworkSerializableTest { Value = 12, Value2 = 3.14f }; + + SpawnPrefabWithData(data); + + yield return WaitForConditionOrTimeOut(() => AllHandlersSynchronized(data)); + AssertOnTimeout("Not all handlers synchronized"); + } + + [UnityTest] + public IEnumerator InstantiationPayload_LateJoinersReceiveData() + { + var data = new NetworkSerializableTest { Value = 42, Value2 = 2.71f }; + var spawned = SpawnPrefabWithData(data); + + yield return WaitForConditionOrTimeOut(() => AllHandlersSynchronized(data)); + AssertOnTimeout("Not all handlers synchronized"); + + // When running with Distributed Authority, test a late-joiner after an ownership change + // The object owner will synchronize the late joining client, showing that the instantiationData will survive host migration. + if (m_DistributedAuthority) + { + var newOwner = m_NetworkManagers.First(m => m.LocalClientId != spawned.OwnerClientId); + spawned.ChangeOwnership(newOwner.LocalClientId); + + yield return WaitForConditionOrTimeOut(() => + { + if (newOwner.SpawnManager.SpawnedObjects.TryGetValue(spawned.NetworkObjectId, out var clientObject)) + { + return clientObject.OwnerClientId == newOwner.LocalClientId; + } + + return false; + }); + AssertOnTimeout($"Timed out while waiting for Client-{newOwner.LocalClientId} to own object"); + } + + // Late join a client + yield return CreateAndStartNewClient(); + + // Confirm late joiner got correct data + yield return WaitForConditionOrTimeOut(() => m_LateJoinPrefabHandler.InstantiationData.IsSynchronizedWith(data)); + AssertOnTimeout("Late joiner received incorrect data"); + } + + private void RegisterPrefabHandler(NetworkManager manager, out PrefabInstanceHandlerWithData handler) + { + handler = new PrefabInstanceHandlerWithData(m_Prefab); + manager.PrefabHandler.AddHandler(m_Prefab, handler); + } + + private NetworkObject SpawnPrefabWithData(NetworkSerializableTest data) + { + var instance = UnityEngine.Object.Instantiate(m_Prefab).GetComponent(); + GetAuthorityNetworkManager().PrefabHandler.SetInstantiationData(instance, data); + instance.Spawn(); + return instance; + } + + private bool AllHandlersSynchronized(NetworkSerializableTest expectedData) + { + return m_ClientHandlers.All(handler => handler.InstantiationData.IsSynchronizedWith(expectedData)); + } + + private class PrefabInstanceHandlerWithData : NetworkPrefabInstanceHandlerWithData + { + private readonly GameObject m_Prefab; + public NetworkSerializableTest InstantiationData; + + public PrefabInstanceHandlerWithData(GameObject prefab) + { + m_Prefab = prefab; + } + + public override NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, NetworkSerializableTest data) + { + InstantiationData = data; + return UnityEngine.Object.Instantiate(m_Prefab, position, rotation).GetComponent(); + } + + public override void Destroy(NetworkObject networkObject) + { + UnityEngine.Object.DestroyImmediate(networkObject.gameObject); + } + } + + private struct NetworkSerializableTest : INetworkSerializable + { + public int Value; + public float Value2; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Value); + serializer.SerializeValue(ref Value2); + } + + public bool IsSynchronizedWith(NetworkSerializableTest other) + => Value == other.Value && Math.Abs(Value2 - other.Value2) < 0.0001f; + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs.meta new file mode 100644 index 0000000000..4cb79bddfb --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58d62dad203ba5440838bedbc22b90f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: