Skip to content
Open
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
18 changes: 5 additions & 13 deletions src/gamepad.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::f32::consts::PI;

use crate::{zoom_condition, ThirdPersonCamera};
use bevy::{prelude::*, window::PrimaryWindow};
use std::f32::consts::PI;

pub struct GamePadPlugin;

Expand Down Expand Up @@ -73,29 +72,22 @@ pub fn orbit_gamepad(
if cam.mouse_orbit_button_enabled && !gamepad.pressed(cam.gamepad_settings.mouse_orbit_button) {
return;
}

let x_axis = gamepad.right_stick().x;
let y_axis = gamepad.right_stick().y;
let (x, y) = (gamepad.right_stick().x, gamepad.right_stick().y);

let deadzone = 0.5;
let mut rotation = Vec2::ZERO;
let (x, y) = (x_axis, y_axis);
if x.abs() > deadzone || y.abs() > deadzone {
rotation = Vec2::new(x, y);
}

if rotation.length_squared() > 0.0 {
let window = window_q.single().unwrap();
let delta_x = {
let delta = rotation.x / window.width()
* std::f32::consts::PI
* 2.0
* cam.gamepad_settings.sensitivity.x;
delta
};
let delta_x = rotation.x / window.width() * PI * 2.0 * cam.gamepad_settings.sensitivity.x;
let delta_y = -rotation.y / window.height() * PI * cam.gamepad_settings.sensitivity.y;

let yaw = Quat::from_rotation_y(-delta_x);
let pitch = Quat::from_rotation_x(-delta_y);

cam_transform.rotation = yaw * cam_transform.rotation; // rotate around global y axis

let new_rotation = cam_transform.rotation * pitch;
Expand Down
120 changes: 80 additions & 40 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod gamepad;
mod mouse;

use std::f32::consts::PI;

use bevy::{
prelude::*,
window::{CursorGrabMode, PrimaryWindow},
Expand All @@ -13,9 +15,7 @@ use mouse::MousePlugin;
/// ```
/// use bevy::prelude::*;
/// use bevy_third_person_camera::ThirdPersonCameraPlugin;
/// fn main() {
/// App::new().add_plugins(ThirdPersonCameraPlugin);
/// }
/// App::new().add_plugins(ThirdPersonCameraPlugin);
/// ```
pub struct ThirdPersonCameraPlugin;

Expand Down Expand Up @@ -70,6 +70,10 @@ pub struct ThirdPersonCamera {
/// The smaller the value, the greater the zoom distance. 0.1 would essentially look like 'first person'.
/// Default is 0.7
pub aim_zoom: f32,
/// Boundaries for orbiting.
/// Can be used for a floor bound, side change or artistic dramatic effect
/// where you could only move camera in a narrow corridor.
pub bounds: Vec<Bound>,
/// Flag to indicate if the cursor lock toggle functionality is turned on.
/// When enabled and the cursor lock is NOT active, the mouse can freely move about the window without the camera's transform changing.
/// Example usage: Browsing a character inventory without moving the camera.
Expand Down Expand Up @@ -110,6 +114,9 @@ pub struct ThirdPersonCamera {
/// The speed at which the x offset will transition.
/// Default is 5.0
pub offset_toggle_speed: f32,
/// Pitch limit in radians.
/// Defaults to just under 90 degrees (PI/2 - 0.05) = 89.5
pub pitch_limit: f32,
/// Flag to indicate whether a camera zoom is applied or not.
/// Default is true
pub zoom_enabled: bool,
Expand All @@ -131,6 +138,7 @@ impl Default for ThirdPersonCamera {
aim_button: MouseButton::Right,
aim_speed: 3.0,
aim_zoom: 0.7,
bounds: vec![Bound::NO_FLIP],
cursor_lock_key: KeyCode::Space,
cursor_lock_toggle_enabled: true,
gamepad_settings: CustomGamepadSettings::default(),
Expand All @@ -143,13 +151,62 @@ impl Default for ThirdPersonCamera {
offset_toggle_enabled: false,
offset_toggle_speed: 5.0,
offset_toggle_key: KeyCode::KeyE,
pitch_limit: PI / 2.0 - 0.05,
zoom_enabled: true,
zoom: Zoom::new(1.5, 3.0),
zoom_sensitivity: 1.0,
}
}
}

impl ThirdPersonCamera {
pub fn with_custom_settings(mut self, gamepad_settings: CustomGamepadSettings) -> Self {
self.gamepad_settings = gamepad_settings;
self
}
}

/// Bounds to restrict camera movement
///
/// Example:
/// ```rust,no_run
///
/// let bounds = vec![
/// // Ground plane: don't go below y = 0
/// Bound {
/// normal: Vec3::Y,
/// point: Vec3::ZERO,
/// },
/// // Left wall: x ≥ -5.0
/// Bound {
/// normal: Vec3::X,
/// point: Vec3::new(-5.0, 0.0, 0.0),
/// },
/// // Right wall: x ≤ 5.0
/// Bound {
/// normal: -Vec3::X,
/// point: Vec3::new(5.0, 0.0, 0.0),
/// },
/// ],
/// ```
pub struct Bound {
pub normal: Vec3,
pub point: Vec3,
}

impl Bound {
// Ensures camera doesn't flip downside
pub const NO_FLIP: Bound = Bound {
normal: Vec3::NEG_Y, // pointing downward
point: Vec3::ZERO, // since this is an orientation-only check, point can be origin
};
// Ensures camera doesn't go lower than floor
pub const ABOVE_FLOOR: Bound = Bound {
normal: Vec3::Y, // pointing downward
point: Vec3::ZERO, // since this is an orientation-only check, point can be origin
};
}

/// Sets the zoom bounds (min & max)
pub struct Zoom {
pub min: f32,
Expand Down Expand Up @@ -195,19 +252,9 @@ impl Offset {
/// use bevy::prelude::*;
/// use bevy_third_person_camera::{CustomGamepadSettings, ThirdPersonCamera};
/// fn spawn_camera(mut commands: Commands) {
/// let gamepad = Gamepad::new(0);
/// let settings = CustomGamepadSettings::default()
/// commands.spawn((
/// ThirdPersonCamera {
/// gamepad_settings: CustomGamepadSettings {
/// aim_button: GamepadButton::new(gamepad, GamepadButtonType::LeftTrigger2),
/// mouse_orbit_button: GamepadButton::new(gamepad, GamepadButtonType::LeftTrigger),
/// offset_toggle_button: GamepadButton::new(gamepad, GamepadButtonType::DPadRight),
/// sensitivity: Vec2::new(7.0, 4.0),
/// zoom_in_button: GamepadButton::new(gamepad, GamepadButtonType::DPadUp),
/// zoom_out_button: GamepadButton::new(gamepad, GamepadButtonType::DPadDown),
/// },
/// ..default()
/// },
/// ThirdPersonCamera::default().with_custom_settings(settings)
/// Camera3dBundle::default(),
/// ));
/// }
Expand Down Expand Up @@ -299,6 +346,7 @@ fn aim_condition(cam_q: Query<&ThirdPersonCamera, With<ThirdPersonCamera>>) -> b
cam.aim_enabled
}

#[allow(clippy::type_complexity)]
fn aim(
mut cam_q: Query<
(&mut ThirdPersonCamera, &Transform),
Expand All @@ -317,17 +365,11 @@ fn aim(
return;
};

let gamepad = match gamepad_q.single() {
Ok(value) => Some(value),
Err(_) => None,
};

let is_gamepad_aiming = match gamepad {
Some(gp) => gp.pressed(cam.gamepad_settings.aim_button),
None => false,
};

// check if aim button was pressed
let is_gamepad_aiming = gamepad_q
.single()
.map(|gp| gp.pressed(cam.gamepad_settings.aim_button))
.unwrap_or_default();
let is_mouse_aiming = mouse.pressed(cam.aim_button);

if is_mouse_aiming || is_gamepad_aiming {
Expand All @@ -351,30 +393,28 @@ fn aim(
} else {
cam.zoom.radius -= zoom_factor;
}
} else {
if let Some(radius_copy) = cam.zoom.radius_copy {
let zoom_factor = (radius_copy / cam.aim_zoom) * cam.aim_speed * time.delta_secs();

// stop zooming out if current radius is greater than original radius
if cam.zoom.radius >= radius_copy || cam.zoom.radius + zoom_factor >= radius_copy {
cam.zoom.radius = radius_copy;
cam.zoom.radius_copy = None;
} else {
cam.zoom.radius += (radius_copy / cam.aim_zoom) * cam.aim_speed * time.delta_secs();
}
} else if let Some(radius_copy) = cam.zoom.radius_copy {
let zoom_factor = (radius_copy / cam.aim_zoom) * cam.aim_speed * time.delta_secs();

// stop zooming out if current radius is greater than original radius
if cam.zoom.radius >= radius_copy || cam.zoom.radius + zoom_factor >= radius_copy {
cam.zoom.radius = radius_copy;
cam.zoom.radius_copy = None;
} else {
cam.zoom.radius += (radius_copy / cam.aim_zoom) * cam.aim_speed * time.delta_secs();
}
}
}

pub fn zoom_condition(cam_q: Query<&ThirdPersonCamera, With<ThirdPersonCamera>>) -> bool {
pub fn zoom_condition(cam_q: Query<&ThirdPersonCamera>) -> bool {
let Ok(cam) = cam_q.single() else {
return false;
};
return cam.zoom_enabled && cam.cursor_lock_active;
cam.zoom_enabled && cam.cursor_lock_active
}

// only run toggle_x_offset if `offset_toggle_enabled` is true
fn toggle_x_offset_condition(cam_q: Query<&ThirdPersonCamera, With<ThirdPersonCamera>>) -> bool {
fn toggle_x_offset_condition(cam_q: Query<&ThirdPersonCamera>) -> bool {
let Ok(cam) = cam_q.single() else {
return false;
};
Expand All @@ -383,7 +423,7 @@ fn toggle_x_offset_condition(cam_q: Query<&ThirdPersonCamera, With<ThirdPersonCa

// inverts the x offset. Example: left shoulder view -> right shoulder view & vice versa
fn toggle_x_offset(
mut cam_q: Query<&mut ThirdPersonCamera, With<ThirdPersonCamera>>,
mut cam_q: Query<&mut ThirdPersonCamera>,
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
btns: Query<&Gamepad>,
Expand Down
51 changes: 33 additions & 18 deletions src/mouse.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use std::f32::consts::PI;

use crate::{zoom_condition, ThirdPersonCamera};
use bevy::{
input::mouse::{MouseMotion, MouseWheel},
prelude::*,
window::PrimaryWindow,
};

use crate::{zoom_condition, ThirdPersonCamera};
use std::f32::consts::PI;

pub struct MousePlugin;

Expand All @@ -22,19 +20,20 @@ fn orbit_condition(cam_q: Query<&ThirdPersonCamera>) -> bool {
let Ok(cam) = cam_q.single() else {
return true;
};
return cam.cursor_lock_active;
cam.cursor_lock_active
}

// heavily referenced https://bevy-cheatbook.github.io/cookbook/pan-orbit-camera.html
#[allow(clippy::type_complexity)]
pub fn orbit_mouse(
window_q: Query<&Window, With<PrimaryWindow>>,
mut cam_q: Query<(&ThirdPersonCamera, &mut Transform), With<ThirdPersonCamera>>,
mut cam_q: Query<(&ThirdPersonCamera, &mut Transform)>,
mouse: Res<ButtonInput<MouseButton>>,
mut mouse_evr: EventReader<MouseMotion>,
) {
let mut rotation = Vec2::ZERO;
for ev in mouse_evr.read() {
rotation = ev.delta;
rotation += ev.delta;
}

let Ok((cam, mut cam_transform)) = cam_q.single_mut() else {
Expand All @@ -49,22 +48,38 @@ pub fn orbit_mouse(

if rotation.length_squared() > 0.0 {
let window = window_q.single().unwrap();
let delta_x = {
let delta = rotation.x / window.width() * std::f32::consts::PI * cam.sensitivity.x;
delta
};

// Calculate pitch/yaw deltas
let delta_x = rotation.x / window.width() * PI * cam.sensitivity.x;
let delta_y = rotation.y / window.height() * PI * cam.sensitivity.y;

// Current rotation
let yaw = Quat::from_rotation_y(-delta_x);
let pitch = Quat::from_rotation_x(-delta_y);
cam_transform.rotation = yaw * cam_transform.rotation; // rotate around global y axis

// Calculate the new rotation without applying it to the camera yet
let new_rotation = cam_transform.rotation * pitch;
let new_rotation = yaw * cam_transform.rotation * pitch;

// check if new rotation will cause camera to go beyond the 180 degree vertical bounds
let mut passes_bounds = true;
let up_vector = new_rotation * Vec3::Y;
if up_vector.y > 0.0 {

for bound in &cam.bounds {
// Check NO_FLIP manually
if bound.normal == Vec3::NEG_Y && bound.point == Vec3::ZERO {
if up_vector.y <= 0.0 {
passes_bounds = false;
break;
}
} else {
// Position-based bounds (e.g. floor)
let rot_matrix = Mat3::from_quat(new_rotation);
let new_position = rot_matrix * Vec3::new(0.0, 0.0, cam.zoom.radius);
let to_cam = new_position - bound.point;
if bound.normal.dot(to_cam) < 0.0 {
passes_bounds = false;
break;
}
}
}

if passes_bounds {
cam_transform.rotation = new_rotation;
}
}
Expand Down