Skip to content

Commit 512bb55

Browse files
refactor: replacing custom pool implementation with ObjectPool [MTT-6263] (#824)
* replacing custom pool implementation with native one * removing unnecessary external public apis
1 parent 0281f5e commit 512bb55

File tree

2 files changed

+70
-101
lines changed

2 files changed

+70
-101
lines changed

Assets/Scripts/Infrastructure/NetworkObjectPool.cs

Lines changed: 69 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,62 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Runtime.CompilerServices;
43
using Unity.Netcode;
54
using UnityEngine;
65
using UnityEngine.Assertions;
6+
using UnityEngine.Pool;
77

88
namespace Unity.BossRoom.Infrastructure
99
{
1010
/// <summary>
11-
/// Object Pool for networked objects, used for controlling how objects are spawned by Netcode. Netcode by default will allocate new memory when spawning new
12-
/// objects. With this Networked Pool, we're using custom spawning to reuse objects.
11+
/// Object Pool for networked objects, used for controlling how objects are spawned by Netcode. Netcode by default
12+
/// will allocate new memory when spawning new objects. With this Networked Pool, we're using the ObjectPool to
13+
/// reuse objects.
1314
/// 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.
14-
/// Hooks to NetworkManager's prefab handler to intercept object spawning and do custom actions
15+
/// Hooks to NetworkManager's prefab handler to intercept object spawning and do custom actions.
1516
/// </summary>
1617
public class NetworkObjectPool : NetworkBehaviour
1718
{
18-
private static NetworkObjectPool _instance;
19-
20-
public static NetworkObjectPool Singleton { get { return _instance; } }
19+
public static NetworkObjectPool Singleton { get; private set; }
2120

2221
[SerializeField]
2322
List<PoolConfigObject> PooledPrefabsList;
2423

25-
HashSet<GameObject> prefabs = new HashSet<GameObject>();
26-
27-
Dictionary<GameObject, Queue<NetworkObject>> pooledObjects = new Dictionary<GameObject, Queue<NetworkObject>>();
24+
HashSet<GameObject> m_Prefabs = new HashSet<GameObject>();
2825

29-
private bool m_HasInitialized = false;
26+
Dictionary<GameObject, ObjectPool<NetworkObject>> m_PooledObjects = new Dictionary<GameObject, ObjectPool<NetworkObject>>();
3027

3128
public void Awake()
3229
{
33-
if (_instance != null && _instance != this)
30+
if (Singleton != null && Singleton != this)
3431
{
35-
Destroy(this.gameObject);
32+
Destroy(gameObject);
3633
}
3734
else
3835
{
39-
_instance = this;
36+
Singleton = this;
4037
}
4138
}
4239

4340
public override void OnNetworkSpawn()
4441
{
45-
InitializePool();
42+
// Registers all objects in PooledPrefabsList to the cache.
43+
foreach (var configObject in PooledPrefabsList)
44+
{
45+
RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount);
46+
}
4647
}
4748

4849
public override void OnNetworkDespawn()
4950
{
50-
ClearPool();
51+
// Unregisters all objects in PooledPrefabsList from the cache.
52+
foreach (var prefab in m_Prefabs)
53+
{
54+
// Unregister Netcode Spawn handlers
55+
NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab);
56+
m_PooledObjects[prefab].Clear();
57+
}
58+
m_PooledObjects.Clear();
59+
m_Prefabs.Clear();
5160
}
5261

5362
public void OnValidate()
@@ -65,16 +74,12 @@ public void OnValidate()
6574
/// <summary>
6675
/// Gets an instance of the given prefab from the pool. The prefab must be registered to the pool.
6776
/// </summary>
68-
/// <param name="prefab"></param>
69-
/// <returns></returns>
70-
public NetworkObject GetNetworkObject(GameObject prefab)
71-
{
72-
return GetNetworkObjectInternal(prefab, Vector3.zero, Quaternion.identity);
73-
}
74-
75-
/// <summary>
76-
/// Gets an instance of the given prefab from the pool. The prefab must be registered to the pool.
77-
/// </summary>
77+
/// <remarks>
78+
/// To spawn a NetworkObject from one of the pools, this must be called on the server, then the instance
79+
/// returned from it must be spawned on the server. This method will then also be called on the client by the
80+
/// PooledPrefabInstanceHandler when the client receives a spawn message for a prefab that has been registered
81+
/// here.
82+
/// </remarks>
7883
/// <param name="prefab"></param>
7984
/// <param name="position">The position to spawn the object at.</param>
8085
/// <param name="rotation">The rotation to spawn the object with.</param>
@@ -89,107 +94,71 @@ public NetworkObject GetNetworkObject(GameObject prefab, Vector3 position, Quate
8994
/// </summary>
9095
public void ReturnNetworkObject(NetworkObject networkObject, GameObject prefab)
9196
{
92-
var go = networkObject.gameObject;
93-
go.SetActive(false);
94-
pooledObjects[prefab].Enqueue(networkObject);
97+
m_PooledObjects[prefab].Release(networkObject);
9598
}
9699

97100
/// <summary>
98-
/// Adds a prefab to the list of spawnable prefabs.
101+
/// Builds up the cache for a prefab.
99102
/// </summary>
100-
/// <param name="prefab">The prefab to add.</param>
101-
/// <param name="prewarmCount"></param>
102-
public void AddPrefab(GameObject prefab, int prewarmCount = 0)
103+
void RegisterPrefabInternal(GameObject prefab, int prewarmCount)
103104
{
104-
var networkObject = prefab.GetComponent<NetworkObject>();
105+
NetworkObject CreateFunc()
106+
{
107+
return Instantiate(prefab).GetComponent<NetworkObject>();
108+
}
105109

106-
Assert.IsNotNull(networkObject, $"{nameof(prefab)} must have {nameof(networkObject)} component.");
107-
Assert.IsFalse(prefabs.Contains(prefab), $"Prefab {prefab.name} is already registered in the pool.");
110+
void ActionOnGet(NetworkObject networkObject)
111+
{
112+
networkObject.gameObject.SetActive(true);
113+
}
108114

109-
RegisterPrefabInternal(prefab, prewarmCount);
110-
}
115+
void ActionOnRelease(NetworkObject networkObject)
116+
{
117+
networkObject.gameObject.SetActive(false);
118+
}
111119

112-
/// <summary>
113-
/// Builds up the cache for a prefab.
114-
/// </summary>
115-
private void RegisterPrefabInternal(GameObject prefab, int prewarmCount)
116-
{
117-
prefabs.Add(prefab);
120+
void ActionOnDestroy(NetworkObject networkObject)
121+
{
122+
Destroy(networkObject.gameObject);
123+
}
118124

119-
var prefabQueue = new Queue<NetworkObject>();
120-
pooledObjects[prefab] = prefabQueue;
121-
for (int i = 0; i < prewarmCount; i++)
125+
m_Prefabs.Add(prefab);
126+
127+
// Create the pool
128+
m_PooledObjects[prefab] = new ObjectPool<NetworkObject>(CreateFunc, ActionOnGet, ActionOnRelease, ActionOnDestroy, defaultCapacity: prewarmCount);
129+
130+
// Populate the pool
131+
var prewarmNetworkObjects = new List<NetworkObject>();
132+
for (var i = 0; i < prewarmCount; i++)
133+
{
134+
prewarmNetworkObjects.Add(m_PooledObjects[prefab].Get());
135+
}
136+
foreach (var networkObject in prewarmNetworkObjects)
122137
{
123-
var go = CreateInstance(prefab);
124-
ReturnNetworkObject(go.GetComponent<NetworkObject>(), prefab);
138+
m_PooledObjects[prefab].Release(networkObject);
125139
}
126140

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

131-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
132-
private GameObject CreateInstance(GameObject prefab)
133-
{
134-
return Instantiate(prefab);
135-
}
136-
137145
/// <summary>
138146
/// This matches the signature of <see cref="NetworkSpawnManager.SpawnHandlerDelegate"/>
139147
/// </summary>
140148
/// <param name="prefab"></param>
141149
/// <param name="position"></param>
142150
/// <param name="rotation"></param>
143151
/// <returns></returns>
144-
private NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation)
152+
NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation)
145153
{
146-
var queue = pooledObjects[prefab];
147-
148-
NetworkObject networkObject;
149-
if (queue.Count > 0)
150-
{
151-
networkObject = queue.Dequeue();
152-
}
153-
else
154-
{
155-
networkObject = CreateInstance(prefab).GetComponent<NetworkObject>();
156-
}
157-
158-
// Here we must reverse the logic in ReturnNetworkObject.
159-
var go = networkObject.gameObject;
160-
go.SetActive(true);
154+
var networkObject = m_PooledObjects[prefab].Get();
161155

162-
go.transform.position = position;
163-
go.transform.rotation = rotation;
156+
var noTransform = networkObject.transform;
157+
noTransform.position = position;
158+
noTransform.rotation = rotation;
164159

165160
return networkObject;
166161
}
167-
168-
/// <summary>
169-
/// Registers all objects in <see cref="PooledPrefabsList"/> to the cache.
170-
/// </summary>
171-
public void InitializePool()
172-
{
173-
if (m_HasInitialized) return;
174-
foreach (var configObject in PooledPrefabsList)
175-
{
176-
RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount);
177-
}
178-
m_HasInitialized = true;
179-
}
180-
181-
/// <summary>
182-
/// Unregisters all objects in <see cref="PooledPrefabsList"/> from the cache.
183-
/// </summary>
184-
public void ClearPool()
185-
{
186-
foreach (var prefab in prefabs)
187-
{
188-
// Unregister Netcode Spawn handlers
189-
NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab);
190-
}
191-
pooledObjects.Clear();
192-
}
193162
}
194163

195164
[Serializable]
@@ -212,8 +181,7 @@ public PooledPrefabInstanceHandler(GameObject prefab, NetworkObjectPool pool)
212181

213182
NetworkObject INetworkPrefabInstanceHandler.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
214183
{
215-
var netObject = m_Pool.GetNetworkObject(m_Prefab, position, rotation);
216-
return netObject;
184+
return m_Pool.GetNetworkObject(m_Prefab, position, rotation);
217185
}
218186

219187
void INetworkPrefabInstanceHandler.Destroy(NetworkObject networkObject)

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1111
### Changed
1212
* Replaced our polling for lobby updates with a subscription to the new Websocket based LobbyEvents (#805). This saves up a significant amount of bandwidth usage to and from the service, since updates are infrequent in this game. Now clients and hosts only use up bandwidth on the Lobby service when it is needed. With polling, we used to send a GET request per client once every 2s. The responses were between ~550 bytes and 900 bytes, so if we suppose an average of 725 bytes and 100 000 concurrent users (CCU), this amounted to around 725B * 30 calls per minute * 100 000 CCU = 2.175 GB per minute. Scaling this to a month would get us 93.96 TB per month. In our case, since the only changes to the lobbies happen when a user connects or disconnects, most of that data was not necessary and can be saved to reduce bandwidth usage. Since the cost of using the Lobby service depends on bandwidth usage, this would also save money on an actual game.
1313
* Simplified reconnection flow by offloading responsibility to ConnectionMethod (#804). Now the ClientReconnectingState uses the ConnectionMethod it is configured with to handle setting up reconnection (i.e. reconnecting to the Lobby before trying to reconnect to the Relay server if it is using Relay and Lobby). It can now also fail early and stop retrying if the lobby doesn't exist anymore.
14+
* Replaced our custom pool implementation using queues with ObjectPool (#824)
1415

1516
### Cleanup
1617
* Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786)

0 commit comments

Comments
 (0)