0 votes

I use Area2Ds for the hurt boxes of my hazards.

If my player enters one of these areas, a take_damage() method is called, which lowers his health and turns him invincible for a period.

The problem is that if the player simply stays within the hazard's area until his invincibility wears off, because the area's on_body_entered signal has already been emitted, it will not emit again to trigger another call of my player's take_damage().

Obviously this is not ideal. If a player is lingering around a hazard after he's hurt by it, you'd like him to be hurt again when his invincibility wears off.

How can I fix this?

Is there a way to "refresh" an Area2D so that it forgets about which bodies are currently in it, and checks again for whether it should emit the on_body_entered signal?

in Engine by (1,571 points)

6 Answers

0 votes

What if you disabled the players collider area/shape/check while invincible, would that work?

by (953 points)
+2 votes

I don't know if there is specific reason you need to use the signal, but it might be better if you created an Area2D get overlapping bodies method rather than using the signal.

by (60 points)
+1 vote

I will assume you have a timer for the invencibility time. So you could make a bool flag to check continuous damage, and add a timeout() signal from Timer node.

var is_taking_damage = false

func take_damage():
     timer.start()
     is_invencible = true
     is_taking_damage = true

func on_body_enter(body):
     take_damage()

func on_body_exit(body):
     is_taking_damage = false

func on_timer_timeout():
     if (is_taking_damage == true):
          take_damage()
     else:
          is_invencible = false
by (191 points)
+1 vote

Using the body_entered signal is a good idea because you get an instant callback when the character's body first overlaps with the area. Using get_overlapping_bodies is a valid alternative but it's error-prone: you need extra checks to make sure you only damage the player at regular time intervals.

There are a few solutions to this problem, but here's one I often use: when the character enters the damaged area, you can either queue it free or remove the child temporarily. After a short amount of time, you add the area back to the game:

# CharacterDamager.gd
const DAMAGE_COOLDOWN = 0.4
onready var hitbox = $HitBox # The node that detects the player's body

func _on_HitBox_body_entered(body):
    remove_child(hitbox)
    yield(get_tree.create_timer(DAMAGE_COOLDOWN), "timeout")
    add_child(hitbox)

At this point, the character will take damage again the next time it moves inside the area. You can add a statement like hitbox.position = hitbox.position upon adding it back to the tree to have it check for overlapping bodies.

by (58 points)
+1 vote

Keep references to objects that can take_damage in the hurtbox

var affected_entities = [];

func on_body_entered_callback(body):
    if (body.has_method("take_damage"):
        affected_entities.append(body);

func on_body_exited_callback(body):
    affected_entities.erase(body);

Then just add a timer to the hurtbox which will call dealdamage that calls takedamage on everything in affectedentities. Maybe make onbodyenteredcallback start the timer if it adds the first body to the array and onbodyexitedcallback stop the timer when nothing's inside. Remember to check validity bodies in affectedentities.

by (27 points)
+1 vote

One thing that has worked for me is that an Area2D has a monitoring property (I'm using Godot 3.x).
If you set monitoring to false, then set it to true after your cooldown, it will reset the state and detect bodies that are still inside the Area2D.

So, something like:

onready var hit_box = $Area2D
func _on_hit_box_body_entered(body: Node) -> void:
    hit_box.monitoring = false

func _on_cooldown_timer_timeout() -> void:
    hit_box.monitoring = true. # will detect nodes that have not left the Area2D

You can also rework Nathan's more straightforward and imho simpler approach if you'd rather not add/remove the node from the tree:

func _on_HitBox_body_entered(body):
    hitbox.monitoring = false
    yield(get_tree.create_timer(DAMAGE_COOLDOWN), "timeout")
    hitbox.monitoring = true
by (16 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.