12 KiB
Guide
Conventions
Naming
- Bundles:
*Bundlesuffix (e.g.,PlayerBundle,TerrainBundle) - Components:
*Componentsuffix (e.g.,MeshComponent,MovementComponent) - States: Plain names without suffix (e.g.,
IdleState,WalkingState); implStatetrait - Intents:
*Intentsuffix for one-frame events (e.g.,CameraTransitionIntent,FollowPlayerIntent) - Systems:
*_systemsuffix for functions (e.g.,player_input_system,trigger_system) - Storage containers: lowercase plural (e.g.,
world.transforms,world.movements) - Tags (marker components): bare unit type in
Storage<()>(e.g.,player_tags,bubble_tags)
Formatting
- Run
cargo fmtwithbrace_style = "AlwaysNextLine",control_brace_style = "AlwaysNextLine" - NO inline comments unless absolutely necessary—code must be self-documenting
- Doc comments (
///) only for public APIs and complex algorithms - NO inline paths in code—always use
usestatements at file level - NO
usestatements inside functions or impl blocks—all imports at module level
Imports
- Group imports: standard library, external crates, internal modules (in that order)
- Use fully-qualified module paths in
usestatements; never nest unnecessarily - Example:
use crate::components::{CameraComponent, FollowComponent}; use crate::world::World; use glam::Vec3;
Patterns
Intent-Based Communication (One-Frame Queues)
Why: Systems don't call each other directly. This decouples producer from consumer and maintains a flat pipeline.
How: Intent is a simple struct in a queue (Vec<T>). Producer inserts, consumer reads and removes.
// components/intent.rs
pub struct CameraTransitionIntent {
pub duration: f32,
}
// systems/camera.rs - consumer
pub fn camera_intent_system(
follow_player_intents: &mut Storage<FollowPlayerIntent>,
stop_following_intents: &mut Storage<StopFollowingIntent>,
camera_transition_intents: &mut Storage<CameraTransitionIntent>,
follows: &mut Storage<FollowComponent>,
transforms: &mut Storage<Transform>,
cameras: &mut Storage<CameraComponent>,
player_tags: &Storage<()>,
camera_transitions: &mut Storage<CameraTransition>,
) {
let transition_entities: Vec<EntityHandle> = camera_transition_intents.all();
for entity in transition_entities {
let duration = camera_transition_intents
.get(entity)
.map(|i| i.duration)
.unwrap_or(0.5);
start_camera_transition(camera_transitions, transforms, cameras, entity, duration);
camera_transition_intents.remove(entity);
}
}
Bundle Pattern (Entity Factory)
Why: Encapsulate all initialization logic for related components into one spawnable unit.
// bundles/player.rs
pub struct PlayerBundle {
pub position: Vec3,
}
impl Bundle for PlayerBundle {
fn spawn(self, world: &mut World) -> Result<EntityHandle, String> {
let entity = world.spawn();
// Physics
let rigidbody = RigidBodyBuilder::kinematic_position_based()
.translation(self.position.into())
.build();
let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody);
world.physics.insert(entity, PhysicsComponent { rigidbody: rigidbody_handle, .. });
// State machine with transitions
let mut state_machine = StateMachine::new::<FallingState>();
state_machine.register_state(|w: &mut World| &mut w.falling_states);
state_machine.add_transition::<FallingState, IdleState>(move |world| {
is_grounded(world, entity_id) && !has_input(world, entity_id)
});
world.state_machines.insert(entity, state_machine);
// Tags
world.player_tags.insert(entity, ());
Ok(entity)
}
}
State Machine with Type-Safe Transitions
Why: Encapsulate state logic (on_enter, on_exit, physics_update) and guard transitions with closures.
// states/player_states.rs - implement State trait
impl State for IdleState {
fn tick_time(&mut self, delta: f32) {
self.time_in_state += delta;
}
fn on_enter(&mut self, world: &mut World, entity: EntityHandle) {
// Apply damping on enter
world.physics.with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| {
let current_velocity = *rb.linvel();
rb.set_linvel(Vector::new(0.0, current_velocity.y, 0.0), true);
});
});
}
fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32) {
// Ground snapping
let current_y = world.physics.with(entity, |p| {
PhysicsManager::with_rigidbody_mut(p.rigidbody, |rb| rb.translation().y)
}).flatten();
// ...
}
}
// bundles/player.rs - register transitions
state_machine.add_transition::<IdleState, WalkingState>(move |world| {
world
.inputs
.with(entity_id, |i| i.move_direction.length() > 0.01)
.unwrap_or(false)
});
Storage.with/with_mut Pattern (Closure-Based Access)
Why: Avoids holding mutable references across multiple accesses; satisfies borrow checker.
// systems/spotlight_sync.rs
pub fn spotlight_sync_system(spotlights: &Storage<SpotlightComponent>, transforms: &Storage<Transform>) -> Vec<Spotlight> {
let mut result = Vec::new();
for entity in spotlights.all() {
if let Some(spotlight_component) = spotlights.get(entity) {
if let Some(transform) = transforms.get(entity) {
let position = transform.position + spotlight_component.offset;
result.push(Spotlight::new(position, ..));
}
}
}
result
}
// With mutation
world.movements.with_mut(entity, |movement| {
movement.movement_context.is_floored = true;
});
System Function Signature (Explicit Storage Parameters)
Why: Pass only the specific storages (or read-only &World) that the system needs. Makes data dependencies explicit for the reader and borrow checker. This is the standard pattern—all systems follow this.
// Good: explicit dependencies
pub fn camera_follow_system(
follows: &mut Storage<FollowComponent>,
cameras: &Storage<CameraComponent>,
transforms: &mut Storage<Transform>,
) {
let camera_entities: Vec<_> = follows.all();
for camera_entity in camera_entities {
if let Some(follow) = follows.get(camera_entity) {
if let Some(camera) = cameras.get(camera_entity) {
transforms.with_mut(camera_entity, |t| {
t.position = follow.target_position + new_offset;
});
}
}
}
}
// Read-only system
pub fn spotlight_sync_system(spotlights: &Storage<SpotlightComponent>, transforms: &Storage<Transform>) -> Vec<Spotlight> { .. }
// Dialog system example
pub fn dialog_system(
entities: &mut EntityManager,
trigger_events: &[TriggerEvent],
dialog_sources: &Storage<DialogSourceComponent>,
bubble_tags: &mut Storage<()>,
dialog_bubbles: &mut Storage<DialogBubbleComponent>,
transforms: &mut Storage<Transform>,
names: &mut Storage<String>,
player_tags: &Storage<()>,
projectile_tags: &mut Storage<()>,
dialog_projectiles: &mut Storage<DialogProjectileComponent>,
dialog_outcomes: &mut Vec<DialogOutcomeEvent>,
delta: f32,
) { .. }
Default Trait for Configuration Components
Why: Sensible defaults allow bundle code to be cleaner; override what's needed.
// components/jump.rs
impl Default for JumpComponent {
fn default() -> Self {
Self {
jump_height: 5.0,
jump_duration: 0.5,
air_control_force: 100.0,
max_air_momentum: 3.0,
air_damping_active: 0.95,
air_damping_passive: 0.9,
jump_curve: CubicBez::new((0.0, 0.0), (0.4, 1.0), (0.6, 1.0), (1.0, 0.0)),
jump_context: JumpContext::default(),
}
}
}
// Usage in bundle
world.jumps.insert(entity, JumpComponent::default());
Physics Closure Pattern
Why: Avoids lifetime tangles when borrowing rigidbody from PhysicsManager static storage.
// states/player_states.rs
world.physics.with(entity, |physics| {
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
let vel = *rigidbody.linvel();
rigidbody.set_linvel(Vector::new(vel.x, 0.0, vel.z), true);
});
});
Anti-Patterns
Direct System-to-System Calls
Don't do this:
// ❌ Bad: tightly coupled, hard to debug
fn system_a(world: &mut World) {
system_b_logic(world); // Hidden dependency
}
Do this instead:
// ✅ Good: intent in queue, flat pipeline
pub fn system_a(cameras: &mut Storage<CameraComponent>) {
cameras.insert(entity, CameraTransitionIntent { .. });
}
pub fn system_b(camera_transitions: &mut Storage<CameraTransition>) {
for entity in camera_transitions.all() {
// process
camera_transitions.remove(entity);
}
}
Holding Mutable References Across Multiple Storage Accesses
Don't do this:
// ❌ Bad: won't compile (borrow checker)
let mut movement = world.movements.get_mut(entity).unwrap();
let mut transform = world.transforms.get_mut(entity).unwrap();
movement.foo = transform.position.x;
Do this instead:
// ✅ Good: use closures
world.movements.with_mut(entity, |movement| {
world.transforms.with(entity, |transform| {
movement.foo = transform.position.x;
});
});
Inline Paths in Code
Don't do this:
// ❌ Bad
let velocity = *crate::physics::PhysicsManager::with_rigidbody_mut(..);
Do this instead:
// ✅ Good: use statements at top
use crate::physics::PhysicsManager;
// ... in code
let velocity = *PhysicsManager::with_rigidbody_mut(..);
Overly Generic (World/&mut World) Parameters
Don't do this:
// ❌ Bad: implicit dependencies, breaks borrow checker
pub fn camera_follow_system(world: &mut World) {
// What storages do we actually need?
}
Do this instead:
// ✅ Good: explicit, focused dependencies
pub fn camera_follow_system(
follows: &mut Storage<FollowComponent>,
cameras: &Storage<CameraComponent>,
transforms: &mut Storage<Transform>,
) {
// Clear what data is needed
}
Testing
Structure
Tests are inline with source (#[cfg(test)] mod tests). Focus on:
- Component initialization and state transitions
- Bundle spawning and validation
- Physics helper calculations
- Intent queue ordering
What to Test
-
State transitions: Verify conditions gate transitions correctly
#[test] fn test_idle_to_walking_transition() { let world = setup_test_world(); world.inputs.with_mut(player, |i| i.move_direction = Vec3::X); assert!(should_transition_to_walking(&world, player)); } -
Bundle spawning: Ensure all components are inserted
-
Default values: Verify sensible defaults in
Defaultimpls -
Physics calculations: Unit-test slope calculations, terrain queries
Running Tests
cargo test
cargo test --lib # Unit tests only
cargo test -- --nocapture # Show output
References
- See CLAUDE.md in project root for authoritative style rules (no inline comments, doc comments for public APIs only)
- See docs/self-gating-systems.md for detailed intent-based pipeline patterns
- See
src/states/player_states.rsfor canonical State impl examples - See
src/bundles/player.rsfor complete Bundle pattern with state machine setup - See
src/systems/camera.rsfor canonical system function signatures with explicit storage parameters - See
src/systems/dialog_system.rsfor complex multi-storage system example