Multiple Raycasts, or ShapeCast2D

Godot Version

4.2.2

Question

I was wondering about 2 different ways of going about an idea, and wanted an experienced opinion on the performance of both methods.

I saw a similar issue here: Making a raycast not stop after the first collision - #6 by Monday

I have the same concept, I wanted to make a raycast that would hit something, and then continue on and find everything within the path. I considered making it a recursive function that would just call it self with the same end point everytime, but a new start point based upon the previous collision but I was unsure about a few aspects.

  • The documentation says that accessing space safely can only be done during physics_process, but if I wanted to make a recursive function, would calling that function within physics_process, and passing the space as a parameter still be safe?
  • Would calling multiple raycasts be more efficient than just creating one ShapeCast2D? The documentation says that ShapeCast2D is more computationally expensive, but I was unsure by how much. I assume it’s based upon the amount of shapes created for the ShapeCast2D.

Currently, to make the ShapeCast2D shapes small enough, I have to make them very small and that makes a lot of them, which I would think is less than ideal. It’s currently set as squares that are 1x1.

I was also confused about the margin property, as it says if it is set higher, it is more consistent, sacrificing precision. Is there a visual reference of what that looks like? Is it like a buffer between each shape within the ShapeCast2D? I couldn’t tell when I looked at it.

Here is a picture of a working version of my concept with visible ShapeCast2Ds, where they are intersecting with each other (shows by the white stars).
test

Here is the code used for this:

#below is within the script for the beams, which is on each player and the statue

func _physics_process(delta):
	var size_beginning_point
	var size_end_point
	if is_end_tether:
		size_beginning_point = Vector2((.1 * -(self.global_position.x - previous_object.global_position.x)), (.1 * -(self.global_position.y - previous_object.global_position.y)))
		size_end_point = Vector2((-(self.global_position.x - previous_object.global_position.x) + .2 * ((self.global_position.x - previous_object.global_position.x))),
								(-(self.global_position.y - previous_object.global_position.y) + .2 * ((self.global_position.y - previous_object.global_position.y))))
	    beam_shape_cast.position = size_beginning_point
	    beam_shape_cast.target_position = size_end_point
	else:
		size_beginning_point = Vector2((.1 * -(self.global_position.x - previous_object.global_position.x)), (.1 * -(self.global_position.y - previous_object.global_position.y)))
		size_end_point = Vector2((-(self.global_position.x - previous_object.global_position.x) + .2 * ((self.global_position.x - previous_object.global_position.x))),
								(-(self.global_position.y - previous_object.global_position.y) + .2 * ((self.global_position.y - previous_object.global_position.y))))
	    beam_shape_cast.position = size_beginning_point
	    beam_shape_cast.target_position = size_end_point
	
	if tethered_object != null:
		var next_tether = tether_vertices[tether_vertices.find(self) + 1]
		if "beam_collision" in previous_tether:
				beam_shape_cast.add_exception(previous_tether.beam_area)
				beam_shape_cast.add_exception(next_tether.beam_area)
		else:
				beam_shape_cast.add_exception(next_tether.beam_area)
	else:
		if "beam_collision" in previous_tether:
				beam_shape_cast.add_exception(previous_tether.beam_area)
	if !beam_shape_cast.collision_result.is_empty():
		for element in beam_shape_cast.collision_result:
			if element["collider"] != null:
				if ((anchor != element["collider"].get_parent().previous_object) && (previous_object != element["collider"].get_parent().anchor) && (anchor != element["collider"].get_parent().anchor)):
					beam_intersection(size_beginning_point, size_end_point, beam_shape_cast.get_collision_point(beam_shape_cast.collision_result.find(element)), beam_area, element["collider"])

func beam_intersection(raycast_beginning, raycast_ending, point_of_collision, beam_1, beam_2):
	manage_intersection.emit(point_of_collision, beam_1, beam_2)

#below code is on the parent node, which manages all beams and intersections

func manage_intersections(point_of_collision, beam_1, beam_2):
	var found = false
	if !created_intersections.is_empty():
		for element in created_intersections:
			if (beam_1 == element.beam_1 && beam_2 == element.beam_2) || ((beam_2 == element.beam_1 && beam_1 == element.beam_2)):
				element.receive_info(point_of_collision)
				found = true
				break
			
		if !found:
			var intersection = intersection_sprite.instantiate()
			created_intersections.append(intersection)
			intersection.global_position = -(self.global_position - point_of_collision)
			intersection.beam_1 = beam_1
			intersection.beam_2 = beam_2
			intersection.intersection_queue_free.connect(remove_created_intersection)
			add_child(intersection)
	else:
		var intersection = intersection_sprite.instantiate()
		created_intersections.append(intersection)
		intersection.global_position = -(self.global_position - point_of_collision)
		intersection.beam_1 = beam_1
		intersection.beam_2 = beam_2
		intersection.intersection_queue_free.connect(remove_created_intersection)
		add_child(intersection)

#this last part is the code for the intersection sprite

extends AnimatedSprite2D

var beam_1
var beam_2
var location

@onready var timer = $Timer

signal intersection_queue_free

func _process(delta):
	if beam_1 == null || beam_2 == null:
		intersection_queue_free.emit(self)
		self.queue_free()
		pass

func _physics_process(delta):
	if location != null:
		self.global_position = location

func receive_info(point_of_collision):
	if point_of_collision == null:
		intersection_queue_free.emit(self)
		self.queue_free()
	else:
		location = point_of_collision
		timer.start(0.05)


func _on_timer_timeout():
	intersection_queue_free.emit(self)
	self.queue_free()

Any help would be appreciated, still learning godot and gdscript and so if anything here is a bad habit to get into or a bad practice let me know, thanks!

As long as you call your recursive function from physics_process() it should be safe.

I did this very basic test:

extends Node2D

func _ready() -> void:
	var t: int = Time.get_ticks_usec()

	for i: int in 10000:
		var r := cast_ray()

	print(Time.get_ticks_usec() - t)
	t = Time.get_ticks_usec()

	for i: int in 10000:
		var r := cast_shape()

	print(Time.get_ticks_usec() - t)

func cast_ray() -> Dictionary:
	var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
	var parameters := PhysicsRayQueryParameters2D.create(Vector2.ZERO, Vector2(100.0, 0.0))
	return space_state.intersect_ray(parameters)

func cast_shape() -> Array[Dictionary]:
	var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
	var params := PhysicsShapeQueryParameters2D.new()
	params.shape = RectangleShape2D.new()
	return space_state.intersect_shape(params)

For me it prints something like:
16310
35273
So the shape is ~2.5 times slower, so if you have to cast more then two rays than a single shape might be better. I haven’t done proper tests with different sizes and when they intersect objects though, but maybe you can modify it to your case.

edit: also I never actually used the shape cast code for anything so not sure if it works :sweat_smile:

1 Like

You can totally do that, either with or without a hard abort limit set in the loop. A line cast is usually faster though, unless you are really just hitting 1-3 objects.
Also you don’t have to modify the start point. You can just add the object(s) you already hit into the exclude array and cast again, see: PhysicsRayQueryParameters2D — Godot Engine (stable) documentation in English

The only issue I see is that this is very inefficient and gets slow very quickly. Each call to intersect_ray creates and returns a new dictionary, requiring multiple function calls and memory allocations. The RayCast2D node is quite a bit faster than using the intersect_ray function directly and should be fast enough even on low-end computers if you only need a few hundred ray casts each frame.

Thank you for your response, that’s good to know that calling the function within the physics_process would still be safe. That test you did too is interesting, I’m only planning on 4 players, so plus an original statue would mean there could only be 5 beams at once, and I believe only 2 beams of the 5 can collide so at most I would be making 3 raycasts from a single beam.

1 Like

Thanks for your response, I figured if I were to make a loop the stop would just be the final raycast hitting nothing, but I may mess around with the concept. By “line cast” do you mean raycast? I believe with my limit of players and objects I wouldn’t only be hitting at most 2 objects, but I’d have to do the math and make sure of that. That’s also a good point about adding it to the exceptions for the array, I like that idea better than moving the start point.

I forgot exacltly where I read it, but I know that I had read somewhere that using RayCast2D nodes were meant for more static raycasts, or ones that do not change target position each frame, and that code based raycasts using intersect_ray were better for raycasts that are constantly changing position. Is that true or am I mistaken?

If the RayCast2D would be a better choice, would it still be prefereable over the ShapeCast2D, and would I still need to create more RayCast2D nodes through a recursive function to see all objects within the path?