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]