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 6 commits into
base: develop-2.0.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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(16,Allocator.Temp, int.MaxValue)));
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(16, Allocator.Temp, int.MaxValue)));
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
62 changes: 61 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 @@ -1811,6 +1819,8 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla
}
}

NetworkManager.PrefabHandler.InjectInstantiationPayload(this);

NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene);

if ((NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost) || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer))
Expand Down Expand Up @@ -2812,6 +2822,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 +2893,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 +2965,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 +3045,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 +3201,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 +3298,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
@@ -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 @@ -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="INetworkInstantiationPayloadSynchronizer"/> alongside this interface to receive the data before instantiation.
/// </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>
Expand Down Expand Up @@ -223,6 +226,30 @@ public bool RemoveHandler(uint globalObjectIdHash)
/// <returns>true or false</returns>
internal bool ContainsHandler(uint networkPrefabHash) => m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabHash) || m_PrefabInstanceToPrefabAsset.ContainsKey(networkPrefabHash);

/// <summary>
/// Injects instantiation payload on the networkObject
/// </summary>
/// <param name="networkObject">Injection target</param>
internal void InjectInstantiationPayload(NetworkObject networkObject)
{
if (m_PrefabAssetToPrefabHandler.TryGetValue(networkObject.GlobalObjectIdHash, out var prefabInstanceHandler))
{
if (prefabInstanceHandler is INetworkInstantiationPayloadSynchronizer synchronizer)
{
var fastBufferWriter = new FastBufferWriter(16, Collections.Allocator.Temp, int.MaxValue);
var instantiationPayloadBufferWriter = new BufferSerializer<BufferSerializerWriter>(new BufferSerializerWriter(fastBufferWriter));
synchronizer.OnSynchronize(ref instantiationPayloadBufferWriter);
if (fastBufferWriter.Length > 0)
{
unsafe
{
networkObject.InstantiationPayload = new FastBufferReader(fastBufferWriter.GetUnsafePtr(), Collections.Allocator.Persistent, fastBufferWriter.Length);
}
}
}
}
}

/// <summary>
/// Returns the source NetworkPrefab's <see cref="NetworkObject.GlobalObjectIdHash"/>
/// </summary>
Expand Down Expand Up @@ -252,10 +279,20 @@ 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> InstantiationPayloadSerializer, Vector3 position, Quaternion rotation) where T : IReaderWriter
{
if (m_PrefabAssetToPrefabHandler.TryGetValue(networkPrefabAssetHash, out var prefabInstanceHandler))
{
if (InstantiationPayloadSerializer.IsReader)
{
FastBufferReader instantiationPayloadReader = InstantiationPayloadSerializer.GetFastBufferReader();
bool ShouldSychronize = instantiationPayloadReader.IsInitialized && instantiationPayloadReader.Length > 0;
if (ShouldSychronize && prefabInstanceHandler is INetworkInstantiationPayloadSynchronizer synchronizer)
{
synchronizer.OnSynchronize(ref InstantiationPayloadSerializer);
}
}

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)
Expand Down
Loading