Object Pooling: Reusing Instead of Rebuilding
Creating and destroying objects during play is one of the most expensive habits a game can have; a pool keeps finished objects in reserve and hands them out again, trading a little memory for steady frames.

Some objects in a game live for a long time: the terrain, the buildings, the player's own character. Others flicker in and out of existence with startling frequency. A single volley of arrows may involve dozens of projectiles that fly for a second and vanish; a spell may scatter sparks that last only a few frames; a wave of enemies may rise, be cut down and be replaced by the next. If each of these is built anew with Instantiate and torn down with Destroy, the game spends much of its effort on birth and burial rather than on the life between.
Object pooling is the old and simple remedy. Instead of destroying an object when it is no longer needed, the game deactivates it and places it in a reserve. When a new object of the same kind is wanted, one is taken from the reserve, reset, and set loose again. The idea predates Unity by many years and appears wherever programs create and discard many similar objects, from database connections to threads, but in games it has a special urgency, because the cost of creation is paid inside a frame that has a strict deadline.
The pattern is easy to state and surprisingly easy to get subtly wrong. The difficulties lie not in the container itself but in the state that clings to a recycled object, in the question of how many objects to keep, and in the discipline of returning what was borrowed. Unity now ships its own pooling classes, which remove much of the boilerplate, yet the thinking behind them still has to be done by whoever uses them.
The price of creation
Calling Instantiate on a prefab does far more than allocate a block of memory. The engine must create a new GameObject and copy every component on it, together with every child in its hierarchy, building native objects in the C++ core and managed wrappers for scripts. It must register colliders with the physics engine, renderers with the rendering system, and animators with the animation system. Then it calls Awake and OnEnable on every script, and later Start, each of which may perform its own setup, look up other components, or allocate further memory.
Destroy has costs of its own. The object is not removed immediately; Unity marks it and finishes the destruction at the end of the current frame, after which it calls OnDisable and OnDestroy, unregisters everything that was registered, and releases native memory. The managed wrappers left behind become garbage that the collector will eventually have to find. A game that destroys many objects per second is therefore feeding the garbage collector steadily, and the resulting collections arrive as the familiar irregular hitches that make motion feel uneven.
For a single object these costs are small, measured in fractions of a millisecond for a simple prefab. The trouble is multiplication. A weapon firing ten rounds a second, a dozen enemies doing the same, and an impact effect for each hit quickly turns into hundreds of creations and destructions per second, and the cost of each scales with the complexity of the prefab. The Profiler makes this visible: spikes labelled with instantiation and with garbage collection, rising whenever the action on screen becomes most intense, which is exactly when the player most needs a smooth frame.
The pattern itself
At its heart a pool is a stack or a queue of inactive objects of one kind, along with two operations. To get an object, the pool removes one from its reserve if any are available, or creates a new one if the reserve is empty, and then activates it. To release an object, the caller hands it back, the pool deactivates it, typically with SetActive(false), and stores it for later. The object never dies; it merely sleeps between uses, keeping its components, its native data and its place in memory.
A hand written pool in Unity often amounts to a small MonoBehaviour holding a reference to a prefab and a Stack of instances. When a bullet is needed, the pool pops one or instantiates a fresh copy, positions it, and activates it. When the bullet hits something or flies too far, it calls back to the pool rather than calling Destroy. The bullet's own script needs to know its pool, which is commonly done by having the pool assign itself to a field on each instance when that instance is first created.
Prewarming is the natural companion of the pattern. If a pool begins empty, the first burst of demand will still trigger creation, exactly at the moment the game is busiest. Creating a sensible number of instances during loading, while a loading screen hides the cost, means the reserve is already full when the action begins. The right number comes from observation: play the busiest scene, watch how many objects are active at the peak, and prewarm a little above that figure.
Unity's built-in pools
Since Unity 2021.1 the engine has included a pooling API in the UnityEngine.Pool namespace. Its central class, the generic ObjectPool, is constructed with a creation function and optional callbacks for the moment an object is taken from the pool, the moment it is returned, and the moment it is destroyed because the pool is full. Its constructor also accepts a flag called collectionCheck, a default capacity for the internal collection, and a maximum size beyond which returned objects are destroyed rather than kept.
Using it is pleasantly brief. You create the pool with a function that instantiates the prefab, an action on get that activates the object, an action on release that deactivates it, and an action on destroy that calls Destroy. Then gameplay code calls Get to borrow an instance and Release to return it. The pool handles the reserve, counts how many objects are active and inactive, and creates new ones only when it must. The same namespace offers LinkedPool, as well as ListPool, HashSetPool and DictionaryPool for borrowing temporary collections.
The collection check is worth leaving enabled during development. Releasing the same object twice is a classic pooling bug: the object ends up in the reserve two times, two different callers later receive the same instance, and the result is a projectile that teleports or an enemy that seems to be in two places at once. With the check enabled, the pool throws an exception the moment a duplicate release happens, which turns a baffling symptom into an immediate, traceable error. The check runs in the editor only, so it costs nothing in a shipped build.
Resetting state
The hardest part of pooling is that a recycled object remembers its previous life. A bullet taken from the pool still carries the velocity it had when it struck its last target, unless something clears it. An enemy still has the health it had when it fell, the animation state it died in, and perhaps a reference to the last thing it was chasing. Every piece of state that the original prefab initialized at creation must now be restored deliberately each time the object is reused, or the past will leak into the present.
The usual approach is to give pooled scripts a single method that restores them to a fresh state, called from the pool's get callback or from OnEnable. For a projectile this means zeroing the Rigidbody's velocity and angular velocity (the velocity property is named linearVelocity in Unity 6), resetting a lifetime timer, and clearing any TrailRenderer with its Clear method so that the new shot does not draw a streak from where the old one died. For a particle effect it means calling Clear and then Play on the ParticleSystem so it starts from the beginning.
Coroutines and event subscriptions need special attention. Deactivating a GameObject stops its coroutines, which is usually helpful, but it also means a routine started in Start will not run again on reuse, because Start is called only once in an object's lifetime. Setup that must happen on every reuse belongs in OnEnable, and cleanup in OnDisable. Likewise, an object that subscribes to an event when it activates should unsubscribe when it returns to the pool, or a sleeping instance will keep responding to messages it should not hear.
Bullets, sparks and the dead
Projectiles are the textbook case. They are numerous, short lived and almost identical, and their state is small: a position, a direction, a speed, a damage value. A pool sized to the maximum number of projectiles that can be in flight at once removes nearly all creation cost from combat. A useful refinement is to return a projectile to the pool when it hits something or exceeds its range, never by a delayed Destroy call, so that every instance has exactly one path back home.
Visual effects follow closely. Impact sparks, dust puffs, blood spatters and muzzle flashes are often whole prefabs with one or more ParticleSystem components, and a ParticleSystem can notify its owner when it finishes if its Stop Action is set to Callback, which triggers OnParticleSystemStopped on a script attached to the same object. That callback is an elegant place to release the effect back to its pool, since it fires exactly when the last particle has faded.
Enemies are the most demanding case, because their state is rich. Health, targets, navigation paths, animation and perhaps temporary effects must all be reset, and a NavMeshAgent may need to be warped to its new spawn position with its Warp method rather than simply moved. In a game like Crown & Ashes, where the Hunger rises in numbers each night and falls again before dawn, pooling the dead is an obvious fit: the same bodies can return night after night, reset and repositioned, rather than being built from nothing every time the sun goes down.
When not to pool
Pooling has costs that are easy to overlook. A pool holds memory for objects that are not in use, sometimes a great deal of it if the objects are complex or the pool is generously prewarmed. It adds code that must be maintained, and it introduces the whole category of bugs that come from stale state and double releases. For an object that appears once in a scene, or a few times a minute, the cost of Instantiate is negligible and a pool would add complexity without any measurable gain.
The decision should come from measurement, as most performance decisions should. Open the Profiler during the busiest moments of play and look for instantiation, destruction and garbage collection spikes. If they are there and they correspond to objects that are created and destroyed frequently, pooling those objects is likely to help. If they are not there, the time is better spent elsewhere, and the code is better left plain.
Seen from a distance, pooling is a small change of perspective rather than a clever trick. It treats an object not as something that is born and dies but as something that is put away and taken out again, like tools hung on a wall at the end of a working day. The effort of creation is paid once, ideally behind a loading screen, and from then on the game only borrows and returns. When the screen fills with arrows and sparks and the dead, that simple habit is what keeps each frame arriving on time.


