Skip to main content

GameObjectPool.ixx File

Object pool for efficient GameObject management and recycling. More...

Included Headers

#include <memory> #include <unordered_map> #include <vector> #include <cassert> #include <limits> #include <span> #include <helios.engine.ecs.types> #include <helios.engine.ecs.EntityHandle> #include <helios.engine.common.types.VersionId> #include <helios.util.Guid>

Namespaces Index

namespacehelios
namespaceengine

Main engine module aggregating core infrastructure and game systems. More...

namespaceruntime

Runtime infrastructure for game execution and lifecycle orchestration. More...

namespacepooling

GameObject pooling for efficient object recycling. More...

Classes Index

classGameObjectPool

Object pool for efficient GameObject lifecycle management. More...

Description

Object pool for efficient GameObject management and recycling.

Provides a memory-efficient pooling mechanism for frequently spawned and despawned GameObjects, such as projectiles, particles, or enemies. Pre-allocates objects at construction time and reuses them to avoid runtime allocation overhead.

File Listing

The file content with the documentation metadata removed is:

1/**
2 * @file GameObjectPool.ixx
3 * @brief Object pool for efficient GameObject management and recycling.
4 *
5 * Provides a memory-efficient pooling mechanism for frequently spawned and despawned
6 * GameObjects, such as projectiles, particles, or enemies. Pre-allocates objects at
7 * construction time and reuses them to avoid runtime allocation overhead.
8 */
9module;
10
11#include <memory>
12#include <unordered_map>
13#include <vector>
14#include <cassert>
15#include <limits>
16#include <span>
17
18
19export module helios.engine.runtime.pooling.GameObjectPool;
20
21import helios.util.Guid;
22import helios.engine.ecs.EntityHandle;
23import helios.engine.ecs.types;
24import helios.engine.common.types.VersionId;
25
27
28
29 /**
30 * @brief Object pool for efficient GameObject lifecycle management.
31 *
32 * @details GameObjectPool manages a fixed-size collection of GameObject identifiers,
33 * tracking which objects are currently active (in-use) and which are inactive (available).
34 * This pattern eliminates runtime allocation overhead for frequently spawned entities
35 * like projectiles, particles, or enemies.
36 *
37 * The pool uses O(1) operations for both acquire and release:
38 * - **acquire**: Pops from the inactive list and adds to active tracking
39 * - **release**: Swap-and-pop removal from active list, push to inactive
40 *
41 * GameObjects themselves are owned by GameWorld; this pool only tracks their EntityHandles.
42 *
43 * @todo Prevent duplicate EntityHandles from being added to the pool.
44 */
46
47
48 protected:
49
50 /**
51 * @brief Maps active GameObject EntityIds to their index in activeGameObjects_.
52 *
53 * Enables O(1) lookup for release operations.
54 */
55 std::vector<size_t> activeIndex_;
56
57 /**
58 * @brief Tracks version numbers for active EntityHandles.
59 *
60 * Used to validate that a release operation targets the correct entity version.
61 */
62 std::vector<size_t> versionIndex_;
63
64 /**
65 * @brief List of EntityHandles for currently active (in-use) GameObjects.
66 */
67 std::vector<helios::engine::ecs::EntityHandle> activeGameObjects_;
68
69 /**
70 * @brief List of EntityHandles for currently inactive (available) GameObjects.
71 */
72 std::vector<helios::engine::ecs::EntityHandle> inactiveGameObjects_;
73
74 /**
75 * @brief The maximum number of objects this pool manages.
76 */
77 size_t poolSize_ = 0;
78
79 /**
80 * @brief Minimum EntityId in the pool (used for sparse array offset).
81 */
82 helios::engine::ecs::types::EntityId minEntityId_ = std::numeric_limits<helios::engine::ecs::types::EntityId>::max();
83
84 /**
85 * @brief Maximum EntityId in the pool (used for sparse array sizing).
86 */
87 helios::engine::ecs::types::EntityId maxEntityId_ = std::numeric_limits<helios::engine::ecs::types::EntityId>::lowest();
88
89 /**
90 * @brief Offset for sparse array indexing (equals minEntityId_ after lock).
91 */
92 size_t delta_ = 0;
93
94 /**
95 * @brief Unique identifier for this pool instance.
96 */
98
99 /**
100 * @brief True if the pool is locked and ready for acquire/release operations.
101 */
102 bool locked_ = false;
103
104 public:
105
106
107 /**
108 * @brief Constructs a GameObjectPool with the specified capacity.
109 *
110 * @details Pre-allocates internal storage for the given pool size.
111 * The pool starts empty; use `addInactive()` or a factory to populate it.
112 *
113 * @param poolSize The maximum number of GameObjects this pool can manage.
114 */
116 size_t poolSize
117 ) :
118 poolSize_(poolSize),
119 guid_(helios::util::Guid::generate()) {
120 activeGameObjects_.reserve(poolSize);
121 inactiveGameObjects_.reserve(poolSize);
122 }
123
124
125 /**
126 * @brief Returns the unique identifier of this pool.
127 *
128 * @return The Guid assigned to this pool instance.
129 */
130 [[nodiscard]] helios::util::Guid guid() const noexcept {
131 return guid_;
132 }
133
134 /**
135 * @brief Returns the maximum capacity of this pool.
136 *
137 * @return The pool size specified at construction.
138 */
139 [[nodiscard]] size_t size() const noexcept {
140 return poolSize_;
141 }
142
143 /**
144 * @brief Acquires an inactive GameObject from the pool.
145 *
146 * @details Removes a EntityHandle from the inactive list and adds it to the active
147 * tracking structures. The caller is responsible for activating the actual
148 * GameObject in the GameWorld.
149 *
150 * @param[out] entityHandle Receives the EntityHandle of the acquired object on success.
151 *
152 * @return True if an object was acquired, false if the pool is exhausted.
153 */
154 [[nodiscard]] bool acquire(helios::engine::ecs::EntityHandle& entityHandle) {
155
156 if (inactiveGameObjects_.empty()) {
157 return false;
158 }
159
160 entityHandle = inactiveGameObjects_.back();
161 inactiveGameObjects_.pop_back();
162
163
164 auto idx = entityHandle.entityId - delta_;
165
166 if (activeIndex_.size() <= idx) {
169 }
170
171 activeIndex_[idx] = activeGameObjects_.size();
172 versionIndex_[idx] = entityHandle.versionId;
173
174 activeGameObjects_.push_back(entityHandle);
175
176 return true;
177 }
178
179 /**
180 * @brief Checks if the pool is locked.
181 *
182 * @return True if the pool is locked and ready for acquire/release operations.
183 */
184 [[nodiscard]] bool isLocked() const noexcept {
185 return locked_;
186 }
187
188 /**
189 * @brief Locks the pool for acquire/release operations.
190 *
191 * @details After locking, no more EntityHandles can be added via `addInactive()`.
192 * The sparse arrays are sized based on the min/max EntityIds added.
193 */
194 void lock() noexcept {
195 locked_ = true;
199 }
200
201 /**
202 * @brief Adds a EntityHandle to the inactive list without acquiring it.
203 *
204 * @details Used during pool initialization to register pre-created GameObjects.
205 * Fails if the pool is already at capacity.
206 *
207 * @param entityHandle The EntityHandle of the GameObject to add.
208 *
209 * @return True if added successfully, false if pool is full.
210 */
212
213 assert(!locked_ && "Pool is locked");
214
215 assert(entityHandle.isValid() && "Unexpected invalid entityHandle");
216
217 const size_t used = (activeCount() + inactiveCount());
218
219 minEntityId_ = std::min(minEntityId_, entityHandle.entityId);
220 maxEntityId_ = std::max(maxEntityId_, entityHandle.entityId);
221
222
223 if (used < size()) {
224 inactiveGameObjects_.push_back(entityHandle);
225 return true;
226 }
227
228 return false;
229 }
230
231 /**
232 * @brief Releases a GameObject back to the pool by its EntityHandle.
233 *
234 * @details
235 * Validates the EntityHandle against both the GameWorld and the active tracking list.
236 * Uses swap-and-pop for O(1) removal from the active list. The object is
237 * marked inactive and added to the inactive list for future acquisition.
238 *
239 * @param entityHandle The unique identifier of the GameObject to release.
240 *
241 * @return True if the object was successfully released, false if the EntityHandle
242 * was not found in the GameWorld or not tracked as active.
243 */
244 bool release(const helios::engine::ecs::EntityHandle entityHandle) {
245
246 assert(entityHandle.isValid() && "Unexpected invalid entityHandle");
247
248 assert(entityHandle.entityId >= delta_ && "Unexpected entityHandle");
249
250 const auto sparseIdx = entityHandle.entityId - delta_;
251
252 assert(sparseIdx < activeIndex_.size() && "Unexpected sparse index");
253
254 const auto denseIndex = activeIndex_[sparseIdx];
256 return false;
257 }
258
259 assert(versionIndex_[sparseIdx] == entityHandle.versionId && "Version mismatch");
260
261 auto lastEntityHandle = activeGameObjects_.back();
262
263 if (denseIndex != activeGameObjects_.size() - 1) {
264 // swap the last entityHandle in activeGameObjects with the
265 // entityHandle to remove, effectively overwriting entityHandle
266 // to release with a currently active entityHandle
267 activeGameObjects_[denseIndex] = lastEntityHandle;
268 activeIndex_[lastEntityHandle.entityId-delta_] = denseIndex;
269 versionIndex_[lastEntityHandle.entityId-delta_] = lastEntityHandle.versionId;
270 }
271
272
273 // the swap operation has create a duplicate entry,
274 // remove the one at the tail
275 activeGameObjects_.pop_back();
276
277 // clear the queried entityHandle from active index and update
278 // inactiveGameObjects
281
282 inactiveGameObjects_.push_back(entityHandle);
283
284 return true;
285 }
286
287 /**
288 * @brief Releases and permanently removes a GameObject from the pool.
289 *
290 * @details Unlike `release()`, this method does not add the EntityHandle back to the
291 * inactive list. Use this when a pooled object is being destroyed rather than
292 * recycled.
293 *
294 * @param entityHandle The unique identifier of the GameObject to remove.
295 *
296 * @return True if removed successfully, false if EntityHandle was not active.
297 */
299
300 assert(entityHandle.isValid() && "Unexpected invalid entityHandle");
301
302 const auto sparseIdx = entityHandle.entityId - delta_;
303
304 assert(sparseIdx < activeIndex_.size() && "Unexpected sparse index");
305
306 const auto denseIndex = activeIndex_[sparseIdx];
307
309 return false;
310 }
311
312 assert(versionIndex_[sparseIdx] == entityHandle.versionId && "Version mismatch");
313
314 if (denseIndex != activeGameObjects_.size() - 1) {
315 const auto lastEntityHandle = activeGameObjects_.back();
316 activeIndex_[lastEntityHandle.entityId - delta_] = denseIndex;
317 versionIndex_[lastEntityHandle.entityId - delta_] = lastEntityHandle.versionId;
318 activeGameObjects_[denseIndex] = lastEntityHandle;
319 }
320
321 activeGameObjects_.pop_back();
322
325
326 return true;
327
328 }
329
330
331 /**
332 * @brief Returns the number of active game objects.
333 *
334 * @return The number of active game objects.
335 */
336 [[nodiscard]] size_t activeCount() const noexcept {
337 return activeGameObjects_.size();
338 }
339
340 /**
341 * @brief Returns the number of inactive game objects.
342 *
343 * @return The number of inactive game objects.
344 */
345 [[nodiscard]] size_t inactiveCount() const noexcept {
346 return inactiveGameObjects_.size();
347 }
348
349 /**
350 * @brief Returns a span of all inactive EntityHandles.
351 *
352 * @return Span of inactive EntityHandles.
353 */
354 std::span<helios::engine::ecs::EntityHandle> inactiveGameObjects() {
356 };
357
358 /**
359 * @brief Returns a span of all active EntityHandles.
360 *
361 * @return Span of active EntityHandles.
362 */
363 std::span<helios::engine::ecs::EntityHandle> activeGameObjects() {
364 return activeGameObjects_;
365 };
366 };
367
368}

Generated via doxygen2docusaurus 2.0.0 by Doxygen 1.15.0.