M3.1 — Hitboxes & Hurtboxes¶
What you'll learn
- Build reusable
HitboxandHurtboxArea2Dcomponents shared by player and enemies. - Configure collision layers (what a node is on) vs masks (what it scans) — the top combat bug.
- Carry
damageon the hitbox and forward it from the hurtbox onarea_entered.
How it applies
- Combat is the genre's core verb, and this is its plumbing. Every hit in the game — player on enemy, enemy on player, a trap on anyone — runs through hitbox-meets-hurtbox. Getting this seam clean once means every later combat feature attaches to it instead of reinventing it.
- Layers/masks are a fairness and correctness surface. Whether the player's attack can hit the player (it must not), whether enemies hit each other (usually not), whether a hit registers at all — all are decided by the layer/mask matrix. A wrong bit is a bug that looks like "damage sometimes doesn't happen," which is miserable to debug without understanding the matrix.
- Components are testable seams. A
Hurtboxthat knows nothing about who owns it can be dropped onto the player, a skeleton, or a destructible barrel, and exercised in isolation. This is the unit boundary a tester wants: the component is the unit, the actor is the integration. - Decoupling deal-damage from take-damage. The hitbox does not know what it hit; the hurtbox does not know what hit it. Each side only knows its own job, so new attackers and new victims compose without touching each other — the same respond-don't-reach-in principle as signals.
Concepts¶
The pattern¶
Split combat contact into two responsibilities, each an Area2D:
- A hitbox is the damaging region of an attack — the arc of a sword swing, the body of a projectile. It carries how much damage it deals. It is active only while the attack is live.
- A hurtbox is the damageable region of an actor — usually a bit smaller than the sprite so near misses don't count. It receives damage and forwards it to the actor's Health (M3.2).
When a hitbox overlaps a hurtbox, the hurtbox takes the hitbox's damage. Using two Area2D nodes (which
detect overlap without physically colliding) keeps damage detection separate from the body movement and
wall collision built in M2 — a sword arc should register a hit without shoving the enemy like a solid
object.
Layers and masks: "I am" vs "I look for"¶
Every Area2D and physics body has two bit sets:
- Collision layer — the layers the node is on. "I am a player hurtbox."
- Collision mask — the layers the node scans. "I look for enemy hitboxes."
Detection is asymmetric and one-directional by configuration: node A detects node B when A's mask
includes a layer B is on. For combat you deliberately make the hitbox scan for hurtboxes (or
vice-versa) and not the reverse, so each contact is handled once. Name the layers in
Project → Project Settings → Layer Names → 2D Physics so the matrix is readable instead of raw bit
numbers.
This book's layer assignment:
| Layer | Name | What is on it |
|---|---|---|
| 1 | world | TileMap walls |
| 2 | player | player body |
| 3 | enemy | enemy bodies |
| 4 | player_hurtbox | the player's Hurtbox |
| 5 | enemy_hurtbox | each enemy's Hurtbox |
| 6 | player_hitbox | the player's attack Hitbox |
| 7 | enemy_hitbox | each enemy's attack Hitbox |
| 8 | pickup | loot drops |
The combat wiring that follows from it:
- Player's attack Hitbox: on
player_hitbox(layer 6), mask scansenemy_hurtbox(layer 5). - Each enemy's Hurtbox: on
enemy_hurtbox(layer 5); it does not need a mask if the hitbox is the detector. (You can detect from either side; pick one side as the detector and be consistent. This book detects from the hurtbox side — see below — so the hurtbox masks the attacker's hitbox layer.)
Example
The classic bug: the player's attack does nothing against an enemy. You check the damage value (fine),
the animation (fine), the overlap (the shapes clearly cross). The defect is in the matrix: the
hurtbox's mask doesn't include the layer the hitbox is on, so area_entered never fires. Nothing
in the code is wrong; one checkbox is. This is why the layer/mask mental model — layer = I am,
mask = I look for — has to be solid before writing combat.
Detecting from the hurtbox side¶
A robust convention (used by the GDQuest combat demo and this book): the hurtbox is the detector. It
masks the attacker's hitbox layer and, on area_entered, reads the entering hitbox's damage and emits
its own hurt(amount) signal. The hitbox is then dumb — it just exists with a damage value while the
attack is active. Centralizing detection on the hurtbox means each actor decides for itself what it does
when hit (take damage, flash, knock back), which is exactly where that decision belongs.
Reusable component scenes¶
Both boxes are tiny scenes you author once and instance on every actor:
Example
Hitbox — an Area2D with a CollisionShape2D and a damage value. The whole script:
# res://scripts/hitbox.gd
class_name Hitbox
extends Area2D
## Damage this hitbox deals to any hurtbox it overlaps while active.
@export var damage: int = 1
No logic — it is a damage-tagged region. M3.3 enables/disables it during the attack animation.
Example
Hurtbox — an Area2D that, on contact with a hitbox, reads the damage and announces it:
# res://scripts/hurtbox.gd
class_name Hurtbox
extends Area2D
## Emitted when a hitbox overlaps. The owner's Health (M3.2) connects to this.
signal hurt(amount: int)
func _ready() -> void:
area_entered.connect(_on_area_entered)
func _on_area_entered(area: Area2D) -> void:
var hitbox := area as Hitbox
if hitbox == null:
return
hurt.emit(hitbox.damage)
area as Hitbox is a safe cast: if the overlapping area is not a Hitbox, it yields null and the
guard returns. So the hurtbox only reacts to genuine hitboxes, ignoring other Area2D (pickups,
triggers) that happen to overlap.
Walkthrough¶
- Name the layers:
Project → Project Settings → Layer Names → 2D Physics. Set layers 1–8 to the names in the table above. (Naming is cosmetic but turns every later layer/mask checkbox from a guess into a choice.) - Build the Hitbox component:
Scene → New Scene → Area2D, renameHitbox, add aCollisionShape2Dchild (a shape covering the attack's reach). Attachres://scripts/hitbox.gd(the two-line class). Save asres://scenes/component/hitbox.tscn. - Build the Hurtbox component: new scene,
Area2Droot renamedHurtbox, add aCollisionShape2Dchild (sized a bit inside the sprite for fairness). Attachres://scripts/hurtbox.gd. Save asres://scenes/component/hurtbox.tscn. - Set the layer/mask bits in the Inspector:
- On
hurtbox.tscn'sHurtbox: you will set its layer per-owner (player vs enemy) when you instance it, and its mask to the attacker's hitbox layer. For now, on the player's instance, set layer =player_hurtbox(4), mask =enemy_hitbox(7). (The player's hurtbox looks for enemy hitboxes.) - On
hitbox.tscn'sHitbox: set per-owner when instanced — the player's attack hitbox onplayer_hitbox(6); it needs no mask because the hurtbox is the detector. - Instance a
Hurtboxon the player: openplayer.tscn, draghurtbox.tscnontoPlayer. Position its shape over the character. Confirm its layer =player_hurtbox, mask =enemy_hitbox. - There is no attacker yet to test against — M3.3 builds the player's attack hitbox and M4 the enemy's. For now, confirm the components exist, the layers are named, and the player carries a hurtbox.
Optional sanity check
Temporarily instance a Hitbox somewhere overlapping the player's Hurtbox, set the hitbox's layer
to enemy_hitbox (7) and a damage of 5, and add a temporary print("hurt ", amount) to the
hurtbox's hurt handler (or connect the signal to a temporary printer). Run: the hurtbox should print
hurt 5 once on overlap. Then flip the hurtbox's mask off enemy_hitbox and confirm it prints
nothing — the live demonstration that the mask, not the overlap, gates detection. Remove the temp
hitbox and prints.
Self-check quiz¶
Q1 — A hitbox clearly overlaps a hurtbox, the damage value is set, but no hit registers. Where is the bug most likely to be?
A. The damage value is too low to register.
B. The detector's collision mask does not include the layer the other box is on, so
area_entered never fires.
C. Area2D cannot detect other Area2D.
D. The sprites are not overlapping, only the shapes.
Reveal answer
B. Overlap alone is not enough — detection requires the detector's mask to include a layer the
target is on. A wrong bit there means the signal never fires regardless of overlap or damage. A is
false (any positive damage registers; zero still fires the signal). C is false — Area2D
detecting Area2D is exactly the mechanism. D inverts what matters: the shapes' overlap is what
counts, not the sprites'.
Q2 — Why does the hurtbox's handler cast with area as Hitbox and return if the result is null?
A. To convert the damage to an integer.
B. So the hurtbox reacts only to actual hitboxes — ignoring other Area2D (pickups, triggers) it may
overlap, and avoiding the error of reading .damage on an area that has no such property.
C. Because area_entered passes a Hitbox only sometimes.
D. The cast converts the entering area into the actor that owns it, so .damage resolves on the owner.
Reveal answer
B. area_entered passes any Area2D, not just hitboxes; the safe cast yields null for
anything that isn't a Hitbox, and the guard skips it — so the hurtbox both ignores non-hitboxes
(pickups, triggers) and never reads .damage on an area that lacks it (which would error). A is
false (no conversion happens). C misstates the signal — it passes whatever entered, which is why
the type check exists. D is false: as Hitbox narrows the type to Hitbox, it does not reach the
owning actor.
Q3 — Why are the hitbox and hurtbox built as separate component scenes rather than baked into each actor?
A. Components render faster.
B. A dependency-free component can be reused on the player, every enemy, and destructibles, and tested
in isolation; baking it into actors duplicates and couples it.
C. Godot requires Area2D to be in its own scene.
D. To reduce the project's file count.
Reveal answer
B. The reuse and isolation are the point: one Hurtbox scene serves every damageable thing,
and a fix applies everywhere at once. Baking the boxes into each actor copies the logic per actor
and couples it to that actor's internals. A is false. C is false (areas work anywhere). D is
backwards — components add files, and that is worth it.
Integration question¶
Q4 — open
Trace a complete player-hits-enemy event using only what this chapter built, and name the layer/mask bits that must be correct at each step. Then explain why the hurtbox, not the hitbox, was chosen as the detector, and what that choice means for an enemy that should flash and knock back when hit versus one that should only take damage.
Reveal expected answer
The event: the player's attack Hitbox (on layer player_hitbox/6, damage set, active during
the swing) overlaps the enemy's Hurtbox (on enemy_hurtbox/5, mask including player_hitbox/6
because the hurtbox is the detector). The overlap fires the hurtbox's area_entered; it casts the
area to Hitbox, reads damage, and emits hurt(damage), which the enemy's Health (M3.2)
consumes. The bits that must be right: the hitbox must be on player_hitbox, and the hurtbox's
mask must include player_hitbox; if either is wrong the signal never fires. The hurtbox was
chosen as the detector so each actor owns its own reaction to being hit: the enemy's Hurtbox emits
a generic hurt(amount), and that enemy decides what to do with it — one enemy's setup connects
hurt only to its Health (take damage), another also connects it to a flash and a knockback
(M3.4). Centralizing detection on the victim keeps the attacker dumb and lets reactions vary per
actor without changing the hitbox at all.