Once your characters move, the next thing they want to do is hit each other. The clean way to do combat in Godot is the hitbox and hurtbox pattern. It sounds like two names for the same thing. It isn't, and the distinction is what makes the whole system work.
Hitbox vs hurtbox
A hitbox deals damage. It's the swing of a sword, the blast radius of a grenade, the spikes on a trap. It's attached to the thing doing the hurting.
A hurtbox receives damage. It's the area of a character that can be hit. It's attached to the thing that can get hurt.
When a hitbox overlaps a hurtbox, damage happens. Keeping them separate means an enemy's attack can hit the player without the enemy accidentally damaging itself, and you can have a character that deals damage in one spot while being vulnerable in another.
Both are just Area2D nodes. The difference is entirely in how you wire them up.
Set up collision layers
This is the part people skip, and then nothing works. Go to Project Settings, Layer Names, 2D Physics, and name a few layers so you don't lose track. Something like:
- Layer 2:
player_hurtbox - Layer 3:
enemy_hurtbox - Layer 4:
player_hitbox - Layer 5:
enemy_hitbox
The rule: a hitbox sits on its own layer and masks the hurtbox layer it should hit. So the player's hitbox is on player_hitbox and masks enemy_hurtbox. It can only ever detect enemy hurtboxes. That's how you stop friendly fire without writing a single if check.
The hurtbox
The hurtbox is the simple one. It's an Area2D with a CollisionShape2D, and it forwards hits to a health component:
extends Area2Dclass_name Hurtbox@export var health: HealthComponentfunc take_hit(damage: int) -> void: if health: health.take_damage(damage)The hitbox
The hitbox carries a damage value and watches for hurtboxes entering it:
extends Area2Dclass_name Hitbox@export var damage: int = 10func _ready() -> void: area_entered.connect(_on_area_entered)func _on_area_entered(area: Area2D) -> void: if area is Hurtbox: area.take_hit(damage)That's the entire system. When an enemy hurtbox enters the player's hitbox, area_entered fires, we confirm it's a hurtbox, and we deal damage. The health component (which you can build with the health and damage guide) takes it from there and tells the UI to update through signals.
Turning the hitbox on and off
A sword shouldn't deal damage when it's sheathed. Enable the hitbox's collision only during the active frames of an attack. The easiest way is an AnimationPlayer track that toggles the CollisionShape2D's disabled property, or do it in code:
func attack() -> void: $Hitbox/CollisionShape2D.disabled = false await get_tree().create_timer(0.2).timeout $Hitbox/CollisionShape2D.disabled = trueUse set_deferred("disabled", ...) if Godot warns you about changing collision state mid-physics-step. That warning trips up a lot of people, and deferring the change is the fix.
Build a real combat system
Hitboxes and hurtboxes are the foundation, but real combat needs hit reactions, knockback, invincibility frames, and attack animations driving the timing. Our 2D Combat quest builds all of that into a working top-down fighter, including player death and respawn. Once the pattern clicks, every melee weapon, projectile, and trap in your game uses the same two nodes.
FAQ
What's the difference between a hitbox and a hurtbox?
A hitbox deals damage and is attached to whatever is attacking, like a sword swing. A hurtbox receives damage and is attached to whatever can be hit, like a character's body. Damage happens when a hitbox overlaps a hurtbox. Separating them prevents attackers from hurting themselves.
Should hitboxes be Area2D or PhysicsBody2D?
Area2D. You want to detect overlaps and deal damage, not push objects around or block movement. Area2D's area_entered signal is exactly the overlap detection combat needs, without the physics collision of a body node.
How do collision layers and masks work for combat?
A node's layer is what it is; its mask is what it scans for. Put a hitbox on its own layer and set its mask to only the hurtbox layer it should damage. That way the player's attacks detect enemy hurtboxes and nothing else, which kills friendly fire without any code.