Object Pool

Intent: Improve performance and memory use by reusing objects from a fixed pool instead of allocating and freeing them individually.

Acron Mapping

Pattern role

Acron equivalent

Pool

Python list of pre-created NodePath wrappers (hidden when inactive)

Acquire

pool.acquire() — finds a free slot, shows the node, returns it

Release

pool.release(obj) — hides the node, marks slot free

Model caching

ModelPool — Acron’s built-in model-file cache (not a node pool)

Built-in particle pool

ParticleEffect — use it for visual particles rather than rolling your own

The Acron-specific trick: hide (node.hide()) inactive pool members instead of removing them from the scene graph. This avoids the cost of re-parenting and re-loading.

How to Implement

  1. At startup, pre-create N instances: load the model once (hits ModelPool), then .copyTo(render) N times.

  2. Call .hide() on every instance. Store them in a deque as the “free list.”

  3. acquire() pops from the free list, calls .show(), positions the object, returns it.

  4. release(obj) calls .hide(), optionally .setPos(0, 0, -9999) (safety), pushes back to free list.

  5. Always remove the per-object task before calling release().

Code Sketch (Python)

from collections import deque

class BulletPool:
    def __init__(self, size: int = 64):
        self._free = deque()
        proto = loader.loadModel('assets/bullet.bam')
        for _ in range(size):
            node = proto.copyTo(render)
            node.hide()
            self._free.append(node)

    def acquire(self):
        if not self._free:
            return None        # pool exhausted — caller must handle gracefully
        node = self._free.popleft()
        node.show()
        return node

    def release(self, node) -> None:
        node.hide()
        self._free.append(node)


class Bullet:
    def __init__(self, pool, pos, velocity):
        self._pool = pool
        self.node = pool.acquire()
        if self.node is None:
            return           # silently skip if pool is full
        self.node.setPos(pos)
        self._vel = velocity
        self.dead = False
        taskMgr.add(self._update, f'bullet_{id(self)}')

    def _update(self, task):
        if self.dead:
            return task.done
        dt = globalClock.getDt()
        self.node.setPos(self.node.getPos() + self._vel * dt)
        if task.time > 3.0:
            self.destroy()
        return task.cont

    def destroy(self):
        self.dead = True
        taskMgr.remove(f'bullet_{id(self)}')
        if self.node:
            self._pool.release(self.node)
            self.node = None

Pitfalls

  • Pool exhaustion: always check if acquire() returns None and handle it (skip spawn, play an “ammo empty” sound). Never silently drop the None.

  • Leaked tasks on release: ensure the per-bullet task is removed before calling pool.release(). A hidden node with a running task still runs logic every frame.

  • ModelPool ≠ node pool: ModelPool caches the raw model file data, not live scene graph nodes. You still need to manage node pooling yourself for avoiding repeated copyTo() / removeNode() calls.