agent update
This commit is contained in:
135
docs/self-gating-systems.md
Normal file
135
docs/self-gating-systems.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
// components/intent.rs
|
||||
pub struct FollowPlayerIntent;
|
||||
pub struct StopFollowingIntent;
|
||||
pub struct CameraTransitionIntent { pub duration: f32 }
|
||||
```
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
```rust
|
||||
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` — no `FollowComponent` = no work
|
||||
- `dialog_camera_system` — no active bubbles = no work
|
||||
- `player_input_system` — camera not following = no player input
|
||||
- `camera_noclip_system` — camera has `FollowComponent` = 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
|
||||
|
||||
1. Define the struct in `components/intent.rs`
|
||||
2. Add a `Storage<T>` field to `World` (+ `new()` + `despawn()`)
|
||||
3. Producers insert into the storage
|
||||
4. A consumer system reads, acts, and removes
|
||||
5. Done — no other code changes
|
||||
Reference in New Issue
Block a user