Area 3D not detecting bodies? (making explosion)

Godot Version

v4.4.1

Question

I am trying to make an explosion effect with Area3D, but get_overlapping_bodies() isn’t returning anything. I made sure monitoring is turned on and layers are correct.

extends Area3D

@export var explosion_duration := 0.5
@export var damage := 50

func _ready():
	# Play visual effect
	$GPUParticles3D.one_shot = true
	await get_tree().physics_frame
	
	apply_effect()

	# Queue free after effect ends
	await get_tree().create_timer(explosion_duration).timeout
	queue_free()


func apply_effect():
	var bodies = get_overlapping_bodies()
	
	print("get_overlapping_bodies(): ", bodies)
	
	for body in bodies:
		if body.has_method("apply_damage"):
			body.apply_damage(damage)

Explosion and NPC properties


Layers

Using Jolt

Spawning explosion code:

func detonate():
	var explosion = explosion_scene.instantiate()
	get_parent().add_child(explosion)
	explosion.global_position = self.global_position

As explained in the documentation Area3D.get_overlapping_bodies():

For performance reasons (collisions are all processed at the same time) this list is modified once during the physics step, not immediately after objects are moved. Consider using signals instead.

You’ll need to wait at least 2 physics frames: 1 for the spawn of the object and 1 to update the overlapping objects list after the area has been moved.

You should be using signals instead like:

body_entered.connect(func(body):
    apply_effect()
, CONNECT_ONE_SHOT)
1 Like

I accidently figured out I needed to wait 2 frames on my own.

await get_tree().physics_frame
await get_tree().physics_frame

About the signal solution, I tried it and it triggered correctly, but I am not sure how to get all bodies in an area. If I use CONNECT_ONE_SHOT I only get the first body, and without it signal is triggered multiple times with

[body1]
[body1, body2]
[body1, body2, body3]...

Ah, I didn’t notice what the apply_effect() function was doing. I though it was just playing some animation. In that case just call the apply_damage() function in the callback without any flag:

    body_entered.connect(func(body):
        body.has_method("apply_damage"):
          body.apply_damage(damage)
	)

Working solution based on your suggestion, ty

func _ready():
	body_entered.connect(_on_body_entered)
	$GPUParticles3D.one_shot = true


func _on_body_entered(body) -> void:
	print("body: ", body)

	if body.has_method("apply_damage"): body.apply_damage(damage)
	set_deferred("monitoring", false)
	await get_tree().create_timer(explosion_duration).timeout
	queue_free()

# output
# body: body1
# body: body2

Still I wonder if it’s viable to do something like this if I need to get all overlapping bodies at once, for example if I need to know how many targets were caught in the explosion for some game logic

func _ready():
	$GPUParticles3D.one_shot = true
	apply_effect()


func apply_effect():
	await get_tree().physics_frame
	await get_tree().physics_frame
	
	# can get a full list at once
	var bodies = get_overlapping_bodies()
	print("bodies: ", bodies)
	
	for body in bodies:
		if body.has_method("apply_damage"):
			body.apply_damage(damage)
	
	self.monitoring = false
	await get_tree().create_timer(explosion_duration).timeout
	queue_free()

# output (all bodies at the same time)
# bodies: [body1, body2]

May be worth trying out direct physics access.

something alone these lines on _ready?

var state := get_world_3d().direct_space_state
var shape_rid := PhysicsServer3D.sphere_shape_create()
PhysicsServer3D.shape_set_data(shape_rid, 10) # radius

var shape_query := PhysicsShapeQueryParameters3D.new()
shape_query.shape_rid = shape_rid

var results: Array[Dictionary] = state.intersect_shape(shape_query)
1 Like

Saw something like this on github, but I didn’t want to use it because it returns everything ignoring layers/masks set in the editor. Also shape_data radius must be a float 10.0

func _ready():
	$GPUParticles3D.one_shot = true
	var state := get_world_3d().direct_space_state
	var shape_rid := PhysicsServer3D.sphere_shape_create()
	PhysicsServer3D.shape_set_data(shape_rid, 10.0) # radius must be float

	var shape_query := PhysicsShapeQueryParameters3D.new()
	shape_query.shape_rid = shape_rid
	shape_query.collision_mask = (1 << 2) | (1 << 3) # set collision layers manually

	var results: Array[Dictionary] = state.intersect_shape(shape_query)
	print(results)

Good catch, reading the docs closer seem to recommend using shape instead of shape_rid; I think your collision layers are off by one you’d want 1 << 1 | 1 << 2 for layers 2 and 3, or using binary notaion 0b110

var state := get_world_3d().direct_space_state
var sphere := SphereShape3D.new()
sphere.radius = 10.0

var shape_query := PhysicsShapeQueryParameters3D.new()
shape_query.shape = sphere

shape_query.collision_mask = 0b110 # masking 2 and 3

var results: Array[Dictionary] = state.intersect_shape(shape_query)