DEV Community

saltmire
saltmire

Posted on • Originally published at saltmire.github.io

Godot 4 screen shake and hit-stop in one script

"Game feel" is mostly a handful of cheap effects stacked on top of otherwise
normal gameplay. Two of the highest-impact ones are screen shake and
hit-stop. Here is how to do both in Godot 4 with code you can reuse.

Screen shake on a Camera2D

The trick is to offset the camera by a random amount each frame and decay that
amount over time. Attach this to your Camera2D:

extends Camera2D

var _trauma := 0.0        # 0..1, how much shake is queued
var _decay := 4.0         # how fast it fades per second
var _max_offset := 12.0   # pixels at full trauma

func shake(amount: float) -> void:
    _trauma = min(_trauma + amount, 1.0)

func _process(delta: float) -> void:
    if _trauma <= 0.0:
        offset = Vector2.ZERO
        return
    _trauma = max(_trauma - _decay * delta, 0.0)
    var t := _trauma * _trauma           # square it: small hits stay subtle
    offset = Vector2(
        randf_range(-1, 1) * _max_offset * t,
        randf_range(-1, 1) * _max_offset * t
    )
Enter fullscreen mode Exit fullscreen mode

Call camera.shake(0.4) on a hit. Squaring the trauma (t = _trauma * _trauma)
is what makes it feel good — light hits barely nudge, big hits kick hard.

Hit-stop (freeze frames)

Hit-stop briefly freezes the game on impact so a hit reads as heavy. In Godot 4
you scale Engine.time_scale down, wait, then restore it. Use an unscaled timer
so the freeze itself is not slowed:

func hit_stop(duration := 0.08, scale := 0.05) -> void:
    Engine.time_scale = scale
    # ignore_time_scale so the wait runs in real time
    await get_tree().create_timer(duration, true, false, true).timeout
    Engine.time_scale = 1.0
Enter fullscreen mode Exit fullscreen mode

await hit_stop() on a big hit. Keep it short — 60–120 ms is plenty. Longer than
that and the game feels laggy instead of punchy.

Stacking them

A satisfying hit usually fires several of these at once: shake + hit-stop + a
white flash on the sprite + damage numbers + a little particle burst. Each is a
few lines, but wiring all of them, tuned, for every hit type — light hit, heavy
hit, crit, death — is where the boilerplate piles up.

func on_enemy_hit(enemy, damage):
    camera.shake(0.3)
    await hit_stop()
    flash(enemy.sprite)
    spawn_damage_number(enemy.global_position, damage)
    # ...and a particle burst, and sound, per hit type
Enter fullscreen mode Exit fullscreen mode

Skip the wiring

The individual effects are free to build. If you would rather call one function —
Impact.hit(enemy, "heavy") — and get shake, curved hit-stop, flash, damage
numbers and shader FX in tuned combos, that is what Saltmire Impact is. The
building-block pieces (Juice, FX, Trail, Spark) are free and open source; Impact
is the orchestrator that ties them together.


If you'd rather drop this in than build it, Saltmire Impact does it as a ready-made tool: https://saltmire.itch.io/saltmire-impact

Originally published at https://saltmire.github.io/godot-4-screen-shake-hit-stop.html

Top comments (0)