Floating point numbers are a nuisance to deal with; often quite necessary--but they bring with them a number of caveats in their usage, that integers simply don't fall victim to.

In short, the issue is that 6 * 0.1 does not equal 0.6.

I won't go into the details and inner workings of why, as there's many better explanations than I could provide, already out there. No, this is a more practical article that will provide an example of where you might encounter an issue, and how to work around it. It doesn't attempt to be all-encompassing or even that thorough; instead, I hope this article plants a seed in your mind so that when you encounter an issue, you have a direction to focus your questions.

The most important questions are the ones we know to ask, and the most frustrating issues are those we don't have the questions for, which leaves us unable to receive an answer.


Vector normalization.

Pay close attention to this little code-snippet.

void Vector2::normalize() {

	real_t l = x * x + y * y;
	if (l != 0) {

		l = Math::sqrt(l);
		x /= l;
		y /= l;
	}
}
Vector2::normalize method from Godot engine source.

Pretty straightforward; calculate the squared magnitude of the vector, check if it's larger than zero, and then calculate the unit length if it is. The reason for the check against zero should be obvious--division by zero does not result in defined behavior.

Most of the time, the behavior of this method is exactly what we expect. Call it on a vector with a non-zero length and we normalize it; call it on a zero-length vector and we still have a zero-length vector. But what if we calculated our zero-length vector through arithmetic? What if we expected a zero-length vector, but that's not actually what we have--where what we have is something very close, but not quite zero-length? We get exactly what normalize thinks we want: a unit vector of length one.

var _forward: Vector2
var _back: Vector2
var _left: Vector2
var _right: Vector2

func calculate_axes(forward: Vector2) -> void:
	_forward = forward.normalized()
    _back = -_forward
    _left = _forward.rotated(-0.5 * PI)
    _right = _forward.rotated(0.5 * PI)

This sample code takes an arbitrary forward vector and calculates 4 cardinal unit vectors: forward, left, right and back.

If you were to use these unit vectors in some movement code, you might expect that _forward and _back might cancel each other out if added together, same as _left and _right. But that's where floating point precision issues rear their ugly head.

func _physics_process(delta: float) -> void:
	var move_direction: Vector2 = Vector2()
    var move_speed: float = 250 # Arbitrary pixels per second

	if Input.is_action_pressed("ui_up"):
		move_direction += _forward

	if Input.is_action_pressed("ui_down"):
		move_direction += _back

	if Input.is_action_pressed("ui_left"):
		move_direction += _left

	if Input.is_action_pressed("ui_right"):
		move_direction += _right

	move_direction = move_direction.normalized()
	get_parent().global_translate(move_direction * move_speed * delta)

If you hold the respective ui_left and ui_right keys together, you'll find your character moves backwards. How could that be? We aren't pressing the key for ui_down at all, and both _left and _right are explicitly 90° rotated from _forward.

What's happening? First, what we expect if we call our calculate_axes method, given Vector2.UP as our forward argument.

_forward = (0, -1)
_back = (0, 1)
_left = (-1, 0)
_right = (1, 0)
What we expect to get.
_forward = (0, -1)
_back = (0, 1)
_left = (-1, 0.0000000874227766)
_right = (1, 0)
What we actually get.

Obviously something has gone wrong here. The y-axis may be close to zero, but close doesn't cut it here. Adding _left and _right together no longer cancels each other out, instead it leaves us with a slight but present positive y value, which when normalized, now produces (0, 1), which just so happens to be the same as _back.

So how do we combat these issues? Often the best method is to find an alternative way of accomplishing our goal. Something that bypasses the source of our issue to start with. In our example, we generate our left and right directions by rotating our forward vector. In our particular case, there's a way to calculate these without performing any arithmetic on our vectors.

_left = Vector2(_forward.y, -_forward.x)
_right = Vector2(-_forward.y, _forward.x)

Given an arbitrary unit vector, this will always produce a vector rotated 90° to the left and a vector rotated 90° to the right, without worry of precision errors.

Unfortunately, alternatives such as these aren't always available to us. In those cases where we're forced to deal with our incorrect calculations, we have to settle for approximations.

We're fortunate that Godot provides us with built-in approximation methods, such as:

bool is_equal_approx(a: float, b: float)
bool is_equal_approx(v: Vector2)
bool is_zero_approx(s: float)
# ...etc...

How approximations work can differ; the simplest form has an arbitrary threshold, where if the difference between two values is smaller than said threshold, they're considered equal.

An alternative method (that you admittedly aren't likely to use in GDScript), is calculating a unit of least precision (ULP) tolerance between two values. In short, ULP is the minimum difference in value that can be represented by two adjacent floating point numbers.

In the IEEE754 standard that most modern computers use for representing floating point numbers, these adjacent numbers are also a single bit off from each other, so a shorthand way of comparing them, is to convert to their binary representation and calculate the difference there.

In the end, it's not so important about how you solve your dilemma, it's how you go about solving it. Now that you know the issue even exists, you have the tools necessary to research potential solutions to your specific issue.