diff --git a/examples/camera/2d_screen_shake.rs b/examples/camera/2d_screen_shake.rs index dcdcd68811..0a0aaa780d 100644 --- a/examples/camera/2d_screen_shake.rs +++ b/examples/camera/2d_screen_shake.rs @@ -1,70 +1,216 @@ -//! This example showcases a 2D screen shake using concept in this video: `` +//! This example showcases how to implement 2D screen shake. +//! It follows the GDC talk ["Math for Game Programmers: Juicing Your Cameras With Math"](https://www.youtube.com/watch?v=tu-Qe66AvtY) by Squirrel Eiserloh +//! +//! The key features are: +//! - Camera shake is dependent on a "trauma" value between 0.0 and 1.0. The more trauma, the stronger the shake. +//! - Trauma automatically decays over time. +//! - The camera shake will always only affect the camera `Transform` up to a maximum displacement. +//! - The camera's `Transform` is only affected by the shake for the rendering. The `Transform` stays "normal" for the rest of the game logic. +//! - All displacements are governed by a noise function, guaranteeing that the shake is smooth and continuous. +//! This means that the camera won't jump around wildly. //! //! ## Controls //! -//! | Key Binding | Action | -//! |:-------------|:---------------------| -//! | Space | Trigger screen shake | +//! | Key Binding | Action | +//! |:---------------------------------|:---------------------------| +//! | Space (pressed repeatedly) | Increase camera trauma | -use bevy::{prelude::*, render::camera::SubCameraView, sprite::MeshMaterial2d}; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; +use bevy::{ + input::common_conditions::input_just_pressed, math::ops::powf, prelude::*, + sprite::MeshMaterial2d, +}; -const CAMERA_DECAY_RATE: f32 = 0.9; // Adjust this for smoother or snappier decay -const TRAUMA_DECAY_SPEED: f32 = 0.5; // How fast trauma decays -const TRAUMA_INCREMENT: f32 = 1.0; // Increment of trauma per frame when holding space +// Before we implement the code, let's quickly introduce the underlying constants. +// They are later encoded in a `CameraShakeConfig` component, but introduced here so we can easily tweak them. +// Try playing around with them and see how the shake behaves! -// screen_shake parameters, maximum addition by frame not actual maximum overall values -const MAX_ANGLE: f32 = 0.5; -const MAX_OFFSET: f32 = 500.0; +/// The trauma decay rate controls how quickly the trauma decays. +/// 0.5 means that a full trauma of 1.0 will decay to 0.0 in 2 seconds. +const TRAUMA_DECAY_PER_SECOND: f32 = 0.5; -#[derive(Component)] -struct Player; +/// The trauma exponent controls how the trauma affects the shake. +/// Camera shakes don't feel punchy when they go up linearly, so we use an exponent of 2.0. +/// The higher the exponent, the more abrupt is the transition between no shake and full shake. +const TRAUMA_EXPONENT: f32 = 2.0; + +/// The maximum angle the camera can rotate on full trauma. +/// 10.0 degrees is a somewhat high but still reasonable shake. Try bigger values for something more silly and wiggly. +const MAX_ANGLE: f32 = 10.0_f32.to_radians(); + +/// The maximum translation the camera will move on full trauma in both the x and y directions. +/// 20.0 px is a low enough displacement to not be distracting. Try higher values for an effect that looks like the camera is wandering around. +const MAX_TRANSLATION: f32 = 20.0; + +/// How much we are traversing the noise function in arbitrary units per second. +/// This dictates how fast the camera shakes. +/// 20.0 is a fairly fast shake. Try lower values for a more dreamy effect. +const NOISE_SPEED: f32 = 20.0; + +/// How much trauma we add per press of the space key. +/// A value of 1.0 would mean that a single press would result in a maximum trauma, i.e. 1.0. +const TRAUMA_PER_PRESS: f32 = 0.4; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, (setup_scene, setup_instructions, setup_camera)) - .add_systems(Update, (screen_shake, trigger_shake_on_space)) + // At the start of the frame, restore the camera's transform to its unshaken state. + .add_systems(PreUpdate, reset_transform) + .add_systems( + Update, + // Increase trauma when the space key is pressed. + increase_trauma.run_if(input_just_pressed(KeyCode::Space)), + ) + // Just before the end of the frame, apply the shake. + // This is ordered so that the transform propagation produces correct values for the global transform, which is used by Bevy's rendering. + .add_systems(PostUpdate, shake_camera.before(TransformSystems::Propagate)) .run(); } +/// Let's start with the core mechanic: how do we shake the camera? +/// This system runs right at the end of the frame, so that we can sneak in the shake effect before rendering kicks in. +fn shake_camera( + camera_shake: Single<(&mut CameraShakeState, &CameraShakeConfig, &mut Transform)>, + time: Res