Skip to content

Commit d3c8303

Browse files
refactor and updates (#246)
* refactor and updates This updates the object pooling example to reflect the changes from PR-977: Unity-Technologies/com.unity.netcode.gameobjects#977 * additional notes This adds additional information about using more than one prefab with an override. This also adds a link to the to-be-existing folder in the master branch. * refactor Removed some code from advanced example that was not being used. * refactor Updating to most recent changes from PR-955 (yet to be merged) * refactor making changes to reflect the final version that was merged into the develop branch. Co-authored-by: Briancoughlin <76997526+Briancoughlin@users.noreply.github.com>
1 parent da04454 commit d3c8303

File tree

1 file changed

+289
-42
lines changed

1 file changed

+289
-42
lines changed

docs/advanced-topics/object-pooling.md

Lines changed: 289 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ id: object-pooling
33
title: Object Pooling
44
---
55

6-
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.
7-
8-
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.
6+
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.
97

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

@@ -15,92 +13,341 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc
1513
```csharp
1614
public interface INetworkPrefabInstanceHandler
1715
{
18-
NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation);
19-
void HandleNetworkPrefabDestroy(NetworkObject networkObject);
16+
NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation);
17+
void Destroy(NetworkObject networkObject);
2018
}
2119
```
22-
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`.
20+
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`.
2321

22+
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`.
2423
```csharp
24+
using System.Collections;
2525
using System.Collections.Generic;
2626
using UnityEngine;
27-
using MLAPI;
28-
using MLAPI.Spawning;
27+
using Unity.Netcode;
2928

30-
public class CustomPrefabHandlerExample : NetworkBehaviour, INetworkPrefabInstanceHandler
29+
public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabInstanceHandler
3130
{
3231
[SerializeField]
33-
private GameObject m_OriginalPrefab;
32+
private GameObject m_ObjectToPool;
3433

3534
[SerializeField]
36-
private GameObject m_TargetPrefabToSpawn;
35+
private int m_ObjectPoolSize = 15;
3736

3837
[SerializeField]
39-
private int m_ObjectPoolSize = 15;
38+
[Range(1, 5)]
39+
private int m_SpawnsPerSecond = 2;
40+
41+
private List<GameObject> m_ObjectsPool;
4042

41-
private List<NetworkObject> m_NetworkObjectsPool;
43+
private bool m_IsSpawningObjects;
4244

43-
private void Start()
45+
public override void OnNetworkSpawn()
4446
{
4547
if (NetworkManager && NetworkManager.PrefabHandler != null)
4648
{
47-
NetworkManager.PrefabHandler.AddHandler(m_OriginalPrefab.GetComponent<NetworkObject>(), this);
49+
NetworkManager.PrefabHandler.AddHandler(m_ObjectToPool, this);
4850
}
49-
}
5051

51-
public override void NetworkStart()
52-
{
53-
if (m_OriginalPrefab != null && m_TargetPrefabToSpawn != null)
52+
// This assures we have the right prefab
53+
if (IsClient)
5454
{
55-
m_NetworkObjectsPool = new List<NetworkObject>();
55+
m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool);
56+
}
57+
58+
if (m_ObjectToPool != null)
59+
{
60+
m_ObjectsPool = new List<GameObject>();
5661
for (int i = 0; i < m_ObjectPoolSize; i++)
5762
{
58-
InstantiateNewNetworkObject();
63+
InstantiatePoolObject().SetActive(false);
5964
}
6065
}
66+
67+
// Host and Server spawn the objects
68+
if (IsServer)
69+
{
70+
StartCoroutine(SpawnObjects());
71+
}
6172
}
6273

63-
private NetworkObject InstantiateNewNetworkObject()
74+
private GameObject InstantiatePoolObject()
6475
{
65-
var gameObject = Instantiate(m_TargetPrefabToSpawn);
66-
var networkObject = gameObject.GetComponent<NetworkObject>();
67-
gameObject.SetActive(false);
68-
m_NetworkObjectsPool.Add(networkObject);
69-
return networkObject;
76+
m_ObjectsPool.Add(Instantiate(m_ObjectToPool));
77+
return m_ObjectsPool[m_ObjectsPool.Count - 1];
7078
}
7179

72-
private NetworkObject GetNextSpawnObject()
80+
private GameObject GetNextSpawnObject()
7381
{
74-
foreach (var networkObject in m_NetworkObjectsPool)
82+
foreach (var gameObject in m_ObjectsPool)
7583
{
76-
if (!networkObject.IsSpawned)
84+
if (!gameObject.activeInHierarchy)
7785
{
78-
return networkObject;
86+
return gameObject;
7987
}
8088
}
8189
//We are out of objects, expand our pool by 1 more NetworkObject
82-
return InstantiateNewNetworkObject();
90+
return InstantiatePoolObject();
91+
}
92+
93+
public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
94+
{
95+
var gameObject = GetNextSpawnObject();
96+
gameObject.SetActive(true);
97+
gameObject.transform.position = position;
98+
gameObject.transform.rotation = rotation;
99+
return gameObject.GetComponent<NetworkObject>();
100+
}
101+
102+
private void OnDisable()
103+
{
104+
if (NetworkManager && NetworkManager.PrefabHandler != null)
105+
{
106+
NetworkManager.PrefabHandler.RemoveHandler(m_ObjectToPool);
107+
}
108+
}
109+
110+
public void Destroy(NetworkObject networkObject)
111+
{
112+
if (m_ObjectsPool.Contains(networkObject.gameObject))
113+
{
114+
networkObject.gameObject.SetActive(false);
115+
}
116+
}
117+
118+
private IEnumerator SpawnObjects()
119+
{
120+
//Exit if we are a client or we happen to not have a NetworkManager
121+
if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer))
122+
{
123+
yield return null;
124+
}
125+
126+
m_IsSpawningObjects = true;
127+
128+
var entitySpawnUpdateRate = 1.0f;
129+
while (m_IsSpawningObjects)
130+
{
131+
entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond;
132+
133+
GameObject go = GetNextSpawnObject();
134+
if (go != null)
135+
{
136+
go.SetActive(true);
137+
go.transform.position = transform.position;
138+
139+
float ang = Random.Range(0.0f, 2 * Mathf.PI);
140+
go.GetComponent<GenericPooledObjectBehaviour>().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4);
141+
142+
var no = go.GetComponent<NetworkObject>();
143+
if (!no.IsSpawned)
144+
{
145+
no.Spawn(true);
146+
}
147+
}
148+
yield return new WaitForSeconds(entitySpawnUpdateRate);
149+
}
150+
}
151+
}
152+
153+
```
154+
155+
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.
156+
157+
```csharp
158+
using System.Collections;
159+
using System.Collections.Generic;
160+
using UnityEngine;
161+
using Unity.Netcode;
162+
163+
/// <summary>
164+
/// This is an example of using more than one Network Prefab override when using a custom handler
165+
/// USAGE NOTE: When using more than one network prefab, it is important to understand that each
166+
/// client determines what prefab they will be using and will not be synchronized across other clients.
167+
/// This feature is primarily to be used for things like platform specific Network Prefabs where
168+
/// things like collision models or graphics related assets might need to vary between platforms.
169+
/// The usage of different visual assets used is strictly for example purposes only.
170+
/// </summary>
171+
public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler
172+
{
173+
private GameObject m_ObjectToPool;
174+
175+
[SerializeField]
176+
private GameObject m_ObjectToOverride;
177+
178+
[SerializeField]
179+
private List<GameObject> m_ObjectOverrides;
180+
181+
[SerializeField]
182+
private int m_ObjectPoolSize = 15;
183+
184+
[SerializeField]
185+
[Range(1, 5)]
186+
private int m_SpawnsPerSecond = 2;
187+
188+
private Dictionary<int, List<GameObject>> m_ObjectsPool;
189+
private List<string> m_NameValidation;
190+
191+
private bool m_IsSpawningObjects;
192+
193+
public override void OnNetworkSpawn()
194+
{
195+
// Register your object to be overridden (m_ObjectToOverride) with this INetworkPrefabInstanceHandler implementation
196+
if (NetworkManager && NetworkManager.PrefabHandler != null)
197+
{
198+
NetworkManager.PrefabHandler.AddHandler(m_ObjectToOverride, this);
199+
}
200+
201+
// Start with the base object to be overridden (i.e. Server mode will always use this)
202+
m_ObjectToPool = m_ObjectToOverride;
203+
204+
// Host and Client need to do an extra step
205+
if (IsClient)
206+
{
207+
// Makes sure we have the right prefab to create a pool for (i.e. Clients and Hosts)
208+
m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool);
209+
210+
// Host Only:
211+
// Since the host will be spawning overrides, we need to manually create the link between the
212+
// m_ObjectToOverride and the objects that could override it (i.e. m_ObjectOverrides)
213+
if (IsHost)
214+
{
215+
// While this seems redundant, we could theoretically have several objects that we could potentially be spawning
216+
NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues(m_ObjectToOverride, m_ObjectOverrides);
217+
}
218+
}
219+
220+
m_ObjectsPool = new Dictionary<int, List<GameObject>>();
221+
m_NameValidation = new List<string>();
222+
for (int x = 0; x < m_ObjectOverrides.Count; x++)
223+
{
224+
// If we are a server, then we just create a big pool of the same base override object
225+
// otherwise for Host and Client we use the list of object overrides
226+
var objectIndex = (IsServer && !IsHost) ? 0 : x;
227+
var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[objectIndex];
228+
229+
if (!m_ObjectsPool.ContainsKey(objectIndex))
230+
{
231+
m_ObjectsPool.Add(objectIndex, new List<GameObject>());
232+
}
233+
234+
for (int y = 0; y < m_ObjectPoolSize; y++)
235+
{
236+
var newObject = Instantiate(objectToPool);
237+
238+
// One way to verify this object exists
239+
// You could also make this a dictionary that linked to the actual GameObject instance
240+
newObject.name += m_ObjectsPool[objectIndex].Count.ToString();
241+
m_NameValidation.Add(newObject.name);
242+
243+
// Make sure we start this object as inactive
244+
newObject.SetActive(false);
245+
m_ObjectsPool[objectIndex].Add(newObject);
246+
}
247+
}
248+
249+
// Host and Server spawn the objects
250+
if (IsServer)
251+
{
252+
StartCoroutine(SpawnObjects());
253+
}
83254
}
84255

85-
public NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation)
256+
private GameObject GetNextSpawnObject(int synchronizedIndex = -1)
86257
{
87-
var networkObject = GetNextSpawnObject();
88-
networkObject.gameObject.SetActive(true);
89-
networkObject.transform.position = position;
90-
networkObject.transform.rotation = rotation;
91-
return networkObject;
258+
// If we are just a server use index 0, otherwise we are a host or client so get a random override object to spawn
259+
var indexType = IsServer && !IsHost ? 0 : Random.Range(0, m_ObjectOverrides.Count - 1);
260+
261+
if (m_ObjectsPool.ContainsKey(indexType))
262+
{
263+
foreach (var gameObject in m_ObjectsPool[indexType])
264+
{
265+
if (!gameObject.activeInHierarchy)
266+
{
267+
return gameObject;
268+
}
269+
}
270+
// We are out of objects, get the type of object we need to instantiate and add to the pool
271+
var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[indexType];
272+
273+
// Expand our pool by 1 more NetworkObject
274+
var newObject = Instantiate(objectToPool);
275+
var genericObjectPooledBehaviour = NetworkObject.GetComponent<GenericPooledObjectBehaviour>();
276+
genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex = (IsServer && !IsHost) ? Random.Range(0, m_ObjectOverrides.Count - 1) : indexType;
277+
m_ObjectsPool[indexType].Add(newObject);
278+
return newObject;
279+
}
280+
// If requesting a bad index return null
281+
return null;
92282
}
93283

94-
public void HandleNetworkPrefabDestroy(NetworkObject networkObject)
284+
public void OnSynchronizeWrite(NetworkWriter networkWriter, NetworkObject networkObject)
95285
{
96-
if (m_NetworkObjectsPool.Contains(networkObject))
286+
var genericObjectPooledBehaviour = NetworkObject.GetComponent<GenericPooledObjectBehaviour>();
287+
networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex);
288+
}
289+
290+
public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
291+
{
292+
var gameObject = GetNextSpawnObject();
293+
gameObject.SetActive(true);
294+
gameObject.transform.position = position;
295+
gameObject.transform.rotation = rotation;
296+
return gameObject.GetComponent<NetworkObject>();
297+
}
298+
299+
public void Destroy(NetworkObject networkObject)
300+
{
301+
if (m_NameValidation.Contains(networkObject.gameObject.name))
97302
{
98303
networkObject.gameObject.SetActive(false);
99304
}
100305
}
306+
307+
/// <summary>
308+
/// Spawns the objects.
309+
/// </summary>
310+
/// <returns>IEnumerator</returns>
311+
private IEnumerator SpawnObjects()
312+
{
313+
//Exit if we are a client or we happen to not have a NetworkManager
314+
if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer))
315+
{
316+
yield return null;
317+
}
318+
319+
m_IsSpawningObjects = true;
320+
321+
var entitySpawnUpdateRate = 1.0f;
322+
323+
while (m_IsSpawningObjects)
324+
{
325+
entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond;
326+
327+
GameObject go = GetNextSpawnObject();
328+
if (go != null)
329+
{
330+
go.SetActive(true);
331+
go.transform.position = transform.position;
332+
333+
float ang = Random.Range(0.0f, 2 * Mathf.PI);
334+
go.GetComponent<GenericPooledObjectBehaviour>().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4);
335+
336+
var no = go.GetComponent<NetworkObject>();
337+
if (!no.IsSpawned)
338+
{
339+
no.Spawn(true);
340+
}
341+
}
342+
yield return new WaitForSeconds(entitySpawnUpdateRate);
343+
}
344+
}
101345
}
102346
```
103347

104-
Registering your own spawn handlers allows you to pool all networked objects on clients as they are destroyed and spawned on your clients.
348+
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!
349+
350+
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.
351+
352+
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.
105353

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

0 commit comments

Comments
 (0)