One-shot shape casts in 2D

I have worked out a way to do 2D shape casts in Godot in script. This is useful for instantaneous checks that require a radius, where a ray cast would not suffice. However, it seems there is no straight-forward way of doing this with Godot’s 2D physics API.

It’s pretty easy to do a 2D ray cast in Godot. You set up a PhysicsRayQueryParameters2D, and get the physics space with get_world_2d().direct_space_state, then call intersect_ray.
This gives you back the point where the ray hit a collider, if it hit something.

There is nothing similar for casting shapes. There are two functions: cast_motion and collide_shape. These both use PhysicsShapeQueryParameters2D, which has a motion parameter.

collide_shape gives you back two vectors per collision, one on the outside of the shape you cast and one on the outside of the collider it hit. They have no bearing on where the shape should end up and don’t give you any information about where the intersection point is. Since the values returned are vectors, there’s no information about the collider that was hit or the collision point or normal. I struggle to imagine a use case for the data returned by this function in the context of a shape sweep. It might be useful for the miniscule motion you get simulating physics, but the results are inaccurate enough that I would hesitate to trust it.

cast_motion gives you back two floats, which add up to 1, with the first representing the proportion of the motion that is clear, and then for some reason also a proportion of the motion that is after the collision point, and no other information. This gives you the position of the shape when it hits something, rather than the point in space where it intersected the object it hit, and also means you don’t have a normal or a reference to the offending collider.

At least with cast_motion, after getting back a position, you can change the query to have this location (moved forward just a tiny amount so that the intersection actually happens) and then call get_rest_info. This gives you back an intersection point.

Here, then, is code that does a shape sweep:

# sweeps a shape along the given query motion, and returns a dictionary 
# with information about the first collision that happens, or an empty
# dictionary in the event of no collision.
func shape_sweep(world2d: World2D, query: PhysicsShapeQueryParameters2D):
	var return_dictionary = {}
	var space_state = world2d.direct_space_state
	# first need to do an overlap since existing collisions are ignored
	var data = space_state.get_rest_info(query)
	if not data.is_empty():
		return_dictionary.position = data.point
		return_dictionary.shape_position = query.transform.origin
		return_dictionary.collider = instance_from_id(data.collider_id)
		return_dictionary.normal = data.normal
		return return_dictionary
	
	var clear_motion = space_state.cast_motion(query)
	var start_pos = query.transform.get_origin()
	var end_pos = start_pos + query.motion
	if clear_motion[0] == 1:
		return return_dictionary
	else:
		end_pos = start_pos + clear_motion[0] * query.motion
		var end_contact_point : Vector2
		end_contact_point = start_pos + (clear_motion[0] + 0.01) * query.motion
		query.transform = Transform2D(0, end_contact_point)
		query.motion = Vector2.ZERO
		data = space_state.get_rest_info(query)
		if data.is_empty():
			return return_dictionary
		else:
			return_dictionary.position = data.point
			return_dictionary.shape_position = end_pos
			return_dictionary.collider = instance_from_id(data.collider_id)
			return_dictionary.normal = data.normal
			return return_dictionary

	

To use this you still have to create a shape, and build a 2D query:

# (in ready() for example)
	shape = CircleShape2D.new()
	shape.radius = radius
...
# (when you want to do your cast)
	var query = PhysicsShapeQueryParameters2D.new()
	query.collide_with_areas = false
	query.collide_with_bodies = true
	query.collision_mask = 1
	query.motion = m_pos
	query.shape = shape
	query.transform = transform
	var rd = shape_sweep(query)
	if rd.is_empty():
		text.text = "no contact"
	else:
		end_pos = rd.end_position
		end_contact_point = rd.end_contact_point
		text.text = "contact point: " + str(end_contact_point)
		other_collider = rd.other_collider

I’d agree with claims that this is a lot of code compared to doing a raycast. But it does work (I haven’t checked the value of the normal yet, actually, but the rest is ok).

Cheers

[edited to improve the code]

Here’s a project demonstrating this. The initial collision method uses collide_shape. Press space to switch to cast_motion.

Is there a question, or a problem you have, that you need some help with?
If not, then it seems like it belongs more to the Showcase or Resources category than Help :slight_smile:

1 Like

That would make sense yes. I couldn’t find a way to remove the help tag when I was posting! Maybe a mod could move it?
edit: ahhhhhaaaha sorted

1 Like

I’d imagine this works for 3D too but I haven’t had a look at that yet.