There are a lot of ways to shake a camera in Godot 4. As I’ve been trying some of them out, I wanted to share a few techniques that can be copy/pasted or expanded on. While the the following example is typically referred to as “simple” or “basic” screen shake, I don’t think it necessarily means something “harder” or more “complex” is always better. This is a chaotic screen shake and, sometimes, that’s what you want.

The foundation

Set up the camera with the following node structure:

  • CharacterBody2D - our player (could be any subclass of Node2D)
    • Camera2D - this is our main camera
      • Camera2D - this is the camera we’ll shake

We’re not going to be shaking the main camera. We’ll have a secondary “shake” camera for that.1 2

The properties on the Camera2D we’ll be using to shake the camera are the offset and the rotation.

IMPORTANT

Under your shake camera, in the editor, go to Camera2D > Ignore Rotation and DISABLE it. If you don’t, your camera will never rotate while it’s shaking!

Let’s get started.

Simple camera shake

How it works

One of the simplest ways to shake the camera is to define some sort of max_offset, max_rotation, and decay. The max_offset will represent the maximum amount we want to offset the camera from the parent camera’s position. The max_rotation will be the maximum amount we rotate the camera while shaking. With rotation, a little goes a long way. The decay will represent how fast we decrease the intensity from the max_offset.

We initiate the shake by calling .shake() on the SimpleShakeCamera. This simply sets our internal representation of the current shake offset and the current shake rotation to the max_offset and max_rotation we set in the editor.

Every frame, we’ll decrease the internal representations by using the decay, which is also set in the editor. When the shake camera notices there is some shaking going on, it’ll take over as the main camera. Once it sees that the shake has mostly worn off, it’ll reset itself and give control back to the main camera.

The code

Here’s one example of how we could implement all of this.

class_name SimpleShakeCamera
extends Camera2D


@export var camera: Camera2D
@export var max_offset := Vector2(10.0, 10.0)
@export var max_rotation := 0.01
@export var decay := 5.0


var _shake_offset := Vector2.ZERO
var _shake_rotation := 0.0


func _process(delta: float) -> void:
	_update_current_camera()

	if not is_current():
		return

	_shake_offset = _shake_offset.lerp(Vector2.ZERO, decay * delta)
	_shake_rotation = lerp(_shake_rotation, 0.0, decay * delta)

	offset = camera.offset + Vector2(
		randf_range(-_shake_offset.x, _shake_offset.x),
		randf_range(-_shake_offset.y, _shake_offset.y),
	)

	rotation = camera.rotation + randf_range(-_shake_rotation, _shake_rotation)


func _update_current_camera() -> void:
	if not _shake_offset.is_zero_approx() and not is_current():
		make_current()
	elif _shake_offset.is_zero_approx() and not camera.is_current():
		_make_camera_current()


func _make_camera_current() -> void:
	offset = Vector2.ZERO
	rotation = 0.0

	_shake_offset = Vector2.ZERO
	_shake_rotation = 0.0

	camera.make_current()


func shake() -> void:
	_shake_offset = max_offset
	_shake_rotation = max_rotation

A few quick notes

Why do we need a reference to the parent camera?

@export var camera: Camera2D

I prefer to have a solid reference to my camera, instead of assuming it’s the parent. This is just a matter of taste. Feel free to get it any way you like.

What’s the deal with the parent camera’s offset?

offset = camera.offset + Vector2(
  randf_range(-_shake_offset.x, _shake_offset.x),
  randf_range(-_shake_offset.y, _shake_offset.y),
)

This is necessary to account for any offset our parent camera might have. If you have an offset on the parent and don’t add it here, your camera will snap to the parent camera’s position to shake. This would be very jarring.

Why use .is_zero_approx()?

func _update_current_camera() -> void:
	if not _shake_offset.is_zero_approx() and not is_current():
		make_current()
	elif _shake_offset.is_zero_approx() and not camera.is_current():
		_make_camera_current()

If you’ve ever watched lerp move a value down to 0, it can take a really long time going over extremely small, non-zero values (i.e. 0.0000045). I’d prefer to switch back to the main camera as soon as we’re reasonably close to zero and stop doing the extra calculations.


  1. Squirrel Eiserloh, Math for Game Programmers: Juicing Your Cameras With Math 

  2. Using a secondary camera can protect us from accidentally altering any other properties on the main camera. What if we’re actually using the offset on the main camera? What if those offsets are changing while we’re shaking? And don’t worry, only one of them will be on at a time.