//! A simple example of setting up a first-person character controlled player. use bevy::render::camera::Projection; use bevy::window::CursorGrabMode; use bevy::{ input::mouse::MouseMotion, prelude::*, window::{Cursor, PrimaryWindow}, }; use bevy_mod_wanderlust::{ ControllerBundle, ControllerInput, ControllerPhysicsBundle, ControllerSettings, WanderlustPlugin, }; use bevy_rapier3d::prelude::*; use std::f32::consts::FRAC_2_PI; fn main() { App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { cursor: Cursor { visible: false, grab_mode: CursorGrabMode::Locked, ..default() }, ..default() }), ..default() }), RapierPhysicsPlugin::::default(), // This plugin was causing unhelpful glitchy orange planes, so it's commented out until // it's working again // RapierDebugRenderPlugin::default(), WanderlustPlugin, aether_spyglass::SpyglassPlugin, )) .insert_resource(Sensitivity(1.0)) .add_systems(Startup, setup) // Add to PreUpdate to ensure updated before movement is calculated .add_systems( Update, ( movement_input.before(bevy_mod_wanderlust::movement), mouse_look, toggle_cursor_lock, ), ) .run() } #[derive(Component, Default, Reflect)] #[reflect(Component)] struct PlayerCam; #[derive(Component, Default, Reflect)] #[reflect(Component)] struct PlayerBody; #[derive(Reflect, Resource)] struct Sensitivity(f32); fn setup( mut commands: Commands, mut meshes: ResMut>, mut mats: ResMut>, ) { let mesh = meshes.add( shape::Capsule { radius: 0.5, depth: 1.0, ..default() } .into(), ); let material = mats.add(Color::WHITE.into()); commands .spawn(( ControllerBundle { settings: ControllerSettings::character(), physics: ControllerPhysicsBundle { // Lock the axes to prevent camera shake whilst moving up slopes locked_axes: LockedAxes::ROTATION_LOCKED, ..default() }, ..default() }, Name::from("Player"), PlayerBody, )) .insert(PbrBundle { mesh, material: material.clone(), ..default() }) .with_children(|commands| { commands .spawn(( Camera3dBundle { transform: Transform::from_xyz(0.0, 0.5, 0.0), projection: Projection::Perspective(PerspectiveProjection { fov: 90.0 * (std::f32::consts::PI / 180.0), aspect_ratio: 1.0, near: 0.3, far: 1000.0, }), ..default() }, PlayerCam, )) .with_children(|commands| { let mesh = meshes.add(shape::Cube { size: 0.5 }.into()); commands.spawn(PbrBundle { mesh, material: material.clone(), transform: Transform::from_xyz(0.0, 0.0, -0.5), ..default() }); }); }); let mesh = meshes.add( shape::Plane { size: 10.0, ..default() } .into(), ); commands.spawn(( PbrBundle { mesh, material: material.clone(), transform: Transform::from_xyz(0.0, -10.0, 0.0), ..default() }, Collider::halfspace(Vec3::Y * 10.0).unwrap(), Name::from("Ground"), )); commands.spawn(PointLightBundle { transform: Transform::from_xyz(0.0, -5.0, 0.0), ..default() }); let (hw, hh, hl) = (1.5, 0.5, 5.0); let mesh = meshes.add( shape::Box { min_x: -hw, max_x: hw, min_y: -hh, max_y: hh, min_z: -hl, max_z: hl, } .into(), ); commands.spawn(( PbrBundle { mesh, material: material.clone(), transform: Transform::from_xyz(-3.5, -8.0, 0.3).with_rotation(Quat::from_euler( EulerRot::XYZ, 0.5, 0.0, 0.0, )), ..default() }, Name::from("Slope"), Collider::cuboid(hw, hh, hl), )); let (hw, hh, hl) = (0.25, 3.0, 5.0); let mesh = meshes.add( shape::Box { min_x: -hw, max_x: hw, min_y: -hh, max_y: hh, min_z: -hl, max_z: hl, } .into(), ); commands.spawn(( PbrBundle { mesh: mesh.clone(), material: material.clone(), transform: Transform::from_xyz(3.5, -8.0, 0.0), ..default() }, Name::from("Wall"), Collider::cuboid(hw, hh, hl), )); commands.spawn(( PbrBundle { mesh, material, transform: Transform::from_xyz(6.5, -8.0, 0.0).with_rotation(Quat::from_euler( EulerRot::XYZ, 0.0, -std::f32::consts::FRAC_PI_4, 0.0, )), ..default() }, Name::from("Wall"), Collider::cuboid(hw, hh, hl), )); } fn movement_input( mut body: Query<&mut ControllerInput, With>, camera: Query<&GlobalTransform, (With, Without)>, input: Res>, ) { let tf = camera.single(); let mut player_input = body.single_mut(); let mut dir = Vec3::ZERO; if input.pressed(KeyCode::A) { dir += -tf.right(); } if input.pressed(KeyCode::D) { dir += tf.right(); } if input.pressed(KeyCode::S) { dir += -tf.forward(); } if input.pressed(KeyCode::W) { dir += tf.forward(); } dir.y = 0.0; player_input.movement = dir.normalize_or_zero(); player_input.jumping = input.pressed(KeyCode::Space); } fn mouse_look( mut cam: Query<&mut Transform, With>, mut body: Query<&mut Transform, (With, Without)>, sensitivity: Res, mut input: EventReader, ) { let mut cam_tf = cam.single_mut(); let mut body_tf = body.single_mut(); let sens = sensitivity.0; let mut cumulative: Vec2 = -(input.iter().map(|motion| &motion.delta).sum::()); // Vertical let rot = cam_tf.rotation; // Ensure the vertical rotation is clamped if rot.x > FRAC_2_PI && cumulative.y.is_sign_positive() || rot.x < -FRAC_2_PI && cumulative.y.is_sign_negative() { cumulative.y = 0.0; } cam_tf.rotate(Quat::from_scaled_axis( rot * Vec3::X * cumulative.y / 180.0 * sens, )); // Horizontal let rot = body_tf.rotation; body_tf.rotate(Quat::from_scaled_axis( rot * Vec3::Y * cumulative.x / 180.0 * sens, )); } fn toggle_cursor_lock( input: Res>, mut windows: Query<&mut Window, With>, ) { if input.just_pressed(KeyCode::Escape) { let mut window = windows.single_mut(); match window.cursor.grab_mode { CursorGrabMode::Locked => { window.cursor.grab_mode = CursorGrabMode::None; window.cursor.visible = true; } _ => { window.cursor.grab_mode = CursorGrabMode::Locked; window.cursor.visible = false; } } } }