Search Unity

How to deal with float values imprecision?

Discussion in 'Scripting' started by K1kk0z90_Unity, Dec 22, 2015.

  1. K1kk0z90_Unity

    K1kk0z90_Unity

    Joined:
    Jun 2, 2013
    Posts:
    83
    Hi all! :)
    I would like to ask you how do you deal with the problem of float values imprecision.
    I'll start immediately with an example: I'm making a 2D Breakout clone, without using physics, but manually moving the ball by changing its Transform position, and checking for collisions using circle casts. Here's the code for the ball's movements:

    Code (CSharp):
    1. private void Update()
    2. {
    3.    // NOTE: velocity is a Vector2 variable used to store the speed at which the ball must move
    4.    RaycastHit2D hit = Physics2D.CircleCast(transform.position, collider.radius, velocity.normalized,
    5.      velocity.magnitude * Time.deltaTime, LayerMask.GetMask("Bricks"));
    6.    if (hit)
    7.    {
    8.      transform.position = hit.centroid;
    9.      velocity = Vector2.Reflect(velocity, hit.normal);
    10.    }
    11.    else
    12.    {
    13.      transform.Translate(velocity * Time.deltaTime, Space.Self);
    14.    }
    15. }
    In theory this should work... however the reality is that float variables use approximation, so my code results in the ball getting stuck when colliding with bricks. Why? The problem is this line of code:

    Code (CSharp):
    1. transform.position = hit.centroid;
    Because of approximation, hit.centroid values are not precise: for instance, in my case, hit.centroid.y ideally should be equal to -0.3f, but its value is -0.295f, which is slightly above the correct position, and this results in the ball's circle collider to overlap with brick's box collider (see image below). This means that next frame, when the circle cast is performed again, it will detect the brick again. This happens even if I have set the Queries Start In Colliders flag to unchecked in Physics2D settings (I don't know if it is a Unity bug).



    So, how could I solve this issue? I thought to round float values to the second decimal place. I used this extension method:

    Code (CSharp):
    1. public static float Round(this float value, int decimalPlaces)
    2. {
    3.    double mult = System.Math.Pow(10.0, decimalPlaces);
    4.    double result = System.Math.Round(mult * value) / mult;
    5.    return (float)result;
    6. }
    then, I modified the guilty line of code like this:

    Code (CSharp):
    1. transform.position = hit.centroid.Round(2);
    Great, that works when the ball is moving upwards (velocity.y > 0) and it collides with a brick! BUT, the problem persists when the ball collides with a brick while moving down (velocity.y < 0). This happens for the same reason, that is approximation: in my case hit.centroid.y results -6.255f, while its correct value should be -6.25f, but rounding -6.255f with the Round() method returns -6.3f!

    So, I shouldn't simply round the value to the nearest one: when the ball is moving towards the up direction (velocity.y > 0) I should round the value towards negative infinity (so performing a Floor operation), otherwise round the value towards positive infinity (Ceiling operation). Since the Mathf.Floor() and Mathf.Ceil() methods round to an integer value while I need to round to a certain decimal place, I decided to write my methods. So, here it is:
    Code (CSharp):
    1. public static float Floor(this float value, int decimalPlaces)
    2. {
    3.    double mult = System.Math.Pow(10.0, decimalPlaces);
    4.    double tmpValue = mult * value;
    5.    double tmpFloor = System.Math.Floor(tmpValue);
    6.    double result = tmpFloor / mult;
    7.    return (float)result;
    8. }
    Testing this method I noted it doesn't work correctly. For example, it fails this simple test:

    Code (CSharp):
    1. [Test]
    2. public void FloatFloor()
    3. {
    4.    float a = 1.193f;
    5.    Assert.AreEqual(a.Floor(3), 1.193f);
    6. }
    Debugging I discovered why it doesn't work: try to guess? The problem is, again, float value approximation! In fact, tmpValue's value is incorrect: multiplying mult (that equals to 1000.0) by value (that equals 1.193f), the result is not 1193.0 like it should be, but it is 1192.9999589920044. This approximation error frustrates subsequent computation.

    I don't know how to get around this issue. Have you ever developed a game without using built-in physics? How do you deal with float values approximation when using ray/circle/box casts?
    Thank you in advance for your help! :)
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    6,528
    That's not float value imprecision. 0.005 is far too large for that. Float imprecision is more in the neighborhood of 0.0000001. Only your final function's results look like a product of floating point imprecision.

    However, if I may assume that your sprites use the default value of 100 pixels per unit, 0.005 is exactly half a pixel, and that would be exactly the kind of rounding error I'd expect from such a system. More importantly, it's a known quantity; it's very likely to be 0.005 no matter what, but just to make sure we're prepared for diagonals, let's go ahead and make our margin 0.01.

    In any case, the solution is the same.... just move it slightly backwards against the direction of travel. Now, you know the direction of travel, you sent it to the CircleCast function!
    Code (csharp):
    1. Vector3 modifiedCentroid = hit.centroid - velocity.normalized * 0.01f;
    2. transform.position = modifiedCentroid;
    You may also consider filing this as a bug with Unity; it may be a good idea for them to make things a little more precise on their end, and/or make CircleCast a little bit "forgiving" when it comes to colliders that are just barely within the "starting" end of the cast.
     
    K1kk0z90_Unity likes this.
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,657
    For the hit.centroid value, that actually sounds about right.

    This is what the documentation says about that value:

    http://docs.unity3d.com/ScriptReference/RaycastHit2D-centroid.html

    Now a raycast aren't exactly perfect casts through a space. It's all done with a lot of approximations. In Box2D it queries the nearest AABB's for all fixtures in the world in the direction of some line, it then goes through them from closest to farthest, testing each fixture if the ray hits its shape. Depending the shape, this test is slightly different, with all its own approximations to gather up the information about what edge of the shape (if poly), the point of contact, the normal, etc.

    So little error creeps in their.

    But either way... as the documentation says. The point is (when it is in contact). You should assume that said point, if placed there, will continue to be in contact (unless one of the bodies moves). So of course if we continue testing again, it should result in true.

    If you don't want it to be in contact when it hits, move it away a little bit.

    Here's how I would do it...

    1) calculate amount of position change that should occur based on velocity and deltaTime.
    2) perform circlecast
    3) if no hit, move... if hit then calculate distance from current position to centroid of the hit point. This distance should be less than the total distance that would be moved.
    4) reflect the velocity vector
    5) sum the vector from current position to centroid with a vector in the direction of the new velocity but at length of what is left over (dist - distToCentroid). You could make this distance have a min, so that you ensure always moving some value off.
     
    K1kk0z90_Unity likes this.
  4. K1kk0z90_Unity

    K1kk0z90_Unity

    Joined:
    Jun 2, 2013
    Posts:
    83
    Thank you both for your answers! :)
    So I understand the best way to deal with this is to slightly move the object away from the contact point.
    Maybe I'll file it as a bug then. Also, I would send as a bug the fact that it still detects the hit even if I set the Queries Start In Collider flag to unchecked: I would expect it not to do it, since the circle cast starts when the colliders are overlapping each other.
     
  5. paulmuren

    paulmuren

    Joined:
    Jul 1, 2016
    Posts:
    3
    Came across this problem and couldn't believe what I was seeing.
    No idea where the 0.005 unit overreach is coming from.
    Code (CSharp):
    1. const float centroidOffset = 0.005f;
    2. adjustment = hit.normal * centroidOffset; //gives you the opposite of the overreach vector
    3. position = hit.centroid + adjustment; //position is now right where you would expect
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,657
    Well... that's on a RaycastHit2d (from the context I assume since 'centroid' is a member of that).

    Where'd you get the RaycastHit2d from?

    Is say this because note the documentation for 'centroid':
    https://docs.unity3d.com/ScriptReference/RaycastHit2D-centroid.html

     
  7. paulmuren

    paulmuren

    Joined:
    Jul 1, 2016
    Posts:
    3
    @lordofduct in my case, 'hit' is returned from a Physics2D.CapsuleCast

    I think you were correct to point out that the documentation says "when it is in contact."
    My best guess is that placing the centroid 0.005 units too far is Unity's way of ensuring the cast shape is 'in contact' with the other collider instead of just tangent to it.

    Cool to see you're still replying to this thread 3 years later!
     
  8. NOP_W

    NOP_W

    Joined:
    Jul 25, 2018
    Posts:
    4
    Not for sure, but Physic/Physics2D is part of physics system, so the related logic should be in fixedUpdate for making sure before you call a cast mehod colliders have updated theirs position.