Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
267 changes: 267 additions & 0 deletions examples/camera/free_cam_controller.rs
Original file line number Diff line number Diff line change
@@ -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<ButtonInput<KeyCode>>) {
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<InfoText>>, 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<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
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()
},
));
}