Files

12 KiB

Guide

Conventions

Naming

  • Bundles: *Bundle suffix (e.g., PlayerBundle, TerrainBundle)
  • Components: *Component suffix (e.g., MeshComponent, MovementComponent)
  • States: Plain names without suffix (e.g., IdleState, WalkingState); impl State trait
  • Intents: *Intent suffix for one-frame events (e.g., CameraTransitionIntent, FollowPlayerIntent)
  • Systems: *_system suffix 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 fmt with brace_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 use statements at file level
  • NO use statements 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 use statements; 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 Default impls

  • 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.rs for canonical State impl examples
  • See src/bundles/player.rs for complete Bundle pattern with state machine setup
  • See src/systems/camera.rs for canonical system function signatures with explicit storage parameters
  • See src/systems/dialog_system.rs for complex multi-storage system example