use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
use bevy_camera::Camera;
use bevy_ecs::prelude::*;
use bevy_input::keyboard::KeyCode;
use bevy_input::mouse::{
AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseScrollUnit,
};
use bevy_input::touch::Touches;
use bevy_input::ButtonInput;
use bevy_log::info;
use bevy_math::curve::{Interval, SampleAutoCurve};
use bevy_math::Curve;
use bevy_math::{ops::exp, Dir3, EulerRot, Quat, StableInterpolate, Vec2, Vec3};
use bevy_time::{Real, Time};
use bevy_transform::prelude::Transform;
use bevy_window::{CursorGrabMode, CursorOptions, Window};
use core::{f32::consts::*, fmt};
pub struct FreeCameraPlugin;
impl Plugin for FreeCameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
RunFixedMainLoop,
(run_freecamera_controller, rotate_freecam_to)
.chain()
.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
);
}
}
const RADIANS_PER_DOT: f32 = 1.0 / 180.0;
#[derive(Component, Clone)]
#[require(FreeCameraState)]
pub struct FreeCamera {
pub sensitivity: f32,
pub key_forward: KeyCode,
pub key_back: KeyCode,
pub key_left: KeyCode,
pub key_right: KeyCode,
pub key_up: KeyCode,
pub key_down: KeyCode,
pub key_run: KeyCode,
pub mouse_key_cursor_grab: MouseButton,
pub keyboard_key_toggle_cursor_grab: KeyCode,
pub key_snap_reverse: KeyCode,
pub axis_top: KeyCode,
pub axis_right: KeyCode,
pub axis_front: KeyCode,
pub walk_speed: f32,
pub run_speed: f32,
pub scroll_factor: f32,
pub friction: f32,
pub rotation_speed: f32,
pub vertical_movement_axis: VerticalMovementAxis,
}
impl Default for FreeCamera {
fn default() -> Self {
Self {
sensitivity: 0.2,
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::Right,
keyboard_key_toggle_cursor_grab: KeyCode::KeyM,
key_snap_reverse: KeyCode::ControlLeft,
axis_top: KeyCode::Numpad7,
axis_right: KeyCode::Numpad3,
axis_front: KeyCode::Numpad1,
walk_speed: 5.0,
run_speed: 15.0,
scroll_factor: 0.04879016,
friction: 40.0,
rotation_speed: PI / 16.0 * 60.0,
vertical_movement_axis: VerticalMovementAxis::default(),
}
}
}
impl fmt::Display for FreeCamera {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"
Freecamera 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
[{:?} + ]{:?}\t- Snap to Up (+Y)/Down (-Y)
[{:?} + ]{:?}\t- Snap to Right (+X)/Left (-X)
[{:?} + ]{:?}\t- Snap to Front (-Z)/Back (+Z)",
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,
self.key_snap_reverse,
self.axis_top,
self.key_snap_reverse,
self.axis_right,
self.key_snap_reverse,
self.axis_front,
)
}
}
#[derive(Debug, Default, Clone, Copy)]
pub enum VerticalMovementAxis {
#[default]
World,
Local,
}
#[derive(Component)]
pub struct FreeCameraState {
pub enabled: bool,
initialized: bool,
pub pitch: f32,
pub yaw: f32,
pub speed_multiplier: f32,
pub velocity: Vec3,
pub rotation_curve: Option<(f32, SampleAutoCurve<Quat>)>,
}
impl Default for FreeCameraState {
fn default() -> Self {
Self {
enabled: true,
initialized: false,
pitch: 0.0,
yaw: 0.0,
speed_multiplier: 1.0,
velocity: Vec3::ZERO,
rotation_curve: None,
}
}
}
pub fn run_freecamera_controller(
time: Res<Time<Real>>,
mut windows: Query<(&Window, &mut CursorOptions)>,
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
touch_input: Res<Touches>,
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 FreeCameraState, &FreeCamera), With<Camera>>,
) {
let dt = time.delta_secs();
let Ok((mut transform, mut state, config)) = query.single_mut() else {
return;
};
if !state.initialized {
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
state.yaw = yaw;
state.pitch = pitch;
state.initialized = true;
info!("{}", *config);
}
if !state.enabled {
if *toggle_cursor_grab || *mouse_cursor_grab {
*toggle_cursor_grab = false;
*mouse_cursor_grab = false;
for (_, mut cursor_options) in &mut windows {
cursor_options.grab_mode = CursorGrabMode::None;
cursor_options.visible = true;
}
}
return;
}
let scroll = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => {
accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
}
};
state.speed_multiplier *= exp(config.scroll_factor * scroll);
state.speed_multiplier = state.speed_multiplier.clamp(f32::EPSILON, f32::MAX);
let mut axis_input = Vec3::ZERO;
if key_input.pressed(config.key_forward) {
axis_input.z += 1.0;
}
if key_input.pressed(config.key_back) {
axis_input.z -= 1.0;
}
if key_input.pressed(config.key_right) {
axis_input.x += 1.0;
}
if key_input.pressed(config.key_left) {
axis_input.x -= 1.0;
}
if key_input.pressed(config.key_up) {
axis_input.y += 1.0;
}
if key_input.pressed(config.key_down) {
axis_input.y -= 1.0;
}
let mut cursor_grab_change = false;
if key_input.just_pressed(config.keyboard_key_toggle_cursor_grab) {
*toggle_cursor_grab = !*toggle_cursor_grab;
cursor_grab_change = true;
}
if mouse_button_input.just_pressed(config.mouse_key_cursor_grab) {
*mouse_cursor_grab = true;
cursor_grab_change = true;
}
if mouse_button_input.just_released(config.mouse_key_cursor_grab) {
*mouse_cursor_grab = false;
cursor_grab_change = true;
}
let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
if axis_input != Vec3::ZERO {
let max_speed = if key_input.pressed(config.key_run) {
config.run_speed * state.speed_multiplier
} else {
config.walk_speed * state.speed_multiplier
};
state.velocity = axis_input.normalize() * max_speed;
} else {
let friction = config.friction.clamp(0.0, f32::MAX);
state.velocity.smooth_nudge(&Vec3::ZERO, friction, dt);
if state.velocity.length_squared() < 1e-6 {
state.velocity = Vec3::ZERO;
}
}
if state.velocity != Vec3::ZERO {
let forward = *transform.forward();
let right = *transform.right();
let up = match config.vertical_movement_axis {
VerticalMovementAxis::World => Vec3::Y,
VerticalMovementAxis::Local => *transform.up(),
};
transform.translation += state.velocity.x * dt * right
+ state.velocity.y * dt * up
+ state.velocity.z * dt * forward;
}
if cursor_grab_change {
if cursor_grab {
for (window, mut cursor_options) in &mut windows {
if !window.focused {
continue;
}
cursor_options.grab_mode = CursorGrabMode::Locked;
cursor_options.visible = false;
}
} else {
for (_, mut cursor_options) in &mut windows {
cursor_options.grab_mode = CursorGrabMode::None;
cursor_options.visible = true;
}
}
}
if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
state.pitch = (state.pitch
- accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * config.sensitivity)
.clamp(-PI / 2., PI / 2.);
state.yaw -= accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * config.sensitivity;
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
}
for touch in touch_input.iter() {
if touch.delta() != Vec2::ZERO {
state.pitch = (state.pitch - touch.delta().y * RADIANS_PER_DOT * config.sensitivity)
.clamp(-PI / 2., PI / 2.);
state.yaw -= touch.delta().x * RADIANS_PER_DOT * config.sensitivity;
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
}
}
let mod_key_pressed = key_input.pressed(config.key_snap_reverse);
let mut rotate_to = None;
if key_input.just_pressed(config.axis_front) {
if mod_key_pressed {
rotate_to = Some((Dir3::Z, Dir3::Y));
} else {
rotate_to = Some((Dir3::NEG_Z, Dir3::Y));
}
}
if key_input.just_pressed(config.axis_right) {
if mod_key_pressed {
rotate_to = Some((Dir3::NEG_X, Dir3::Y));
} else {
rotate_to = Some((Dir3::X, Dir3::Y));
}
}
if key_input.just_pressed(config.axis_top) {
if mod_key_pressed {
rotate_to = Some((Dir3::NEG_Y, Dir3::NEG_Z));
} else {
rotate_to = Some((Dir3::Y, Dir3::Z));
}
}
if let Some((dir, up)) = rotate_to {
let start = transform.rotation;
let target = Transform::default().looking_to(dir, up).rotation; let angle = target.angle_between(start);
let rotation_time = angle / config.rotation_speed;
if let Ok(interval) = Interval::new(0.0, rotation_time) {
let curve = SampleAutoCurve::new(interval, [start, target])
.expect("Interval should be in bounds as start and end are finite numbers");
state.rotation_curve = Some((0.0, curve));
}
}
}
pub fn rotate_freecam_to(
mut query: Query<(&mut Transform, &mut FreeCameraState), With<Camera>>,
time: Res<Time<Real>>,
) {
let Ok((mut transform, mut state)) = query.single_mut() else {
return;
};
if !state.enabled {
return;
}
let Some((progress, curve)) = state.rotation_curve.as_mut() else {
return;
};
*progress += time.delta_secs();
transform.rotation = curve.sample_clamped(*progress);
if !curve.domain().contains(*progress) {
state.rotation_curve = None;
}
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
state.pitch = pitch;
state.yaw = yaw;
}