Skip to content

Commit

Permalink
Better camera (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac8668 authored Jan 11, 2024
1 parent 280f9b9 commit df6781a
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 147 deletions.
171 changes: 171 additions & 0 deletions src/camera.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::prelude::*;

/// A resource for the state of the in-game smooth camera.
#[derive(Resource)]
pub struct TrackingCamera {
/// The position in world space of the origin of this camera.
pub position: Vec2,

/// The current target of the camera; what it smoothly focuses on.
pub target: Vec2,

/// The half-size of the rectangle around the center of the screen where
/// the player can move without the camera retargeting. When the player
/// leaves this rectangle, the camera will retarget to include the player
/// back into this region of the screen.
pub tracking_size: Vec2,

/// The half-size of the rectangle around the center of the screen where
/// the camera will smoothly interpolate. If the player leaves this region,
/// the camera will clamp to keep the player within it.
pub clamp_size: Vec2,

/// A dead distance from the edge of the tracking region to the player
/// where the camera will not perform any tracking, even if the player is
/// minutely outside of the tracking region. This is provided so that the
/// camera can recenter even if the player has not moved since a track.
pub dead_zone: Vec2,

/// The proportion (between 0.0-1.0) that the camera reaches its target
/// from its initial position during a second's time.
pub speed: f64,

/// A timeout to recenter the camera on the player even if the player has
/// not left the tracking rectangle.
pub recenter_timeout: f32,

/// The duration in seconds since the player has left the tracking rectangle.
///
/// When this duration reaches `recenter_timeout`, the player will be
/// recentered.
pub last_track: f32,
}

impl Default for TrackingCamera {
fn default() -> Self {
Self {
position: Vec2::ZERO,
target: Vec2::ZERO,
tracking_size: vec2(16.0, 9.0),
clamp_size: vec2(48.0, 28.0),
dead_zone: Vec2::splat(0.1),
speed: 0.98,
recenter_timeout: 3.0,
last_track: 0.0,
}
}
}

impl TrackingCamera {
/// Update the camera with the current position and this frame's delta time.
pub fn update(&mut self, player_pos: Vec2, dt: f64) {
// update target with player position
self.track_player(player_pos);

// track time since last time we had to track the player
let new_last_track = self.last_track + dt as f32;

// test if we've triggered a recenter
if self.last_track < self.recenter_timeout && new_last_track > self.recenter_timeout {
// target the player
self.target = player_pos;
}

// update the duration since last track
self.last_track = new_last_track;

// lerp the current position towards the target
// correct lerp degree using delta time
// perform pow() with high precision
let lerp = 1.0 - (1.0 - self.speed).powf(dt) as f32;
self.position = self.position.lerp(self.target, lerp);
}

/// Helper function to clamp a rectangle (given as a half-size at the
/// origin) so that a point lays within it. Returns an offset to apply to
/// the rectangle, if any was required.
pub fn clamp_rect(half_size: Vec2, point: Vec2) -> Option<Vec2> {
let mut ox = None;
let mut oy = None;

if point.x > half_size.x {
ox = Some(point.x - half_size.x);
} else if point.x < -half_size.x {
ox = Some(point.x + half_size.x);
}

if point.y > half_size.y {
oy = Some(point.y - half_size.y);
} else if point.y < -half_size.y {
oy = Some(point.y + half_size.y);
}

if let (None, None) = (ox, oy) {
None
} else {
Some(vec2(ox.unwrap_or(0.0), oy.unwrap_or(0.0)))
}
}

pub fn track_player(&mut self, player_pos: Vec2) {
// get current relative position to player
let rel_pos = player_pos - self.position;

// track the player and reset last track if change was necessary
if let Some(offset) = Self::clamp_rect(self.tracking_size, rel_pos) {
// skip tracking if it falls within the dead zone
if !(self.dead_zone).cmpgt(offset.abs()).all() {
self.target = self.position + offset;
self.last_track = 0.0;
}
}

// clamp the player within the screen
if let Some(offset) = Self::clamp_rect(self.clamp_size, rel_pos) {
self.position += offset;
}
}
}

pub fn update_camera(
query: Query<&Transform, With<Player>>,
mut camera_q: Query<&mut Transform, (With<Camera>, Without<Player>)>,
mut tracking: ResMut<TrackingCamera>,
time: Res<Time>,
) {
let transform = query.single();
let mut camera_transform = camera_q.single_mut();
let dt = time.delta_seconds_f64();
tracking.update(transform.translation.xy(), dt);
camera_transform.translation = tracking.position.extend(2.0);
}

//TODO Make this event based
fn on_resize_system(
mut camera: Query<&mut Transform, With<Camera>>,
window: Query<&Window>,
zoom: Res<Zoom>,
) {
let mut camera_transform = camera.single_mut();
let Ok(window) = window.get_single() else {
return;
};

let x = 1920. / window.width() * zoom.0;
let y = 1080. / window.height() * zoom.0;

camera_transform.scale.x = x.min(y);
camera_transform.scale.y = x.min(y);
}

#[derive(Resource)]
pub struct Zoom(pub f32);

pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (update_camera, on_resize_system))
.insert_resource(Zoom(0.23))
.insert_resource(TrackingCamera::default());
}
}
3 changes: 3 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub const RUN_SPEED: f32 = 3.5;
pub const TOOL_DISTANCE: f32 = 32.;
pub const TOOL_RANGE: f32 = 12.;

pub const ZOOM_LOWER_BOUND: f32 = 0.15;
pub const ZOOM_UPPER_BOUND: f32 = 0.30;

// Engine consts

//This was a "scale" const for the atoms, but we can just zoom in, so it was removed
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bevy::prelude::*;
mod actors;
mod animation;
mod atom;
mod camera;
mod chunk;
mod chunk_group;
mod chunk_manager;
Expand All @@ -15,8 +16,8 @@ mod particles;
mod player;
mod prelude {
pub use crate::{
actors::*, animation::*, atom::*, chunk::*, chunk_group::*, chunk_manager::*, consts::*,
debug::*, geom_tools::*, manager_api::*, materials::*, particles::*, player::*,
actors::*, animation::*, atom::*, camera::*, chunk::*, chunk_group::*, chunk_manager::*,
consts::*, debug::*, geom_tools::*, manager_api::*, materials::*, particles::*, player::*,
};
pub use bevy::input::mouse::MouseScrollUnit;
pub use bevy::input::mouse::MouseWheel;
Expand Down Expand Up @@ -53,6 +54,7 @@ fn main() {
animation::AnimationPlugin,
ParticlesPlugin,
MaterialsPlugin,
CameraPlugin,
))
.add_systems(Startup, setup);

Expand Down
148 changes: 3 additions & 145 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,12 @@ pub fn update_player(
input: (ResMut<Input<KeyCode>>, EventReader<MouseWheel>),
mut player: Query<(&mut Actor, &mut Player, &mut AnimationIndices)>,
chunk_manager: ResMut<ChunkManager>,
mut camera: Query<&mut Transform, (Without<Tool>, With<Camera>)>,
materials: (Res<Assets<Materials>>, Res<MaterialsHandle>),
time: Res<Time>,
mut zoom: ResMut<Zoom>,
) {
let (mut actor, mut player, mut anim_idxs) = player.single_mut();
let (keys, mut scroll_evr) = input;
let mut camera_transform = camera.single_mut();
let materials = materials.0.get(materials.1 .0.clone()).unwrap();

// Gravity
Expand Down Expand Up @@ -206,7 +205,8 @@ pub fn update_player(
//Zoom
for ev in scroll_evr.read() {
if ev.unit == MouseScrollUnit::Line {
camera_transform.scale *= 0.9_f32.powi(ev.y as i32);
zoom.0 *= 0.9_f32.powi(ev.y as i32);
zoom.0 = zoom.0.clamp(ZOOM_LOWER_BOUND, ZOOM_UPPER_BOUND);
}
}

Expand Down Expand Up @@ -333,146 +333,6 @@ pub fn tool_system(
}
}

/// A resource for the state of the in-game smooth camera.
#[derive(Resource)]
pub struct TrackingCamera {
/// The position in world space of the origin of this camera.
pub position: Vec2,

/// The current target of the camera; what it smoothly focuses on.
pub target: Vec2,

/// The half-size of the rectangle around the center of the screen where
/// the player can move without the camera retargeting. When the player
/// leaves this rectangle, the camera will retarget to include the player
/// back into this region of the screen.
pub tracking_size: Vec2,

/// The half-size of the rectangle around the center of the screen where
/// the camera will smoothly interpolate. If the player leaves this region,
/// the camera will clamp to keep the player within it.
pub clamp_size: Vec2,

/// A dead distance from the edge of the tracking region to the player
/// where the camera will not perform any tracking, even if the player is
/// minutely outside of the tracking region. This is provided so that the
/// camera can recenter even if the player has not moved since a track.
pub dead_zone: Vec2,

/// The proportion (between 0.0-1.0) that the camera reaches its target
/// from its initial position during a second's time.
pub speed: f64,

/// A timeout to recenter the camera on the player even if the player has
/// not left the tracking rectangle.
pub recenter_timeout: f32,

/// The duration in seconds since the player has left the tracking rectangle.
///
/// When this duration reaches `recenter_timeout`, the player will be
/// recentered.
pub last_track: f32,
}

impl Default for TrackingCamera {
fn default() -> Self {
Self {
position: Vec2::ZERO,
target: Vec2::ZERO,
tracking_size: vec2(32.0, 16.0),
clamp_size: vec2(96.0, 64.0),
dead_zone: Vec2::splat(0.1),
speed: 0.98,
recenter_timeout: 3.0,
last_track: 0.0,
}
}
}

impl TrackingCamera {
/// Update the camera with the current position and this frame's delta time.
pub fn update(&mut self, player_pos: Vec2, dt: f64) {
// update target with player position
self.track_player(player_pos);

// track time since last time we had to track the player
let new_last_track = self.last_track + dt as f32;

// test if we've triggered a recenter
if self.last_track < self.recenter_timeout && new_last_track > self.recenter_timeout {
// target the player
self.target = player_pos;
}

// update the duration since last track
self.last_track = new_last_track;

// lerp the current position towards the target
// correct lerp degree using delta time
// perform pow() with high precision
let lerp = 1.0 - (1.0 - self.speed).powf(dt) as f32;
self.position = self.position.lerp(self.target, lerp);
}

/// Helper function to clamp a rectangle (given as a half-size at the
/// origin) so that a point lays within it. Returns an offset to apply to
/// the rectangle, if any was required.
pub fn clamp_rect(half_size: Vec2, point: Vec2) -> Option<Vec2> {
let mut ox = None;
let mut oy = None;

if point.x > half_size.x {
ox = Some(point.x - half_size.x);
} else if point.x < -half_size.x {
ox = Some(point.x + half_size.x);
}

if point.y > half_size.y {
oy = Some(point.y - half_size.y);
} else if point.y < -half_size.y {
oy = Some(point.y + half_size.y);
}

if let (None, None) = (ox, oy) {
None
} else {
Some(vec2(ox.unwrap_or(0.0), oy.unwrap_or(0.0)))
}
}

pub fn track_player(&mut self, player_pos: Vec2) {
// get current relative position to player
let rel_pos = player_pos - self.position;

// track the player and reset last track if change was necessary
if let Some(offset) = Self::clamp_rect(self.tracking_size, rel_pos) {
// skip tracking if it falls within the dead zone
if !self.dead_zone.cmpgt(offset.abs()).all() {
self.target = self.position + offset;
self.last_track = 0.0;
}
}

// clamp the player within the screen
if let Some(offset) = Self::clamp_rect(self.clamp_size, rel_pos) {
self.position += offset;
}
}
}

pub fn update_camera(
query: Query<&Transform, With<Player>>,
mut camera_q: Query<&mut Transform, (With<Camera>, Without<Player>)>,
mut tracking: ResMut<TrackingCamera>,
time: Res<Time>,
) {
let transform = query.single();
let mut camera_transform = camera_q.single_mut();
let dt = time.delta_seconds_f64();
tracking.update(transform.translation.xy(), dt);
camera_transform.translation = tracking.position.extend(2.0);
}

pub fn update_player_sprite(mut query: Query<(&mut Transform, &Actor), With<Player>>) {
let (mut transform, actor) = query.single_mut();
let top_corner_vec = vec3(actor.pos.x as f32, -actor.pos.y as f32, 2.);
Expand All @@ -491,12 +351,10 @@ impl Plugin for PlayerPlugin {
(
update_player.after(chunk_manager_update),
update_player_sprite,
update_camera,
tool_system.after(chunk_manager_update),
),
)
.insert_resource(SavingTask::default())
.insert_resource(TrackingCamera::default())
.add_systems(PostStartup, player_setup.after(manager_setup));
}
}

0 comments on commit df6781a

Please sign in to comment.