Skip to content

feat: Allow to pass custom data on Spawn calls #3419

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 9 commits into from
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -797,8 +797,9 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne
// Server-side spawning (only if there is a prefab hash or player prefab provided)
if (!NetworkManager.DistributedAuthorityMode && response.CreatePlayerObject && (response.PlayerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null))
{
var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, response.Position ?? null, response.Rotation ?? null)
: NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null);
var instantiationPayloadWriter = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(new FastBufferWriter(0,Allocator.Temp)));
var playerObject = response.PlayerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(response.PlayerPrefabHash.Value, ownerClientId, ref instantiationPayloadWriter, response.Position ?? null, response.Rotation ?? null)
: NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash, ownerClientId, ref instantiationPayloadWriter, response.Position ?? null, response.Rotation ?? null);

// Spawn the player NetworkObject locally
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(
Expand Down Expand Up @@ -950,7 +951,8 @@ internal void CreateAndSpawnPlayer(ulong ownerId)
if (playerPrefab != null)
{
var globalObjectIdHash = playerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash;
var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation);
var instantiationPayloadWriter = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(new FastBufferWriter(0, Allocator.Temp)));
var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, ref instantiationPayloadWriter, playerPrefab.transform.position, playerPrefab.transform.rotation);
networkObject.IsSceneObject = false;
networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene);
}
Expand Down
60 changes: 59 additions & 1 deletion com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Unity.Collections;
using Unity.Netcode.Components;
#if UNITY_EDITOR
using UnityEditor;
Expand Down Expand Up @@ -45,6 +46,13 @@ public sealed class NetworkObject : MonoBehaviour
[SerializeField]
internal uint InScenePlacedSourceGlobalObjectIdHash;

/// <summary>
/// Metadata sent during the instantiation process.
/// Retrieved in INetworkCustomSpawnDataSynchronizer before instantiation,
/// and available to INetworkPrefabInstanceHandler.Instantiate() for custom handling by user code.
/// </summary>
internal FastBufferReader InstantiationPayload;

/// <summary>
/// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0
/// </summary>
Expand Down Expand Up @@ -2812,6 +2820,7 @@ internal struct SceneObject
public ulong NetworkObjectId;
public ulong OwnerClientId;
public ushort OwnershipFlags;
public FastBufferReader InstantiationPayload;

public bool IsPlayerObject
{
Expand Down Expand Up @@ -2882,6 +2891,12 @@ public bool SpawnWithObservers
set => ByteUtility.SetBit(ref m_BitField, 10, value);
}

public bool HasInstantiationPayload
{
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 @@ -2948,12 +2963,26 @@ public void Serialize(FastBufferWriter writer)
var writeSize = 0;
writeSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
writeSize += FastBufferWriter.GetWriteSize<int>();
if (HasInstantiationPayload)
{
writeSize += FastBufferWriter.GetWriteSize<int>();
writeSize += InstantiationPayload.Length;
}

if (!writer.TryBeginWrite(writeSize))
{
throw new OverflowException("Could not serialize SceneObject: Out of buffer space.");
}

if (HasInstantiationPayload)
{
writer.WriteValueSafe(InstantiationPayload.Length);
unsafe
{
writer.WriteBytes(InstantiationPayload.GetUnsafePtr(), InstantiationPayload.Length);
}
}

if (HasTransform)
{
writer.WriteValue(Transform);
Expand Down Expand Up @@ -3014,12 +3043,34 @@ public void Deserialize(FastBufferReader reader)
readSize += HasTransform ? FastBufferWriter.GetWriteSize<TransformData>() : 0;
readSize += FastBufferWriter.GetWriteSize<int>();

int preInstanceDataSize = 0;
if (HasInstantiationPayload)
{
if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize<int>()))
{
throw new OverflowException($"Could not deserialize SceneObject: Reading past the end of the buffer ({nameof(InstantiationPayload)} size)");
}

reader.ReadValueSafe(out preInstanceDataSize);
readSize += FastBufferWriter.GetWriteSize<int>();
readSize += preInstanceDataSize;
}

// Try to begin reading the remaining bytes
if (!reader.TryBeginRead(readSize))
{
throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer");
}

if (HasInstantiationPayload)
{
unsafe
{
InstantiationPayload = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, preInstanceDataSize);
reader.Seek(reader.Position + preInstanceDataSize);
}
}

if (HasTransform)
{
reader.ReadValue(out Transform);
Expand Down Expand Up @@ -3148,7 +3199,9 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId = NetworkManager
NetworkSceneHandle = NetworkSceneHandle,
Hash = CheckForGlobalObjectIdHashOverride(),
OwnerObject = this,
TargetClientId = targetClientId
TargetClientId = targetClientId,
HasInstantiationPayload = InstantiationPayload.IsInitialized,
InstantiationPayload = InstantiationPayload
};

// Handle Parenting
Expand Down Expand Up @@ -3243,6 +3296,11 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf
// in order to be able to determine which NetworkVariables the client will be allowed to read.
networkObject.OwnerClientId = sceneObject.OwnerClientId;

// Even though the Instantiation Payload is typically consumed during the spawn message handling phase,
// we still assign it here to preserve the original spawn metadata for potential inspection, diagnostics,
// or in case future systems want to access it directly without relying on synchronization messages.
networkObject.InstantiationPayload = sceneObject.InstantiationPayload;

// Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours
networkObject.InvokeBehaviourNetworkPreSpawn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,14 @@ internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader read
// We dont know what size to use. Try every (more collision prone)
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
{
// handler can remove itself, cache the name for metrics
var messageName = m_MessageHandlerNameLookup32[hash];
messageHandler32(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount);
}

if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
{
// handler can remove itself, cache the name for metrics
var messageName = m_MessageHandlerNameLookup64[hash];
messageHandler64(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount);
}
}
else
Expand All @@ -202,19 +198,15 @@ internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader read
case HashSize.VarIntFourBytes:
if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32))
{
// handler can remove itself, cache the name for metrics
var messageName = m_MessageHandlerNameLookup32[hash];
messageHandler32(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount);
}
break;
case HashSize.VarIntEightBytes:
if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64))
{
// handler can remove itself, cache the name for metrics
var messageName = m_MessageHandlerNameLookup64[hash];
messageHandler64(sender, reader);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, messageName, bytesCount);
m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount);
}
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Unity.Netcode
{
/// <summary>
/// Interface for synchronizing custom instantiation payloads during NetworkObject spawning.
/// Used alongside <see cref="INetworkPrefabInstanceHandler"/> to extend instantiation behavior.
/// </summary>
public interface INetworkInstantiationPayloadSynchronizer
{
/// <summary>
/// Provides a method for synchronizing instantiation payload data during the spawn process.
/// Extends <see cref="INetworkPrefabInstanceHandler"/> to allow passing additional data prior to instantiation
/// to help identify or configure the local object instance that should be linked to the spawned NetworkObject.
///
/// This method is invoked immediately before <see cref="INetworkPrefabInstanceHandler.Instantiate"/> is called,
/// allowing you to cache or prepare information needed during instantiation.
/// </summary>
/// <param name="serializer">The buffer serializer used to read or write custom instantiation data.</param>
void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter;
}
}

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 @@ -252,12 +252,33 @@ 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<T>(uint networkPrefabAssetHash, ulong ownerClientId, ref BufferSerializer<T> preInstanceDataSerializer, Vector3 position, Quaternion rotation) where T : IReaderWriter
{
if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler))
{
if (prefabInstanceHandler is INetworkInstantiationPayloadSynchronizer synchronizer)
{
synchronizer.OnSynchronize(ref preInstanceDataSerializer);
}
var networkObjectInstance = prefabInstanceHandler.Instantiate(ownerClientId, position, rotation);

if (networkObjectInstance != null)
{
if (preInstanceDataSerializer.IsReader)
{
networkObjectInstance.InstantiationPayload = preInstanceDataSerializer.GetFastBufferReader();
}
else
{
var writer = preInstanceDataSerializer.GetFastBufferWriter();
if (writer.Length > 0)
{
unsafe
{
networkObjectInstance.InstantiationPayload = new FastBufferReader(writer.GetUnsafePtr(), Collections.Allocator.Persistent, writer.Length);
}
}
}
}
//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))
Expand Down
Loading