Component

Intent: Allow a single game entity to span multiple domains (physics, rendering, audio, AI) without coupling those domains to each other.

Acron Mapping

Acron’s scene graph is inherently hierarchical: a game entity is a NodePath root, and domain-specific sub-nodes hang off it. Logic components are Python objects that own their domain’s sub-node.

Pattern role

Acron equivalent

Entity

Root NodePath (or a Python class that owns one)

Render component

Child NodePath / Actor reparented to the entity root

Physics component

CollisionNode attached as a child; traverser + handler registered

Audio component

AudioSound managed by a Python wrapper

Logic component

Python class with a taskMgr task; holds a reference to the entity root

How to Implement

  1. Create an entity root: self.root = render.attachNewNode('player').

  2. For each domain create a component class that receives the root NodePath and sets up its own sub-tree/resources.

  3. Keep components as independent as possible. Where components must interact, prefer messenger events over direct references — but the book notes that some selective coupling between components that genuinely need to talk is acceptable.

  4. Each component manages its own task and cleans up in a destroy() method.

Code Sketch (Python)

class RenderComponent:
    def __init__(self, root):
        self.actor = Actor('assets/player.bam', {'run': 'assets/player-run.bam'})
        self.actor.reparentTo(root)

    def destroy(self):
        self.actor.cleanup()
        self.actor.removeNode()

class PhysicsComponent:
    def __init__(self, root):
        from acron.core import CollisionSphere, CollisionNode
        cn = CollisionNode('playerCol')
        cn.addSolid(CollisionSphere(0, 0, 0, 0.5))
        self.col_np = root.attachNewNode(cn)
        base.cTrav.addCollider(self.col_np, base.pusher)

    def destroy(self):
        base.cTrav.removeCollider(self.col_np)
        self.col_np.removeNode()

class AudioComponent:
    def __init__(self):
        self.footstep = loader.loadSfx('sfx/step.ogg')

    def play_step(self):
        self.footstep.play()

    def destroy(self):
        pass

class Player:
    def __init__(self):
        self.root = render.attachNewNode('player')
        self.render = RenderComponent(self.root)
        self.physics = PhysicsComponent(self.root)
        self.audio = AudioComponent()

    def destroy(self):
        self.audio.destroy()
        self.physics.destroy()
        self.render.destroy()
        self.root.removeNode()

Pitfalls

  • Cross-component coupling: resist the urge to pass self.render into PhysicsComponent. Prefer messenger.send() for cross-domain signals. Where two components genuinely must talk (e.g. physics notifying the animation component), a direct reference is acceptable — the goal is minimising coupling, not eliminating it entirely.

  • Destruction order: destroy components in reverse dependency order before calling root.removeNode(). Removing the root first orphans component sub-nodes but doesn’t call their cleanup.

  • Task naming: use unique names per instance (f'playerAI_{id(self)}') to avoid taskMgr.add() silently replacing another entity’s task.