Advertisement

Increasing a Float Value Correctly and Floating-Point Problem

Started by July 07, 2023 08:45 PM
9 comments, last by LorenzoGatti 1 year, 4 months ago

I have two float type variables. One of them is a velocity variable that moves the character upwards. The other is an acceleration variable that increases this velocity. For example:

var displacement: float = 241
var time: float = 1*60
var acceleration: Vector2 = Vector2(0, (2 * displacement) / (time * time))
var velocity: Vector2 = Vector2(0, -acceleration.y * time)

func apply():
    velocity += acceleration
    position += velocity

func update(delta):
	apply()

In this case the current value of the acceleration variable is (0, 0.133889) and the current value of the velocity variable is (0, -8.033333).
Each time the apply() function is called, the acceleration is added to the velocity and the velocity variable, whose initial value was a negative number, starts to increase towards 0.
However, due to the floating-point problem, the value of the velocity is never exactly 0, but a number slightly larger than 0. So the acceleration continues to be added to the velocity and the velocity is added to the character's position and the character starts to move downwards this time. However, each time the character falls towards the ground, it will be lower than its initial position. In other words, the character will always stand lower than the ground the character stood on when he first started the game.

When the character falls to the ground and stops, the value of velocity is not (0, -8.033333) but (0, -8.033329). This difference of 0.000004 always prevents the character from staying in the correct position.

I'm sure game programmers have encountered this problem many times before. But I wonder how they can correctly increment/decrement float variables in such a problem. What is your solution? Thank you.

None

You get six decimal digits of precision in a float. Your number is a rounding error. Floating point math is an approximation and is filled with rounding errors. The fact that you're relying on it being more precise suggests you're misusing them. Direct comparison and direct equality tests are typically a bug.

Think about what a velocity difference that small means. Unless you're talking about enormous distances or enormous times — far greater than typically used in games — those numbers won't be noticed by a human. If you really must have something affected by such a small tolerance, you'll need to do something other than an iterative approach. When accumulation would give overwhelming accumulated error, direct computation from base values rather than accumulation over time is often the right approach. In a physics system like you describe you're talking of being too near the limits of what makes sense to compute, and also a reason that physics systems become difficult to develop.

Many systems have functions that limit precision and drop very small numbers. For example, Unreal often uses a constant “KINDA_SMALL_NUMBER” of 1e-4f for tolerance and truncating, and uses FMath functions like IsNearlyEqual(), IsNearlyZero(), IsWithin() and similar for comparison, and Unity has Mathf.Approximately() for similar use.

Advertisement

@frob The game engine I use is Godot. As you said, I get the instant velocity value every time the character falls and stops, and the output of its value in a few trials is as follows:

-8.033325
-8.033333
-8.033325
-8.033333
-8.033325
-8.033333

I see it is following a pattern. The difference between the two values is 0.000008. Now I understand that, as you say, such a small difference will not be noticeable.

But every time the character jumps and falls to the ground, he is always lower than his initial position on the ground.

func _process(delta):
    match state:
        Standing:
            if Input.is_action_just_pressed("ui_up"):
                state = Jumping
        Jumping:
            if velocity.y > 0:
                state = Falling
            else:
                apply()
        Falling:
            apply()
            if is_equal_approx(-velocity.y, -acceleration.y*time):
                state = Standing
                velocity = -velocity

None

Again, floating point is an approximation and you only get six decimal digits of precision.

That means the numbers must be treated as equivalent, -8.033333 and -8.033325 are within rounding error of the same number. You are misunderstanding how they work if you don't treat them as equivalent. It is not just that a human wouldn't notice, they are approximation of the same value.

Many floating point operations work within a tolerance; you can compute it twice with the same input values and get slightly different results and still be valid. You can also have the same printed value but with results that are not binary identical. There are even results that equality tests with == will show they are equivalent but comparing the results with memcmp() show they are different binary values.

If you don't know what you are doing, I would limit your comparison to five digits. That is still accurate within the width of a human hair if you are working in meters. It takes both education and experience to get a feel for how floating point numbers work. It is best to get rid of the misconception that they are an exact number. That means both are -8.0333.

The problem is not exactly with float precision as frob says, but is a problem with how you detect and handle the collision logic with the ground.

The first thing you need is a way to detect the collision in a robust way. It seems like you are just checking to see if the velocity is the opposite of the starting velocity when jumping. This will not work very well (due to precision and accumulated error). You want to detect collisions based on the position. For example, if the ground is at Y=0, the player is in collision if position.y ≤ 0.

The second thing you should do is to correct for any interpenetration of the player and the ground once there is a collision. If the ground is at Y=0 and the player position.y = -0.1, then you need to push the player out of the ground so that it rests on the ground but does not penetrate it. This is easily done in simple cases if you know the surface normal of the ground plane (n=(0,1,0) in this case), and penetration depth (0.1 in this case). Move the player the penetration depth distance along the normal vector to push player out of the ground.

The third thing you need is a better system for time integrating the positions of objects (what is in your apply() function). The canonical form of semi implicit euler integration is like this:

  1. Set force vectors for all objects to 0.
  2. Apply forces (e.g. gravity force = g*mass)
  3. Integrate force to velocity: velocity += force * deltaTime / mass
  4. Detect collisions (use velocity for continuous collision detection)
  5. Resolve collisions (e.g. resolve penetrations and solve constraints)
  6. Integrate velocity to position: position += velocity * deltaTime

You should probably just use the engine's built-in physics engine if it has one, it will simplify a lot of tasks, especially collision detection and response.

For some background information about why floating point behavior is what it is, search for “what every programmer should know about floating point”.

Advertisement

Floating point error aside, the fact that you are not scaling acceleration by delta is a red flag. Your character will move faster on faster computers and slower on slower computers, which is almost certainly not what you want.

…or he could fix the timestep of his simulation.

alvaro said:

…or he could fix the timestep of his simulation.

Yes, using _physics_process (fixed frame rate) instead of _process (variable frame rate) would also work. Although I would still recommend multiplying by delta, just to make it easier to change the physics frame rate later.

  • Functions update() and _process() ignore delta.
  • When the character jumps, you should apply an impulse immediately, not set up magic numbers for the next timestep.
  • The test for transitioning from Falling to Standing doesn't make a lot of sense; if it seems to work, it is by mistake.
    You should compare the height the character would have at the end of the timestep with the height of the platform: if it is below, this is the timestep when the character stops (at platform height with velocity 0) instead of falling full distance at the computed velocity.

Generally, respect physical laws and physical laws will respect you.
By not maintaining a completely consistent and dimensionally correct physical simulation you are turning your back to straightforward architecture (only two simple questions: what are my velocity and position this timestep?) and embracing messy and brittle representations, such as the already serious proliferation of ad hoc states.

Omae Wa Mou Shindeiru

This topic is closed to new replies.

Advertisement