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

Draft
wants to merge 40 commits into
base: develop-2.0.0
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
40 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
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
@@ -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<T>`, a variant of `INetworkPrefabInstanceHandler` that provides access to custom instantiation data directly within the `Instantiate()` method. (#3430)

### Fixed

30 changes: 27 additions & 3 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
@@ -57,6 +57,13 @@ public uint PrefabIdHash
}
}

/// <summary>
/// InstantiationData sent during the instantiation process.
/// Retrieved in <see cref="INetworkPrefabInstanceHandlerWithData.ReadInstantiationData{T}(ref BufferSerializer{T})"/>
/// and available to INetworkPrefabInstanceHandler.Instantiate() 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>
@@ -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<BufferSerializerWriter>(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
/// <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, 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<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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
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 interface INetworkPrefabInstanceHandlerWithData<T> 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<T>();
}

internal class HandlerWrapper<T> : INetworkPrefabInstanceHandlerWithData where T : struct, INetworkSerializable
{
private readonly INetworkPrefabInstanceHandlerWithData<T> _impl;

public HandlerWrapper(INetworkPrefabInstanceHandlerWithData<T> impl) => _impl = impl;

public bool HandlesDataType<U>() => 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);
}
}

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
@@ -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 <see cref="GameObject.SetActive(bool)"/> 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 <see cref="INetworkPrefabInstanceHandlerWithData{T}"/> instead.
/// </summary>
/// <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>
@@ -57,6 +60,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]
@@ -75,6 +84,10 @@ public bool AddHandler(GameObject networkPrefabAsset, INetworkPrefabInstanceHand
{
return AddHandler(networkPrefabAsset.GetComponent<NetworkObject>().GlobalObjectIdHash, instanceHandler);
}
public bool AddHandler<T>(GameObject networkPrefabAsset, INetworkPrefabInstanceHandlerWithData<T> instanceHandler) where T : struct, INetworkSerializable
{
return AddHandler(networkPrefabAsset.GetComponent<NetworkObject>().GlobalObjectIdHash, instanceHandler);
}

/// <summary>
/// Use a <see cref="NetworkObject"/> to register a class that implements the <see cref="INetworkPrefabInstanceHandler"/> interface with the <see cref="NetworkPrefabHandler"/>
@@ -86,6 +99,10 @@ public bool AddHandler(NetworkObject prefabAssetNetworkObject, INetworkPrefabIns
{
return AddHandler(prefabAssetNetworkObject.GlobalObjectIdHash, instanceHandler);
}
public bool AddHandler<T>(NetworkObject prefabAssetNetworkObject, INetworkPrefabInstanceHandlerWithData<T> instanceHandler) where T : struct, INetworkSerializable
{
return AddHandler(prefabAssetNetworkObject.GlobalObjectIdHash, instanceHandler);
}

/// <summary>
/// Use a <see cref="NetworkObject.GlobalObjectIdHash"/> to register a class that implements the <see cref="INetworkPrefabInstanceHandler"/> interface with the <see cref="NetworkPrefabHandler"/>
@@ -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<T>(uint globalObjectIdHash, INetworkPrefabInstanceHandlerWithData<T> instanceHandler) where T : struct, INetworkSerializable
{
if (!m_PrefabAssetToPrefabHandler.ContainsKey(globalObjectIdHash))
return AddHandler(globalObjectIdHash, new HandlerWrapper<T>(instanceHandler));
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>())
{
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<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!
@@ -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)
/// <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>
/// <typeparam name="T"></typeparam>
/// <param name="objectHash"></param>
/// <param name="serializer"></param>
internal FastBufferReader GetInstantiationDataReader<T>(uint objectHash, ref BufferSerializer<T> 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;
}

/// <summary>
/// Returns the source NetworkPrefab's <see cref="NetworkObject.GlobalObjectIdHash"/>
/// </summary>
@@ -252,23 +342,38 @@ 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)
{
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;
}

/// <summary>
Loading