5.2 KiB
Intent-Based Architecture
Systems communicate through intents — one-frame typed data in shared queues — not through direct function calls. This decouples producers from consumers: the system that wants something to happen does not know or care which system processes it.
The Pattern
Producer World (shared state) Consumer
──────── ──────────────────── ────────
detects dialog change
↓
insert(camera, CameraTransitionIntent)
↓
camera_transition_intents
camera_intent_system reads it
sets up CameraTransition component
removes intent
An intent is ephemeral (one frame). The state change it triggers (a component mutation) persists.
Intent Types
Intents are plain structs stored in Storage<T> on World, keyed by target entity:
// components/intent.rs
pub struct FollowPlayerIntent;
pub struct StopFollowingIntent;
pub struct CameraTransitionIntent { pub duration: f32 }
// world.rs
pub follow_player_intents: Storage<FollowPlayerIntent>,
pub stop_following_intents: Storage<StopFollowingIntent>,
pub camera_transition_intents: Storage<CameraTransitionIntent>,
Adding a new intent = one struct + one storage field. Nothing else changes.
Producing Intents
Any code with access to the storage can submit. The entity is the target:
// Event handler wants camera to follow player — doesn't call camera functions
world.follow_player_intents.insert(camera_entity, FollowPlayerIntent);
// Dialog system detects state change — doesn't know how transitions work
world.camera_transition_intents.insert(camera_entity, CameraTransitionIntent { duration: 0.8 });
Producers don't import consumer modules. They only know about the intent type and the storage.
Consuming Intents
A consumer system reads, acts, and removes:
pub fn camera_intent_system(world: &mut World) {
// 1. Read
let follow_entities = world.follow_player_intents.all();
for entity in follow_entities {
// 2. Act — all the follow setup logic lives here
start_camera_following(world, entity);
// 3. Remove (consume)
world.follow_player_intents.remove(entity);
}
// ... same for stop_following_intents, camera_transition_intents
}
The consumer owns the implementation. start_camera_following is a private function inside the camera module — no other module can call it directly.
Why This Matters
Decoupling. The dialog system doesn't import camera functions. It inserts a CameraTransitionIntent. If the camera system changes how transitions work, the dialog system is unaffected.
Multiple producers, same path. Editor toggle, dialog state changes, and future cutscene systems all produce the same CameraTransitionIntent. They all go through the same processing. No special cases.
Testability. To test camera transitions: insert a CameraTransitionIntent, call camera_intent_system, check the result. No need to simulate dialog state or editor toggles.
Additive behavior. To add a new reaction to StopFollowingIntent (e.g., play a sound), write a new system that reads the intent before the camera system consumes it. No existing code changes.
Execution Order
The main loop is a flat pipeline. Order encodes causality:
Input + intent generation (camera_input, player_input, dialog_transition_detect)
↓
Intent processing (camera_intent_system)
↓
Camera behavior (noclip, dialog_camera, follow, transition, ground_clamp)
↓
Editor overlay (UI only — not a game system)
↓
Fixed-step physics (state_machine, physics, triggers, dialog)
↓
Per-frame systems (state_machine, rotate, trees, spotlights, snow)
↓
Render
Moving a system changes the data flow. The order is the contract.
Systems Also Self-Gate
Because intents express what should happen, systems naturally have nothing to do when their data is absent:
camera_follow_system— noFollowComponent= no workdialog_camera_system— no active bubbles = no workplayer_input_system— camera not following = no player inputcamera_noclip_system— camera hasFollowComponent= skip
The main loop doesn't branch on mode flags. Systems check their own data.
When to Use Intents vs. Direct Mutation
| Situation | Approach |
|---|---|
| One system wants another to do something | Intent |
| A system updating its own components | Direct mutation |
| Per-frame continuous computation | Components + tick system |
| Persistent state (is the camera following?) | Component (FollowComponent) |
| One-shot request (start following) | Intent (FollowPlayerIntent) |
Adding New Intents
- Define the struct in
components/intent.rs - Add a
Storage<T>field toWorld(+new()+despawn()) - Producers insert into the storage
- A consumer system reads, acts, and removes
- Done — no other code changes