How to refresh Area2Ds?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Diet Estus

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?

:bust_in_silhouette: Reply From: flurick

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

:bust_in_silhouette: Reply From: TheGreatButtby

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.

I am having a similar problem but the body is in the area from the start. But after the restart, the signal is not called.(Solution?)
For the problem in this post, I used a flag variable that continuously calls the damage function.

Gazi Safin | 2022-12-09 10:54

:bust_in_silhouette: Reply From: gtkampos

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

I tried this , but the problem is I cannot put take_damage() in on_timer_timeout()
since I need a value for take_damage() that is take_damage(body.damage) lets it retrieve the value stored in the damage variable, wich is found in the script of my hitbox (so that I can use different variables for different values since there will be different weapons ect)
when I put take_damage() in on_timer_timeout() I recieve error(60,1): Too few arguments for “take_damage()” call. Expected at least one.

if I put an argument into take_damage() like take_damage(body.damage)
error the identiefer “body” isn’t declared in the current scope
so I declare it on_timer_timeout(body)
no errors, run program
E 0:00:05.025 emit_signal: Error calling method from signal ‘timeout’: ‘KinematicBody2D(Entitybase.gd)::_on_Timer_timeout’: Method expected 1 arguments, but called with 0…
<C++ Source> core/object.cpp:1242 @ emit_signal()

It cannot be done in this way, cannot put take_damage() in on_timer_timeout()

IhaveAproblem | 2022-11-03 00:58

:bust_in_silhouette: Reply From: Nathan Lovato

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.

:bust_in_silhouette: Reply From: Demiu

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 deal_damage that calls take_damage on everything in affected_entities. Maybe make on_body_entered_callback start the timer if it adds the first body to the array and on_body_exited_callback stop the timer when nothing’s inside. Remember to check validity bodies in affected_entities.

:bust_in_silhouette: Reply From: Smij

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