diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 4f56610a46..4511ef691d 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -20,6 +20,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/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 114154f1de..62eb01f9f0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -57,6 +57,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. /// @@ -2837,6 +2843,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; @@ -2925,6 +2937,12 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } + if (HasInstantiationData) + { + BytePacker.WriteValuePacked(writer, OwnerObject.InstantiationData.Length); + writer.WriteBytesSafe(OwnerObject.InstantiationData); + } + // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); @@ -3101,7 +3119,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 @@ -3166,8 +3185,15 @@ 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) { + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + + //Synchronize the instantiation data if needed + FastBufferReader instantiationDataReader = sceneObject.HasInstantiationData ? networkManager.PrefabHandler.GetInstantiationDataReader(sceneObject.Hash, reader) : default; + //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, instantiationDataReader); + + instantiationDataReader.Dispose(); if (networkObject == null) { @@ -3200,7 +3226,6 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf networkObject.InvokeBehaviourNetworkPreSpawn(); // Synchronize NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); // If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index fe0dd270e9..131071fc8c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -21,6 +21,10 @@ public interface INetworkPrefabInstanceHandler /// 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 @@ -57,6 +61,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 +108,44 @@ 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; } + public void SetInstantiationData(GameObject gameObject, T instantiationData) where T : struct, INetworkSerializable + { + if (gameObject.TryGetComponent(out var networkObject)) + { + SetInstantiationData(networkObject, instantiationData); + } + } + public void SetInstantiationData(NetworkObject networkObject, T data) 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."); + } + + using var writer = new FastBufferWriter(4, Collections.Allocator.Temp, int.MaxValue); + var serializer = new BufferSerializer(new BufferSerializerWriter(writer)); + + try + { + data.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 +241,7 @@ public bool RemoveHandler(uint globalObjectIdHash) m_PrefabInstanceToPrefabAsset.Remove(networkPrefabHashKey); } + m_PrefabAssetToPrefabHandlerWithData.Remove(globalObjectIdHash); return m_PrefabAssetToPrefabHandler.Remove(globalObjectIdHash); } @@ -223,6 +266,38 @@ 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); + } + + /// + /// Reads the instantiation data for a given + /// + internal FastBufferReader GetInstantiationDataReader(uint objectHash, FastBufferReader fastBufferReader) + { + if (!TryGetHandlerWithData(objectHash, out _)) + { + if (NetworkManager.Singleton.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"No handler with data found for object hash {objectHash}."); + } + return default; + } + + ByteUnpacker.ReadValuePacked(fastBufferReader, out int dataSize); + var position = fastBufferReader.Position; + var dataReader = new FastBufferReader(fastBufferReader, Collections.Allocator.Temp, dataSize, position); + fastBufferReader.Seek(position + dataSize); + return dataReader; + } + /// /// Returns the source NetworkPrefab's /// @@ -252,23 +327,30 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) /// /// /// - internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation) + internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader instantiationDataReader = default) { - if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) + NetworkObject networkObjectInstance = null; + if (instantiationDataReader.IsInitialized) { - 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, instantiationDataReader); } - - return networkObjectInstance; } - - return null; + else + { + if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) + { + 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)) + { + m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); + } + return networkObjectInstance; } /// 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..198133d5d1 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabInstanceHandlerWithData.cs @@ -0,0 +1,41 @@ +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Specialized version of that receives + /// custom instantiation data injected by the server before spawning. + /// + public abstract class NetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandlerWithData where T : struct, INetworkSerializable + { + 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, FastBufferReader reader) + { + var startPosition = reader.Position; + reader.ReadValueSafe(out T payload); + var length = reader.Position - startPosition; + + NetworkObject networkObject = Instantiate(ownerClientId, position, rotation, payload); + reader.Seek(startPosition); + if (networkObject.InstantiationData == null || networkObject.InstantiationData.Length != length) + { + networkObject.InstantiationData = new byte[length]; + } + reader.ReadBytesSafe(ref networkObject.InstantiationData, length); + return networkObject; + } + + NetworkObject INetworkPrefabInstanceHandler.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) => Instantiate(ownerClientId, position, rotation, default); + } + + internal interface INetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandler + { + bool HandlesDataType(); + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader reader); + } +} 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 3b5bb2e6e0..b3e691b229 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -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, FastBufferReader instantiationDataReader = default) { 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, instantiationDataReader); 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, FastBufferReader instantiationDataReader = default) { 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, instantiationDataReader); } 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..3a94ff61c0 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + internal class NetworkPrefabHandlerWithDataTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 4; + private const string k_TestPrefabObjectName = "NetworkPrefabTestObject"; + private GameObject m_Prefab; + private PrefabInstanceHandlerWithData[] m_ClientHandlers; + + 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 }; + SpawnPrefabWithData(data); + + yield return WaitForConditionOrTimeOut(() => AllHandlersSynchronized(data)); + AssertOnTimeout("Not all handlers synchronized"); + + // 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 void SpawnPrefabWithData(NetworkSerializableTest data) + { + var instance = UnityEngine.Object.Instantiate(m_Prefab).GetComponent(); + GetAuthorityNetworkManager().PrefabHandler.SetInstantiationData(instance, data); + instance.Spawn(); + } + + 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: