This document outlines the recommended strategy for managing actors and lifecycle cleanup using the Supervisor and actors_registry. This pattern enables robust, low-coupling architecture inspired by ECS and real-world game engine designs.
- Actor Lifecycle & Registry Ownership Strategy
- Principle 1: Only the Orchestrator Needs
actors_registry - Principle 2: Orchestrator Handles Actor Deletion
- Problem: Zombie References
- Safe Zombie Cleanup Strategy
- Orchestrator Implementation
- Movement, Animation, and Conflict Resolution
- All other systems (Grid, Input, Pipeline, Behaviours) work with actor references already handed to them.
- They do not need global access to the registry.
- ✅ This simplifies dependencies and prevents circular imports.
The Supervisor is responsible for:
- Marking the actor as
.is_deleted = True - Removing it from
actors_registry - Letting all other systems clean up lazily when they interact with the deleted actor
✅ This mirrors standard patterns in Unreal, Unity, and ECS-based engines.
- Predictable invalidation
- Centralized control
- No race conditions or hard crashes from dangling references
"What if something holds a reference to a deleted actor and never uses it again?"
These are "zombies":
- Actor no longer participates in the game
- Still exists in:
Cell._holdersPuppeteer.puppetinput_registry.subscriberscommand_pipeline.queue
Each system checks .is_deleted before using an actor:
if actor.is_deleted:
self._holders.discard(actor)
returnclass Orchestrator(Actor):
...Allows:
- Behaviours to react to messages like
REQUEST_DELETE,KEY_PRESSED, etc. - Fully event-driven design
def tick():
for actor in self.actors.get_active_actors():
if not actor.should_retain_pending_actions(): # or via message
actor.pending_actions.clear()✅ Might even be extracted into a PendingActionManager behaviour
Actors send:
Message(
sender=actor.name,
body=MessageBody(message_type=MessageTypes.REQUEST_DELETE, payload=Reason(...))
)Orchestrator handles via a LifecycleManagerBehaviour:
- Marks actor
.is_deleted = True - Optionally calls
actor.finalize() - Removes from registry or queues
✅ Fully controlled, safe deletion
On startup or actor creation, Orchestrator can:
input_router.register(puppeteer)Or send an event to the input handler via:
Message(..., message_type=REGISTER_INPUT_HANDLER, ...)✅ Keeps input system modular and replaceable
Orchestrator gets messages like:
Message(sender="application", body=KeyPressEventLogRecord(dt=..., key=..., down=...))Handled by OrchestratorInputBehaviour, which:
- Parses log entries
- Converts to
KEY_DOWN/KEY_UP - Sends messages to corresponding puppeteer(s)
✅ Abstracts input source from simulation logic
At init:
self.actors: ActorCollection = self.level_factory.levels["level1"].actors✅ Holds the truth, no need for globals
orchestrator/
├── orchestrator.py # The Actor
├── behaviours/
│ ├── input_handler.py # From KeyPressEventLogRecord
│ ├── pending_manager.py # Clears actor pending_actions
│ └── lifecycle_manager.py # Handles REQUEST_DELETE, etc.
For detailed information about the movement system, animation handling, and conflict resolution mechanics, see:
📖 Movement, Animation, and Conflict Resolution
This document covers:
- Grid-based movement with smooth visual animations
- Strength-based push mechanics between actors
- Conflict resolution without rollback systems
- Visual overlapping and animation decoupling