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 |
Render component |
Child |
Physics component |
|
Audio component |
|
Logic component |
Python class with a |
How to Implement¶
Create an entity root:
self.root = render.attachNewNode('player').For each domain create a component class that receives the root
NodePathand sets up its own sub-tree/resources.Keep components as independent as possible. Where components must interact, prefer
messengerevents over direct references — but the book notes that some selective coupling between components that genuinely need to talk is acceptable.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.renderintoPhysicsComponent. Prefermessenger.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 avoidtaskMgr.add()silently replacing another entity’s task.