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 core idea: two areas, opposite jobs
- A Hitbox is an
Area2Don your attack (a sword swing, a bullet, a stomp). It only scans - it never needs to be detected. - A Hurtbox is an
Area2Don 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
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)
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)
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
)
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
Comments
No comments yet. Start the discussion.