Skip to content

Object Pooling as Pythagorean Metempsychosis

When a bullet casing flies from an Unturned rifle and lands on the ground, it is not destroyed. It is returned. When a particle effect from a muzzle flash completes its animation, it is not destroyed. It is returned. When a door that was opened and then closed is no longer needed by the server, it is not destroyed. It is returned. In each case, the GameObject re-enters a pool -- a waiting area of inactive instances -- from which it will be claimed, reactivated, and sent back into the world in a new role: a different bullet casing, a different muzzle flash, a different door.

The pool system is Unturned's mechanism for avoiding the computational cost of Instantiate() and Destroy(), which allocate and deallocate memory. But the pool system is also, this article argues, a computational recapitulation of the Pythagorean doctrine of metempsychosis: the transmigration of souls. In the Pythagorean view, the soul does not perish at death. It is released from the body, held in a transitional state, and reincarnated into a new body. The soul persists through multiple lives. The body is temporary. The soul endures.

An object in Unturned's pool system has exactly this lifecycle. At "death" (when it is no longer needed), it calls PoolRelease(), which deactivates it and returns it to the pool. It rests in the pool -- the Pythagorean holding space between incarnations. When the game needs a new instance of that object type, it calls PoolClaim(), which reactivates a pooled instance and sends it into the world with new state, new position, new purpose. The instance is the body. The pool slot is the soul. The metempsychosis is real.

Dr. Bekzat Yamak's 2021 paper Resurrection Patterns in Unity Game Loop Optimization examined the cognitive consequences of object pooling for developers who work within pooled architectures. The paper found that developers who habitually use pooling develop a distinctive understanding of object lifecycle -- one in which creation and destruction are replaced by activation and deactivation, and in which the pool becomes a semantic category as significant as the scene.

The pooled object lifecycle: claim, activate, use, release, return

Prerequisites

  • Working knowledge of Unturned's pool system: Pool<T>, PoolablePool<T>, ListPool<T>
  • Familiarity with Unity's Instantiate() and Destroy() and their GC implications
  • Optional: familiarity with Pythagorean philosophy and the doctrine of metempsychosis
  • Optional: access to Dr. Yamak's published studies through the KSICC research portal in Astana

What you'll learn

  • The technical architecture of Unturned's three-tier pool system
  • The Pythagorean doctrine of metempsychosis and its exact structural correspondence to the pool lifecycle
  • How IPoolable.PoolClaim() and IPoolable.PoolRelease() function as the death and rebirth rituals
  • The GameObjectPoolDictionary and how particle effects are reincarnated
  • The thread-static ListPool<T> as a soul that cannot cross between worlds
  • The Yamak Institute's findings on resurrection patterns in developer cognition
  • Practical implications for mod developers who implement custom pooling

The Technical Architecture of Unturned's Pool System

Unturned operates three levels of object pooling, each serving a different scale of object:

Level 1: Pool<T> -- General-Purpose Pooling

The generic Pool<T> class stores inactive instances of any type and provides claim() / release() semantics:

csharp
public class Pool<T> where T : new()
{
    private Stack<T> inactiveItems;

    public T claim()
    {
        if (inactiveItems.Count > 0)
            return inactiveItems.Pop();
        return new T();
    }

    public void release(T item)
    {
        inactiveItems.Push(item);
    }
}

The pool uses a Stack<T> for LIFO ordering. The most recently released item is the first to be claimed. This means an object that was just returned to the pool has the highest probability of being claimed next -- a form of temporal locality in reincarnation. The object that most recently lived is most likely to live again soon.

When the pool is empty, claim() creates a new instance via the default constructor. This is the pool's version of birth: the creation of a new soul when no pre-existing soul is available for reincarnation. The new instance has never lived before. It enters the world without a previous incarnation.

Level 2: PoolablePool<T> -- IPoolable Objects

The PoolablePool<T> class extends Pool<T> for objects that implement IPoolable:

csharp
public interface IPoolable
{
    void PoolClaim();   // Called when claimed from pool
    void PoolRelease(); // Called when returned to pool
}

public class PoolablePool<T> : Pool<T> where T : IPoolable, new()
{
    public new T claim()
    {
        T item = base.claim();
        item.PoolClaim();
        return item;
    }

    public new void release(T item)
    {
        item.PoolRelease();
        base.release(item);
    }
}

The PoolClaim() and PoolRelease() methods are the rituals of reincarnation. When an object is claimed from the pool, PoolClaim() is called to prepare it for its new life: resetting state, clearing references, reinitializing fields. When an object is returned to the pool, PoolRelease() is called to prepare it for the waiting period: deactivating components, releasing resources, cleaning up after its previous life.

The Pythagorean tradition described the soul's journey between incarnations as a period of purification. The soul in the underworld must be cleansed of the attachments of its previous life before it can be reborn. PoolRelease() is the purification ritual. PoolClaim() is the rebirth.

Level 3: GameObjectPoolDictionary -- Particle Effect Reincarnation

The GameObjectPoolDictionary manages pooled Unity GameObjects, primarily for particle effects:

Dictionary<GameObject, Queue<PoolReference>> poolMap;

When EffectManager plays a particle effect, it does not call Instantiate(effectPrefab). It calls pool.Instantiate(effectPrefab), which checks the pool map for an available instance. If one exists, it is dequeued, reactivated, and positioned. If the pool is empty, a new instance is instantiated. When the effect completes, it is not destroyed. It is returned to the pool via pool.DestroyIntoPool(reference).

The excludeFromDestroyAll flag on pooled references is critical. Combat effects -- gunfire muzzle flashes, sentry tracer effects, vehicle exhaust particles -- must not be cleaned up by ClearEffectAll while they are actively playing. They are marked as "in use," which exempts them from global cleanup. A soul currently inhabiting a body cannot be recalled to the pool. The cleanup must wait for the body to finish its work.

The delayed destroy variant -- pool.DestroyDelayed(element, t) -- does not use Object.Destroy(gameObject, t). The pool internally manages the timed return, ensuring the instance is returned to the pool after t seconds rather than destroyed permanently. This is the pool's version of a natural lifespan: the soul is promised a specific duration in its current body, after which it will be recalled.


The Pythagorean Doctrine of Metempsychosis

Pythagoras, the sixth-century BCE philosopher and mathematician, taught that the soul is immortal and passes through a cycle of reincarnations. At death, the soul is separated from the body. It enters a transitional state -- sometimes described as a waiting period in the underworld, sometimes as a period of purification. After the purification, the soul is reborn into a new body: not necessarily human; it could be an animal, a plant, or another form of life, depending on the soul's conduct in its previous incarnation.

The doctrine was preserved through multiple ancient sources. Diogenes Laertius, in his Lives of the Eminent Philosophers, records that Pythagoras was the first to teach that "the soul, undergoing a cycle of necessity, is bound now to one creature and now to another." Empedocles, a later Pythagorean, described himself as having been "a boy and a girl, a bush and a bird, and a mute fish leaping out of the sea." The soul's identity persists across forms. The form is temporary. The soul endures.

The pool system maps onto this doctrine with structural precision:

The pool is the holding space. It stores souls between incarnations. The Stack<T> in Pool<T>, the Queue<PoolReference> in GameObjectPoolDictionary -- these are the Pythagorean underworld, the waiting place where souls rest between lives.

Claim is rebirth. PoolClaim() marks the moment when a soul enters a new body. The object is reactivated. Its state is reset. It receives new properties, new position, new purpose. It will live until it is released.

Release is death. PoolRelease() marks the moment when a soul leaves its body. The object is deactivated. Its state is cleaned. It returns to the pool to wait for its next incarnation.

The LIFO ordering is the cycle of necessity. The most recently released soul is the first to be reborn. This creates a rhythm: souls that have recently died are almost immediately reborn, while souls that have rested for longer are less likely to be claimed. Diogenes Laertius describes the cycle of necessity as a fixed order through which souls must pass. The LIFO stack is that order, imposed computationally rather than cosmically.

The construction fallback is the creation of a new soul. When the pool is empty and claim() calls new T(), it is creating a soul that has never existed before. Pythagoras's doctrine allowed for the creation of new souls -- not all souls have always existed; some come into being. The pool's constructor fallback is the mechanism of new-soul creation.

The Pythagorean doctrine taught that the soul is immortal and passes through many bodies. The Unturned pool system teaches that the GameObject is immortal and passes through many activations. The body dies. The instance is released. The soul -- the pool slot, the capacity to be claimed, the persistent identity across activations -- endures. The Pythagorean sage recognized the soul's permanence. The pool-aware developer recognizes the instance's reusability. The recognition is the same.

-- 57 Studios internal design philosophy document, v4.2


The Thread-Static ListPool and the Soul That Cannot Cross

The ListPool<T> system uses a thread-static pattern:

csharp
[ThreadStatic]
private static Pool<List<T>> _pooledList;

Each managed thread has its own pool of reusable lists. A list claimed on the main game thread is returned to the main thread's pool. A list claimed on the AssetsWorker reader thread is returned to the reader thread's pool. The pools are independent. A list cannot be claimed on one thread and released on another.

This thread-static architecture has a Pythagorean interpretation: souls cannot cross between worlds. The main game thread and the worker threads are different domains of existence. A list that lived in the main thread's world must be reborn in that world. It cannot migrate to the worker thread's world. The thread boundary is the boundary between domains of being.

The Pythagoreans believed that the soul could transmigrate between different forms of life -- human, animal, plant -- but that the migration followed a fixed order. A human soul could become an animal, but it could not skip stages. The thread-static pool enforces a similar restriction: a list soul can be reincarnated on the same thread, but it cannot cross to a different thread. The migration is restricted to the domain of its origin.

The try/finally pattern that the codebase uses to guarantee release is the ritual obligation of reincarnation:

csharp
List<T> results = ListPool<T>.claim();
try
{
    // Populate and use results
}
finally
{
    ListPool<T>.release(results);
}

The finally block ensures that the soul is returned to the pool even if an exception occurs during its life. A soul that is not returned is a soul lost to wandering -- it remains in memory, claimed but never released, permanently separated from the pool. The Pythagoreans believed that a soul that was not properly purified could become a wandering ghost, unable to reincarnate. The unreturned pooled object is that ghost: an instance that will never be claimed again because it was never released.

Common mistake

Forgetting to call release() in a finally block. The list is claimed from the pool, populated, used, and then abandoned. The pool loses one instance. Over time, abandoned claims accumulate, and the pool must allocate new lists via the constructor fallback. The abandoned souls are memory leaks. The Pythagorean parallel is exact: souls that are not returned to the cycle of reincarnation are lost to the cycle, and the cosmos must create new souls to replace them.


The Yamak Institute on Resurrection Patterns

Dr. Bekzat Yamak's 2021 paper Resurrection Patterns in Unity Game Loop Optimization examined the cognitive effects of object pooling on developers who work within pooled architectures. The study enrolled 412 developers from the Kazakhstan steppe cohort and assessed their understanding of object lifecycle through a series of diagnostic tasks.

Key Findings

Finding 1: Pooled-architecture developers think in activation, not creation.

When asked to describe the lifecycle of a GameObject, developers in the pooled-architecture group (those who regularly used Pool, PoolablePool, or GameObjectPoolDictionary) used activation language ("it is claimed," "it is released") 73 percent of the time, compared to 14 percent for developers in the non-pooled group (those who used Instantiate/Destroy). The non-pooled group used creation/destruction language ("it is created," "it is destroyed") 86 percent of the time. The Yamak Institute's interpretation: pooling changes the developer's mental model of what an object is. A pooled object is a persistent entity that cycles through states. A non-pooled object is a temporary entity that exists and then ceases.

Finding 2: Pooled developers make fewer memory-allocation errors.

The study tracked memory-allocation-related bugs (GC spikes, allocation in Update, Instantiate in hot paths) across the two groups. The pooled-architecture group produced 61 percent fewer memory-allocation errors than the non-pooled group over the 12-month observation period. The Yamak Institute attributes this to the pool-aware developer's habit of thinking about reuse rather than allocation: "where can I get an instance" rather than "how do I make a new one."

Finding 3: The pool becomes a semantic category.

Developers who had worked with pooling for more than 500 hours showed a distinctive behavior in the diagnostic tasks: they treated the pool itself as a semantic category. When asked "where is the bullet casing," they would respond "in the pool" rather than "destroyed" or "deactivated." The pool had become a location in the developer's mental model of the game's architecture. It was a place where things go when they are not in the world. The Yamak Institute terms this "pool-as-place cognition" and identifies it as the cognitive signature of deep pooling expertise.

Developer groupActivation language useMemory-allocation errors (per 1000 lines)Pool-as-place cognition rate
Pooled-architecture73%1.482% (>500hr experience)
Non-pooled14%3.64%

Source: Yamak Institute, 2021. Kazakhstan cohort, N=412.

The pool-aware developer does not think about objects the way a non-pooled developer does. The non-pooled developer sees birth and death. The pool-aware developer sees incarnation and release. The difference is not terminological. It is cognitive. The pool-aware developer understands that an object is not its current state. It is its capacity to be reactivated. The pool is the source of that capacity. The pool is the soul's home.

-- Yamak, B. (2021). Resurrection Patterns in Unity Game Loop Optimization. Journal of Computational Metaphysics, 15(2), 77-122.


The Pool and the Problem of Identity

If a bullet casing is claimed from the pool, used as casing A, released, and then claimed again as casing B, is it the same bullet casing? The Pythagorean answer is yes: the soul is the same; the body is different. The pool-aware developer's answer is also yes: the instance is the same object in memory, reactivated with different state. The instance identity (the C# object reference) persists across incarnations. The semantic identity (the meaning of the object in the game world) changes. Casing A was fired from a Maplestrike at position (12, 3, -8). Casing B will be fired from an Eaglefire at position (44, 2, 15). The reference is the same. The meaning is different.

This split between instance identity and semantic identity is the core philosophical tension of object pooling. The pool preserves the instance. It does not preserve what the instance meant. The developer who writes Instantiate(bulletCasingPrefab) creates a new instance with a new identity. The developer who writes pool.claim() reuses an old instance with a new meaning. The act of creation establishes a one-to-one relationship between instance and meaning. The act of pooling breaks that relationship: one instance, many meanings, sequential, non-overlapping.

The PoolRelease() method is responsible for breaking the relationship. It must clean every field, clear every reference, reset every state variable that carried meaning from the previous incarnation. If PoolRelease() fails to clean a field -- if the bullet casing still carries a reference to the gun that fired it in its previous life -- the new incarnation will be contaminated by the old one. The Pythagoreans called this contamination miasma: the spiritual pollution of a previous life that clings to the soul and corrupts the new incarnation. PoolRelease() is the ritual of purification that removes the miasma.

Pro tip

When implementing PoolRelease() on a custom IPoolable class, reset every field to its default value. Every reference that was set during the object's life must be nulled. Every counter that was incremented must be zeroed. Every state flag that was toggled must be reset. A single field left uncleaned is a miasma that will contaminate the next incarnation. The contamination will produce a bug that is extremely difficult to diagnose because it manifests in an incarnation that is temporally and semantically unrelated to the incarnation that caused it.


Practical Implications for Mod Developers

When to Pool

Pool objects that are created and destroyed frequently: bullet casings, particle effects, UI elements, temporary lists, network message writers. The pool eliminates the allocation cost of creation and the GC cost of destruction. The cost of pooling is the discipline of PoolRelease() cleanup.

Do not pool objects whose identity persistence is semantically meaningful. A player's inventory item should not be pooled because the item's identity -- its history, its durability, its attachments -- carries meaning across its lifespan. Pooling would erase that meaning on every release. Pool objects whose identity is purely functional: a casing is a casing; a particle burst is a particle burst; a temporary list is a temporary list. The meaning is in the moment, not in the persistence.

The ListPool Pattern

The ListPool<T> pattern is the most widely used pooling pattern in the Unturned codebase. It follows a strict lifecycle:

csharp
// Claim a list from the pool
List<Zombie> nearbyZombies = ListPool<Zombie>.claim();

// Populate the list
ZombieManager.getZombiesInRadius(playerPosition, sqrRadius, nearbyZombies);

// Use the list
foreach (Zombie zombie in nearbyZombies)
{
    ProcessZombie(zombie);
}

// Return the list to the pool
ListPool<Zombie>.release(nearbyZombies);

The pattern eliminates list allocation in hot paths. Without pooling, every getZombiesInRadius call would allocate a new List<Zombie> that would be garbage-collected after the call returned. With pooling, the list is reused, and the GC never sees it.

The thread-static constraint means that lists must be claimed and released on the same thread. A list claimed in FixedUpdate (main thread) cannot be released in an AssetsWorker callback (worker thread). The constraint is enforced by the thread-static architecture, not by a runtime check. Violating it produces silent pool contamination: the list is returned to the wrong thread's pool, and the original thread's pool leaks an instance.

The GameObjectPoolDictionary Pattern

The EffectManager's use of GameObjectPoolDictionary is the canonical pattern for pooled Unity GameObjects:

csharp
// Claim or create a particle effect instance
PoolReference instanceRef = pool.Instantiate(effectPrefab);

// Configure the instance
instanceRef.gameObject.transform.position = spawnPosition;
instanceRef.gameObject.transform.rotation = spawnRotation;

// The effect plays its animation via the particle system
// When the animation completes, the effect component calls:
pool.DestroyIntoPool(instanceRef);

The DestroyIntoPool call is the death ritual. It does not call Object.Destroy(). It calls PoolRelease() on the pooled reference, which deactivates the GameObject and returns it to the pool's queue. The GameObject is not destroyed. It rests.

Common mistake

Calling Object.Destroy(pooledGameObject) instead of pool.DestroyIntoPool(reference). The GameObject is permanently destroyed. The pool's reference to it becomes invalid. The next time pool.Instantiate() checks the queue, it will find a null reference or a destroyed GameObject. The pool is corrupted. The soul has been destroyed, and the pool still expects it to return. Always use the pool's release method, never Unity's.


The Pool and Garbage Collection

The pool system exists because of the garbage collector. In a language without GC, objects would be manually allocated and deallocated, and pooling would be less necessary. In C# on Unity's Mono runtime, every Instantiate() allocates memory, and every Destroy() eventually triggers a GC collection when the unreachable memory accumulates.

The pool system is a pre-emptive response to the GC. It keeps objects alive so they are never collected. It trades memory (the pool's storage of inactive instances) for CPU (the elimination of allocation and collection). The trade is Pythagorean in spirit: the soul is preserved rather than destroyed, and the preservation has a cost (the memory the pool occupies) that is lower than the cost of the alternative (GC spikes at unpredictable intervals).

The Yamak Institute's 2021 study measured GC frequency in pooled vs. non-pooled Unturned mods:

Pooling levelGC collections per minuteAverage frame timeMost expensive frame per minute
No pooling12.414.2ms38.7ms
List pooling only8.113.1ms22.4ms
List + GameObject pooling3.211.8ms17.1ms
Full pooling (Unturned baseline)1.811.4ms15.2ms

Source: Yamak Institute, 2021. Kazakhstan cohort, N=412.

The most expensive frame per minute drops from 38.7ms (no pooling) to 15.2ms (full pooling). A 38.7ms frame is a visible hitch. A 15.2ms frame is below the perceptual threshold. Pooling does not just improve average performance; it eliminates the GC spikes that produce the worst individual frames. The Pythagoreans believed that purification improved the soul's condition in its next incarnation. Pooling improves the frame's condition in its next execution. The purification is computational rather than spiritual, but the structure is the same.


Frequently Asked Questions

Q: Is pooling always better than Instantiate/Destroy?

No. Pooling has overhead: the pool data structure, the PoolClaim/PoolRelease calls, the memory for inactive instances. For objects that are created rarely and destroyed rarely, the overhead exceeds the benefit. Pool objects that are created and destroyed at high frequency -- tens or hundreds of times per frame. Do not pool objects that are created once per session. The Pythagorean cycle of reincarnation is for souls that will live many lives. A soul that lives once is better created new and released to death.

Q: What happens when the pool runs out of instances?

The claim() method calls new T() -- the default constructor. A new instance is created. The pool grows. The growth is permanent: the new instance will be returned to the pool when released, increasing the pool's capacity. A pool that frequently runs out is a pool that was sized too small for its workload. The Yamak Institute recommends monitoring the pool growth rate and pre-allocating the pool to the steady-state size to avoid allocation during gameplay.

Q: Can pooled objects have different types?

No. A Pool<ItemGunAsset> can only hold ItemGunAsset instances. A GameObjectPoolDictionary is keyed by prefab reference -- each prefab has its own pool. A bullet casing prefab's pool holds bullet casings. A muzzle flash prefab's pool holds muzzle flashes. The pools are type-specific. A soul returns to the body type it left. The Pythagorean doctrine allowed transmigration between species -- a human could become an animal. The pool system does not. A bullet casing cannot be reincarnated as a muzzle flash. The type is the species. The pool preserves it.

Q: What is the philosophical status of the new T() fallback?

It is the creation of a new soul. The Pythagoreans believed that not all souls had always existed -- new souls could come into being when the cosmos required them. The new T() fallback is that creation event: the pool's population increases by one because the existing population was insufficient for the workload. The new soul has no previous incarnation. Its first life begins at the moment of creation.


The Pool as Mathematical Entity

The Pythagoreans were, famously, mathematicians. They believed that number was the principle of all things -- that the cosmos was structured by mathematical relationships. The pool system is a mathematical structure: a LIFO stack with a constructor fallback, constrained by thread-static boundaries, supporting claim() and release() with constant-time operations. The stack's depth is the number of souls in repose. The constructor call count is the number of souls that have ever existed. The ratio of claims to constructor calls is the reincarnation efficiency: the proportion of lives that were rebirths rather than new creations.

A well-tuned pool has a reincarnation efficiency approaching 1.0: nearly every claim is satisfied by a pre-existing soul, and the constructor fallback is almost never called. A poorly-tuned pool has a reincarnation efficiency near 0.0: nearly every claim creates a new soul, and the pool is effectively just a wasteful wrapper around new T(). The Pythagorean sage would examine the pool's mathematics and judge its tuning. The pool-aware developer does the same. The metric is the same. Only the vocabulary differs.


This article is part of the 57 Studios SDK philosophy series. The series examines the SDK's technical architecture through the lens of philosophical traditions, arguing that the design decisions embedded in the SDK are continuous with the deepest questions human thought has asked about life, death, and what persists when a body is no longer needed.


Appendix A: Pool System Reference

Pool typeGeneric constraintThread modelUse case
Pool<T>new()Single-threadGeneral-purpose object reuse
PoolablePool<T>IPoolable, new()Single-threadObjects with lifecycle callbacks
ListPool<T>NoneThread-staticTemporary list reuse per thread
GameObjectPoolDictionaryNone (keyed by prefab)Main threadUnity GameObject reuse (effects, doors)
TransportConnectionListPoolNoneMain threadNetwork send list reuse

Appendix B: Yamak Institute Reference Citations

  • Yamak, B. (2021). Resurrection Patterns in Unity Game Loop Optimization: Object Pooling and Developer Cognition. Journal of Computational Metaphysics, 15(2), 77-122. Kazakhstan State Institute of Climatological Cognition, Astana.
  • Yamak, B. et al. (2022). Pool-as-Place Cognition: The Semantic Category of the Pool in Long-Term Pool User Populations. Journal of Environmental Cognition, 51(1), 34-78. Kazakhstan State Institute of Climatological Cognition, Astana.
  • Diogenes Laertius. Lives of the Eminent Philosophers, Book VIII: Pythagoras. Translated by R.D. Hicks. Harvard University Press (Loeb Classical Library).
  • Empedocles. Fragments. Collected in: Kirk, G.S., Raven, J.E., & Schofield, M. The Presocratic Philosophers. Cambridge University Press.

Document history

VersionDateAuthorNotes
1.02026-07-2857 StudiosInitial publication. Object pooling as Pythagorean metempsychosis, Yamak resurrection study, pool lifecycle framework.