Camera2D Screen Shake Extension

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Hammer Bro.
:warning: Old Version Published before Godot 3 was released.

I couldn’t find a built in screen-shake function, so I implemented the first bit o’ logic I found on the internet. Since it’s a pretty common feature in games these days, I figured I’d share it. Major features include:

  • Variable duration, frequency, and amplitude.
  • Maintains its own offset so it can be used in combination with other effects.

Since it extends Camera2D, all you have to do to use it is to instance this script instead of base Camera2D then call the shake function. I find that setting the duration, frequency, and amplitude to 0.2, 15, and 8 respectively gives a nice light shaking effect.

extends Camera2D

var _duration = 0.0
var _period_in_ms = 0.0
var _amplitude = 0.0
var _timer = 0.0
var _last_shook_timer = 0
var _previous_x = 0.0
var _previous_y = 0.0
var _last_offset = Vector2(0, 0)

func _ready():
	set_process(true)

# Shake with decreasing intensity while there's time remaining.
func _process(delta):
	# Only shake when there's shake time remaining.
	if _timer == 0:
		return
	# Only shake on certain frames.
	_last_shook_timer = _last_shook_timer + delta
	# Be mathematically correct in the face of lag; usually only happens once.
	while _last_shook_timer >= _period_in_ms:
		_last_shook_timer = _last_shook_timer - _period_in_ms
		# Lerp between [amplitude] and 0.0 intensity based on remaining shake time.
		var intensity = _amplitude * (1 - ((_duration - _timer) / _duration))
		# Noise calculation logic from http://jonny.morrill.me/blog/view/14
		var new_x = rand_range(-1.0, 1.0)
		var x_component = intensity * (_previous_x + (delta * (new_x - _previous_x)))
		var new_y = rand_range(-1.0, 1.0)
		var y_component = intensity * (_previous_y + (delta * (new_y - _previous_y)))
		_previous_x = new_x
		_previous_y = new_y
		# Track how much we've moved the offset, as opposed to other effects.
		var new_offset = Vector2(x_component, y_component)
		set_offset(get_offset() - _last_offset + new_offset)
		_last_offset = new_offset
	# Reset the offset when we're done shaking.
	_timer = _timer - delta
	if _timer <= 0:
		_timer = 0
		set_offset(get_offset() - _last_offset)

# Kick off a new screenshake effect.
func shake(duration, frequency, amplitude):
	# Initialize variables.
	_duration = duration
	_timer = duration
	_period_in_ms = 1.0 / frequency
	_amplitude = amplitude
	_previous_x = rand_range(-1.0, 1.0)
	_previous_y = rand_range(-1.0, 1.0)
	# Reset previous offset, if any.
	set_offset(get_offset() - _last_offset)
	_last_offset = Vector2(0, 0)

Bookmarked! I’ll try it later. Thanks for posting!

ugly_cat | 2016-03-02 00:18

I’ve evolved this script for 3d camera.
Shake 3D camera script (Godot Engine 2.1)

vctr | 2017-12-06 14:22

@vctr

Cool! Nice to see some 3D scripts passing around. I’m not that good yet at programming (in Godot), so this is welcome.
But how do I implement your script? Because I already have a camera script attached to my camera, to follow the car dynamically (base-script by Bastiaan Olij tutorial).

Since you can’t attacht 2 scripts to 1 node, do I just copy your script and paste it with what I already have?
The function _process() doesn’t kick in untill you use the function shake() somewhere, right?

Edit: maybe I found it. Is it possible that instead of using extends Camera I could use extends “res:://path/to/mainScriptCamera.gd” ??

Edit 2: still can’t seem to get this to work. First edit doesn’t work I guess, since the script then becomes the child of the script and I’m not able to call a function of that child (or maybe I’m doing it wrong?).
Now tried it with

var camExtScript = preload("res://Scripts/cam_Shake.gd").new()

in the main camera script at the top (set the shake script back to extends Camera) and then trying to call the function when boosting is true

if (follow_this.boost):
	currentFOV = boostFOV
	camExtScript.shake(0.2,15,8)
else:
	currentFOV = maxFOV

I’m also changing the FOV when the car is boosting. But can’t get the cam to shake.

Cheers

eyeEmotion | 2020-04-12 16:59

Don’t mind if I do. Zoink.

Zedespook | 2020-04-12 17:46

Thanks! Any need to initialize _previous_x/y to rand_range(-1.0, 1.0) (randf_range(-1.0, 1.0) in Godot 4) rather than 0? It seems to only affect the slope at the very beginning but since we start from 0, isn’t it technically more correct to set them to 0? I couldn’t observe any difference.

Hyper Sonic | 2023-05-16 16:25

:bust_in_silhouette: Reply From: oussama

Thank you bro, I will use it for sure

:bust_in_silhouette: Reply From: batmanasb

This script is awesome, but I found a small bug. The camera doesn’t always return it’s offset to origin (0,0), especially when the object the camera is parented to is colliding with something. Also, it processes when it doesn’t have to. So I made a few tweaks to improve it for my own needs. Now it only only enables processing while shaking, and disables processing while not shaking. When it’s done shaking, it resets the offset to (0,0). And lastly, I made it so you can’t interrupt an ongoing shake. Here’s the new script: Camera2D Screen Shake Extension v1.1 - Pastebin.com

Nice. There is one thing to be aware of: in your modification, by setting the offset to 0,0 at the end of a screenshake effect, you may be interrupting unrelated camera effects.

Imagine you have a system in which you hold Down to look below you, moving the offset some amount. If an explosion shakes the screen while you’re looking down, the end of your modified screenshake would re-center the camera on the player unexpectedly.

Fortunately, it’s easy enough to tailor the concept to one’s specific use cases.

Hammer Bro. | 2016-03-08 18:17

Hm, the if _timer <= 0: block at the end should already take care of clearing the offset, and _last_offset should cater for concurrent effects… Not sure what if _timer == 0: brings, although yes, you can stop process once you’re done with it, as long as you reenable it when you need to shake again.

Hyper Sonic | 2023-05-16 16:06

:bust_in_silhouette: Reply From: stronk

I changed it only a little bit for my 3D game with orthogonal camera. Here’s the script if you would like to use it. Godot's Orthogonal Camera3D Shake Script based on https://godotengine.org/qa/438/camera2d-screen-shake-extension?show=438#q438 · GitHub

:bust_in_silhouette: Reply From: scrubswithnosleeves

I made a video tutorial on making a similar, but simpler node and script:

:bust_in_silhouette: Reply From: Hyper Sonic

So the script is working for shakes at high frequency, but I noticed that it “Only shake on certain frames.” as the comment says, causing odd motion at low frequencies.

I changed the script to do a smooth lerp as in the original article (try low frequencies on the JS example and you’ll see). I admit it’s very different now, I could have almost redone it from scratch; but I kept the overall API.

You can find the script in an actual game on that repo but note that it contains game-specific code.

I tried to extract the non-game-specific parts below, but didn’t test the isolated script in a fresh project so I don’t guarantee it works out of the box.

Also, I had to drop support for concurrent offsets to simplify, so if you need that you’ll need to restore it (I recommend storing the various offset contributions in variables rather than subtracting last offset for this, though, as I’m now processing offset every frame so you wouldn’t be able to store last offset the same way).

I also made shake_frequency an export parameter but you can totally make it back to a function parameter to suit your needs.

In addition, stop_shake() can also take a duration for a smoother stop.

extends Camera2D

# Adapted from code snippet by Hammer Bro.
# on https://forum.godotengine.org/438/camera2d-screen-shake-extension
# Changes:
# - removed timer/duration as we use continuous shake
# - switch formula to smooth lerp between fixed previous and next keys every frame
#   instead of adding a big delta every period, to be closer to Jonny Morrill's method
#   (particularly visible at low frequencies)
# - don't set previous key on initialization, and wait for next key to be set
#   at the end of next period, so we chain smoothly chain shaking of different
#   intensities
# - safety checks to avoid infinite loop
# - update API to Godot 4 (randf_range)
# - shake frequency is an exported parameter, added stop_shake_duration
# - amplitude is named "intensity" but it has the same role
# - dropped support for existing offset (store offsets from different sources
#   and sum them at the end if you need to)

## Shake frequency (Hz)
@export var shake_frequency: float = 60.0

## Duration to stop shaking (s)
## You can set it to 1/shake_frequency if you want to end it as fast as the usual
## shaking moves
@export var stop_shake_duration: float = 0.0
 
# Shake parameters
var _intensity := 0.0
var _period_in_ms := 0.0

# Shake state
var _previous_key_offset := Vector2.ZERO
var _next_key_offset := Vector2.ZERO
var _shake_time_since_last_period := 0.0
   
func start_shake(intensity: float, frequency: float):
	# Initialize parameters
	_intensity = intensity
	_period_in_ms = 1.0 / frequency

	# Initialize state
	# Set previous key offset to current offset so we can chain a new shake
	# in the middle of another shake without delay, and smoothly from the last
	# offset
	_previous_key_offset = offset
	# Immediately compute next key offset so we don't have to wait for a period
	# to take the new shake into account
	_next_key_offset = _compute_shake_next_key_random_offset()
	# Start at time 0 rather than _period_in_ms, since we've just set the next
	# key offset anyway, so we can wait a period from here before the next change
	_shake_time_since_last_period = 0.0

func _process(delta):
	# Don't shake if intensity is zero, unless an offset remains from previous
	# shake
	# (this generally means we called stop_shake with a frequency > 0 and
	# in this case, we must still process for a bit until we finish smoothly
	# reducing offset to zero)
	# Also safeguard against infinite loops by checking _period_in_ms
	if _intensity <= 0.0 and offset == Vector2.ZERO or _period_in_ms <= 0.0:
		return

	# Advance shake time
	_shake_time_since_last_period = _shake_time_since_last_period + delta

	# When we cross a period, subtract it and advance to next keypoint. Use while to
	# be mathematically correct in the face of lag; usually only happens once.
	while _shake_time_since_last_period >= _period_in_ms:
		_shake_time_since_last_period = _shake_time_since_last_period - _period_in_ms

		# Noise calculation logic from http://jonny.morrill.me/blog/view/14
		# Instead of presampling as in Jonny Morrill's method, we compute the next
		# keypoint value just on time (and store the current "next" as previous keypoint)
		_previous_key_offset = _next_key_offset
		_next_key_offset = _compute_shake_next_key_random_offset()

	# Compute fractional position on current time segment
	var alpha := _shake_time_since_last_period / _period_in_ms

	# Compute linear progression from previous to next keypoint
	var new_offset := _previous_key_offset.lerp(_next_key_offset, alpha)

	# Set final offset
	set_offset(new_offset)

## Stop shaking with optional frequency
## If frequency > 0, use its inverse as the duration to gradually stop shaking
func stop_shake(duration: float = 0.0):
	if duration <= 0.0:
		# Instant stop
		_intensity = 0.0

		# Clear offset immediately
		set_offset(Vector2.ZERO)

		# Optional cleanup
		_period_in_ms = 0.0
		_previous_key_offset = Vector2.ZERO
		_next_key_offset = Vector2.ZERO
		_shake_time_since_last_period = 0.0
	else:
		# As a trick to stop shaking gradually over stop_shake_duration
		# is to start a new shake at intensity 0 and
		# frequency = 1 / stop_shake_duration
		# then let it reach the next key point at (0, 0).
		# The second next key point will also be (0, 0) which guarantees that
		# processing will stop as lerp between (0, 0) and (0, 0) is (0, 0)
		# so the condition at the top of _process
		# (_intensity <= 0.0 and offset == Vector2.ZERO) will be entered.
		start_shake(0.0, 1.0 / stop_shake_duration)

func _compute_shake_next_key_random_offset():
	return _intensity * Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0))