Actor pattern
Use message passing to keep Byte Engine systems independent.
Use actor-style boundaries to keep Byte Engine systems independent while they share events and state changes.
Each system owns its state, receives messages that describe changes, and publishes messages for other systems. An object doesn't need its own scheduler to take part in this pattern. When another system needs to know about a change, send a small message instead of sharing direct mutable access.
What counts as an actor?
In Byte Engine, an actor is a system that owns state and communicates across a channel-like boundary.
Common examples:
- The input manager owns device, trigger, and action state.
- The world coordinates gameplay, physics, transforms, and deletion.
- Render systems mirror the cameras, lights, meshes, and transforms that they need.
- Audio workers receive generator-creation messages.
- Application code can listen for events and publish updates as a small system.
Focus on the boundary, not the actor label. For each system, identify its input messages, output messages, and owned state.
Message sending
Use these core APIs:
DefaultChannel<T>: publishes messages of one typeDefaultListener<T>: reads pending messagesFilteredListener: reads only messages that match a predicateFactory<T>: creates entities and broadcastsCreateMessage<T>with a stable handleDeleteMessage: broadcasts deletion intent
Create shared entities through a factory so each system receives the same stable
handle. Send changes through a typed update message such as
TransformationUpdate. Send deletion intent through a deletion channel so each
system can remove its mirrored state.
Your application only publishes the event. It doesn't need to know which rendering, physics, inspection, or gameplay systems receive it.
Why this shape helps
Debug one boundary at a time
Messages give you observable boundaries.
When camera movement is wrong, you can inspect the input action event, the local movement integration, and the outgoing TransformationUpdate separately.
When objects are not deleted, you can put the breakpoint on DeleteMessage::new or on the delete-channel listener rather than stepping through every world subsystem.
Small messages also make logging useful. An action name, handle, seat, device, and value are enough to explain most input behavior without dumping whole engine state.
Move work between threads
Message boundaries make thread ownership explicit. A worker can own its internal state and consume messages from the main application instead of sharing mutable structures with it. The audio setup follows this shape: application code creates generators, the audio worker observes those creations, and rendering audio stays inside the audio system.
This doesn't make every message free to send across every thread automatically. The useful property is architectural: when a system is ready to move to a worker thread, its communication boundary is already shaped like a queue of explicit inputs.
Keep systems independent
Factories and channels let systems mirror only what they need. Creating a body can notify physics. Deriving a renderable from the same handle can notify rendering. Sending a transform update can inform physics, rendering, gameplay helpers, and debugging tools without the sender knowing that list.
This keeps feature code narrow. A camera controller can publish transform updates without knowing how rendering consumes them. A gameplay rule can publish deletion intent without knowing how physics or rendering release their mirrored state. Application code doesn't need to become an integration layer for every engine subsystem.
Store data for each system
The pattern encourages each system to keep its own read-friendly representation of data. Physics can store bodies in the shape it needs for simulation. Rendering can store GPU-facing renderables. Application code can keep local camera velocity, direction, and game-specific state close to the loop that updates it.
Handles connect those representations without requiring one global mutable object graph. Messages then describe how the representations should stay in sync.
Keep allocation pressure low
Message passing can reduce allocation pressure when messages stay small and specific. Instead of cloning whole objects or rebuilding cross-system state every frame, the engine can pass handles, compact value updates, or short event payloads.
The current input path follows the same principle during update: records are compacted to the latest value per source, action values are resolved from current state, and temporary per-frame work can use frame scratch allocation.
Send intent and small changes:
- Send "this handle has this transform."
- Send "this action resolved to this value."
- Send "delete this handle."
Avoid messages that smuggle large ownership graphs across systems unless that is the actual domain event.
Choosing the right message
Use a factory when other systems must mirror a new entity. Use a typed channel when existing state changes. Use a deletion message when systems must release their mirrored state independently. Use a filtered listener when a consumer needs only part of an event stream.
These choices cover most engine and application message flows.
Practical guidance
Prefer messages that name the domain event, not the implementation detail.
TransformationUpdate is clearer than a generic "world changed" event because listeners can understand the payload without knowing the sender.
Keep ownership local. If a system owns a cache, buffer, physics body, or render resource, let it update that data from messages instead of exposing mutable access to other systems.
Keep the payload compact. Stable handles, typed values, and small structs are easier to debug, cheaper to clone for listeners, and friendlier to future worker-thread boundaries.
Next, see how input handling applies these message boundaries to devices and application actions.