Object Pooling for Game Performance: A Practical Guide for Bullets, Particles, Enemies, and UI Feedback
If your roguelike spawns bullets, particles, enemies, loot, floating damage numbers, or temporary UI effects every second, object pooling is one of the first performance patterns worth checking. This guide explains when pooling helps, when it makes code worse, and how to apply it across common game engines.
The Core Claim
Object pooling is not a universal optimization. It is useful when the same kind of short-lived object is created and destroyed repeatedly during gameplay.
Good candidates:
- Bullets and projectiles
- Hit sparks and particles
- Floating damage numbers
- Enemy corpses or short-lived summons
- Loot popups
- Temporary UI markers
- Audio emitters
Poor candidates:
- Rare boss objects
- Long-lived managers
- Objects with complex ownership
- Objects whose reset rules are harder than recreating them
The purpose of pooling is to trade allocation churn for predictable reuse.
Why Games Use Pools
Real-time games care about frame consistency. Even if average performance looks fine, repeated allocation and cleanup can create visible spikes.
Pooling helps by:
- Creating objects ahead of time or reusing existing ones.
- Keeping inactive objects ready for the next request.
- Resetting state instead of constructing a new object.
- Avoiding bursts of allocation during combat-heavy moments.
In a roguelike, this matters most when many events happen at once: room entry, enemy waves, chain reactions, projectile patterns, loot explosions, or area damage ticks.
The Basic Shape
type Reset<T> = (item: T) => void;
type Create<T> = () => T;
class ObjectPool<T> {
private available: T[] = [];
private active = new Set<T>();
constructor(
private create: Create<T>,
private reset: Reset<T>,
initialSize = 32,
private maxSize = 256
) {
for (let i = 0; i < initialSize; i++) {
this.available.push(this.create());
}
}
acquire(): T | null {
const item = this.available.pop() ?? this.createIfAllowed();
if (!item) return null;
this.active.add(item);
return item;
}
release(item: T) {
if (!this.active.delete(item)) return;
this.reset(item);
this.available.push(item);
}
private createIfAllowed(): T | null {
if (this.active.size + this.available.length >= this.maxSize) {
return null;
}
return this.create();
}
}
The important part is not the exact code. The important part is the lifecycle:
create or prewarm -> acquire -> activate -> use -> deactivate -> reset -> release
Reset Rules Matter More Than the Pool
Most pooling bugs come from incomplete reset logic. A pooled object remembers everything you forget to clear.
For a projectile, reset all of this:
- Position
- Velocity
- Rotation
- Lifetime
- Damage
- Owner
- Collision state
- Visual state
- Trail or particle state
- Event subscriptions
For a floating damage number, reset:
- Text
- Color
- Scale
- Alpha
- Lifetime
- Animation state
- Parent/container
- Sorting layer
If the reset function becomes larger and riskier than the constructor, the object may not be a good pooling candidate.
Engine Notes
Phaser
Phaser Arcade Physics groups can act as practical pools. Use inactive objects, reactivate them when needed, and disable them again when their lifetime ends. This is especially useful for bullets, hit effects, and damage-number text objects.
Unity
Unity provides UnityEngine.Pool.ObjectPool<T>, which is a strong default for projectiles, particles, and temporary gameplay objects. Pair it with clear OnGet, OnRelease, and OnDestroy behavior. Unity's managed memory guidance is also relevant: reducing per-frame allocations reduces pressure on garbage collection.
Unreal Engine
Unreal projects often implement actor or component pools for expensive spawn/despawn flows. Keep pooled actors clearly inactive when released: collision off, tick disabled if possible, hidden, detached from old owners, and reset before reuse.
Godot
Godot projects can pool nodes by keeping inactive instances under a manager node. Disable processing, visibility, and collision when released. Re-enable only what the object needs when acquired.
Capacity Planning
Do not guess forever. Measure peak demand and set capacity from observed gameplay.
Recommended workflow:
- Start with a small pool.
- Log when the pool expands or fails to acquire.
- Play the highest-density encounter.
- Record the peak active count.
- Set the warm size near the normal peak.
- Set the hard max above the extreme peak.
- Decide what happens when the pool is exhausted.
Common exhaustion strategies:
| Strategy | Use when | Risk |
|---|---|---|
| Drop request | Cosmetic effects | Missing feedback |
| Reuse oldest | Damage numbers, particles | Visual popping |
| Expand up to max | Bullets, enemies | Memory growth |
| Fail loudly in dev | Core gameplay objects | Requires fallback |
Anti-Patterns
| Anti-pattern | Symptom | Fix |
|---|---|---|
| Pooling everything | Code becomes harder without measurable gains | Pool only high-churn object types |
| No hard max | Memory grows during extreme encounters | Add max capacity and exhaustion behavior |
| Incomplete reset | Old state appears in new objects | Centralize reset and test reuse |
| Hidden subscriptions | Released object still reacts to events | Unsubscribe on release |
| Pool owns gameplay decisions | Pool decides damage, target, or reward logic | Keep pools responsible only for lifecycle |
| One giant generic pool | Object-specific reset rules get lost | Use one pool per object type |
Checklist
- [ ] The object type is created and destroyed frequently during normal play.
- [ ] Reset rules are explicit and tested.
- [ ] The pool has an initial size and a hard maximum.
- [ ] Exhaustion behavior is defined.
- [ ] Released objects stop ticking, colliding, rendering, and listening to events.
- [ ] High-density encounters are profiled.
- [ ] Pool metrics are visible in development builds.
- [ ] Cosmetic pools are allowed to drop requests before harming frame time.
A Practical Roguelike Example
Start with three pools:
ProjectilePoolfor bullets, arrows, spells, and thrown weapons.DamageNumberPoolfor combat feedback.HitEffectPoolfor sparks, splashes, and impact visuals.
Do not start by pooling enemies unless spawning is already measured as a real cost. Enemies often carry AI state, pathfinding state, equipment, buffs, and event subscriptions, which makes reset logic more dangerous.
References
- Robert Nystrom. "Object Pool." Game Programming Patterns.
- Unity Technologies.
UnityEngine.Pool.ObjectPool<T>scripting API. - Unity Technologies. "Understanding automatic memory management."
- Phaser documentation.
Phaser.Physics.Arcade.Group.