Skip to main content

Object pooling

Netcode for GameObjects (Netcode) provides built-in support for Object Pooling, which allows you to override the default Netcode destroy and spawn handlers with your own logic. This allows you to store destroyed network objects in a pool to reuse later. This is useful for often used objects, such as projectiles, and is a way to increase the application's overall performance. By pre-instantiating and reusing the instances of those objects, object pooling removes the need to create or destroy objects at runtime, which can save a lot of work for the CPU. This means that instead of creating or destroying the same object over and over again, it's simply deactivated after use, then, when another object is needed, the pool recycles one of the deactivated objects and reactivates it.

See Introduction to Object Pooling to learn more about the importance of pooling objects.

NetworkPrefabInstanceHandler

You can register your own spawn handlers by including the INetworkPrefabInstanceHandler interface and registering with the NetworkPrefabHandler.

    public interface INetworkPrefabInstanceHandler
{
NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation);
void Destroy(NetworkObject networkObject);
}

Netcode will use the Instantiate and Destroy methods in place of default spawn handlers for the NetworkObject used during spawning and despawning. Because the message to instantiate a new NetworkObject originates from a Host or Server, both won't have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the INetworkPrefabInstanceHandler implementation is registered with NetworkPrefabHandler (NetworkManager.PrefabHandler) and a Host or Server spawns the registered/associated NetworkObject.

The following example is from the Boss Room Sample. It shows how object pooling is used to handle the different projectile objects. In that example, the class NetworkObjectPool is the data structure containing the pooled objects and the class PooledPrefabInstanceHandler is the handler implementing INetworkPrefabInstanceHandler.

Assets/Scripts/Infrastructure/NetworkObjectPool.cs
using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Pool;

namespace Unity.BossRoom.Infrastructure
{
/// <summary>
/// Object Pool for networked objects, used for controlling how objects are spawned by Netcode. Netcode by default
/// will allocate new memory when spawning new objects. With this Networked Pool, we're using the ObjectPool to
/// reuse objects.
/// Boss Room uses this for projectiles. In theory it should use this for imps too, but we wanted to show vanilla spawning vs pooled spawning.
/// Hooks to NetworkManager's prefab handler to intercept object spawning and do custom actions.
/// </summary>
public class NetworkObjectPool : NetworkBehaviour
{
public static NetworkObjectPool Singleton { get; private set; }

[SerializeField]
List<PoolConfigObject> PooledPrefabsList;

HashSet<GameObject> m_Prefabs = new HashSet<GameObject>();

Dictionary<GameObject, ObjectPool<NetworkObject>> m_PooledObjects = new Dictionary<GameObject, ObjectPool<NetworkObject>>();

public void Awake()
{
if (Singleton != null && Singleton != this)
{
Destroy(gameObject);
}
else
{
Singleton = this;
}
}

public override void OnNetworkSpawn()
{
// Registers all objects in PooledPrefabsList to the cache.
foreach (var configObject in PooledPrefabsList)
{
RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount);
}
}

public override void OnNetworkDespawn()
{
// Unregisters all objects in PooledPrefabsList from the cache.
foreach (var prefab in m_Prefabs)
{
// Unregister Netcode Spawn handlers
NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab);
m_PooledObjects[prefab].Clear();
}
m_PooledObjects.Clear();
m_Prefabs.Clear();
}

public void OnValidate()
{
for (var i = 0; i < PooledPrefabsList.Count; i++)
{
var prefab = PooledPrefabsList[i].Prefab;
if (prefab != null)
{
Assert.IsNotNull(prefab.GetComponent<NetworkObject>(), $"{nameof(NetworkObjectPool)}: Pooled prefab \"{prefab.name}\" at index {i.ToString()} has no {nameof(NetworkObject)} component.");
}
}
}

/// <summary>
/// Gets an instance of the given prefab from the pool. The prefab must be registered to the pool.
/// </summary>
/// <remarks>
/// To spawn a NetworkObject from one of the pools, this must be called on the server, then the instance
/// returned from it must be spawned on the server. This method will then also be called on the client by the
/// PooledPrefabInstanceHandler when the client receives a spawn message for a prefab that has been registered
/// here.
/// </remarks>
/// <param name="prefab"></param>
/// <param name="position">The position to spawn the object at.</param>
/// <param name="rotation">The rotation to spawn the object with.</param>
/// <returns></returns>
public NetworkObject GetNetworkObject(GameObject prefab, Vector3 position, Quaternion rotation)
{
var networkObject = m_PooledObjects[prefab].Get();

var noTransform = networkObject.transform;
noTransform.position = position;
noTransform.rotation = rotation;

return networkObject;
}

/// <summary>
/// Return an object to the pool (reset objects before returning).
/// </summary>
public void ReturnNetworkObject(NetworkObject networkObject, GameObject prefab)
{
m_PooledObjects[prefab].Release(networkObject);
}

/// <summary>
/// Builds up the cache for a prefab.
/// </summary>
void RegisterPrefabInternal(GameObject prefab, int prewarmCount)
{
NetworkObject CreateFunc()
{
return Instantiate(prefab).GetComponent<NetworkObject>();
}

void ActionOnGet(NetworkObject networkObject)
{
networkObject.gameObject.SetActive(true);
}

void ActionOnRelease(NetworkObject networkObject)
{
networkObject.gameObject.SetActive(false);
}

void ActionOnDestroy(NetworkObject networkObject)
{
Destroy(networkObject.gameObject);
}

m_Prefabs.Add(prefab);

// Create the pool
m_PooledObjects[prefab] = new ObjectPool<NetworkObject>(CreateFunc, ActionOnGet, ActionOnRelease, ActionOnDestroy, defaultCapacity: prewarmCount);

// Populate the pool
var prewarmNetworkObjects = new List<NetworkObject>();
for (var i = 0; i < prewarmCount; i++)
{
prewarmNetworkObjects.Add(m_PooledObjects[prefab].Get());
}
foreach (var networkObject in prewarmNetworkObjects)
{
m_PooledObjects[prefab].Release(networkObject);
}

// Register Netcode Spawn handlers
NetworkManager.Singleton.PrefabHandler.AddHandler(prefab, new PooledPrefabInstanceHandler(prefab, this));
}
}

[Serializable]
struct PoolConfigObject
{
public GameObject Prefab;
public int PrewarmCount;
}

class PooledPrefabInstanceHandler : INetworkPrefabInstanceHandler
{
GameObject m_Prefab;
NetworkObjectPool m_Pool;

public PooledPrefabInstanceHandler(GameObject prefab, NetworkObjectPool pool)
{
m_Prefab = prefab;
m_Pool = pool;
}

NetworkObject INetworkPrefabInstanceHandler.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
{
return m_Pool.GetNetworkObject(m_Prefab, position, rotation);
}

void INetworkPrefabInstanceHandler.Destroy(NetworkObject networkObject)
{
m_Pool.ReturnNetworkObject(networkObject, m_Prefab);
}
}

}

Let's have a look at NetworkObjectPool first. PooledPrefabsList has a list of prefabs to handle, with an initial number of instances to spawn for each. The RegisterPrefabInternal method, called in OnNetworkSpawn, initializes the different pools for each Prefab as ObjectPools inside the m_PooledObjects dictionary. It also instantiates the handlers for each Prefab and registers them. To use these objects, a user then needs to obtain it via the GetNetworkObject method before spawning it, then return the object to the pool after use with ReturnNetworkObject before despawning it. This only needs to be done on the server, as the PooledPrefabInstanceHandler will handle it on the client(s) when the network object's Spawn or Despawn method is called, via its Instantiate and Destroy methods. Inside those methods, the PooledPrefabInstanceHandler simply calls the pool to get the corresponding object, or to return it.