diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 3069602122..78b10250a2 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -11,6 +11,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added - 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 `INetworkPrefabInstanceHandlerWithData`, 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 65413c09e4..73e983179a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -57,6 +57,13 @@ public uint PrefabIdHash } } + /// + /// InstantiationData sent during the instantiation process. + /// Retrieved in + /// and available to INetworkPrefabInstanceHandler.Instantiate() for custom handling by user code. + /// + internal byte[] InstantiationData; + /// /// All component instances associated with a component instance. /// @@ -2885,6 +2892,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; @@ -2973,6 +2986,12 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(OwnerObject.GetSceneOriginHandle()); } + if (HasInstantiationData) + { + writer.WriteValueSafe(OwnerObject.InstantiationData.Length); + writer.WriteBytesSafe(OwnerObject.InstantiationData); + } + // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); @@ -3151,7 +3170,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 @@ -3216,8 +3236,13 @@ 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, ref bufferSerializer) : default; + //Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject); + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(sceneObject, instantiationDataReader); if (networkObject == null) { @@ -3250,7 +3275,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/INetworkPrefabInstanceHandlerWithData.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandlerWithData.cs new file mode 100644 index 0000000000..822e0f5d10 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandlerWithData.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Specialized version of that receives + /// custom instantiation data injected by the server before spawning. + /// + public interface INetworkPrefabInstanceHandlerWithData where T : struct, INetworkSerializable + { + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, T instantiationData); + void Destroy(NetworkObject networkObject); + } + + internal interface INetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandler + { + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader reader); + bool HandlesDataType(); + } + + internal class HandlerWrapper : INetworkPrefabInstanceHandlerWithData where T : struct, INetworkSerializable + { + private readonly INetworkPrefabInstanceHandlerWithData _impl; + + public HandlerWrapper(INetworkPrefabInstanceHandlerWithData impl) => _impl = impl; + + public bool HandlesDataType() => typeof(T) == typeof(U); + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader reader) + { + reader.ReadValueSafe(out T _payload); + return _impl.Instantiate(ownerClientId, position, rotation, _payload); + } + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) => _impl.Instantiate(ownerClientId, position, rotation, default); + public void Destroy(NetworkObject networkObject) => _impl.Destroy(networkObject); + } +} \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandlerWithData.cs.meta b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandlerWithData.cs.meta new file mode 100644 index 0000000000..3895ba444c --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/INetworkPrefabInstanceHandlerWithData.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/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index fe0dd270e9..6913457640 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -20,6 +20,9 @@ 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 @@ -57,6 +60,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] @@ -75,6 +84,10 @@ public bool AddHandler(GameObject networkPrefabAsset, INetworkPrefabInstanceHand { return AddHandler(networkPrefabAsset.GetComponent().GlobalObjectIdHash, instanceHandler); } + public bool AddHandler(GameObject networkPrefabAsset, INetworkPrefabInstanceHandlerWithData instanceHandler) where T : struct, INetworkSerializable + { + return AddHandler(networkPrefabAsset.GetComponent().GlobalObjectIdHash, instanceHandler); + } /// /// Use a to register a class that implements the interface with the @@ -86,6 +99,10 @@ public bool AddHandler(NetworkObject prefabAssetNetworkObject, INetworkPrefabIns { return AddHandler(prefabAssetNetworkObject.GlobalObjectIdHash, instanceHandler); } + public bool AddHandler(NetworkObject prefabAssetNetworkObject, INetworkPrefabInstanceHandlerWithData instanceHandler) where T : struct, INetworkSerializable + { + return AddHandler(prefabAssetNetworkObject.GlobalObjectIdHash, instanceHandler); + } /// /// Use a to register a class that implements the interface with the @@ -98,11 +115,47 @@ 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 bool AddHandler(uint globalObjectIdHash, INetworkPrefabInstanceHandlerWithData instanceHandler) where T : struct, INetworkSerializable + { + if (!m_PrefabAssetToPrefabHandler.ContainsKey(globalObjectIdHash)) + return AddHandler(globalObjectIdHash, new HandlerWrapper(instanceHandler)); + 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()) + { + throw new Exception("[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! @@ -199,6 +252,10 @@ public bool RemoveHandler(uint globalObjectIdHash) m_PrefabInstanceToPrefabAsset.Remove(networkPrefabHashKey); } + if(m_PrefabAssetToPrefabHandlerWithData.TryGetValue(globalObjectIdHash, out var handlerWithData)) + { + m_PrefabAssetToPrefabHandlerWithData.Remove(globalObjectIdHash); + } return m_PrefabAssetToPrefabHandler.Remove(globalObjectIdHash); } @@ -223,6 +280,39 @@ 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, ref BufferSerializer serializer) where T : IReaderWriter + { + if (!serializer.IsReader || !TryGetHandlerWithData(objectHash, out INetworkPrefabInstanceHandlerWithData synchronizableHandler)) + { + return default; + } + + FastBufferReader fastBufferReader = serializer.GetFastBufferReader(); + // Reads the expected size of the instantiation data + fastBufferReader.ReadValueSafe(out int dataSize); + int dataStartPos = fastBufferReader.Position; + var result = new FastBufferReader(fastBufferReader, Collections.Allocator.Temp, dataSize, dataStartPos); + fastBufferReader.Seek(dataStartPos + dataSize); + return result; + } + /// /// Returns the source NetworkPrefab's /// @@ -252,23 +342,38 @@ 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) + { + NetworkObject networkObjectInstance = instantiationDataReader.IsInitialized + ? InstantiateNetworkPrefabWithData(networkPrefabAssetHash, ownerClientId, position, rotation, instantiationDataReader) + : InstantiateNetworkPrefabDefault(networkPrefabAssetHash, 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) + RegisterPrefabInstance(networkObjectInstance, networkPrefabAssetHash); + return networkObjectInstance; + } + + private NetworkObject InstantiateNetworkPrefabDefault(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation) { if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) - { - var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation); + return 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.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) - { - m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); - } + private NetworkObject InstantiateNetworkPrefabWithData(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader instantiationDataReader) + { + if (m_PrefabAssetToPrefabHandlerWithData.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler)) + return prefabInstanceHandler.Instantiate(ownerClientId, position, rotation, instantiationDataReader); + return null; + } - return networkObjectInstance; + private void RegisterPrefabInstance(NetworkObject networkObjectInstance, uint networkPrefabAssetHash) + { + if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + { + m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); } - - return null; } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index ba9db8329d..eb529f2240 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -779,14 +779,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 @@ -807,22 +807,22 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow break; case NetworkPrefabOverride.Hash: case NetworkPrefabOverride.Prefab: + { + // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the + // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically + // but might want to use the same source network prefab as an in-scene placed NetworkObject. + // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically + // spawning them so the original prefab placed is preserved and this is not needed) + if (inScenePlacedWithNoSceneManagement) { - // When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the - // SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically - // but might want to use the same source network prefab as an in-scene placed NetworkObject. - // (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically - // spawning them so the original prefab placed is preserved and this is not needed) - if (inScenePlacedWithNoSceneManagement) - { - networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; - } - else - { - networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; - } - break; + networkPrefabReference = networkPrefab.SourcePrefabToOverride ? networkPrefab.SourcePrefabToOverride : networkPrefab.Prefab; } + else + { + networkPrefabReference = NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; + } + break; + } } } @@ -875,7 +875,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; @@ -888,7 +888,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..9551eeecb4 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerWithDataTests.cs @@ -0,0 +1,168 @@ +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 +{ + internal class NetworkPrefabHandlerWithDataTests + { + private const int k_ClientCount = 4; + private const string k_TestPrefabObjectName = "NetworkPrefabTestObject"; + private uint m_ObjectId = 1; + + private GameObject _prefab; + private NetworkManager server; + private NetworkManager[] clients; + + private PrefabInstanceHandlerWithData[] clientHandlers; + + [SetUp] + public void Setup() + { + NetcodeIntegrationTestHelpers.Create(k_ClientCount, out server, out clients); + _prefab = CreateNetworkPrefab(); + + RegisterPrefab(server, out _); + + clientHandlers = new PrefabInstanceHandlerWithData[clients.Length]; + for (int i = 0; i < clients.Length; i++) + { + RegisterPrefab(clients[i], out clientHandlers[i]); + } + } + + [TearDown] + public void Teardown() + { + foreach (var client in clients) + { + client.PrefabHandler.RemoveHandler(_prefab); + client.NetworkConfig.Prefabs.Remove(_prefab); + client.Shutdown(); + } + + server.PrefabHandler.RemoveHandler(_prefab); + server.NetworkConfig.Prefabs.Remove(_prefab); + server.Shutdown(); + + UnityEngine.Object.DestroyImmediate(_prefab); + NetcodeIntegrationTestHelpers.Destroy(); + } + + [UnityTest] + public IEnumerator InstantiationPayload_SyncsCorrectly() + { + yield return StartAndWaitForClients(); + var data = new NetworkSerializableTest { Value = 12, Value2 = 3.14f }; + SpawnPrefabWithData(data); + yield return WaitForAllClientsToSync(data); + } + + [UnityTest] + public IEnumerator InstantiationPayload_LateJoinersReceiveData() + { + yield return StartAndWaitForClients(); + var data = new NetworkSerializableTest { Value = 42, Value2 = 2.71f }; + SpawnPrefabWithData(data); + + // Disconnect and destroy one client to simulate late join + var lateJoiner = clients[0]; + lateJoiner.Shutdown(); + yield return null; + + var lateJoinerIndex = 0; + clients[lateJoinerIndex] = NetcodeIntegrationTestHelpers.CreateNewClient(k_ClientCount); + RegisterPrefab(clients[lateJoinerIndex], out clientHandlers[lateJoinerIndex]); + + NetcodeIntegrationTestHelpers.StartOneClient(clients[lateJoinerIndex]); + yield return NetcodeIntegrationTestHelpers.WaitForClientConnected(clients[lateJoinerIndex]); + + // Confirm late joiner got correct data + var timeoutHelper = new TimeoutHelper(); + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => clientHandlers[lateJoinerIndex].instantiationData.IsSynchronizedWith(data)); + Assert.False(timeoutHelper.TimedOut, "Late joiner did not synchronize properly with instantiation data."); + } + + private GameObject CreateNetworkPrefab() + { + var guid = NetworkManagerHelper.AddGameNetworkObject($"{k_TestPrefabObjectName}{m_ObjectId++}"); + var networkObject = NetworkManagerHelper.InstantiatedNetworkObjects[guid]; + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + return networkObject.gameObject; + } + + private void RegisterPrefab(NetworkManager manager, out PrefabInstanceHandlerWithData handler) + { + var networkPrefab = new NetworkPrefab { Prefab = _prefab }; + manager.NetworkConfig.Prefabs.Add(networkPrefab); + + handler = new PrefabInstanceHandlerWithData(_prefab); + manager.PrefabHandler.AddHandler(_prefab, handler); + } + + private NetworkObject SpawnPrefabWithData(NetworkSerializableTest data) + { + var instance = GameObject.Instantiate(_prefab).GetComponent(); + server.PrefabHandler.SetInstantiationData(instance, data); + instance.Spawn(); + return instance; + } + + private IEnumerator StartAndWaitForClients() + { + if (!NetcodeIntegrationTestHelpers.Start(true, server, clients)) + Assert.Fail("Failed to start instances"); + + yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients, null, 512); + yield return NetcodeIntegrationTestHelpers.WaitForClientsConnectedToServer(server, clients.Length + 1, null, 512); + } + + private IEnumerator WaitForAllClientsToSync(NetworkSerializableTest expectedData) + { + var timeoutHelper = new TimeoutHelper(); + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => clientHandlers.All(h => h.instantiationData.IsSynchronizedWith(expectedData))); + Assert.False(timeoutHelper.TimedOut, "Data did not synchronize correctly to all clients."); + } + + private class PrefabInstanceHandlerWithData : INetworkPrefabInstanceHandlerWithData + { + public GameObject Prefab; + public NetworkSerializableTest instantiationData; + + public PrefabInstanceHandlerWithData(GameObject prefab) + { + Prefab = prefab; + } + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, NetworkSerializableTest data) + { + instantiationData = data; + return GameObject.Instantiate(Prefab, position, rotation).GetComponent(); + } + + public void Destroy(NetworkObject networkObject) + { + GameObject.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: