Skip to content

feat: Instantiation payload support for INetworkPrefabInstanceHandler #3430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
3ff9326
feat: Network Object Instantiation Payload
Extrys Apr 27, 2025
7f395ba
Merge branch 'Unity-Technologies:develop-2.0.0' into develop-2.0.0
Extrys Apr 27, 2025
f0d49aa
insight on INetworkInstantiationPayloadSynchronizer in the INetworkPr…
Extrys Apr 27, 2025
3f34e83
cleaning diff
Extrys Apr 27, 2025
47680f5
Some fixes for object instantiated in the server without handler
Extrys Apr 27, 2025
b8765a2
Merge branch 'Unity-Technologies:develop-2.0.0' into develop-2.0.0
Extrys Apr 28, 2025
de9fc05
Simplified approach that reuses createObjectMesage bufferSerializer
Extrys Apr 29, 2025
6419ad0
commented non generic serialization methods
Extrys Apr 29, 2025
1d746fe
Merge branch 'feat/InstantiationPayload' into develop-2.0.0
Extrys Apr 29, 2025
4005c50
Cleaning diff
Extrys Apr 29, 2025
a103e49
Cleaning diff
Extrys Apr 29, 2025
43e8ff4
Review Changes
Extrys Apr 30, 2025
0fab0a6
Some more renamigs for making it more closer to unity's naming
Extrys Apr 30, 2025
36a041c
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys May 1, 2025
fd787e8
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys May 2, 2025
96b7af6
Added buffer safety and Tests
Extrys May 5, 2025
e49cb63
Merge remote-tracking branch 'origin/develop-2.0.0' into develop-2.0.0
Extrys May 5, 2025
89eb5c4
Log text change for more clarity
Extrys May 5, 2025
adead9d
Renamed HasInstantiationPayload to HasInstantiationData to mantain na…
Extrys May 5, 2025
e4481b2
CHANGELOG.md entry added
Extrys May 5, 2025
3522fc0
Merge branch 'Unity-Technologies:develop-2.0.0' into develop-2.0.0
Extrys May 5, 2025
6757e63
Solved late-join problems
Extrys May 6, 2025
a5670e3
cleaning diff
Extrys May 6, 2025
426d82c
Updated comment
Extrys May 6, 2025
6899764
Added more Buffer/Synchronization safety changes
Extrys May 6, 2025
c1d6503
Improves handler lookup performance
Extrys May 6, 2025
096c85c
New stateless approach working flawless
Extrys May 8, 2025
7f6c9c7
Test updated
Extrys May 8, 2025
89dd4c0
Test refactor and included late joining test
Extrys May 8, 2025
b992b7c
clean diff
Extrys May 8, 2025
1f27a68
Changelog updated
Extrys May 8, 2025
5b68637
Adds comment for data handling workaround
Extrys May 9, 2025
d42bf95
Cleaning diff, fixed comment accidentally using a chinese version of …
Extrys May 9, 2025
4c7f63d
New requirements fullfilled
Extrys May 12, 2025
2b6befd
removed yet unused method
Extrys May 12, 2025
3dea6aa
extending the interface to directly allow different injection patterns
Extrys May 12, 2025
015e384
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys May 14, 2025
536642b
Pure stateless, non generic approach for sending data through fastBuf…
Extrys May 16, 2025
fa774e7
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys May 16, 2025
0d62ea4
renamings from Inject to Set
Extrys May 16, 2025
3fad66c
Inlined functions and removed terniary call
Extrys May 24, 2025
81d574a
Changed instantiation data throw by an error log
Extrys May 24, 2025
36ec35f
Abstract class approach, removed wrapper
Extrys May 24, 2025
7326a83
Removed the BufferSerializer From the GetInstantiationDataReader method
Extrys May 24, 2025
e8a661c
Using Approach A (unsafe block) to avoid allocations for FastBufferRe…
Extrys May 28, 2025
f47f3d4
Most review comments addressed
Extrys May 28, 2025
6b3963f
Renamed Interface in the Changelog
Extrys May 29, 2025
fc43642
Update formatting and use NetcodeIntegrationTest for test class
EmandM May 30, 2025
729f4d6
fixed late join bug
Extrys May 30, 2025
fea9d7d
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys May 30, 2025
900c7ee
Using BytePacker utilities to optimize the size of the serialized int…
Extrys May 30, 2025
886697e
Updated comment to match new interface name
Extrys Jun 6, 2025
4586a6e
Merge branch 'develop-2.0.0' into develop-2.0.0
Extrys Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`, a variant of `INetworkPrefabInstanceHandler` that provides access to custom instantiation data directly within the `Instantiate()` method. (#3430)

### Fixed

Expand Down
31 changes: 28 additions & 3 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public uint PrefabIdHash
}
}

/// <summary>
/// InstantiationData sent during the instantiation process.
/// Available to read as T parameter to <see cref="NetworkPrefabInstanceHandlerWithData{T}.Instantiate(ulong, Vector3, Quaternion, T)"/> for custom handling by user code.
/// </summary>
internal byte[] InstantiationData;

/// <summary>
/// All <see cref="NetworkTransform"/> component instances associated with a <see cref="NetworkObject"/> component instance.
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<BufferSerializerWriter>(new BufferSerializerWriter(writer));
OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3166,8 +3185,15 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager
/// <returns>The deserialized NetworkObject or null if deserialization failed</returns>
internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false)
{
var bufferSerializer = new BufferSerializer<BufferSerializerReader>(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)
{
Expand Down Expand Up @@ -3200,7 +3226,6 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
networkObject.InvokeBehaviourNetworkPreSpawn();

// Synchronize NetworkBehaviours
var bufferSerializer = new BufferSerializer<BufferSerializerReader>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="GameObject.SetActive(bool)"/> method.
/// </summary>
/// <remarks>
/// 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 <see cref="NetworkPrefabInstanceHandlerWithData{T}"/> instead.
/// </remarks>
/// <param name="ownerClientId">the owner for the <see cref="NetworkObject"/> to be instantiated</param>
/// <param name="position">the initial/default position for the <see cref="NetworkObject"/> to be instantiated</param>
/// <param name="rotation">the initial/default rotation for the <see cref="NetworkObject"/> to be instantiated</param>
Expand Down Expand Up @@ -57,6 +61,12 @@ public class NetworkPrefabHandler
/// </summary>
private readonly Dictionary<uint, INetworkPrefabInstanceHandler> m_PrefabAssetToPrefabHandler = new Dictionary<uint, INetworkPrefabInstanceHandler>();

/// <summary>
/// Links a network prefab asset to a class with the INetworkPrefabInstanceHandlerWithData interface,
/// used to keep a smaller lookup table than <see cref="m_PrefabAssetToPrefabHandler"/> for faster instantiation data injection into NetworkObject
/// </summary>
private readonly Dictionary<uint, INetworkPrefabInstanceHandlerWithData> m_PrefabAssetToPrefabHandlerWithData = new Dictionary<uint, INetworkPrefabInstanceHandlerWithData>();

/// <summary>
/// Links the custom prefab instance's GlobalNetworkObjectId to the original prefab asset's GlobalNetworkObjectId. (Needed for HandleNetworkPrefabDestroy)
/// [PrefabInstance][PrefabAsset]
Expand Down Expand Up @@ -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<T>(GameObject gameObject, T instantiationData) where T : struct, INetworkSerializable
{
if (gameObject.TryGetComponent<NetworkObject>(out var networkObject))
{
SetInstantiationData(networkObject, instantiationData);
}
}
public void SetInstantiationData<T>(NetworkObject networkObject, T data) where T : struct, INetworkSerializable
{
if (!TryGetHandlerWithData(networkObject.GlobalObjectIdHash, out var prefabHandler) || !prefabHandler.HandlesDataType<T>())
{
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<BufferSerializerWriter>(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}");
}
}

/// <summary>
/// HOST ONLY!
/// Since a host is unique and is considered both a client and a server, for each source NetworkPrefab you must manually
Expand Down Expand Up @@ -199,6 +241,7 @@ public bool RemoveHandler(uint globalObjectIdHash)
m_PrefabInstanceToPrefabAsset.Remove(networkPrefabHashKey);
}

m_PrefabAssetToPrefabHandlerWithData.Remove(globalObjectIdHash);
return m_PrefabAssetToPrefabHandler.Remove(globalObjectIdHash);
}

Expand All @@ -223,6 +266,38 @@ public bool RemoveHandler(uint globalObjectIdHash)
/// <returns>true or false</returns>
internal bool ContainsHandler(uint networkPrefabHash) => m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabHash) || m_PrefabInstanceToPrefabAsset.ContainsKey(networkPrefabHash);

/// <summary>
/// Returns the <see cref="INetworkPrefabInstanceHandlerWithData"/> implementation for a given <see cref="NetworkObject.GlobalObjectIdHash"/>
/// </summary>
/// <param name="objectHash"></param>
/// <param name="handler"></param>
/// <returns></returns>
internal bool TryGetHandlerWithData(uint objectHash, out INetworkPrefabInstanceHandlerWithData handler)
{
return m_PrefabAssetToPrefabHandlerWithData.TryGetValue(objectHash, out handler);
}

/// <summary>
/// Reads the instantiation data for a given <see cref="NetworkObject.GlobalObjectIdHash"/>
/// </summary>
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;
}

/// <summary>
/// Returns the source NetworkPrefab's <see cref="NetworkObject.GlobalObjectIdHash"/>
/// </summary>
Expand Down Expand Up @@ -252,23 +327,30 @@ internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash)
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <returns></returns>
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;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using UnityEngine;

namespace Unity.Netcode
{
/// <summary>
/// Specialized version of <see cref="INetworkPrefabInstanceHandler"/> that receives
/// custom instantiation data injected by the server before spawning.
/// </summary>
public abstract class NetworkPrefabInstanceHandlerWithData<T> : 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<TK>() => 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<T>();
NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, FastBufferReader reader);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
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
Expand Down Expand Up @@ -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.
/// </remarks>
internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject)
internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneObject, FastBufferReader instantiationDataReader = default)
{
NetworkObject networkObject = null;
var globalObjectIdHash = sceneObject.Hash;
Expand All @@ -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
{
Expand Down
Loading