Skip to content

Commit

Permalink
Player movement and camera smooth-damp effect
Browse files Browse the repository at this point in the history
  • Loading branch information
theredfish committed Mar 26, 2024
1 parent cb9789b commit 3218652
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2821,6 +2821,17 @@ path = "examples/dev_tools/fps_overlay.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[[example]]
name = "2d_top_down_camera"
path = "examples/camera/2d_top_down_camera.rs"
doc-scrape-examples = true

[package.metadata.example.2d_top_down_camera]
name = "2D top-down camera"
description = "A 2D top-down camera smoothly following player movements"
category = "Camera"
wasm = true

[package.metadata.example.fps_overlay]
name = "FPS overlay"
description = "Demonstrates FPS overlay"
Expand Down
175 changes: 175 additions & 0 deletions examples/camera/2d_top_down_camera.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! Shows how to create a 2D top-down camera to smoothly follow the player.

// Init state, camera and plane
// Cube character movement on a plane: Transform
// Update position on time with delta seconds
// Camera Position: follow player
// 1. Query player
// 2. Query Camera mut
// 3. Update cam position based on player movement
// 4. Optimize for smooth movement with smooth damp
// 5. Level Boundaries: show damp effect on boundaries
// 6. Colliders: to better showcase camera movements (optional)

use bevy::log::{Level, LogPlugin};
use bevy::math::{vec2, vec3};
use bevy::prelude::*;
use bevy::sprite::{MaterialMesh2dBundle, Mesh2dHandle};

static MOVE_SPEED: f32 = 10.;

#[derive(Component, Debug)]
struct Player;

#[derive(Bundle, Debug)]
struct PlayerBundle {
player: Player,
sprite_bundle: SpriteBundle,
}

fn main() {
let default_plugins = DefaultPlugins.set(LogPlugin {
filter: "info,wgpu_core=warn,wgpu_hal=warn,2d_top_down=debug".into(),
level: Level::INFO,
update_subscriber: None,
});

App::new()
.add_plugins(default_plugins)
.add_systems(Startup, (scene_setup, camera_setup).chain())
.add_systems(Update, (update_camera, move_player))
.run();
}

fn scene_setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
// World where we move the player
commands.spawn(MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Rectangle::new(1000.0, 1000.0))),
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
..default()
});

// Player
commands.spawn(PlayerBundle {
player: Player,
sprite_bundle: SpriteBundle {
transform: Transform {
scale: vec3(50., 50., 1.),
translation: vec3(0., 0., 2.),
..default()
},
sprite: Sprite {
color: Color::BLACK,
..default()
},
..default()
},
});

debug!("Scene setup finished");
}

fn camera_setup(mut commands: Commands) {
let camera = Camera2dBundle {
transform: Transform::from_xyz(0., 0., 1.),
..default()
};

commands.spawn(camera);

debug!("Camera setup finished");
}

fn update_camera(
mut camera: Query<&mut Transform, (With<Camera2d>, Without<Player>)>,
player: Query<&Transform, (With<Player>, Without<Camera2d>)>,
time: Res<Time>,
) {
let Ok(mut camera) = camera.get_single_mut() else {
debug!("Camera2d not found");
return;
};

let Ok(player) = player.get_single() else {
debug!("Player not found");
return;
};

let Vec3 { x, y, .. } = player.translation;
let direction = Vec3::new(x, y, camera.translation.z);

let smooth_damp = smooth_damp(
camera.translation,
direction,
Vec3::ZERO,
0.2,
f32::INFINITY,
time.delta_seconds(),
);

camera.translation = smooth_damp;
}

fn move_player(
mut player: Query<&mut Transform, With<Player>>,
time: Res<Time>,
_kb_input: Res<ButtonInput<KeyCode>>,
) {
let Ok(mut player) = player.get_single_mut() else {
debug!("Player not found");
return;
};

let move_delta = vec2(1., 1.) * MOVE_SPEED * time.delta_seconds();
player.translation += move_delta.extend(0.);
}

/// Update a vector `Vec3` towards a target over time (`delta_time`).
/// The smoothness is achieved with a spring-damper like function.
///
/// Algorithm based on Game Programming Gems vol.4,
/// chapter 1.10 "Critically Damped Ease-In/Ease-Out Smoothing".
pub fn smooth_damp(
from: Vec3,
to: Vec3,
mut velocity: Vec3,
mut smoothness: f32,
max_speed: f32,
delta_time: f32,
) -> Vec3 {
// The desired smoothness clamped to the minimum value.
smoothness = f32::max(0.0001, smoothness);
// Corresponds to the spring's natural frequency
let omega = 2. / smoothness;

let x = omega * delta_time;
// Approximation
let exp = 1. / (1. + x + 0.48 * x * x + 0.235 * x * x * x);

let mut distance_x = from.x - to.x;
let mut distance_y = from.y - to.y;
let mut distance_z = from.z - to.z;

let max_distance = max_speed * smoothness;
distance_x = f32::min(f32::max(-max_distance, distance_x), max_distance);
distance_y = f32::min(f32::max(-max_distance, distance_y), max_distance);
distance_z = f32::min(f32::max(-max_distance, distance_z), max_distance);

let temp_x = (velocity.x + omega * distance_x) * delta_time;
let temp_y = (velocity.y + omega * distance_y) * delta_time;
let temp_z = (velocity.z + omega * distance_z) * delta_time;

velocity.x = (velocity.x - omega * temp_x) * exp;
velocity.y = (velocity.y - omega * temp_y) * exp;
velocity.z = (velocity.z - omega * temp_z) * exp;

let x = to.x + (distance_x + temp_x) * exp;
let y = to.y + (distance_y + temp_y) * exp;
let z = to.z + (distance_z + temp_z) * exp;

Vec3::new(x, y, z)
}

0 comments on commit 3218652

Please sign in to comment.