DEV Community

saltmire
saltmire

Posted on • Originally published at saltmire.github.io

Godot 4 hitbox and hurtbox the easy way (damage, teams, i-frames, knockback)

Almost every action game needs the same thing: something that deals damage, and
something that takes it. In Godot 4 the raw tools are Area2D + CollisionShape2D,
but wiring them into a real hit is where people get stuck — collision layers vs
masks, stopping the player from hurting itself, not draining a target every physics
frame, and adding knockback and invincibility. Here's the whole thing, cleanly.

The idea: two areas, opposite jobs

  • A Hitbox is an Area2D on your attack (a sword swing, a bullet, a stomp). It only scans — it never needs to be detected.
  • A Hurtbox is an Area2D on anything that can be damaged. It only needs to be detected — it never scans.

That split is the trick most tutorials skip, and it's why layers get confusing.

Layers vs masks, in one sentence

collision_layer is what you are; collision_mask is what you look for. So:

# hurtbox: I exist on layer 20, and I look for nothing.
collision_layer = 0
set_collision_layer_value(20, true)
collision_mask = 0
monitorable = true
monitoring = false

# hitbox: I exist on nothing, and I look for layer 20.
collision_layer = 0
collision_mask = 0
set_collision_mask_value(20, true)
monitoring = true
Enter fullscreen mode Exit fullscreen mode

Now a hitbox sees hurtboxes and only hurtboxes — no stray collisions with
walls or bodies. Pick any free bit; 20 is just an example.

Teams (so the player doesn't hit itself)

Give each box a team integer and refuse same-team hits:

func _on_area_entered(area):
    if area is Hurtbox and area.team != team:
        area.receive(damage, self)
Enter fullscreen mode Exit fullscreen mode

The bug nobody warns you about: overlapping-on-activate

area_entered only fires when two areas start overlapping. If your hitbox turns
on while it's already sitting on top of the target — very common for a melee swing —
the signal never fires and the hit is silently lost. Fix it by also scanning what's
already inside when the swing starts:

func activate():
    active = true
    for area in get_overlapping_areas():
        _try_hit(area)
Enter fullscreen mode Exit fullscreen mode

Invincibility frames and knockback

After a hit, ignore further hits for a moment, and push the target away:

func receive(damage, source):
    if _invincible: return
    var dir = (global_position - source.global_position).normalized()
    hurt.emit(damage, dir * knockback_strength)
    if invincible_time > 0.0:
        _invincible = true
        get_tree().create_timer(invincible_time).timeout.connect(
            func(): _invincible = false)
Enter fullscreen mode Exit fullscreen mode

That's a complete, correct hitbox/hurtbox: layers handled, friendly fire blocked,
no lost melee hits, i-frames and knockback in place.

Skip the boilerplate

If you'd rather drop in two nodes and move on, Saltmire Hitbox packages exactly
this — Hitbox2D / Hurtbox2D with zero layer setup, plus damage types, crits,
per-type resistances, animation-frame timing and a runtime debug overlay. There's a
free MIT Lite core too if you just want the basics.


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

Originally published at https://saltmire.github.io/godot-4-hitbox-hurtbox.html

Top comments (0)