//! 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 (pressed repeatedly) | Increase camera trauma | use bevy::{ input::common_conditions::input_just_pressed, math::ops::powf, prelude::*, sprite::MeshMaterial2d, }; // 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! /// 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; /// 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)) // 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