Subclass Sandbox

Intent: Define behavior in a subclass using a set of operations provided by its base class.

Acron Mapping

Pattern role

Acron equivalent

Base class with primitives

Python base class that wraps taskMgr, messenger, loader, Actor methods

Subclass

Concrete ability/behavior class that only calls base-class methods

Protected primitives

_play_sound(), _spawn_effect(), _deal_damage(), etc.

Sandbox invariant

Subclasses never import taskMgr, messenger, or loader directly

How to Implement

  1. Create an abstract Superpower (or Ability, Skill) base class that holds references to taskMgr, messenger, loader, and the owning entity’s NodePath.

  2. Implement all Acron interactions as protected helper methods on the base class. These are the “safe primitives.”

  3. Declare def activate(self): raise NotImplementedError as the single entry point subclasses must override.

  4. Subclasses import only the base class — never taskMgr or messenger directly.

  5. The base class handles cleanup: it tracks every task/sound/effect started via its helpers and cancels them in deactivate().

Code Sketch (Python)

class Superpower:
    def __init__(self, owner_node):
        self._owner = owner_node
        self._active_tasks = []
        self._active_sounds = []

    # ── Safe primitives ────────────────────────────────────────────
    def _play_sfx(self, path: str):
        snd = loader.loadSfx(path)
        snd.play()
        self._active_sounds.append(snd)

    def _add_task(self, fn, name: str):
        t = taskMgr.add(fn, f'{name}_{id(self)}')
        self._active_tasks.append(t.name)
        return t

    def _spawn_effect(self, effect_path: str, pos):
        fx = loader.loadModel(effect_path)
        fx.reparentTo(render)
        fx.setPos(pos)
        return fx

    def _deal_damage(self, amount: int):
        messenger.send('damage', [self._owner, amount])

    # ── Lifecycle ──────────────────────────────────────────────────
    def activate(self):
        raise NotImplementedError

    def deactivate(self):
        for name in self._active_tasks:
            taskMgr.remove(name)
        self._active_tasks.clear()
        for snd in self._active_sounds:
            snd.stop()
        self._active_sounds.clear()


# Subclass — only calls sandbox primitives
class FreezeRay(Superpower):
    def activate(self):
        self._play_sfx('sfx/freeze.ogg')
        self._spawn_effect('fx/ice_beam.bam', self._owner.getPos())
        self._add_task(self._beam_task, 'freezeBeam')

    def _beam_task(self, task):
        self._deal_damage(5)
        return task.cont if task.time < 2.0 else task.done

Pitfalls

  • Primitive creep: resist adding engine-specific parameters to base primitives (e.g., _play_sfx(path, volume, loop, 3d_pos, ...)). Keep each helper small; subclasses can call several helpers in sequence instead.

  • Task name collisions: always suffix task names with id(self) (shown above) so two instances of the same subclass don’t share a task name and silently overwrite each other.

  • Forgetting deactivate(): if the caller never calls deactivate(), tasks and sounds leak. Consider tying deactivate() to the entity’s FSM exit method.