Skip to content

Commit

Permalink
Add smooth camera (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
marceline-cramer authored Jan 10, 2024
1 parent 25ac052 commit 280f9b9
Showing 1 changed file with 140 additions and 4 deletions.
144 changes: 140 additions & 4 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,17 +333,151 @@ pub fn tool_system(
}
}

pub fn update_player_sprite(
mut query: Query<(&mut Transform, &Actor), With<Player>>,
/// 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 (mut transform, actor) = query.single_mut();
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.);
let center_vec = top_corner_vec + vec3(actor.width as f32 / 2., -8., 0.);
transform.translation = center_vec;
camera_transform.translation = center_vec;
}

#[derive(Resource, Default)]
Expand All @@ -357,10 +491,12 @@ 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 280f9b9

Please sign in to comment.