Skip to main content
Coding Quests

My hitbox damages the player every frame

Overlap is true on every frame, so anything that reads it in _physics_process fires sixty times a second. Damage on the signal, then lock it out with invincibility frames.

Written against Godot 4.7.

The one line version

Overlap is a state, not an event. It stays true for every frame the two shapes are touching. Anything that reads it inside _physics_process runs sixty times a second, so a 10 damage hitbox is doing 600 damage a second.

the bug
GDScript
1
2
3
4
5
# The bug, and it reads as perfectly reasonable.
func _physics_process(_delta: float) -> void:
for area in $Hurtbox.get_overlapping_areas():
if area.is_in_group("hitbox"):
health -= area.damage

The fix is two halves, and you want both. Take damage from the signal instead of the state, and then stop the receiver being hurt again for a moment.

Half one: damage on the signal

area_entered fires on the frame the overlap STARTS. Once per entry, which is what you wanted.

player.gd
GDScript
1
2
3
4
5
6
7
8
# Damage on the EVENT, not on the state.
func _ready() -> void:
$Hurtbox.area_entered.connect(_on_hurtbox_area_entered)
​
​
func _on_hurtbox_area_entered(area: Area2D) -> void:
if area.is_in_group("hitbox"):
hurt(area.damage)

On its own this is still not enough. Three things fire the signal again: a hitbox re-enabled every swing, two hitboxes overlapping at once, or a hazard you are standing in that toggles. That is what the second half is for.

Half two: invincibility frames

Put them on the thing taking damage, never on the attacker. On the receiver they work no matter how many attackers, projectiles or lingering areas are touching it. On the attacker you have to solve it again for every new source of damage you add.

One second of overlap, both versions

Both scripts below are run by the same GDScript interpreter that grades the lessons on this site. The hitbox calls hurt(10) once per physics frame for 1 second, which is what an overlap read in _physics_process actually does. The numbers are measured from that run.

No guard

Running...

Invincibility frames

Running...

What most people write

player.gd
GDScript
1
2
3
4
5
6
7
8
9
extends CharacterBody2D
​
var health := 100.0
​
​
## Called once per physics frame while a hitbox is overlapping.
func hurt(amount: float) -> void:
health -= amount
​

The fix

player.gd
GDScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extends CharacterBody2D
​
## How long the player cannot be hurt again, in seconds.
@export var invincible_time := 0.5
​
var health := 100.0
var _iframes := 0.0
​
​
func _physics_process(delta: float) -> void:
_iframes = maxf(_iframes - delta, 0.0)
​
​
func hurt(amount: float) -> void:
if _iframes > 0.0:
return
health -= amount
_iframes = invincible_time
​

How to confirm this is your bug

Put one line in the function that takes the damage and watch the output:

the check
GDScript
1
2
3
func hurt(amount: float) -> void:
print("hurt ", amount, " at frame ", Engine.get_physics_frames())
health -= amount

One line per contact means your problem is somewhere else. A wall of lines with the frame number climbing by one each time is this bug, exactly.

Next

Build the whole pipeline instead of patching it

Hitbox, hurtbox, health, i-frames and knockback are one system. It is much easier to get right in one go than to repair a piece at a time. You write the GDScript in the browser and it gets checked as you type.

Start free, 8 lessons