Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bevy 0.14 FPS Camera Pitfalls: From Black Screen to Pointer Lock to Child Entity Camera

This article documents the real debugging chain of “cursor won’t move, camera won’t turn” when porting a Three.js FPS game to macOS with Rust + Bevy 0.14.

Pitfall Chain Overview

#SymptomRoot CauseFix
Black screen in gameNo 3D cameraSpawn Camera3dBundle
UI stacked at top-leftNo parent-child nestingUse with_children
Panic index out of boundsEmpty Vec in InputStateInitialize vec![false; 256]
Cursor won’t move, camera won’t turnCursorMoved stops firing after pointer lockUse MouseMotion events
yaw/pitch updates but screen doesn’t rotateCamera is a separate entitySpawn camera as child of player
Buttons unresponsiveChanged<Interaction> filter misses eventsRemove Changed, poll every frame

④ Core Pitfall: CursorMoved Stops After Pointer Lock

Symptom: After entering the game scene with CursorGrabMode::Locked, mouse movement produces no camera rotation. Logs show no MouseMotion or CursorMoved events.

Root cause: When the pointer is locked, CursorMoved events stop firing because the OS no longer reports absolute cursor positions.

Fix: Use MouseMotion (DeviceEvent-level) which reports relative mouse displacement and is unaffected by pointer lock:

#![allow(unused)]
fn main() {
fn player_look(
    mut mouse_motion: EventReader<MouseMotion>,
    mut player_query: Query<&mut Player, With<PlayerFlag>>,
) {
    for motion in mouse_motion.read() {
        if let Ok(mut player) = player_query.get_single_mut() {
            player.yaw -= motion.delta.x * 0.003;
            player.pitch = (player.pitch - motion.delta.y * 0.003)
                .clamp(-1.54, 1.54);
        }
    }
}
}

⑤ Camera as Child Entity

Fix: Spawn the camera as a child of the player entity:

#![allow(unused)]
fn main() {
fn spawn_player(mut commands: Commands) {
    commands.spawn((
        PlayerFlag,
        SpatialBundle {
            transform: Transform::from_xyz(0.0, 0.0, 0.0),
            ..default()
        },
    )).with_children(|parent| {
        parent.spawn(Camera3dBundle {
            transform: Transform::from_xyz(0.0, 1.6, 0.0),
            ..default()
        });
    });
}
}

⑥ Button Click Not Responding

Fix: Remove Changed<Interaction> filter, poll every frame:

#![allow(unused)]
fn main() {
fn button_handler(
    query: Query<(&Interaction, &Btn)>,
    mut next_state: ResMut<NextState<GameState>>,
) {
    for (interaction, btn) in query.iter() {
        if *interaction == Interaction::Clicked {
            match btn {
                Btn::Start => next_state.set(GameState::Deploy),
                // ...
            }
        }
    }
}
}