use bevy::input::mouse::MouseWheel;
use bevy::prelude::*;
use std::ops::RangeInclusive;
use std::borrow::Cow;
use std::f32::consts::TAU;
const SCROLL_TICK_GRACE_SECS: f64 = 0.05;
pub fn rts_camera_system(
time: Res<Time>,
windows: Res<Windows>,
cursor_scroll_events: Res<Events<MouseWheel>>,
keyboard: Res<Input<KeyCode>>,
mut query: Query<(&mut RtsCamera, &mut Transform, Option<&ZoomSettings>, Option<&PanSettings>, Option<&TurnSettings>)>,
) {
static DEFAULT_ZOOM: ZoomSettings = ZoomSettings::new();
static DEFAULT_PAN: PanSettings = PanSettings::new();
static DEFAULT_TURN: TurnSettings = TurnSettings::new();
for (mut camera, mut transform, zoom, pan, turn) in query.iter_mut() {
let window = windows.get_primary().unwrap();
let cursor = match window.cursor_position() {
Some(pos) => pos,
None => return,
};
let zoom = zoom.unwrap_or(&DEFAULT_ZOOM);
let pan = pan.unwrap_or(&DEFAULT_PAN);
let turn = turn.unwrap_or(&DEFAULT_TURN);
let scroll = camera.cursor_scroll_event_reader.latest(&cursor_scroll_events).map(|e| e.y);
camera.tick(scroll, cursor, window, &keyboard, zoom, pan, turn, &time);
*transform = camera.camera_transform();
}
}
pub struct RtsCamera {
pub looking_at: Vec3,
pub rotation: Quat,
pub yaw: f32,
pub zoom_velocity: f32,
pub pan_velocity: Vec2,
pub turn_velocity: f32,
pub last_scroll_sec: f64,
pub zoom_distance: f32,
pub cursor_scroll_event_reader: EventReader<MouseWheel>,
}
impl Default for RtsCamera {
fn default() -> Self {
RtsCamera {
looking_at: Vec3::zero(),
rotation: Quat::default(),
yaw: 0.0,
zoom_velocity: 0.0,
pan_velocity: Vec2::zero(),
turn_velocity: 0.0,
last_scroll_sec: 0.0,
zoom_distance: 10.0,
cursor_scroll_event_reader: EventReader::default(),
}
}
}
impl RtsCamera {
fn camera_translation(&self) -> Vec3 {
self.looking_at + self.rotation * Vec3::new(0.0, 0.0, self.zoom_distance)
}
fn camera_transform(&self) -> Transform {
let mat4 = Mat4::from_rotation_translation(self.rotation, self.camera_translation());
Transform::from_matrix(mat4)
}
fn rotate(&mut self, angle: f32) {
self.yaw += angle;
if self.yaw > TAU {
self.yaw -= TAU;
}
if self.yaw < 0.0 {
self.yaw += TAU;
}
let rotation_y = Quat::from_rotation_y(angle);
let camera_translation = self.camera_translation();
self.looking_at = (rotation_y * (self.looking_at - camera_translation)) + camera_translation;
}
fn tick(
&mut self,
scroll: Option<f32>,
cursor: Vec2,
window: &Window,
keyboard: &Input<KeyCode>,
zoom: &ZoomSettings,
pan: &PanSettings,
turn: &TurnSettings,
time: &Time,
) {
let (delta, now) = (time.delta_seconds(), time.seconds_since_startup());
let [mut x_decel, mut y_decel, mut turn_decel]: [Deceleration; 3] = Default::default();
let mut zoom_decel = if (now - self.last_scroll_sec) < SCROLL_TICK_GRACE_SECS {
Deceleration { pos: false, neg: false }
} else {
Deceleration { pos: true, neg: true }
};
if cursor.x < pan.mouse_accel_margin {
if cursor.y > window.height() * (1.0 - turn.mouse_turn_margin) {
self.turn_velocity += turn.mouse_accel * delta;
turn_decel.pos = false;
} else {
self.pan_velocity.x -= pan.mouse_accel * delta;
x_decel.neg = false;
}
} else if cursor.x > window.width() as f32 - pan.mouse_accel_margin {
if cursor.y > window.height() * (1.0 - turn.mouse_turn_margin) {
self.turn_velocity -= turn.mouse_accel * delta;
turn_decel.neg = false;
} else {
self.pan_velocity.x += pan.mouse_accel * delta;
x_decel.pos = false;
}
}
if cursor.y < pan.mouse_accel_margin {
self.pan_velocity.y -= pan.mouse_accel * delta;
y_decel.neg = false;
} else if cursor.y > window.height() as f32 - pan.mouse_accel_margin {
self.pan_velocity.y += pan.mouse_accel * delta;
y_decel.pos = false;
}
if pan.right_keys.iter().any(|c| keyboard.pressed(*c)) {
self.pan_velocity.x += pan.keyboard_accel * delta;
x_decel.pos = false;
}
if pan.left_keys.iter().any(|c| keyboard.pressed(*c)) {
self.pan_velocity.x += -pan.keyboard_accel * delta;
x_decel.neg = false;
}
if pan.up_keys.iter().any(|c| keyboard.pressed(*c)) {
self.pan_velocity.y += pan.keyboard_accel * delta;
y_decel.pos = false;
}
if pan.down_keys.iter().any(|c| keyboard.pressed(*c)) {
self.pan_velocity.y += -pan.keyboard_accel * delta;
y_decel.neg = false;
}
if turn.right_keys.iter().any(|c| keyboard.pressed(*c)) {
self.turn_velocity -= turn.keyboard_accel * delta;
turn_decel.neg = false;
}
if turn.left_keys.iter().any(|c| keyboard.pressed(*c)) {
self.turn_velocity += turn.keyboard_accel * delta;
turn_decel.pos = false;
}
if let Some(y) = scroll {
if y > 0.0 {
zoom_decel.pos = false;
} else {
zoom_decel.neg = false;
}
self.zoom_velocity -= y * zoom.scroll_accel;
self.last_scroll_sec = now;
}
if zoom.zoom_in_keys.iter().any(|c| keyboard.pressed(*c)) {
self.zoom_velocity -= zoom.keyboard_accel * delta;
zoom_decel.pos = false;
}
if zoom.zoom_out_keys.iter().any(|c| keyboard.pressed(*c)) {
self.zoom_velocity += zoom.keyboard_accel * delta;
zoom_decel.neg = false;
}
turn_decel.apply(&mut self.turn_velocity, turn.idle_deceleration, delta);
zoom_decel.apply(&mut self.zoom_velocity, zoom.idle_deceleration, delta);
x_decel.apply(&mut self.pan_velocity.x, pan.idle_deceleration, delta);
y_decel.apply(&mut self.pan_velocity.y, pan.idle_deceleration, delta);
if self.pan_velocity.length_squared() > (pan.max_speed * pan.max_speed) {
self.pan_velocity = pan.max_speed * self.pan_velocity.normalize();
}
self.zoom_velocity = clamp(self.zoom_velocity, &(-zoom.max_velocity..=zoom.max_velocity));
self.turn_velocity = clamp(self.turn_velocity, &(-turn.max_speed..=turn.max_speed));
self.zoom_distance += self.zoom_velocity * delta;
self.zoom_distance = clamp(self.zoom_distance, &zoom.distance_range);
if self.zoom_distance == *zoom.distance_range.start() || self.zoom_distance == *zoom.distance_range.end() {
self.zoom_velocity = 0.0;
}
self.rotate(self.turn_velocity * delta);
self.yaw = clamp(self.yaw, &turn.yaw_range);
let pitch = lerp_in_zone(self.zoom_distance, &zoom.angle_change_zone, &zoom.angle_range);
self.rotation = Quat::from_rotation_ypr(self.yaw, -pitch, 0.0);
let forward = Quat::from_rotation_y(self.yaw);
let distance_factor = lerp_in_zone(self.zoom_distance, &zoom.angle_range, &pan.pan_speed_zoom_factor_range);
self.looking_at += forward * (Vec3::unit_x() * self.pan_velocity.x * delta) * distance_factor;
self.looking_at += forward * (-Vec3::unit_z() * self.pan_velocity.y * delta) * distance_factor;
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ZoomSettings {
pub angle_range: RangeInclusive<f32>,
pub angle_change_zone: RangeInclusive<f32>,
pub distance_range: RangeInclusive<f32>,
pub velocity: f32,
pub max_velocity: f32,
pub scroll_accel: f32,
pub keyboard_accel: f32,
pub idle_deceleration: f32,
pub zoom_in_keys: Cow<'static, [KeyCode]>,
pub zoom_out_keys: Cow<'static, [KeyCode]>,
}
impl ZoomSettings {
pub const fn new() -> Self {
ZoomSettings {
angle_range: 0.5705693..=1.1637539,
angle_change_zone: 5.0..=100.0,
distance_range: 5.0..=100.0,
velocity: 0.0,
max_velocity: 5.0,
scroll_accel: 5.0,
keyboard_accel: 5.0,
idle_deceleration: 5.0,
zoom_in_keys: Cow::Borrowed(&[KeyCode::Equals, KeyCode::NumpadAdd]),
zoom_out_keys: Cow::Borrowed(&[KeyCode::NumpadSubtract, KeyCode::Minus]),
}
}
}
impl Default for ZoomSettings {
fn default() -> Self { ZoomSettings::new() }
}
#[derive(Clone, PartialEq, Debug)]
pub struct PanSettings {
pub mouse_accel: f32,
pub mouse_accel_margin: f32,
pub keyboard_accel: f32,
pub max_speed: f32,
pub idle_deceleration: f32,
pub pan_speed_zoom_factor_range: RangeInclusive<f32>,
pub left_keys: Cow<'static, [KeyCode]>,
pub right_keys: Cow<'static, [KeyCode]>,
pub up_keys: Cow<'static, [KeyCode]>,
pub down_keys: Cow<'static, [KeyCode]>,
}
impl PanSettings {
pub const fn new() -> Self {
PanSettings {
mouse_accel: 15.0,
mouse_accel_margin: 10.0,
keyboard_accel: 5.0,
max_speed: 5.0,
idle_deceleration: 17.5,
pan_speed_zoom_factor_range: 1.0..=2.0,
left_keys: Cow::Borrowed(&[KeyCode::Left, KeyCode::A]),
right_keys: Cow::Borrowed(&[KeyCode::Right, KeyCode::D]),
up_keys: Cow::Borrowed(&[KeyCode::Up, KeyCode::W]),
down_keys: Cow::Borrowed(&[KeyCode::Down, KeyCode::S]),
}
}
}
impl Default for PanSettings {
fn default() -> Self { PanSettings::new() }
}
pub struct TurnSettings {
pub mouse_turn_margin: f32,
pub yaw_range: RangeInclusive<f32>,
pub mouse_accel: f32,
pub keyboard_accel: f32,
pub max_speed: f32,
pub idle_deceleration: f32,
pub left_keys: Cow<'static, [KeyCode]>,
pub right_keys: Cow<'static, [KeyCode]>,
}
impl TurnSettings {
pub const fn new() -> Self {
TurnSettings {
mouse_turn_margin: 0.25,
yaw_range: 0.0..=TAU,
mouse_accel: 0.3,
keyboard_accel: 1.8,
max_speed: 1.5,
idle_deceleration: 5.0,
left_keys: Cow::Borrowed(&[KeyCode::Q]),
right_keys: Cow::Borrowed(&[KeyCode::E]),
}
}
}
impl Default for TurnSettings {
fn default() -> Self { TurnSettings::new() }
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Deceleration {
pos: bool,
neg: bool,
}
impl Default for Deceleration {
fn default() -> Self {
Deceleration { pos: true, neg: true }
}
}
impl Deceleration {
fn apply(&self, velocity: &mut f32, magnitude: f32, delta: f32) {
if *velocity == 0.0 {
return;
}
let signum = if self.pos && self.neg {
-velocity.signum()
} else if self.pos {
-1.0
} else if self.neg {
1.0
} else {
return;
};
let max_decel = magnitude * delta;
let decel_magnitude = f32::min(max_decel.abs(), velocity.abs());
*velocity += decel_magnitude * signum;
}
}
#[must_use = "clamp returns the new value and does not modify the original"]
fn clamp(x: f32, range: &RangeInclusive<f32>) -> f32 {
if x > *range.end() {
*range.end()
} else if x < *range.start() {
*range.start()
} else {
x
}
}
#[must_use = "lerp_in_zone returns the new value and does not modify the original"]
fn lerp_in_zone(val: f32, zone: &RangeInclusive<f32>, values: &RangeInclusive<f32>) -> f32 {
let in_zone = clamp(val, zone);
let normalised = (in_zone - *zone.start()) / (*zone.end() - *zone.start());
normalised * (values.end() - values.start()) + values.start()
}