diff --git a/Cargo.toml b/Cargo.toml index 86acffa412df2..4ab6d330942b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4127,6 +4127,18 @@ description = "A first-person camera that uses a world model and a view model wi category = "Camera" wasm = true +[[example]] +name = "free_cam_controller" +path = "examples/camera/free_cam_controller.rs" +doc-scrape-examples = true +required-features = ["free_cam"] + +[package.metadata.example.free_cam_controller] +name = "Free cam camera controller" +description = "Shows the default free cam camera controller." +category = "Camera" +wasm = true + [[example]] name = "projection_zoom" path = "examples/camera/projection_zoom.rs" diff --git a/examples/README.md b/examples/README.md index 349c6adb965f5..0a758d02200f3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -285,6 +285,7 @@ Example | Description [Camera Orbit](../examples/camera/camera_orbit.rs) | Shows how to orbit a static scene using pitch, yaw, and roll. [Custom Projection](../examples/camera/custom_projection.rs) | Shows how to create custom camera projections. [First person view model](../examples/camera/first_person_view_model.rs) | A first-person camera that uses a world model and a view model with different field of views (FOV) +[Free cam camera controller](../examples/camera/free_cam_controller.rs) | Shows the default free cam camera controller. [Projection Zoom](../examples/camera/projection_zoom.rs) | Shows how to zoom orthographic and perspective projection cameras. [Screen Shake](../examples/camera/2d_screen_shake.rs) | A simple 2D screen shake effect diff --git a/examples/camera/free_cam_controller.rs b/examples/camera/free_cam_controller.rs new file mode 100644 index 0000000000000..1443dda8519f3 --- /dev/null +++ b/examples/camera/free_cam_controller.rs @@ -0,0 +1,267 @@ +//! This example showcases the default freecam camera controller. +//! +//! The default freecam controller is useful for exploring large scenes, debugging and editing purposes. To use it, +//! simply add the [`FreeCamPlugin`] to your [`App`] and attach the [`FreeCam`] component to the camera entity you +//! wish to control. +//! +//! ## Default Controls +//! +//! This controller has a simple 6-axis control scheme, and mouse controls for camera orientation. There are also +//! bindings for capturing the mouse, both while holding the button and toggle, a run feature that increases the +//! max speed, and scrolling changes the movement speed. All keybinds can be changed by editing the [`FreeCam`] +//! component. +//! +//! | Default Key Binding | Action | +//! |:--------------------|:-----------------------| +//! | Mouse | Look around | +//! | Left click | Capture mouse (hold) | +//! | M | Capture mouse (toggle) | +//! | WASD | Horizontal movement | +//! | QE | Vertical movement | +//! | Left shift | Run | +//! | Scroll wheel | Change movement speed | +//! +//! The movement speed, sensitivity and friction can also be changed by the [`FreeCam`] component. +//! +//! ## Example controls +//! +//! This example also provides a few extra keybinds to change the camera sensitivity, friction (how fast the camera +//! stops), scroll factor (how much scrolling changes speed) and enabling/disabling the controller. +//! +//! | Key Binding | Action | +//! |:------------|:-----------------------| +//! | Z | Decrease sensitivity | +//! | X | Increase sensitivity | +//! | C | Decrease friction | +//! | V | Increase friction | +//! | F | Decrease scroll factor | +//! | G | Increase scroll factor | +//! | B | Enable/Disable | + +use std::f32::consts::{FRAC_PI_4, PI}; + +use bevy::{ + camera_controller::free_cam::{FreeCam, FreeCamPlugin}, + color::palettes::tailwind, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // Plugin that enables freecam functionality + .add_plugins(FreeCamPlugin) + // Example code plugins + .add_plugins((CameraPlugin, CameraSettingsPlugin, ScenePlugin)) + .run(); +} + +// Plugin that spawns the camera. +struct CameraPlugin; +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, spawn_camera); + } +} + +fn spawn_camera(mut commands: Commands) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 1.0, 0.0).looking_to(Vec3::X, Vec3::Y), + // This component stores all camera settings and state, which is used by the FreeCamPlugin to + // control it. These properties can be changed at runtime, but beware the controller system is + // constantly using and modifying those values unless the enabled field is false. + FreeCam { + sensitivity: 0.2, + friction: 25.0, + walk_speed: 3.0, + run_speed: 9.0, + ..default() + }, + )); +} + +// Plugin that handles camera settings controls and information text +struct CameraSettingsPlugin; +impl Plugin for CameraSettingsPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PostStartup, spawn_text) + .add_systems(Update, (update_camera_settings, update_text)); + } +} + +#[derive(Component)] +struct InfoText; + +fn spawn_text(mut commands: Commands, freecam_query: Query<&FreeCam>) { + commands.spawn(( + Node { + position_type: PositionType::Absolute, + top: px(-16), + left: px(12), + ..default() + }, + children![Text::new(format!("{}", freecam_query.single().unwrap()))], + )); + commands.spawn(( + Node { + position_type: PositionType::Absolute, + bottom: px(12), + left: px(12), + ..default() + }, + children![Text::new(concat![ + "Z/X: decrease/increase sensitivity\n", + "C/V: decrease/increase friction\n", + "F/G: decrease/increase scroll factor\n", + "B: enable/disable controller", + ]),], + )); + + // Mutable text marked with component + commands.spawn(( + Node { + position_type: PositionType::Absolute, + top: px(12), + right: px(12), + ..default() + }, + children![(InfoText, Text::new(""))], + )); +} + +fn update_camera_settings(mut camera_query: Query<&mut FreeCam>, input: Res>) { + let mut free_cam = camera_query.single_mut().unwrap(); + + if input.pressed(KeyCode::KeyZ) { + free_cam.sensitivity = (free_cam.sensitivity - 0.005).max(0.005); + } + if input.pressed(KeyCode::KeyX) { + free_cam.sensitivity += 0.005; + } + if input.pressed(KeyCode::KeyC) { + free_cam.friction = (free_cam.friction - 0.2).max(0.0); + } + if input.pressed(KeyCode::KeyV) { + free_cam.friction += 0.2; + } + if input.pressed(KeyCode::KeyF) { + free_cam.scroll_factor = (free_cam.scroll_factor - 0.02).max(0.02); + } + if input.pressed(KeyCode::KeyG) { + free_cam.scroll_factor += 0.02; + } + if input.just_pressed(KeyCode::KeyB) { + free_cam.enabled = !free_cam.enabled; + } +} + +fn update_text(mut text_query: Query<&mut Text, With>, camera_query: Query<&FreeCam>) { + let mut text = text_query.single_mut().unwrap(); + + let free_cam = camera_query.single().unwrap(); + + text.0 = format!( + "Enabled: {},\nSensitivity: {:.03}\nFriction: {:.01}\nScroll factor: {:.02}\nWalk Speed: {:.02}\nRun Speed: {:.02}\nSpeed: {:.02}", + free_cam.enabled, + free_cam.sensitivity, + free_cam.friction, + free_cam.scroll_factor, + free_cam.walk_speed, + free_cam.run_speed, + free_cam.velocity.length(), + ); +} + +// Plugin that spawns the scene and lighting. +struct ScenePlugin; +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, (spawn_lights, spawn_world)); + } +} + +fn spawn_lights(mut commands: Commands) { + // Main light + commands.spawn(( + PointLight { + color: Color::from(tailwind::ORANGE_300), + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(0.0, 3.0, 0.0), + )); + // Light behind wall + commands.spawn(( + PointLight { + color: Color::WHITE, + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(-3.5, 3.0, 0.0), + )); + // Light under floor + commands.spawn(( + PointLight { + color: Color::from(tailwind::RED_300), + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(0.0, -0.5, 0.0), + )); +} + +fn spawn_world( + mut commands: Commands, + mut materials: ResMut>, + mut meshes: ResMut>, +) { + let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0)); + let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0))); + let sphere = meshes.add(Sphere::new(0.5)); + let wall = meshes.add(Cuboid::new(0.2, 4.0, 3.0)); + + let blue_material = materials.add(Color::from(tailwind::BLUE_700)); + let red_material = materials.add(Color::from(tailwind::RED_950)); + let white_material = materials.add(Color::WHITE); + + // Top side of floor + commands.spawn(( + Mesh3d(floor.clone()), + MeshMaterial3d(white_material.clone()), + )); + // Under side of floor + commands.spawn(( + Mesh3d(floor.clone()), + MeshMaterial3d(white_material.clone()), + Transform::from_xyz(0.0, -0.01, 0.0).with_rotation(Quat::from_rotation_x(PI)), + )); + // Blue sphere + commands.spawn(( + Mesh3d(sphere.clone()), + MeshMaterial3d(blue_material.clone()), + Transform::from_xyz(3.0, 1.5, 0.0), + )); + // Tall wall + commands.spawn(( + Mesh3d(wall.clone()), + MeshMaterial3d(white_material.clone()), + Transform::from_xyz(-3.0, 2.0, 0.0), + )); + // Cube behind wall + commands.spawn(( + Mesh3d(cube.clone()), + MeshMaterial3d(blue_material.clone()), + Transform::from_xyz(-4.2, 0.5, 0.0), + )); + // Hidden cube under floor + commands.spawn(( + Mesh3d(cube.clone()), + MeshMaterial3d(red_material.clone()), + Transform { + translation: Vec3::new(3.0, -2.0, 0.0), + rotation: Quat::from_euler(EulerRot::YXZEx, FRAC_PI_4, FRAC_PI_4, 0.0), + ..default() + }, + )); +}