Event Queue

Intent: Decouple when a message is sent from when it is processed, allowing events to be buffered, reordered, or coalesced.

Acron Mapping

Pattern role

Acron equivalent

Queue

collections.deque drained by a task, or Acron’s messenger (which already defers via the task loop)

Producer

Any code that appends to the deque or calls messenger.send()

Consumer

Task that pops from the deque each frame

messenger.send() dispatches synchronously within the same frame’s task, which is fine for most cases. Use a manual queue only when you need deferred processing, coalescing, or cross-frame buffering.

How to Implement (manual queue)

  1. Create a deque on the system that owns the queue: self._queue = deque().

  2. Producers call self._queue.append(Event(type, payload)) — no direct call to the consumer.

  3. Add a consumer task: taskMgr.add(self._drain, 'eventQueue', sort=50) (high sort number = runs after game logic).

  4. In _drain, process up to N events per frame to avoid frame spikes: pop, dispatch, repeat.

  5. For coalescing (e.g., “only process the last health update”): use a dict keyed by event type instead of a deque.

Code Sketch (Python)

from collections import deque
from dataclasses import dataclass, field
from typing import Any

@dataclass
class GameEvent:
    type: str
    payload: Any = None

class EventQueue:
    MAX_PER_FRAME = 20

    def __init__(self):
        self._queue = deque()
        self._handlers = {}
        taskMgr.add(self._drain, 'eventQueue', sort=50)

    def push(self, event_type, payload=None):
        self._queue.append(GameEvent(event_type, payload))

    def subscribe(self, event_type, handler):
        self._handlers.setdefault(event_type, []).append(handler)

    def _drain(self, task):
        processed = 0
        while self._queue and processed < self.MAX_PER_FRAME:
            evt = self._queue.popleft()
            for handler in self._handlers.get(evt.type, []):
                handler(evt.payload)
            processed += 1
        return task.cont

    def destroy(self):
        taskMgr.remove('eventQueue')

Pitfalls

  • Unbounded growth: without MAX_PER_FRAME, a burst of events can freeze a frame. Cap processing per frame and let the queue drain over multiple frames.

  • Ordering assumptions: unlike messenger.send() (synchronous), queued events arrive one frame late. Code that expects immediate state changes after sending will break.

  • Using the queue for audio: sound triggered via messenger.send() fires synchronously — acceptable. But if play() is deferred to next frame via a queue, the audio cue lags behind the visual, which is usually wrong. Play audio immediately; queue only logic.