Command

Intent: Turn actions into first-class objects so they can be queued, replayed, and undone.

Acron Mapping

Pattern role

Acron equivalent

Command object

Python callable or class with execute() / undo()

Command queue

Python collections.deque processed inside a task

Invoker

taskMgr task that drains the queue each frame

Receiver

NodePath, Actor, or any game object

How to Implement

  1. Define a command class (or use a lambda/partial) with execute() and optionally undo().

  2. Create a deque as the command queue on the controller or input manager.

  3. On input events (self.accept('w', ...)) push command objects onto the queue.

  4. Add a task with taskMgr.add(self._process_commands, 'commandQueue') that calls cmd.execute() each frame and pops the queue.

  5. For undo: maintain a separate undo_stack deque; after execute(), push the command there. Call undo_stack.pop().undo() on undo input.

Code Sketch (Python)

from collections import deque
from direct.showbase.DirectObject import DirectObject

class MoveCommand:
    def __init__(self, node, delta):
        self.node = node
        self.delta = delta
        self.prev_pos = None

    def execute(self):
        self.prev_pos = self.node.getPos()
        self.node.setPos(self.node.getPos() + self.delta)

    def undo(self):
        self.node.setPos(self.prev_pos)

class InputHandler(DirectObject):
    def __init__(self, player):
        self.player = player
        self.queue = deque()
        self.undo_stack = deque(maxlen=32)
        self.accept('w', self.queue.append,
                    [MoveCommand(player, (0, 1, 0))])
        self.accept('u', self._undo)
        taskMgr.add(self._process, 'commandQueue')

    def _process(self, task):
        while self.queue:
            cmd = self.queue.popleft()
            cmd.execute()
            self.undo_stack.append(cmd)
        return task.cont

    def _undo(self):
        if self.undo_stack:
            self.undo_stack.pop().undo()

Pitfalls

  • Event leak: always call self.ignoreAll() in cleanup; forgotten accept() calls keep the handler alive and queue filling after the object is “destroyed”.

  • Frame ordering: commands queued mid-frame execute next frame — if immediate effect is needed, call execute() directly and skip the queue.

  • Undo with NodePath transforms: getPos() returns a copy, so storing it before the move is safe. Watch out for parent-relative vs world-space positions.