diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs
index cbc4b3a5ad..1b16f35740 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs
@@ -1164,6 +1164,17 @@ public override void OnNetworkSpawn()
///
public override void OnNetworkDespawn()
{
+#if UNIFIED_NETCODE
+ // The ghost replicates the kinematic state it reads when spawned, so a pooled server instance needs its authored state back.
+ if (NetworkObject.HasGhost)
+ {
+ if (m_LocalNetworkManager.IsServer)
+ {
+ SetIsKinematic(m_OriginalKinematicState);
+ }
+ return;
+ }
+#endif
if (UseRigidBodyForMotion && HasAuthority)
{
DetachFromFixedJoint();
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index 028e508982..5760614785 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -4241,9 +4241,11 @@ private void InitGhost()
{
Debug.Log($"[{nameof(NetworkObject)}] GhostBridge {name} detected and instantiated.");
}
- if (GhostObject.WasInitialized && NetworkObjectBridge.NetworkObjectId.Value != 0)
+ var networkObjectId = NetworkObjectBridge.NetworkObjectId.Value;
+ // A prefab handler instance is registered by HybridPrefabHandlerSpawnSystem before its Start runs.
+ if (GhostObject.WasInitialized && networkObjectId != 0 && !IsSpawned && !NetworkManager.SpawnManager.GhostSpawnManager.IsGhostPendingSpawn(networkObjectId))
{
- NetworkManager.SpawnManager.GhostSpawnManager.RegisterGhostBridge(NetworkObjectBridge.NetworkObjectId.Value, this);
+ NetworkManager.SpawnManager.GhostSpawnManager.RegisterGhostBridge(networkObjectId, this);
}
}
#endif
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs
new file mode 100644
index 0000000000..cf29cb4352
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs
@@ -0,0 +1,171 @@
+#if UNIFIED_NETCODE
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Netcode.Components;
+using Unity.Transforms;
+using UnityEngine;
+
+namespace Unity.Netcode
+{
+ ///
+ /// Gives a client the instance of a hybrid prefab from its instead of letting N4E instantiate it.
+ ///
+ ///
+ /// The ghost arrives before NGO's spawn message, so the handler receives as the owner.
+ ///
+ [UpdateInGroup(typeof(GhostSimulationSystemGroup))]
+ [UpdateAfter(typeof(GhostUpdateSystem))]
+ [UpdateBefore(typeof(GhostGameObjectSpawnSystem))]
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ internal partial class HybridPrefabHandlerSpawnSystem : SystemBase
+ {
+ private EntityQuery m_PendingSpawnQuery;
+ private EntityQuery m_HybridNetworkManagerQuery;
+
+ protected override void OnCreate()
+ {
+ if (World.IsHost())
+ {
+ Enabled = false;
+ return;
+ }
+ m_PendingSpawnQuery = GetEntityQuery(ComponentType.ReadOnly(), ComponentType.ReadOnly());
+ m_HybridNetworkManagerQuery = GetEntityQuery(ComponentType.ReadOnly());
+ RequireForUpdate(m_PendingSpawnQuery);
+ RequireForUpdate(m_HybridNetworkManagerQuery);
+ }
+
+ protected override void OnUpdate()
+ {
+ var networkManager = m_HybridNetworkManagerQuery.GetSingleton().NetworkManager.Value;
+ if (!networkManager || networkManager.ShutdownInProgress)
+ {
+ return;
+ }
+
+ using var pendingEntities = m_PendingSpawnQuery.ToEntityArray(Allocator.Temp);
+ using var ghostInstances = m_PendingSpawnQuery.ToComponentDataArray(Allocator.Temp);
+ var prefabs = EntityManager.GetBuffer(SystemAPI.GetSingletonEntity()).ToNativeArray(Allocator.Temp);
+
+ for (int i = 0; i < pendingEntities.Length; i++)
+ {
+ var entity = pendingEntities[i];
+ var prefabEntity = prefabs[ghostInstances[i].ghostType].GhostPrefab;
+ var prefabId = EntityManager.GetComponentData(prefabEntity).AssociatedGameObject;
+ var prefab = Resources.EntityIdToObject(prefabId) as GameObject;
+ if (!prefab || !prefab.TryGetComponent(out var prefabNetworkObject) || !networkManager.PrefabHandler.ContainsHandler(prefabNetworkObject.GlobalObjectIdHash))
+ {
+ continue;
+ }
+
+ var localTransform = EntityManager.GetComponentData(entity);
+ var networkObject = networkManager.PrefabHandler.HandleNetworkPrefabSpawn(prefabNetworkObject.GlobalObjectIdHash, NetworkManager.ServerClientId, localTransform.Position, localTransform.Rotation);
+ if (!networkObject || !networkObject.HasGhost || networkObject.GhostObject.IsSpawned)
+ {
+ Debug.LogError($"[{nameof(INetworkPrefabInstanceHandler)}] The handler for {prefab.name} did not return an unspawned instance of it. N4E instantiates this ghost instead.");
+ continue;
+ }
+
+ var gameObject = networkObject.gameObject;
+ var link = new GhostGameObjectLink(gameObject.GetEntityId(), gameObject.transform.GetEntityId());
+ // Released by GhostObject.Despawn in HybridPrefabHandlerDespawnSystem.
+ var entityLink = GhostEntityMapping.AcquireEntityReferenceGameObject(link.AssociatedGameObject, link.AssociatedTransform, prefabId, World.Unmanaged, entity);
+ EntityManager.SetComponentData(entity, link);
+ EntityManager.RemoveComponent(entity);
+ networkObject.GhostObject.ClientRuntimeInitialize(entityLink);
+
+ // A reused instance does not run Start again, so it registers here.
+ networkManager.SpawnManager.GhostSpawnManager.RegisterGhostBridge(networkObject.NetworkObjectBridge.NetworkObjectId.Value, networkObject);
+ }
+ }
+ }
+
+ ///
+ /// Returns a client's hybrid instance to its when N4E despawns its ghost, instead of letting N4E destroy it.
+ ///
+ [UpdateInGroup(typeof(GhostSimulationSystemGroup))]
+ [UpdateAfter(typeof(GhostDespawnSystem))]
+ [UpdateAfter(typeof(PredictedGhostDespawnSystem))]
+ [UpdateAfter(typeof(GhostReceiveSystem))]
+ [UpdateBefore(typeof(GhostGameObjectDespawnManagedSystem))]
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ internal partial class HybridPrefabHandlerDespawnSystem : SystemBase
+ {
+ private EntityQuery m_DespawnTrackingQuery;
+ private EntityQuery m_HybridNetworkManagerQuery;
+
+ protected override void OnCreate()
+ {
+ if (World.IsHost())
+ {
+ Enabled = false;
+ return;
+ }
+ m_DespawnTrackingQuery = GetEntityQuery(typeof(GameObjectDespawnTracking));
+ m_HybridNetworkManagerQuery = GetEntityQuery(ComponentType.ReadOnly());
+ RequireForUpdate(m_DespawnTrackingQuery);
+ RequireForUpdate(m_HybridNetworkManagerQuery);
+ }
+
+ protected override void OnUpdate()
+ {
+ var networkManager = m_HybridNetworkManagerQuery.GetSingleton().NetworkManager.Value;
+ if (!networkManager)
+ {
+ return;
+ }
+
+ var trackingEntity = m_DespawnTrackingQuery.GetSingletonEntity();
+ // Copied out, as GhostGameObjectDespawnManagedSystem does, because of a DynamicBuffer safety handle bug.
+ var despawns = EntityManager.GetBuffer(trackingEntity, isReadOnly: true).ToNativeArray(Allocator.Temp);
+ if (despawns.Length == 0)
+ {
+ return;
+ }
+
+ EntityManager.CompleteDependencyBeforeRW();
+ var spawnedGhostMap = SystemAPI.GetSingletonRW().ValueRO.SpawnedGhostMapRW;
+ var spawnManager = networkManager.SpawnManager;
+ var handled = new NativeList(despawns.Length, Allocator.Temp);
+
+ for (int i = 0; i < despawns.Length; i++)
+ {
+ var despawn = despawns[i].oneDespawn;
+ var gameObjectId = EntityManager.GetComponentData(despawn.entity).AssociatedGameObject;
+ var gameObject = Resources.EntityIdToObject(gameObjectId) as GameObject;
+ if (!gameObject || !gameObject.TryGetComponent(out var networkObject) || !networkObject.HasGhost || !networkManager.PrefabHandler.ContainsHandler(networkObject))
+ {
+ continue;
+ }
+
+ if (spawnedGhostMap.TryGetValue(despawn.ghost, out var mapped) && mapped == despawn.entity)
+ {
+ spawnedGhostMap.Remove(despawn.ghost);
+ }
+
+ var networkObjectId = networkObject.NetworkObjectId;
+ // The ghost can despawn before NGO's destroy message arrives, the same case NetworkObject.OnDestroy handles.
+ if (networkObject.IsSpawned && spawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var spawned) && spawned == networkObject)
+ {
+ spawnManager.OnDespawnObject(networkObject, false);
+ }
+ spawnManager.GhostSpawnManager.MarkNetworkObjectAsDestroying(networkObjectId);
+
+ NetworkSpawnManager.DespawnGhostForPrefabHandler(networkObject);
+ networkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject);
+ handled.Add(i);
+ }
+
+ if (handled.Length == 0)
+ {
+ return;
+ }
+ var buffer = EntityManager.GetBuffer(trackingEntity);
+ for (int i = handled.Length - 1; i >= 0; i--)
+ {
+ buffer.RemoveAt(handled[i]);
+ }
+ }
+ }
+}
+#endif
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs.meta b/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs.meta
new file mode 100644
index 0000000000..ea45da293c
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/HybridPrefabHandlerSystems.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 96f7476d9ad641b48612415fc00f12ce
diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
index 53ade03e9e..1215a6ffb7 100644
--- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs
@@ -997,6 +997,19 @@ private void SpawnGhost(NetworkObject networkObject)
entitiesNetcode.m_ActiveWorld = previousWorld;
}
}
+
+ ///
+ /// Despawns the ghost of a hybrid instance handed to an , which may keep the
+ /// GameObject for reuse. spawns a new ghost when the instance is spawned again.
+ ///
+ internal static void DespawnGhostForPrefabHandler(NetworkObject networkObject)
+ {
+ var ghostObject = networkObject.GhostObject;
+ if (networkObject.HasGhost && ghostObject != null && ghostObject.IsSpawned && ghostObject.DelaySpawning)
+ {
+ ghostObject.Despawn();
+ }
+ }
#endif
///
@@ -1601,6 +1614,10 @@ internal void DespawnAndDestroyNetworkObjects()
if (NetworkManager.PrefabHandler.ContainsHandler(networkObject))
{
OnDespawnObject(networkObject, false);
+#if UNIFIED_NETCODE
+ // The world is disposed after this, so a pooled instance must not keep a ghost linked to it.
+ DespawnGhostForPrefabHandler(networkObject);
+#endif
// Leave destruction up to the handler
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject);
}
@@ -1972,6 +1989,9 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro
{
if (NetworkManager.PrefabHandler.ContainsHandler(networkObject))
{
+#if UNIFIED_NETCODE
+ DespawnGhostForPrefabHandler(networkObject);
+#endif
NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject);
}
else
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs
new file mode 100644
index 0000000000..b450493ab4
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs
@@ -0,0 +1,153 @@
+#if UNIFIED_NETCODE && COM_UNITY_MODULES_PHYSICS
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using Unity.Netcode.Components;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ ///
+ /// Validates that an pools hybrid instances on the server and on clients.
+ ///
+ [TestFixture(HostOrServer.UnifiedHost)]
+ [TestFixture(HostOrServer.UnifiedServer)]
+ internal class HybridPrefabHandlerPoolingTests : NetcodeIntegrationTest
+ {
+ private const int k_InstanceCount = 3;
+ private const int k_SpawnCycles = 2;
+
+ protected override int NumberOfClients => 1;
+
+ private GameObject m_HybridPrefab;
+ private PoolHandler m_AuthorityPool;
+ private PoolHandler m_NonAuthorityPool;
+
+ public HybridPrefabHandlerPoolingTests(HostOrServer hostOrServer) : base(hostOrServer) { }
+
+ protected override bool UseUnifiedTests()
+ {
+ return true;
+ }
+
+ private class PoolHandler : INetworkPrefabInstanceHandler
+ {
+ public readonly List Created = new List();
+ public int Reused;
+ public int Returned;
+
+ private readonly GameObject m_Prefab;
+ private readonly Stack m_Available = new Stack();
+
+ public PoolHandler(GameObject prefab)
+ {
+ m_Prefab = prefab;
+ }
+
+ public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
+ {
+ if (m_Available.Count > 0)
+ {
+ var instance = m_Available.Pop();
+ instance.transform.SetPositionAndRotation(position, rotation);
+ instance.gameObject.SetActive(true);
+ Reused++;
+ return instance;
+ }
+ var created = Object.Instantiate(m_Prefab, position, rotation).GetComponent();
+ Created.Add(created);
+ return created;
+ }
+
+ public void Destroy(NetworkObject networkObject)
+ {
+ networkObject.gameObject.SetActive(false);
+ m_Available.Push(networkObject);
+ Returned++;
+ }
+
+ public void DestroyPooled()
+ {
+ while (m_Available.Count > 0)
+ {
+ var instance = m_Available.Pop();
+ if (instance)
+ {
+ Object.Destroy(instance.gameObject);
+ }
+ }
+ }
+ }
+
+ protected override void OnServerAndClientsCreated()
+ {
+ m_HybridPrefab = CreateNetworkObjectPrefab("HybridPooled");
+ m_HybridPrefab.AddComponent();
+ m_HybridPrefab.AddComponent().isKinematic = false;
+ m_HybridPrefab.AddComponent();
+
+ m_AuthorityPool = new PoolHandler(m_HybridPrefab);
+ m_NonAuthorityPool = new PoolHandler(m_HybridPrefab);
+ m_ServerNetworkManager.PrefabHandler.AddHandler(m_HybridPrefab, m_AuthorityPool);
+ m_ClientNetworkManagers[0].PrefabHandler.AddHandler(m_HybridPrefab, m_NonAuthorityPool);
+ base.OnServerAndClientsCreated();
+ }
+
+ protected override IEnumerator OnTearDown()
+ {
+ // Without a handler, shutdown destroys what is still spawned instead of pooling it where nothing cleans it up.
+ m_ServerNetworkManager.PrefabHandler.RemoveHandler(m_HybridPrefab);
+ m_ClientNetworkManagers[0].PrefabHandler.RemoveHandler(m_HybridPrefab);
+ m_AuthorityPool.DestroyPooled();
+ m_NonAuthorityPool.DestroyPooled();
+ yield return base.OnTearDown();
+ }
+
+ [UnityTest]
+ public IEnumerator SpawnDespawnRespawnUsesPools()
+ {
+ var authority = GetAuthorityNetworkManager();
+ var nonAuthority = GetNonAuthorityNetworkManager();
+ var authorityInstances = new NetworkObject[k_InstanceCount];
+
+ for (int cycle = 0; cycle < k_SpawnCycles; cycle++)
+ {
+ for (int i = 0; i < k_InstanceCount; i++)
+ {
+ var instance = m_AuthorityPool.Instantiate(authority.LocalClientId, new Vector3(i * 2.0f, 0.0f, 0.0f), Quaternion.identity);
+ instance.NetworkManagerOwner = authority;
+ instance.Spawn();
+ authorityInstances[i] = instance;
+ }
+ foreach (var instance in authorityInstances)
+ {
+ yield return WaitForSpawnedOnAllOrTimeOut(instance);
+ AssertOnTimeout($"[Cycle {cycle}] {instance.name} was not spawned on all instances!");
+ }
+
+ Assert.AreEqual(k_InstanceCount, m_NonAuthorityPool.Created.Count, $"[Cycle {cycle}] Client-{nonAuthority.LocalClientId} did not take its instances from its handler!");
+ Assert.AreEqual(cycle * k_InstanceCount, m_NonAuthorityPool.Reused, $"[Cycle {cycle}] Client-{nonAuthority.LocalClientId} did not reuse its pooled instances!");
+ foreach (var instance in authorityInstances)
+ {
+ var clientInstance = nonAuthority.SpawnManager.SpawnedObjects[instance.NetworkObjectId];
+ Assert.IsTrue(m_NonAuthorityPool.Created.Contains(clientInstance), $"[Cycle {cycle}] Client-{nonAuthority.LocalClientId} spawned {clientInstance.name}, which its handler did not provide!");
+ Assert.IsFalse(instance.GetComponent().isKinematic, $"[Cycle {cycle}] {instance.name} is kinematic on the server!");
+ Assert.IsTrue(clientInstance.GetComponent().isKinematic, $"[Cycle {cycle}] {clientInstance.name} is not kinematic on Client-{nonAuthority.LocalClientId}!");
+ }
+
+ foreach (var instance in authorityInstances)
+ {
+ instance.Despawn();
+ }
+ Assert.AreEqual((cycle + 1) * k_InstanceCount, m_AuthorityPool.Returned, $"[Cycle {cycle}] The server did not return its instances to its handler!");
+ yield return WaitForConditionOrTimeOut(() => m_NonAuthorityPool.Returned == (cycle + 1) * k_InstanceCount);
+ AssertOnTimeout($"[Cycle {cycle}] Client-{nonAuthority.LocalClientId} returned {m_NonAuthorityPool.Returned} instances to its handler!");
+ }
+
+ Assert.AreEqual(k_InstanceCount, m_AuthorityPool.Created.Count, "The server created instances instead of reusing its pooled ones!");
+ }
+ }
+}
+#endif
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs.meta
new file mode 100644
index 0000000000..99a1be8cbd
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/HybridPrefabHandlerPoolingTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2a0b8c80c8824b01af467a3c20458f65