2025/10/21

Type
Learning Resource
Format
Study Guide
Version
Godot 4.x
  • Downloadable Demo
  • FAQ/Troubleshooting
Code
Assets
All else
Copyright 2016-2025, GDQuest
Created
2025/10/03
Updated
2025/10/21

Handling attacks and damage with hit and hurt boxes

In many games, dealing and taking damage is part of the core gameplay loop. To make this work, we use hit boxes and hurt boxes. Hit boxes are placed on weapons to deal damage (they hit things). Hurt boxes are placed on characters or objects that can take damage (they're the ones that get "hurt").

This video was made with Godot 3. The guide below has been rewritten for Godot 4.

We usually create dedicated physics shapes to represent hit and hurt boxes for each character or entity that can deal or take damage. In Godot, we implement them using Area2D nodes for 2D games and Area3D for 3D games.

In this guide, you'll learn:

Why would I use separate hit and hurt boxes?

Not all games need hit and hurt boxes separate from character physics: in a platformer you could just detect when your CharacterBody2D player collides with the enemy CharacterBody2D to damage them.

Hit and hurt boxes become necessary for games with more complex combat mechanics. One of the most common use cases is when a character can take damage to multiple body parts, like a headshot or a leg shot. For that you need different physics entities for each body part. Depending on the part that gets hit, you can apply different damage amounts or cripple the character.

How many hit boxes you need depends on the combat mechanics of your game. A sword swing, for example, can be a single hit box that can hit one or multiple hurt boxes.

If combat mechanics are central to your game, you'll often need many hit and hurt boxes that you'll want to tweak often during development. In that case, it's useful to implement them in a way that makes it easy to add, remove, and change them separately from movement physics.

What changed in Godot 4

This section is a quick reference for the video above, which was made with Godot 3. Here's what changed in Godot 4 compared to the video:

  • Annotations over keywords: To make a property available in the editor, we now use the @export annotation instead of the export keyword. Also, we now use @onready instead of the onready keyword to initialize a variable when the node is added to the scene tree.

  • Custom icons: In the demo code, you'll see the @icon annotation to set a custom icon that appears in the scene tree. In Godot 3, we used to declare an icon like this: class_name MyClass, "res://path/to/optional/icon.svg".

  • Signal connections: Signals and functions are now first-class citizens. We connect signals using objects, like area_entered.connect(_area_entered), instead of using plain strings like before: connect("area_entered", self, "_on_area_entered"). This gives you better autocompletion, error detection, and lets you connect to any function, including lambdas.

  • Collision detection: In Godot 3, collision detection worked both ways even when you didn't want it to. Let's say you have a player looking for collectible coins. In Godot 3, those coins would also detect the player, even if the coins weren't set up to scan for anything. In Godot 4, an object only detects collisions if it's actively looking for them through its collision_mask. This means collision detection can work in one direction only, which is both easier to understand and more efficient.

  • Removed _init() functions: In this updated demo, we removed the _init() function definitions in favor of using _ready(). This makes it clearer that the assignments happen when the node is added to the scene tree, and it's a more familiar function for Godot users.

The rest of this guide uses Godot 4 syntax and conventions.

The hit and hurt box setup

First, I recommend downloading the demo project to explore the code and see how it works. There are two important scenes in the demo:

In this demo, we named physics layers 1 and 2 in the project settings as Game World and Hit Boxes, respectively.

Here's how we set up our two types of areas:

You may also want hit boxes to detect when they hit something, for example, to play a different sound or visual effect depending on what they hit.

In that case, you can put hit and hurt boxes on different layers and masks and make them detect each other.

Nathan

Founder and teacher at GDQuest
Why these layer and mask settings work

Collision layers tell Godot what layer an area is on. Collision masks tell Godot which layers an area should look for.

In this demo, we want the hurt boxes to detect when they get hit. We put hit boxes on layer 2 and tell hurt boxes to look for layer 2 through their collision mask. This way, each object that can take damage handles its own damage calculations.

In a larger game project, you'll most likely want to put your damage code on the receiver's end like this. It's easier to have each character or enemy manage how much health it has and what happens when it takes damage (if there are multiple types of damage, resistances, status effects, knockback, etc.).

The hit box

We use Area2D nodes for hit and hurt boxes because they detect overlaps without affecting physics. This means your character won't get pushed around when you hit something.

In our demo, the player has a sword that attacks the enemy.

The sword has a HitArea2D node as a child of the Sprite2D node. This way, the hit box rotates with the sprite when the attack animation plays.

Screenshot showing the sword scene with a hit box

The hit box carries the damage information. Here's the code for our HitArea2D class:

class_name HitArea2D extends Area2D

@export var damage := 10

Making damage an export variable means you can change it in the editor for different weapons without touching the code.

I've also turned off Monitoring. The hit box doesn't need to detect anything. The hurt box on the enemy will detect the hit.

Turning off collision masks, monitoring and monitorable properties, or disabling collision shapes is a great way to improve physics performance as Godot doesn't need to check for collisions between objects that don't need to collide.

If you don't do that, Godot will check for collisions even if you don't use the results, which gets really costly when you have a lot of entities.

Nathan

Founder and teacher at GDQuest

The CollisionShape2D is disabled by checking its Disabled property. We only want the hit box active during the attack animation.

Screenshot showing the HitArea2D's CollisionShape2D disabled in the Inspector

The hurt box

The hurt box receives damage from hit boxes. In our demo, the enemy has a HurtArea2D node that represents where it can take damage.

Like the HitArea2D, we make the HurtArea2D a child of the Sprite2D node so it moves and rotates with the sprite.

Screenshot showing the enemy scene with a hurt box

The hurt box connects to the area_entered signal. When a hit box enters, it calls take_damage() on the owner:

class_name HurtArea2D
extends Area2D


func _ready() -> void:
	area_entered.connect(
		func _on_area_entered(hit_area: HitArea2D) -> void:
			if hit_area != null and owner.has_method("take_damage"):
				owner.take_damage(hit_area.damage)
	)

The owner property is a reference to the root of the scene in which the HurtArea2D is placed. It's a convenient way to access the scene's root node and make a node work as a component, independently of the scene's structure.

Here, I've used duck typing: any scene with a HurtArea2D child can react to damage as long as it has a take_damage() method.

Notice we use HitArea2D as the parameter type instead of Area2D. This gives us two benefits:

How come you can use HitArea2D as a parameter type?

When we want to connect a signal to a function, the function must have a matching signature. For example, the area_entered signal passes an Area2D as an argument to the connected function, so the function must accept an Area2D as a parameter.

Instead of using Area2D as the parameter type, we can use any type that extends Area2D, like our custom HitArea2D. If the area that entered is not an instance of HitArea2D, the function will still be called, but hit_area will be null.

This is equivalent to writing the connected function like this, using the as keyword to cast the Area2D to a HitArea2D:

func _on_hit_area_area_entered(area: Area2D) -> void:
    var hit_area := area as HitArea2D
    if hit_area != null:
        take_damage(hit_area.damage)

NOTE:
This doesn't work the other way around. If we tried to connect the signal to a function that expects a more generic type, like Node2D, it would break because Node2D is too general and doesn't match the expected signature of the signal.

Questions and troubleshooting

What should be the size of my hit and hurt boxes?

The size of your hit and hurt boxes is a design choice that depends on your game. For example, in single-player games, you might make enemy hurt boxes larger than their sprites to make them easier to hit.

At the same time, you could make the player's hurt box smaller than the sprite to make attacks easier to dodge. This makes the game more forgiving and fun for the player.

Download files

Import the zip file in Godot 4.5 or later to explore the code and see how everything works together.

Updates / Code patches

Become an Indie Gamedev with GDQuest!

Don't stop here. Step-by-step tutorials are fun but they only take you so far.

Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.

Nathan

Founder and teacher at GDQuest
  • Starter Kit
  • Learn Gamedev from Zero
Check out GDSchool

You're welcome in our little community

Get help from peers and pros on GDQuest's Discord server!

20,000 membersJoin Server

Contribute to GDQuest's Free Library

There are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!