A gotcha when measuring distance between objects in Godot. (#dev #gameDev #godot)
Overview
So, you’re building a game, and want to do something when the enemy is really close to the player, like 0.5m away. You write a simple script to measure the distance between the two objects, and it doesn’t work as expected. If you’re more experienced in Godot, you might have already guessed what’s going on. If not, let’s dive into it. For this, I’ll be using version 4.2.2 stable of the Godot Engine (gdscript).
The problem
Inside the Enemy script, write a code snippet like this:
var stopping_discance := 0.5
var distance = global_position.distance_to(other_object.global_position)
if distance < stopping_distance:
# Do something, maybe give a Starbucks gift card to the player or something like that...
However, when you run the game, the enemy never stops, even when it’s right next to the player. What’s going on?
The thing is, the distance_to
method returns the distance between the two objects’ origins, not their surfaces.
For the record, the same thing happens with the distance_squared_to
method.
But what does that mean?
This means that the position used to calculate the distance is the origin of the object, which is usually at the center of the object, but when we do this type of thing, we are thingking visually, so we want the distance between the bodies, like if the enemy is standing right in front of the player, we want the distance to be 0.5m, not the distance between their origins (center of the object).
If that’s what you’re expecting, then there’s no probme. Otherwise, let’s look at how to fix it.
Actually, fix is a strong word because it’s not a bug, it’s a feature! It’s just not the feature we want in this case.
Workarounds
Here’s a few possible workarounds, maybe one of them will fit your needs. Before going through aall those options, I’d like to point out that this is not an exausitive list, and maybe you can use one of those solutions as a jumping off point to create your own.
Using RayCast3D
So we add a RayCast3D to the Player scene, and configure it as such:
- Uncheck the option
enabled
; - Leave the transform as it is
(0, 0, 0)
; - Make sure
Exclude Parent
is checked;
I can hear you asking:
Do I need to change the target position of the RayCast3D?
Nope. We’re going to set that via code, so we can leave it as it is.
Wait, why leave the RayCast3D at the center of the object?
Because if it’s at the edge of the body of the NPC, when it’s really close to the player (or other target), the
RayCast3D won’t hit anything, so you won’t ever get distance = 0
.
Here’s a snippet of how to use it:
# Update RayCast3D target position
ray_cast_3d.target_position = other_object.global_position - global_position
ray_cast_3d.force_raycast_update() # Since the RayCast3D is disabled, we need to force the update.
if not ray_cast_3d.is_colliding():
return
var collision_point = ray_cast_3d.get_collision_point()
var collision_distance = global_position.distance_to(collision_point)
Will this work 100% right? Well, no. The reason is that the RayCast3D is at the center of the object, which was the 50% of the problem.
So, how could we fix that as well? Well, since the ray cast 3d collision point is the surface (body) of the object, we can just add an offset to the distance:
# Considering that the object is a CSGBox3D:
@onready var size_offset_x := size.x * 0.5
# Update RayCast3D target position
ray_cast_3d.target_position = other_object.global_position - global_position
ray_cast_3d.force_raycast_update() # Since the RayCast3D is disabled, we need to force the update.
if not ray_cast_3d.is_colliding():
return
var collision_point = ray_cast_3d.get_collision_point()
var collision_distance = global_position.distance_to(collision_point) - size_offset_x
This will do the trick, and now we can get a distance between two bodies, not their origins and the minimum value will be 0.
- Pros:
- It’s simple to implement and use;
- Cons:
- It’s not 100% accurate, so we need to add the offset.
Reference: https://docs.godotengine.org/en/stable/classes/class_raycast3d.html
Using intersect_ray
Another way to do this is to use the intersect_ray
method from the PhysicsDirectSpaceState3D
object.
This one has the convenience of not needing to add a RayCast3D to the scene, but it’s a bit more complex to use.
On version 4.2.2, the method requires an instance of PhysicsRayQueryParameters3D
instead of passing the data directly.
Here’s a snippet of how to use it:
var space = get_viewport().world_3d.direct_space_state
var ray = PhysicsRayQueryParameters3D.new()
ray.from = global_transform.origin
ray.to = other_object.global_transform.origin
ray.exclude = [self]
var hit = space.intersect_ray(ray)
if not hit:
return
var hit_position = hit["position"]
var collision_distance = global_position.distance_to(hit_position)
It’s pretty much the same as the RayCast3D, but we needed to create an instance of PhysicsRayQueryParameters3D
instead
of passing the data.
- Pros:
- It’s also simple to implement and use;
- You don’t need to add a RayCast3D to the scene;
- Cons:
- It’s also not 100% accurate, so we need to add the offset.
- You have to create an instance of
PhysicsRayQueryParameters3D
, so it’s less convenient than the RayCast3D.
(IDK performance wise which one is better, or even if there’s a difference.)
Reference: https://docs.godotengine.org/en/stable/classes/class_physicsdirectspacestate3d.html
Adding a Node3D as a point of reference to measure distance
So, this feels hacky, but it’s a solution that works. You can add a Node3D to the NPC scene, and place it at the edge of the NPC’s body. Then, you can use this node as a reference to measure the distance between the two objects.
Here’s a snippet of how to use it:
# Exactly same as measuring using regular distance_to
# We're just changing the "from" part.
var distance = measure_from_ref_point.global_position.distance_to(other_object.global_position)
- Pros:
- Simplest solution to add;
- Cons:
- It’s also not 100% accurate, we solve the problem of the “from” point, but we still have the “to” point problem.
Demo
Here’s a demo of the proposed solutions:
In this demo image, notice that both objects are side by side. In the legend:
- distance_to: Result of distance_to method;
- distance_to from ref: Is using the solution using a Node3D as a point of reference;
- distance_squared_to: Result of distance_squared_to method;
- Raycast distance: Result of using the RayCast3D method;
- Raycast distance - offset: Result of using the RayCast3D method with the offset;
- Intersect Ray distance: Result of using the intersect_ray method.
From all those methods, the most accurate one is the RayCast3D with the offset, but you could probably use the offset on the other methods as well.
You can also find the source for this demo on my GitHub: https://github.com/brenordv/godot-distance_to-gotcha
Hope that helps. :)