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 |
Subclass |
Concrete ability/behavior class that only calls base-class methods |
Protected primitives |
|
Sandbox invariant |
Subclasses never import |
How to Implement¶
Create an abstract
Superpower(orAbility,Skill) base class that holds references totaskMgr,messenger,loader, and the owning entity’sNodePath.Implement all Acron interactions as protected helper methods on the base class. These are the “safe primitives.”
Declare
def activate(self): raise NotImplementedErroras the single entry point subclasses must override.Subclasses import only the base class — never
taskMgrormessengerdirectly.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 callsdeactivate(), tasks and sounds leak. Consider tyingdeactivate()to the entity’s FSM exit method.