-
Notifications
You must be signed in to change notification settings - Fork 447
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
base: develop-2.0.0
Are you sure you want to change the base?
Changes from 40 commits
3ff9326
7f395ba
f0d49aa
3f34e83
47680f5
b8765a2
de9fc05
6419ad0
1d746fe
4005c50
a103e49
43e8ff4
0fab0a6
36a041c
fd787e8
96b7af6
e49cb63
89eb5c4
adead9d
e4481b2
3522fc0
6757e63
a5670e3
426d82c
6899764
c1d6503
096c85c
7f6c9c7
89dd4c0
b992b7c
1f27a68
5b68637
d42bf95
4c7f63d
2b6befd
3dea6aa
015e384
536642b
fa774e7
0d62ea4
3fad66c
81d574a
36ec35f
7326a83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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."); | ||
Extrys marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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; | ||
Extrys marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't super love this. Allocating a new fast buffer reader can be expensive, especially in the case of late client join. It would be best if we can avoid this allocation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, this is the last thing pending from this review. This one took me a bit to solve, I had to dig deeper into AfastBufferReader.ReadValueSafe(out int dataSize);
int dataStartPos = fastBufferReader.Position;
unsafe
{
byte* ptr = fastBufferReader.GetUnsafePtr() + dataStartPos;
return new FastBufferReader(ptr, Allocator.None, dataSize);
} This is efficient, avoids allocations as it's pointing to the original buffer, and is consistent with the rest of the (already unsafe) API. The only drawback is requiring Alternatively, I tried a safe version using a tracked slice: Binternal readonly struct ReaderSlice
{
public readonly FastBufferReader Reader;
public readonly int Start;
public readonly int Length;
public ReaderSlice(FastBufferReader reader, int startPosition, int length)
{
Reader = reader;
Start = startPosition;
Length = length;
}
public void Read<T>(out T value) where T : struct, INetworkSerializable
{
var currentPos = Reader.Position;
Reader.Seek(Start);
Reader.ReadValueSafe(out value);
Reader.Seek(currentPos);
}
} This avoids unsafe code and keeps the slice strictly tied to instantiation data. It limits misuse but adds friction, since most of the NGO API expects Both approaches work fine and still passing the tests. So I will just wait for your feedback on this :) |
||
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 | ||
Extrys marked this conversation as resolved.
Show resolved
Hide resolved
|
||
? 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) | ||
Extrys marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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> | ||
|
Uh oh!
There was an error while loading. Please reload this page.