Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/camera/2d_screen_shake.rs
6592 views
1
//! This example showcases how to implement 2D screen shake.
2
//! 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
3
//!
4
//! The key features are:
5
//! - Camera shake is dependent on a "trauma" value between 0.0 and 1.0. The more trauma, the stronger the shake.
6
//! - Trauma automatically decays over time.
7
//! - The camera shake will always only affect the camera `Transform` up to a maximum displacement.
8
//! - The camera's `Transform` is only affected by the shake for the rendering. The `Transform` stays "normal" for the rest of the game logic.
9
//! - All displacements are governed by a noise function, guaranteeing that the shake is smooth and continuous.
10
//! This means that the camera won't jump around wildly.
11
//!
12
//! ## Controls
13
//!
14
//! | Key Binding | Action |
15
//! |:---------------------------------|:---------------------------|
16
//! | Space (pressed repeatedly) | Increase camera trauma |
17
18
use bevy::{
19
input::common_conditions::input_just_pressed, math::ops::powf, prelude::*,
20
sprite_render::MeshMaterial2d,
21
};
22
23
// Before we implement the code, let's quickly introduce the underlying constants.
24
// They are later encoded in a `CameraShakeConfig` component, but introduced here so we can easily tweak them.
25
// Try playing around with them and see how the shake behaves!
26
27
/// The trauma decay rate controls how quickly the trauma decays.
28
/// 0.5 means that a full trauma of 1.0 will decay to 0.0 in 2 seconds.
29
const TRAUMA_DECAY_PER_SECOND: f32 = 0.5;
30
31
/// The trauma exponent controls how the trauma affects the shake.
32
/// Camera shakes don't feel punchy when they go up linearly, so we use an exponent of 2.0.
33
/// The higher the exponent, the more abrupt is the transition between no shake and full shake.
34
const TRAUMA_EXPONENT: f32 = 2.0;
35
36
/// The maximum angle the camera can rotate on full trauma.
37
/// 10.0 degrees is a somewhat high but still reasonable shake. Try bigger values for something more silly and wiggly.
38
const MAX_ANGLE: f32 = 10.0_f32.to_radians();
39
40
/// The maximum translation the camera will move on full trauma in both the x and y directions.
41
/// 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.
42
const MAX_TRANSLATION: f32 = 20.0;
43
44
/// How much we are traversing the noise function in arbitrary units per second.
45
/// This dictates how fast the camera shakes.
46
/// 20.0 is a fairly fast shake. Try lower values for a more dreamy effect.
47
const NOISE_SPEED: f32 = 20.0;
48
49
/// How much trauma we add per press of the space key.
50
/// A value of 1.0 would mean that a single press would result in a maximum trauma, i.e. 1.0.
51
const TRAUMA_PER_PRESS: f32 = 0.4;
52
53
fn main() {
54
App::new()
55
.add_plugins(DefaultPlugins)
56
.add_systems(Startup, (setup_scene, setup_instructions, setup_camera))
57
// At the start of the frame, restore the camera's transform to its unshaken state.
58
.add_systems(PreUpdate, reset_transform)
59
.add_systems(
60
Update,
61
// Increase trauma when the space key is pressed.
62
increase_trauma.run_if(input_just_pressed(KeyCode::Space)),
63
)
64
// Just before the end of the frame, apply the shake.
65
// This is ordered so that the transform propagation produces correct values for the global transform, which is used by Bevy's rendering.
66
.add_systems(PostUpdate, shake_camera.before(TransformSystems::Propagate))
67
.run();
68
}
69
70
/// Let's start with the core mechanic: how do we shake the camera?
71
/// This system runs right at the end of the frame, so that we can sneak in the shake effect before rendering kicks in.
72
fn shake_camera(
73
camera_shake: Single<(&mut CameraShakeState, &CameraShakeConfig, &mut Transform)>,
74
time: Res<Time>,
75
) {
76
let (mut camera_shake, config, mut transform) = camera_shake.into_inner();
77
78
// Before we even start thinking about the shake, we save the original transform so it's not lost.
79
// At the start of the next frame, we will restore the camera's transform to this original transform.
80
camera_shake.original_transform = *transform;
81
82
// To generate the transform offset, we use a noise function. Noise is like a random number generator, but cooler.
83
// Let's start with a visual intuition: <https://assets-global.website-files.com/64b6d182aee713bd0401f4b9/64b95974ec292aabac45fc8e_image.png>
84
// The image on the left is made from pure randomness, the image on the right is made from a kind of noise called Perlin noise.
85
// Notice how the noise has much more "structure" than the randomness? How it looks like it has peaks and valleys?
86
// This property makes noise very desirable for a variety of visual effects. In our case, what we want is that the
87
// camera does not wildly teleport around the world, but instead *moves* through the world frame by frame.
88
// We can use 1D Perlin noise for this, which takes one input and outputs a value between -1.0 and 1.0. If we increase the input by a little bit,
89
// like by the time since the last frame, we get a different output that is still "close" to the previous one.
90
91
// This is the input to the noise function. Just using the elapsed time is pretty good input,
92
// since it means that noise generations that are close in time will be close in output.
93
// We simply multiply it by a constant to be able to "speed up" or "slow down" the noise.
94
let t = time.elapsed_secs() * config.noise_speed;
95
96
// Now we generate three noise values. One for the rotation, one for the x-offset, and one for the y-offset.
97
// But if we generated those three noise values with the same input, we would get the same output three times!
98
// To avoid this, we simply add a random offset to each input.
99
// You can think of this as the seed value you would give a random number generator.
100
let rotation_noise = perlin_noise::generate(t + 0.0);
101
let x_noise = perlin_noise::generate(t + 100.0);
102
let y_noise = perlin_noise::generate(t + 200.0);
103
104
// Games often deal with linear increments. For example, if an enemy deals 10 damage and attacks you 2 times, you will take 20 damage.
105
// But that's not how impact feels! Human senses are much more attuned to exponential changes.
106
// So, we make sure that the `shake` value we use is an exponential function of the trauma.
107
// But doesn't this make the value explode? Fortunately not: since `trauma` is between 0.0 and 1.0, exponentiating it will actually make it smaller!
108
// See <https://www.wolframalpha.com/input?i=plot+x+and+x%5E2+and+x%5E3+for+x+in+%5B0%2C+1%5D> for a graph.
109
let shake = powf(camera_shake.trauma, config.exponent);
110
111
// Now, to get the final offset, we multiply this noise value by the shake value and the maximum value.
112
// The noise value is in [-1, 1], so by multiplying it with a maximum value, we get a value in [-max_value, +max_value].
113
// Multiply this by the shake value to get the exponential effect, and we're done!
114
let roll_offset = rotation_noise * shake * config.max_angle;
115
let x_offset = x_noise * shake * config.max_translation;
116
let y_offset = y_noise * shake * config.max_translation;
117
118
// Finally, we apply the offset to the camera's transform. Since we already stored the original transform,
119
// and this system runs right at the end of the frame, we can't accidentally break any game logic by changing the transform.
120
transform.translation.x += x_offset;
121
transform.translation.y += y_offset;
122
transform.rotate_z(roll_offset);
123
124
// Some bookkeeping at the end: trauma should decay over time.
125
camera_shake.trauma -= config.trauma_decay_per_second * time.delta_secs();
126
camera_shake.trauma = camera_shake.trauma.clamp(0.0, 1.0);
127
}
128
129
/// Increase the trauma when the space key is pressed.
130
fn increase_trauma(mut camera_shake: Single<&mut CameraShakeState>) {
131
camera_shake.trauma += TRAUMA_PER_PRESS;
132
camera_shake.trauma = camera_shake.trauma.clamp(0.0, 1.0);
133
}
134
135
/// Restore the camera's transform to its unshaken state.
136
/// Runs at the start of the frame, so that gameplay logic doesn't need to care about camera shake.
137
fn reset_transform(camera_shake: Single<(&CameraShakeState, &mut Transform)>) {
138
let (camera_shake, mut transform) = camera_shake.into_inner();
139
*transform = camera_shake.original_transform;
140
}
141
142
/// The current state of the camera shake that is updated every frame.
143
#[derive(Component, Debug, Default)]
144
struct CameraShakeState {
145
/// The current trauma level in [0.0, 1.0].
146
trauma: f32,
147
/// The original transform of the camera before applying the shake.
148
/// We store this so that we can restore the camera's transform to its original state at the start of the next frame.
149
original_transform: Transform,
150
}
151
152
/// Configuration for the camera shake.
153
/// See the constants at the top of the file for some good default values and detailed explanations.
154
#[derive(Component, Debug)]
155
#[require(CameraShakeState)]
156
struct CameraShakeConfig {
157
trauma_decay_per_second: f32,
158
exponent: f32,
159
max_angle: f32,
160
max_translation: f32,
161
noise_speed: f32,
162
}
163
164
fn setup_camera(mut commands: Commands) {
165
commands.spawn((
166
Camera2d,
167
// Enable camera shake for this camera.
168
CameraShakeConfig {
169
trauma_decay_per_second: TRAUMA_DECAY_PER_SECOND,
170
exponent: TRAUMA_EXPONENT,
171
max_angle: MAX_ANGLE,
172
max_translation: MAX_TRANSLATION,
173
noise_speed: NOISE_SPEED,
174
},
175
));
176
}
177
178
/// Spawn a scene so we have something to look at.
179
fn setup_scene(
180
mut commands: Commands,
181
mut meshes: ResMut<Assets<Mesh>>,
182
mut materials: ResMut<Assets<ColorMaterial>>,
183
) {
184
// Background tile
185
commands.spawn((
186
Mesh2d(meshes.add(Rectangle::new(1000., 700.))),
187
MeshMaterial2d(materials.add(Color::srgb(0.2, 0.2, 0.3))),
188
));
189
190
// The shape in the middle could be our player character.
191
commands.spawn((
192
Mesh2d(meshes.add(Rectangle::new(50.0, 100.0))),
193
MeshMaterial2d(materials.add(Color::srgb(0.25, 0.94, 0.91))),
194
Transform::from_xyz(0., 0., 2.),
195
));
196
197
// These two shapes could be obstacles.
198
commands.spawn((
199
Mesh2d(meshes.add(Rectangle::new(50.0, 50.0))),
200
MeshMaterial2d(materials.add(Color::srgb(0.85, 0.0, 0.2))),
201
Transform::from_xyz(-450.0, 200.0, 2.),
202
));
203
204
commands.spawn((
205
Mesh2d(meshes.add(Rectangle::new(70.0, 50.0))),
206
MeshMaterial2d(materials.add(Color::srgb(0.5, 0.8, 0.2))),
207
Transform::from_xyz(450.0, -150.0, 2.),
208
));
209
}
210
211
fn setup_instructions(mut commands: Commands) {
212
commands.spawn((
213
Text::new("Press space repeatedly to trigger a progressively stronger screen shake"),
214
Node {
215
position_type: PositionType::Absolute,
216
bottom: px(12),
217
left: px(12),
218
..default()
219
},
220
));
221
}
222
223
/// Tiny 1D Perlin noise implementation. The mathematical details are not important here.
224
mod perlin_noise {
225
use super::*;
226
227
pub fn generate(x: f32) -> f32 {
228
// Left coordinate of the unit-line that contains the input.
229
let x_floor = x.floor() as usize;
230
231
// Input location in the unit-line.
232
let xf0 = x - x_floor as f32;
233
let xf1 = xf0 - 1.0;
234
235
// Wrap to range 0-255.
236
let xi0 = x_floor & 0xFF;
237
let xi1 = (x_floor + 1) & 0xFF;
238
239
// Apply the fade function to the location.
240
let t = fade(xf0).clamp(0.0, 1.0);
241
242
// Generate hash values for each point of the unit-line.
243
let h0 = PERMUTATION_TABLE[xi0];
244
let h1 = PERMUTATION_TABLE[xi1];
245
246
// Linearly interpolate between dot products of each gradient with its distance to the input location.
247
let a = dot_grad(h0, xf0);
248
let b = dot_grad(h1, xf1);
249
a.interpolate_stable(&b, t)
250
}
251
252
// A cubic curve that smoothly transitions from 0 to 1 as t goes from 0 to 1
253
fn fade(t: f32) -> f32 {
254
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
255
}
256
257
fn dot_grad(hash: u8, xf: f32) -> f32 {
258
// In 1D case, the gradient may be either 1 or -1.
259
// The distance vector is the input offset (relative to the smallest bound).
260
if hash & 0x1 != 0 {
261
xf
262
} else {
263
-xf
264
}
265
}
266
267
// Perlin noise permutation table. This is a random sequence of the numbers 0-255.
268
const PERMUTATION_TABLE: [u8; 256] = [
269
0x97, 0xA0, 0x89, 0x5B, 0x5A, 0x0F, 0x83, 0x0D, 0xC9, 0x5F, 0x60, 0x35, 0xC2, 0xE9, 0x07,
270
0xE1, 0x8C, 0x24, 0x67, 0x1E, 0x45, 0x8E, 0x08, 0x63, 0x25, 0xF0, 0x15, 0x0A, 0x17, 0xBE,
271
0x06, 0x94, 0xF7, 0x78, 0xEA, 0x4B, 0x00, 0x1A, 0xC5, 0x3E, 0x5E, 0xFC, 0xDB, 0xCB, 0x75,
272
0x23, 0x0B, 0x20, 0x39, 0xB1, 0x21, 0x58, 0xED, 0x95, 0x38, 0x57, 0xAE, 0x14, 0x7D, 0x88,
273
0xAB, 0xA8, 0x44, 0xAF, 0x4A, 0xA5, 0x47, 0x86, 0x8B, 0x30, 0x1B, 0xA6, 0x4D, 0x92, 0x9E,
274
0xE7, 0x53, 0x6F, 0xE5, 0x7A, 0x3C, 0xD3, 0x85, 0xE6, 0xDC, 0x69, 0x5C, 0x29, 0x37, 0x2E,
275
0xF5, 0x28, 0xF4, 0x66, 0x8F, 0x36, 0x41, 0x19, 0x3F, 0xA1, 0x01, 0xD8, 0x50, 0x49, 0xD1,
276
0x4C, 0x84, 0xBB, 0xD0, 0x59, 0x12, 0xA9, 0xC8, 0xC4, 0x87, 0x82, 0x74, 0xBC, 0x9F, 0x56,
277
0xA4, 0x64, 0x6D, 0xC6, 0xAD, 0xBA, 0x03, 0x40, 0x34, 0xD9, 0xE2, 0xFA, 0x7C, 0x7B, 0x05,
278
0xCA, 0x26, 0x93, 0x76, 0x7E, 0xFF, 0x52, 0x55, 0xD4, 0xCF, 0xCE, 0x3B, 0xE3, 0x2F, 0x10,
279
0x3A, 0x11, 0xB6, 0xBD, 0x1C, 0x2A, 0xDF, 0xB7, 0xAA, 0xD5, 0x77, 0xF8, 0x98, 0x02, 0x2C,
280
0x9A, 0xA3, 0x46, 0xDD, 0x99, 0x65, 0x9B, 0xA7, 0x2B, 0xAC, 0x09, 0x81, 0x16, 0x27, 0xFD,
281
0x13, 0x62, 0x6C, 0x6E, 0x4F, 0x71, 0xE0, 0xE8, 0xB2, 0xB9, 0x70, 0x68, 0xDA, 0xF6, 0x61,
282
0xE4, 0xFB, 0x22, 0xF2, 0xC1, 0xEE, 0xD2, 0x90, 0x0C, 0xBF, 0xB3, 0xA2, 0xF1, 0x51, 0x33,
283
0x91, 0xEB, 0xF9, 0x0E, 0xEF, 0x6B, 0x31, 0xC0, 0xD6, 0x1F, 0xB5, 0xC7, 0x6A, 0x9D, 0xB8,
284
0x54, 0xCC, 0xB0, 0x73, 0x79, 0x32, 0x2D, 0x7F, 0x04, 0x96, 0xFE, 0x8A, 0xEC, 0xCD, 0x5D,
285
0xDE, 0x72, 0x43, 0x1D, 0x18, 0x48, 0xF3, 0x8D, 0x80, 0xC3, 0x4E, 0x42, 0xD7, 0x3D, 0x9C,
286
0xB4,
287
];
288
}
289
290