bevy/examples/helpers/camera_controller.rs
Zachary Harrold 6299e3de3b
Add examples/helpers/* as library examples (#18288)
# Objective

Some of Bevy's examples contain boilerplate which is split out into the
`helpers` folder. This allows examples to have access to common
functionality without building into Bevy directly. However, these
helpers are themselves quite high-quality code, and we do intend for
users to read them and even use them. But, we don't list them in the
examples document, and they aren't explicitly checked in CI, only
transitively through examples which import them.

## Solution

- Added `camera_controller` and `widgets` as library examples.

## Testing

- CI

---

## Notes

- Library examples are identical to any other example, just with
`crate-type = ["lib"]` in the `Cargo.toml`. Since they are marked as
libraries, they don't require a `main` function but do require public
items to be documented.
- Library examples opens the possibility of creating examples which
don't need to be actual runnable applications. This may be more
appropriate for certain ECS examples, and allows for adding helpers
which (currently) don't have an example that needs them without them
going stale.
- I learned about this as a concept during research for `no_std`
examples, but believe it has value for Bevy outside that specific niche.

---------

Co-authored-by: mgi388 <135186256+mgi388@users.noreply.github.com>
Co-authored-by: Carter Weinberg <weinbergcarter@gmail.com>
2025-03-13 16:34:16 +00:00

252 lines
8.5 KiB
Rust

//! A freecam-style camera controller plugin.
//! To use in your own application:
//! - Copy the code for the [`CameraControllerPlugin`] and add the plugin to your App.
//! - Attach the [`CameraController`] component to an entity with a [`Camera3d`].
//!
//! Unlike other examples, which demonstrate an application, this demonstrates a plugin library.
use bevy::{
input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit},
prelude::*,
window::CursorGrabMode,
};
use std::{f32::consts::*, fmt};
/// A freecam-style camera controller plugin.
pub struct CameraControllerPlugin;
impl Plugin for CameraControllerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, run_camera_controller);
}
}
/// Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0,
/// but I'm guessing it is a misunderstanding between degrees/radians and then sticking with
/// it because it felt nice.
pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0;
/// Camera controller [`Component`].
#[derive(Component)]
pub struct CameraController {
/// Enables this [`CameraController`] when `true`.
pub enabled: bool,
/// Indicates if this controller has been initialized by the [`CameraControllerPlugin`].
pub initialized: bool,
/// Multiplier for pitch and yaw rotation speed.
pub sensitivity: f32,
/// [`KeyCode`] for forward translation.
pub key_forward: KeyCode,
/// [`KeyCode`] for backward translation.
pub key_back: KeyCode,
/// [`KeyCode`] for left translation.
pub key_left: KeyCode,
/// [`KeyCode`] for right translation.
pub key_right: KeyCode,
/// [`KeyCode`] for up translation.
pub key_up: KeyCode,
/// [`KeyCode`] for down translation.
pub key_down: KeyCode,
/// [`KeyCode`] to use [`run_speed`](CameraController::run_speed) instead of
/// [`walk_speed`](CameraController::walk_speed) for translation.
pub key_run: KeyCode,
/// [`MouseButton`] for grabbing the mouse focus.
pub mouse_key_cursor_grab: MouseButton,
/// [`KeyCode`] for grabbing the keyboard focus.
pub keyboard_key_toggle_cursor_grab: KeyCode,
/// Multiplier for unmodified translation speed.
pub walk_speed: f32,
/// Multiplier for running translation speed.
pub run_speed: f32,
/// Multiplier for how the mouse scroll wheel modifies [`walk_speed`](CameraController::walk_speed)
/// and [`run_speed`](CameraController::run_speed).
pub scroll_factor: f32,
/// Friction factor used to exponentially decay [`velocity`](CameraController::velocity) over time.
pub friction: f32,
/// This [`CameraController`]'s pitch rotation.
pub pitch: f32,
/// This [`CameraController`]'s yaw rotation.
pub yaw: f32,
/// This [`CameraController`]'s translation velocity.
pub velocity: Vec3,
}
impl Default for CameraController {
fn default() -> Self {
Self {
enabled: true,
initialized: false,
sensitivity: 1.0,
key_forward: KeyCode::KeyW,
key_back: KeyCode::KeyS,
key_left: KeyCode::KeyA,
key_right: KeyCode::KeyD,
key_up: KeyCode::KeyE,
key_down: KeyCode::KeyQ,
key_run: KeyCode::ShiftLeft,
mouse_key_cursor_grab: MouseButton::Left,
keyboard_key_toggle_cursor_grab: KeyCode::KeyM,
walk_speed: 5.0,
run_speed: 15.0,
scroll_factor: 0.1,
friction: 0.5,
pitch: 0.0,
yaw: 0.0,
velocity: Vec3::ZERO,
}
}
}
impl fmt::Display for CameraController {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"
Freecam Controls:
Mouse\t- Move camera orientation
Scroll\t- Adjust movement speed
{:?}\t- Hold to grab cursor
{:?}\t- Toggle cursor grab
{:?} & {:?}\t- Fly forward & backwards
{:?} & {:?}\t- Fly sideways left & right
{:?} & {:?}\t- Fly up & down
{:?}\t- Fly faster while held",
self.mouse_key_cursor_grab,
self.keyboard_key_toggle_cursor_grab,
self.key_forward,
self.key_back,
self.key_left,
self.key_right,
self.key_up,
self.key_down,
self.key_run,
)
}
}
fn run_camera_controller(
time: Res<Time>,
mut windows: Query<&mut Window>,
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
mut toggle_cursor_grab: Local<bool>,
mut mouse_cursor_grab: Local<bool>,
mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>,
) {
let dt = time.delta_secs();
let Ok((mut transform, mut controller)) = query.single_mut() else {
return;
};
if !controller.initialized {
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
controller.yaw = yaw;
controller.pitch = pitch;
controller.initialized = true;
info!("{}", *controller);
}
if !controller.enabled {
return;
}
let mut scroll = 0.0;
let amount = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => accumulated_mouse_scroll.delta.y / 16.0,
};
scroll += amount;
controller.walk_speed += scroll * controller.scroll_factor * controller.walk_speed;
controller.run_speed = controller.walk_speed * 3.0;
// Handle key input
let mut axis_input = Vec3::ZERO;
if key_input.pressed(controller.key_forward) {
axis_input.z += 1.0;
}
if key_input.pressed(controller.key_back) {
axis_input.z -= 1.0;
}
if key_input.pressed(controller.key_right) {
axis_input.x += 1.0;
}
if key_input.pressed(controller.key_left) {
axis_input.x -= 1.0;
}
if key_input.pressed(controller.key_up) {
axis_input.y += 1.0;
}
if key_input.pressed(controller.key_down) {
axis_input.y -= 1.0;
}
let mut cursor_grab_change = false;
if key_input.just_pressed(controller.keyboard_key_toggle_cursor_grab) {
*toggle_cursor_grab = !*toggle_cursor_grab;
cursor_grab_change = true;
}
if mouse_button_input.just_pressed(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = true;
cursor_grab_change = true;
}
if mouse_button_input.just_released(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = false;
cursor_grab_change = true;
}
let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
// Apply movement update
if axis_input != Vec3::ZERO {
let max_speed = if key_input.pressed(controller.key_run) {
controller.run_speed
} else {
controller.walk_speed
};
controller.velocity = axis_input.normalize() * max_speed;
} else {
let friction = controller.friction.clamp(0.0, 1.0);
controller.velocity *= 1.0 - friction;
if controller.velocity.length_squared() < 1e-6 {
controller.velocity = Vec3::ZERO;
}
}
let forward = *transform.forward();
let right = *transform.right();
transform.translation += controller.velocity.x * dt * right
+ controller.velocity.y * dt * Vec3::Y
+ controller.velocity.z * dt * forward;
// Handle cursor grab
if cursor_grab_change {
if cursor_grab {
for mut window in &mut windows {
if !window.focused {
continue;
}
window.cursor_options.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false;
}
} else {
for mut window in &mut windows {
window.cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
}
}
}
// Handle mouse input
if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
// Apply look update
controller.pitch = (controller.pitch
- accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * controller.sensitivity)
.clamp(-PI / 2., PI / 2.);
controller.yaw -=
accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * controller.sensitivity;
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, controller.yaw, controller.pitch);
}
}