Byte Engine Docs

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 type
  • DefaultListener<T>: reads pending messages
  • FilteredListener: reads only messages that match a predicate
  • Factory<T>: creates entities and broadcasts CreateMessage<T> with a stable handle
  • DeleteMessage: 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.

On this page