I’m using area3d for hitboxes and hurtboxes. Usually hit detection works correctly but sometimes I get duplicate hits detected when there should only be one hit detected. Characters have several hurtboxes connected to their bones via BoneAttachment3D. The hurt boxes monitor for the hitboxes. At the start of the attack animation the hitbox’s monitorable is set to true. At the end of the attack animation the hitbox’s monitorable is set to false.
Since each character has multiple hurtboxes when the first hurtbox emits area_entered I black list the hitbox (via dictionary entry) to prevent the other hurtboxes from triggering a hit detection. I also connected to a signal that the hitbox emits when it becomes disabled. When the hitbox is disabled I white list it.
Here are the signal handlers. _on_hit_detected is connected to all of the hurtboxes’ area_entered and _on_hitbox_disabled is connected to the signal that the hitbox emits when monitorable is set to false.
func _on_hit_detected(area3d: Area3D) -> void:
assert(area3d is Hitbox, "Hurtbox should only detect hitbox collision.")
var hitbox := area3d as Hitbox
var register_hit: int = !_hitbox_black_list.get(hitbox, false)
# Only emit a hit for the first hurtbox that registers it
if register_hit:
hitbox.disabled.connect(_on_hitbox_disabled)
_hitbox_black_list.set(hitbox, true)
hit_detected.emit(hitbox)
func _on_hitbox_disabled(area3d: Area3D) -> void:
var hitbox := area3d as Hitbox
_hitbox_black_list.set(hitbox, false)
hitbox.disabled.disconnect(_on_hitbox_disabled)
The function that enables/disables the hitbox is called via call_deferred so the signal that triggers _on_hitbox_disabled happens at the end of frame. I thought this would mean that white listing the hitbox happens after all of the area_enter signals are processed, but maybe not?
Your code is overcomplicated. You don’t need to check the type of Object that hit was a HitBox if its on its own layer. I recommend putting hitboxes on their own layer and let the physics engine handle those checks. Doing so mean you don’t have to assert or cast the HitBox, and anything that triggers it incorrectly will trigger an immediate halt to the program and a clear error of where the problem is. Doing it the way you are doing it is much harder to debug because of the errors that get propogated.
You don’t disable Area3Ds to stop collision detection - you disable their CollisionShape3D. This is likely why you are seeeing weirdness.
It is possible that the problem is that your HitBox is just colliding with two different HurtBoxes in the same frame.
Here’s how I would modify your code:
func _on_hit_detected(hitbox: Hitbox) -> void:
var register_hit: int = !_hitbox_black_list.get(hitbox, false)
# Only emit a hit for the first hurtbox that registers it
if register_hit:
hitbox.disabled.connect(_on_hitbox_disabled)
_hitbox_black_list.set(hitbox, true)
hit_detected.emit(hitbox)
func _on_hitbox_disabled(hitbox: Hitbox) -> void:
_hitbox_black_list.set(hitbox, false)
#You will probably want to move this line somewhere else
hitbox.find_child("CollisionShape3D").disabled = true
hitbox.disabled.disconnect(_on_hitbox_disabled)
Having said all that, I recommend not using HurtBoxes and HitBoxes together. While it’s a tried and true game development method, I have found that collision detection works better for me when the Area3D is detecting a PhysicsBody3D. PhysicalBone3D nodes inherit from that type, and so you can use them to detect collisions without adding an Area3D on top of them. (Though I believe you need to turn them on.) You should also, with this method, be able to detect any hit on the body as one, and then determine what bone was hit if you need that detail. Just a thought.
The hurtbox and hitbox are own their own layers. I didn’t realize that the signal handler’s parameter type didn’t need to match the type of the signal’s parameter. Makes sense that they don’t need to match since object are passed as references. I had the assert there to make sure I didn’t miss configure the layers. I write a lot of C code at my job so strict typing and using asserts is ingrained into me.
Made this change and it really improved the issue, but I still can get duplicate hits detected. It is significantly less than before though. Is there somewhere in the documentation that talks about the correct way to disable an area3d? I didn’t see anything about it area3d or collisionshape3d pages. Maybe its over in the physics engine section.
I thought this wouldn’t matter. All of the hurtbox collisions are handled in the the physics process. So even if there are multiple collisions one of them will have to emit area_entered first. At which time the hitbox is black listed and the rest of the collisions on the same frame should set register_hit to false.
I wonder if the issue has to do with the fact that the hitbox is disabled via method track callback and that happens on the idle process and the hurtboxes do their detection on the physics process. But the hitboxes is disabled via call_deferred so I wouldn’t think this should matter.
Maybe the call_deferred to disable the hitbox can happen before the hurtboxes emit area_entered but after the hurtboxes registered the collision? I’m not really sure about the order of operations for things happening at the end of the frame. I’ll try to learn more about this.
You should also, with this method, be able to detect any hit on the body as one, and then determine what bone was hit if you need that detail. Just a thought.
How do I detecting any hit on the body as one with physics bones? If I change my code so that the hitbox does the detection I’ll still need a dictionary to black list the rest of the physics bones which puts me in a similar situation. Unless there something I’m missing about the physics bones?
I’m a BIG fan of strict typing in Godot. You can actually turn on warnings or even errors for lack of strict typing in your project. I have it set at the warning level for my projects.
I let the collision detection layers handle the type checking when possible because its a single Enum with a bit mask. It’s going to be computationally faster than anything I can code in GDScript. But that’s not how tutorials on the subject show things. Another popular test is checking to see if something is in the group “Player” and adding the Player node to that group. An even worse option IMO than checking the class type.
I found this out because I was trying to disable the Area3D on a sword after it swung, and it wasn’t working - it was registering hits before and after the window. Basically toggling it on/off in an AnimationPlayer. I ended up watching some tutorials, and one of them was enabling and disabling the CollisionShape3D. I made that change and it all started working. I have no idea if the documentation mentions it anywhere.
My gut says this is a race condition. Your supposition may be correct. My question to you would be “why does it matter”? Are you needing to detect multiple hits on different parts? I ask because area_entered and body_entered only get triggered once. You cannot enter and exit an area in one frame.
Also, you can funnel all collisions to the same function (which I believe you are doing) and just make sure it only runs once per frame. I’m not sure how you got to the solution you have.
I have never done it. However, in reading the docs, you can turn collisions on for the Skeleton. You will probably have to add a CollisionShape3D to each bone.
As for the blacklist, I don’t know how you got there as a solution. How come it is necessary?
I’m also a fan of static typing.I have my project setup to give me errors for missing static types.
My gut says this is a race condition. Your supposition may be correct. My question to you would be “why does it matter”? Are you needing to detect multiple hits on different parts? I ask because area_entered and body_entered only get triggered once. You cannot enter and exit an area in one frame.
No, just the opposite. I only want detect one hit per attack animation. The characters have multiple hurtboxes. I need to filter out the hits that are detected by the other hurtboxes once the first hurtbox detects a hit.
Also, you can funnel all collisions to the same function (which I believe you are doing) and just make sure it only runs once per frame. I’m not sure how you got to the solution you have.
This is what I’m doing. All of the hurtboxes’ area_entered are connected to the same _on_hit_detected. I can’t make_on_hit_detected only run one per frame as a character could be getting attacked by multiple enemies on the same frame.
As for the blacklist, I don’t know how you got there as a solution. How come it is necessary?
I only want a hit to register once per attack animation. Each character would have multiple physics bones that the hitbox can detect. When the hitbox detects the first physics bone I need some way to prevent detection of any subsequent physics bones (on the same frame or on later frames) to prevent the attack from registering more than once. I don’t want to disable the hitbox’s collisionshape as I want the attack to be able to hit more than one enemy.
I’m not sure if I’m explaining this clearly. After work I’ll record a video that shows the hurtboxes, hitboxes, and attack animations. That might help make it more clear.
Why burden engine’s collision system with your game logic? Maintain a “hittable” flag for the character, set it on attack start and clear it on any hit. Ignore all hit signals if the flag is not set.
Are you suggesting that I shouldn’t use area3ds as hitboxes/hurtboxes?
Maintain a “hittable” flag for the character, set it on attack start and clear it on any hit. Ignore all hit signals if the flag is not set.
I do have a flags to track if the character can be hurt by specific hitboxes. Its the black list dictionary that is mentioned in the above posts. Once the hit is detected via area3d emitting area_entered the hitbox is blacklisted until the attack animation completes.
I don’t understand how a flag alone can inform a character of a hit.
No, I’m suggesting you don’t enable/disable colliders at all. Keep everything enabled at all times and maintain hitability state on your side. If the entity is not hittable, whatever signals you get, just ignore them.
I’ll mull this over, but I’m not sure this would solve my issue. I’d still need to update the hitable flag in an area_entered signal handler and that is basically what I’m already doing.
I think my issue is caused by the the order of operations for things happening at the end of frame. I’m going to change the code up so that the blacklist is cleared on the frame after the hitbox is disabled and see if that makes a difference.
If you have a single flag maintained on your side then you don’t need to worry about frames or order of operations. Once you set the flag, nothing will be able to go past it. Your code has the full control.
Alright, clearing the hitbox from the blacklist on the frame after the hitbox is disabled seems to have been the missing piece to solve my issue. At some point in the future I’ll try to get a better understanding of the order of operations for things at the end of the frame, but for now its working. Since @dragonforge-dev suggestion about the collisionshape made a huge improvement to the issue I’m marking it as the solution.
EDIT: Glad you got it working. You can try this if you want.
What @normalized is saying is what I was also thinking. But I’d say let’s take it a step back. Simplify everything.
Add a CollisionShape3d to your CharacterBody3D that the skeleton is attached to (I assume). Make it a shape that covers the body roughly. A CapsuleShape3D is the most common choice.
Change the HitBox code to add an a body_entered signal. Do this in the Area3D code:
Then do something like this to your Player object:
var is_hit: bool = false
var health: int = 100
func hit(damage: int) -> void:
if is_hit:
return
health -= 100
is_hit = true
is_hit.set_deferred("is_hit", false) # Sets is_hit back after the physics frame is over
Get that working, then develop a more complex version from there.