HitBox/HurtBox with CharacterBody2D and queue_free()

Godot Version

4.3

Question

Hey!

I’ve got an odd issue. I’ve written functioning code for HitBoxes and HurtBoxes using Area2D, and when attaching the HitBox script to a CharacterBody2D projectile it works flawlessly, damaging a CharacterBody2D with a HitBox without any issue. Once I run queue_free() to remove the projectile however, the HitBox collision no longer registers, despite the projectile itself dissapearing upon collision.

Code for HitBox:

class_name HitBox
extends Area2D

@export var damage: int = 1 : set = set_damage, get = get_damage

func set_damage(value: int):
	damage = value

func get_damage() -> int:
	return damage

Code for HurtBox:

class_name HurtBox
extends Area2D

signal recieved_damage(damage: int)

@export var health: Health

func _ready() -> void:
	connect("area_entered", _on_area_entered)

func _on_area_entered(hitbox: HitBox) -> void:
	if hitbox != null:
		health.health -= hitbox.damage
		recieved_damage.emit(hitbox.damage)

And the code for the projectile:

extends CharacterBody2D

const SPEED = 550
var direction = Vector2.ZERO

func _ready() -> void:
	await get_tree().create_timer(3.0).timeout
	queue_free() 

func _process(delta):
	var collisionResult = move_and_collide(direction * SPEED * delta)
	
	#After adding this, the hits no longer register and the damage isnt dealt
	if collisionResult != null:
		queue_free()

The projectile and the target are both CharacterBodies2D and both have their own CollisionShape2D, as well as a sperate CollisionShape2D for the Area2D of the respective HitBox and HurtBox, I can imagine complications arise from having two CollisionShapes, but I’m forced to have one for the physics simulations, and I can’t use the one attached to the Hit/HurtBox for both purposes. I assume that when the projectile disappears, it does so too quickly for the hit to register, but there must be some elegant way to fix this conundrum?

You can try either making your HurtBox’s Collision shape a little bigger than the actual projectile so that the area can have a chance to register the hit slightly earlier than the bullet hits the targe, and you can also wait a frame before queuing your bullet, so that the HurtBox have some time to properly register the hit.

	if collisionResult != null:
		await get_tree().process_frame
		queue_free()
2 Likes

I’ve already attempted changing Collision sizes, and unless I made the HitBox unreasonably large it didn’t really help much, I’ll try to implement your code when I get the chance however!

The idea is for this to be a competitive game, so I’d like the collisions and timings to be relatively tight. Frame queue sounds smart, I’ll give it a shot.

1 Like

What you are doing is hoping that the hitbox _on_area_entered signal triggers first before your projectiles collision check which happens every frame. This is not a good way to do that, it will be unpredictable at best and can cause bugs.

If I recall correctly, area2d entered signal can be a frame late so you have to account for that.

A good solution is to follow Godots’ call down, signal up rule, your hitbox should signal up to the projectile that it has hit something, then the projectile knows it can self destruct.

Or just wait a cool of frames to be sure that the area2d signal has been emitted as posted by the person before me.

2 Likes

I suppose my only hurdle with that is that I’d like to use the HitBox class for all damage, not only projectiles… How would such a signal work in that case, since it’s specific to things that only destruct, like projectiles, whilst not relevant to other forms of damage.

Only things that destruct will connect to that signal. The hit box is going to send the signal any time it hits something and any node that needs the signal to take an action does so.

1 Like

So, I attempted both methods. Adding a delay of 2 frames did solve the issue, but it felt like a band-aid fix (although a viable alternative to keep code simple).

I then tried to wrap my head around and implement the call down, signal up rule… It works, but I would really appreciate if you just skimmed through my logic to see if this is “good code”, or pure spaghetti.

HitBox code:

class_name HitBox
extends Area2D

signal hit_registered

@export var damage: int = 1 : set = set_damage, get = get_damage

func set_damage(value: int):
	damage = value

func get_damage() -> int:
	return damage

HurtBox code:

class_name HurtBox
extends Area2D

signal recieved_damage(damage: int)

@export var health: Health

func _ready() -> void:
	connect("area_entered", _on_area_entered)

func _on_area_entered(hitbox: HitBox) -> void:
	if hitbox != null:
		health.health -= hitbox.damage
		hitbox.emit_signal("hit_registered")
		recieved_damage.emit(hitbox.damage)

Projectile script:

extends CharacterBody2D

const SPEED = 550
var direction = Vector2.ZERO

@onready var hitbox: HitBox = $HitBox

func _ready() -> void:
	
	if hitbox != null:
		hitbox.connect("hit_registered", Callable(self, "_on_hit_registered"))
	
	await get_tree().create_timer(3.0).timeout
	queue_free() 

func _process(delta):
	var collisionResult = move_and_collide(direction * SPEED * delta)

	if collisionResult != null:
		hitbox.emit_signal("hit_registered")

func _on_hit_registered() -> void:
	queue_free()

What I ended up doing is putting the mandatory CollisionShape2D of the CharacterBody2D (projectile) on a different layer/mask to the HitBox CollisionShape2D, so it exclusively collides with only terrain, whilst the CollisionShape2D of the HitBox collides with “entities” or enemies…

Is this a Godot-esque way of going about things, I don’t quite know GDscript or the software well enough to judge for myself yet…

Thank you for your hep thus far!

You are on the right way.

Why is your hurtbox emitting a hitbox signal? You should try to keep the functions of each node encapsulated in the node script so it’s easier to debug.

Maybe it’s the way you’re using hitboxes and hurtboxes. The way I think of it, hitbox makes damage and hurtbox takes damage. Does your projectile actually take damage from another hitbox or your end goal is just to destroy it on impact.

I think for a simple object like a projectile that gets destroyed on impact, all you need is a hitbox added to the projectile node as a child, that sends a message to the projectile node to self destruct on impact.

So, in your projectile script, connect to your hitbox child node like this;

func _ready():
    hitbox.hit_registered.connect(_on_hit_registered)

func _on_hit_registered():
    queue_free()

Then in your hitbox script, you add:

class_name HitBox
extends Area2D

signal hit_registered

func _on_area2d_entered(body):
   ##Do whatever check you need to do here to make sure it hit the right node.
   hit_registered.emit()

You no longer need this in your code:

The hitbox is now completely responsible for detecting if something has been hit and notifying the parent node (projectile) that something has been hit.

This also make the hitbox easily reusable with other nodes.

I hope all these makes sense.

1 Like

Alright, that’s much more elegant of a solution I admit, and the hits do register, and damage is dealt.

Primarily I had the issue that the projectiles didn’t disappear when hitting terrain, then after messing around with the layers, the projectiles don’t even disappear when hitting the HurtBox. I am genuinely losing my mind here, I have tried troubleshooting a fair bit, but I just hope its not too tall a task for you to guide me through this a bit more…

Updated HitBox code:

class_name HitBox
extends Area2D

signal hit_registered

@export var damage: int = 1 : set = set_damage, get = get_damage

func set_damage(value: int):
	damage = value

func get_damage() -> int:
	return damage

func _on_area2d_entered(body):
	hit_registered.emit()

Updated HurtBox code:

class_name HurtBox
extends Area2D

signal recieved_damage(damage: int)

@export var health: Health

func _ready() -> void:
	connect("area_entered", _on_area_entered)

func _on_area_entered(hitbox: HitBox) -> void:
	if hitbox != null:
		health.health -= hitbox.damage
		recieved_damage.emit(hitbox.damage)

Updated Projectile code:

extends CharacterBody2D

const SPEED = 550
var direction = Vector2.ZERO

@onready var hitbox: HitBox = $HitBox

func _ready():
	
	hitbox.hit_registered.connect(_on_hit_registered)
	
	await get_tree().create_timer(3.0).timeout
	queue_free() 

func _process(delta):
	move_and_collide(direction * SPEED * delta)

func _on_hit_registered():
	queue_free()

The HitBox is Layer 1, and Mask 1, 4. (Projectile itself is Mask 1, 4, no Layer)
The HurtBox is Mask 1 and nothing else. (The enemy is Layer 4, Mask 1)

I probably just made some error along the way, and are currently too spaghetti-code-brained to see it, but I’m still getting used to Godot, so I’m thankful for the patience.

First of all, you don’t need the set and get functions here as you are not transforming the variable in any way when it is changed or when you want to fetch it. Remove the set and get functions and just call hitbox.damage when you want to get it.

The issues you are facing is from the way you setup your collision layers.
Layers are the physics layers an object is on and masks are the physics layers an object can detect.

In this case, the Hitbox is on Layer 1 and can detect objects (masks) on Layers (1 and 4), that means if an enemy is on layer 1 or 4, hitbox will be able to detect it, but if the enemy is on layer 3, hit box will not detect it, because you did not set it to detect(mask) object on layer 3. It will also be able to detect other hitboxes because hitbox itself is on layer 1 and it is set to detect (mask) other object on layer 1.

Your hurtbox can detect objects (masks) on Layer 1 but does not have a layer itself, that means no object can detect it since it does not register itself on any layer.

Read more about Layers and Masks in the Godot docs.

2 Likes

Alright, I imagined my layers were off. Those functions will see use later (I think), but I’ll keep your feedback in mind there.

I have a rough understanding of layers and masks, I just get a little confused with the fact I’m forced to have two CollisonShapes on one “entity”, and the need for both Hit/Hurt interactions and terrain collisions…

I’ll mess around with the layers, and get back to you if I’m completely lost :saluting_face:

2 Likes