Skip to content

refactor and updates #246

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

Merged
merged 6 commits into from
Aug 25, 2021
Merged
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
331 changes: 289 additions & 42 deletions docs/advanced-topics/object-pooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ id: object-pooling
title: Object Pooling
---

The MLAPI provides built-in support for Object Pooling, which allows you to override the default MLAPI 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 frequently used objects, such as bullets, and can be used to increase the application's overall performance.
The MLAPI provides built-in support for Object Pooling, which allows you to override the default MLAPI 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 frequently used objects, such as projectiles, and is a way to increase the application's overall performance by decreasing the amount of objects being created over time.

See [Introduction to Object Pooling](https://learn.unity.com/tutorial/introduction-to-object-pooling) to learn more about the importance of pooling objects.

Expand All @@ -15,92 +13,341 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc
```csharp
public interface INetworkPrefabInstanceHandler
{
NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation);
void HandleNetworkPrefabDestroy(NetworkObject networkObject);
NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation);
void Destroy(NetworkObject networkObject);
}
```
MLAPI will use the `HandleNetworkPrefabSpawn` and `HandleNetworkPrefabDestroy` methods in place of default spawn handlers for the `NetworkObject` used during the registration process. In the following implementation example, the `m_OriginalPrefab` property is the prefab we will replace with the `m_TargetPrefabToSpawn`. As such, we register the `CustomPrefabHandlerExample` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_OriginalPrefab`'s `NetworkObject` with a reference to the current instance of `CustomPrefabHandlerExample`.
MLAPI 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 will not have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the `INetworkPrefabInstanceHandler` implementation is registered with `NetworkPrefabHanlder` (`NetworkManager.PrefabHandler`) and a Host or Server spawns the registered/associated `NetworkObject`.

In the following basic pooling example, the `m_ObjectToPool` property is the prefab we want to pool. We register the `NetworkPrefabHandlerObjectPool` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToPool`'s `GameObject` with a reference to the current instance of `NetworkPrefabHandlerObjectPool`. We also take into account any `NetworkManager` defined `NetworkPreab` overrides by calling `NetworkManager.GetNetworkPrefabOverride` while both assigning and passing in our `m_ObjectToPool`.
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MLAPI;
using MLAPI.Spawning;
using Unity.Netcode;

public class CustomPrefabHandlerExample : NetworkBehaviour, INetworkPrefabInstanceHandler
public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabInstanceHandler
{
[SerializeField]
private GameObject m_OriginalPrefab;
private GameObject m_ObjectToPool;

[SerializeField]
private GameObject m_TargetPrefabToSpawn;
private int m_ObjectPoolSize = 15;

[SerializeField]
private int m_ObjectPoolSize = 15;
[Range(1, 5)]
private int m_SpawnsPerSecond = 2;

private List<GameObject> m_ObjectsPool;

private List<NetworkObject> m_NetworkObjectsPool;
private bool m_IsSpawningObjects;

private void Start()
public override void OnNetworkSpawn()
{
if (NetworkManager && NetworkManager.PrefabHandler != null)
{
NetworkManager.PrefabHandler.AddHandler(m_OriginalPrefab.GetComponent<NetworkObject>(), this);
NetworkManager.PrefabHandler.AddHandler(m_ObjectToPool, this);
}
}

public override void NetworkStart()
{
if (m_OriginalPrefab != null && m_TargetPrefabToSpawn != null)
// This assures we have the right prefab
if (IsClient)
{
m_NetworkObjectsPool = new List<NetworkObject>();
m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool);
}

if (m_ObjectToPool != null)
{
m_ObjectsPool = new List<GameObject>();
for (int i = 0; i < m_ObjectPoolSize; i++)
{
InstantiateNewNetworkObject();
InstantiatePoolObject().SetActive(false);
}
}

// Host and Server spawn the objects
if (IsServer)
{
StartCoroutine(SpawnObjects());
}
}

private NetworkObject InstantiateNewNetworkObject()
private GameObject InstantiatePoolObject()
{
var gameObject = Instantiate(m_TargetPrefabToSpawn);
var networkObject = gameObject.GetComponent<NetworkObject>();
gameObject.SetActive(false);
m_NetworkObjectsPool.Add(networkObject);
return networkObject;
m_ObjectsPool.Add(Instantiate(m_ObjectToPool));
return m_ObjectsPool[m_ObjectsPool.Count - 1];
}

private NetworkObject GetNextSpawnObject()
private GameObject GetNextSpawnObject()
{
foreach (var networkObject in m_NetworkObjectsPool)
foreach (var gameObject in m_ObjectsPool)
{
if (!networkObject.IsSpawned)
if (!gameObject.activeInHierarchy)
{
return networkObject;
return gameObject;
}
}
//We are out of objects, expand our pool by 1 more NetworkObject
return InstantiateNewNetworkObject();
return InstantiatePoolObject();
}

public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
{
var gameObject = GetNextSpawnObject();
gameObject.SetActive(true);
gameObject.transform.position = position;
gameObject.transform.rotation = rotation;
return gameObject.GetComponent<NetworkObject>();
}

private void OnDisable()
{
if (NetworkManager && NetworkManager.PrefabHandler != null)
{
NetworkManager.PrefabHandler.RemoveHandler(m_ObjectToPool);
}
}

public void Destroy(NetworkObject networkObject)
{
if (m_ObjectsPool.Contains(networkObject.gameObject))
{
networkObject.gameObject.SetActive(false);
}
}

private IEnumerator SpawnObjects()
{
//Exit if we are a client or we happen to not have a NetworkManager
if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer))
{
yield return null;
}

m_IsSpawningObjects = true;

var entitySpawnUpdateRate = 1.0f;
while (m_IsSpawningObjects)
{
entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond;

GameObject go = GetNextSpawnObject();
if (go != null)
{
go.SetActive(true);
go.transform.position = transform.position;

float ang = Random.Range(0.0f, 2 * Mathf.PI);
go.GetComponent<GenericPooledObjectBehaviour>().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4);

var no = go.GetComponent<NetworkObject>();
if (!no.IsSpawned)
{
no.Spawn(true);
}
}
yield return new WaitForSeconds(entitySpawnUpdateRate);
}
}
}

```

In the next more advanced example, the `m_ObjectToOverride` property is the prefab we will replace with one of the `m_ObjectOverrides` prefabs. As such, we register the `CustomPrefabHandlerObjectPoolOverride` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToOverride` with a reference to the current instance of `CustomPrefabHandlerObjectPoolOverride`. We then have to handle a special case scenario. Since a Host is actually both a client and a server, we need to pre-register the link (association) between the `m_ObjectToOverride` prefab and the `m_ObjectOverrides` prefabs. We do this by calling `NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues` and passing in the `m_ObjectToOverride` and the `m_ObjectOverrides` list. For both the Client and Host, we will create a pool for each prefab type in the m_ObjectOverrides list. If we are just a server (i.e. not a Host), then we only need to create a large pool containing only one prefab type: `m_ObjectToOverride`. We included this example in order to show that the common link between all instances is the `m_ObjectToOverride`'s GlobalObjectIdHash value. The `m_ObjectToOverride`'s GlobalObjectIdHash value is always used to signal the creation or destruction for all messages pertaining this prefab handler override.

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

/// <summary>
/// This is an example of using more than one Network Prefab override when using a custom handler
/// USAGE NOTE: When using more than one network prefab, it is important to understand that each
/// client determines what prefab they will be using and will not be synchronized across other clients.
/// This feature is primarily to be used for things like platform specific Network Prefabs where
/// things like collision models or graphics related assets might need to vary between platforms.
/// The usage of different visual assets used is strictly for example purposes only.
/// </summary>
public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler
{
private GameObject m_ObjectToPool;

[SerializeField]
private GameObject m_ObjectToOverride;

[SerializeField]
private List<GameObject> m_ObjectOverrides;

[SerializeField]
private int m_ObjectPoolSize = 15;

[SerializeField]
[Range(1, 5)]
private int m_SpawnsPerSecond = 2;

private Dictionary<int, List<GameObject>> m_ObjectsPool;
private List<string> m_NameValidation;

private bool m_IsSpawningObjects;

public override void OnNetworkSpawn()
{
// Register your object to be overridden (m_ObjectToOverride) with this INetworkPrefabInstanceHandler implementation
if (NetworkManager && NetworkManager.PrefabHandler != null)
{
NetworkManager.PrefabHandler.AddHandler(m_ObjectToOverride, this);
}

// Start with the base object to be overridden (i.e. Server mode will always use this)
m_ObjectToPool = m_ObjectToOverride;

// Host and Client need to do an extra step
if (IsClient)
{
// Makes sure we have the right prefab to create a pool for (i.e. Clients and Hosts)
m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool);

// Host Only:
// Since the host will be spawning overrides, we need to manually create the link between the
// m_ObjectToOverride and the objects that could override it (i.e. m_ObjectOverrides)
if (IsHost)
{
// While this seems redundant, we could theoretically have several objects that we could potentially be spawning
NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues(m_ObjectToOverride, m_ObjectOverrides);
}
}

m_ObjectsPool = new Dictionary<int, List<GameObject>>();
m_NameValidation = new List<string>();
for (int x = 0; x < m_ObjectOverrides.Count; x++)
{
// If we are a server, then we just create a big pool of the same base override object
// otherwise for Host and Client we use the list of object overrides
var objectIndex = (IsServer && !IsHost) ? 0 : x;
var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[objectIndex];

if (!m_ObjectsPool.ContainsKey(objectIndex))
{
m_ObjectsPool.Add(objectIndex, new List<GameObject>());
}

for (int y = 0; y < m_ObjectPoolSize; y++)
{
var newObject = Instantiate(objectToPool);

// One way to verify this object exists
// You could also make this a dictionary that linked to the actual GameObject instance
newObject.name += m_ObjectsPool[objectIndex].Count.ToString();
m_NameValidation.Add(newObject.name);

// Make sure we start this object as inactive
newObject.SetActive(false);
m_ObjectsPool[objectIndex].Add(newObject);
}
}

// Host and Server spawn the objects
if (IsServer)
{
StartCoroutine(SpawnObjects());
}
}

public NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation)
private GameObject GetNextSpawnObject(int synchronizedIndex = -1)
{
var networkObject = GetNextSpawnObject();
networkObject.gameObject.SetActive(true);
networkObject.transform.position = position;
networkObject.transform.rotation = rotation;
return networkObject;
// If we are just a server use index 0, otherwise we are a host or client so get a random override object to spawn
var indexType = IsServer && !IsHost ? 0 : Random.Range(0, m_ObjectOverrides.Count - 1);

if (m_ObjectsPool.ContainsKey(indexType))
{
foreach (var gameObject in m_ObjectsPool[indexType])
{
if (!gameObject.activeInHierarchy)
{
return gameObject;
}
}
// We are out of objects, get the type of object we need to instantiate and add to the pool
var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[indexType];

// Expand our pool by 1 more NetworkObject
var newObject = Instantiate(objectToPool);
var genericObjectPooledBehaviour = NetworkObject.GetComponent<GenericPooledObjectBehaviour>();
genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex = (IsServer && !IsHost) ? Random.Range(0, m_ObjectOverrides.Count - 1) : indexType;
m_ObjectsPool[indexType].Add(newObject);
return newObject;
}
// If requesting a bad index return null
return null;
}

public void HandleNetworkPrefabDestroy(NetworkObject networkObject)
public void OnSynchronizeWrite(NetworkWriter networkWriter, NetworkObject networkObject)
{
if (m_NetworkObjectsPool.Contains(networkObject))
var genericObjectPooledBehaviour = NetworkObject.GetComponent<GenericPooledObjectBehaviour>();
networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex);
}

public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
{
var gameObject = GetNextSpawnObject();
gameObject.SetActive(true);
gameObject.transform.position = position;
gameObject.transform.rotation = rotation;
return gameObject.GetComponent<NetworkObject>();
}

public void Destroy(NetworkObject networkObject)
{
if (m_NameValidation.Contains(networkObject.gameObject.name))
{
networkObject.gameObject.SetActive(false);
}
}

/// <summary>
/// Spawns the objects.
/// </summary>
/// <returns>IEnumerator</returns>
private IEnumerator SpawnObjects()
{
//Exit if we are a client or we happen to not have a NetworkManager
if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer))
{
yield return null;
}

m_IsSpawningObjects = true;

var entitySpawnUpdateRate = 1.0f;

while (m_IsSpawningObjects)
{
entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond;

GameObject go = GetNextSpawnObject();
if (go != null)
{
go.SetActive(true);
go.transform.position = transform.position;

float ang = Random.Range(0.0f, 2 * Mathf.PI);
go.GetComponent<GenericPooledObjectBehaviour>().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4);

var no = go.GetComponent<NetworkObject>();
if (!no.IsSpawned)
{
no.Spawn(true);
}
}
yield return new WaitForSeconds(entitySpawnUpdateRate);
}
}
}
```

Registering your own spawn handlers allows you to pool all networked objects on clients as they are destroyed and spawned on your clients.
Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them!

When using more than one network prefab, it is important to understand that each client determines what prefab they will be using and will not be synchronized across other clients. This feature is primarily to be used for things like platform specific Network Prefabs where things like collision models or graphics related assets might need to vary between platforms.

You can find full working versions of the above two examples in the [testproject/Assets/Samples/PrefabPool](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/master/testproject/Assets/Samples/PrefabPool) repository directory.

To pool objects on the server side, do not use `Destroy`. Use `NetworkObject.Despawn` first, then manually pool the object.