Search Unity

Float randomly adds ".999" or ".001".

Discussion in 'Scripting' started by Josiah_Ironclad, Dec 28, 2019.

  1. Josiah_Ironclad

    Josiah_Ironclad

    Joined:
    Sep 24, 2019
    Posts:
    156
    I've got this method which I'm currently calling with C in Update;

    Code (CSharp):
    1. public void AddItemToInventory() {
    2.  
    3.     GameObject addedItem = Instantiate(itemPrefab, new Vector3(0, 0, 0), Quaternion.identity, items);
    4.     addedItem.GetComponent<ItemHandler>().itemScriptable = lootTable[Random.Range(0, lootTable.Length)];
    5.     totalInvWeight += addedItem.GetComponent<ItemHandler>().itemScriptable.itemWeight;
    6. }
    It works, but seemingly at random, totalInvWeight will become something like "340.2999" instead of "340.2". Sometimes it has ".999" at the end, sometimes it has ".998", and sometimes ".001". Very rarely it will even be ".09999" or ".00001".

    There's only 3 objects in lootTable, and all of them have either 5, 0.5, or 0.3 as the weight.

    Any ideas why this is happening?

    P.S. I've debugged the value I'm adding, and it's always the right one in the console. Like I'll add a bunch of items, and on the 21st it will jump to ".999", even though the debug line for that item displays its proper weight value. The item number at which this happens is always random.

    I've always had issues with rotation and UI positions getting like ".9999999" or "-6.1327342" or whatever. But never when dealing with floats I create in scripts.
     
    Last edited: Dec 28, 2019
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,770
    I suggest you read bout floating point error. Discussed many times here on forum. Even more on the internet.

    You may want use rounding to int if that what you need.

    Btw, if you print float value directly in console, you will see only few decimal places, which are rounded. Not truncated. Try use myFloat.ToString ("F7") for example. You will see more decimal places.
     
  3. Josiah_Ironclad

    Josiah_Ironclad

    Joined:
    Sep 24, 2019
    Posts:
    156
    Rounding to int won't work. I need the weight to have decimals. And like I said some items have less weight than 1.

    Also even with that suggested line, they are still printing the same values:
    upload_2019-12-28_4-22-37.png
    upload_2019-12-28_4-22-46.png (this is in the UI)
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,770
    Every arithmetic on floats, will introduce floating point error. So I you set float as 5f, print float in console, then you receive 5.000000f
    But once you add or multiply, like float * 10 + 0.6f, then you will have result of floating error, like 50.5999 or 50.600..1. So then you need round, after multiplying by 100. Then multiply by 0.01f. For better arithmetic results, keep values as int.
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Here's the basics.

    For starters. A 'float' in memory takes up a finite amount of space. There are no "infinite repeating" values in memory as that would imply infinite amount of RAM. Rather instead a 32-bit single floating point value fits inside 32-bits of memory.

    Next, floats are stored in scientific notation. The 32-bits are broken up as sign, exponent, and mantissa (mantissa is the fractional part of the scientific notation where the significant values are). They're stored like so:


    So with this information there is the first form of float error that most people run into. Floats are stored in binary, not decimal. And the thing is that numbers that you think are normal in decimal are actually repeating numbers in binary. You know how in decimal 1/3 is 0.3333333333..., well in binary the simple value of 1/10 is a repeating number of 0.00011001100110011... repeating on into infinity. But since there is only a finite amount of space, it gets trimmed off. So 0.1 is stored as 0.0999 (approximately). And MANY other numbers have this problem. Like 0.6 in your case. I mean think about it... 1/10, 3/5, these fractions are made up of values not easily factorable by powers of 2 which binary is.

    The other form of float error is sig value range. Single floats only really store ~7 decimal digits of sig value (give or take depending the number since it's actually 24 digits of binary sig value which can vary in decimal). As a result changes in a float that are smaller than the current sig value range won't register.

    Say you have the number:
    745,672,190,000

    And you try to add 1

    You will result in:
    745,672,190,000

    Why? Because the sig value of the larger number is out of range of 1. When added you get 745,672,190,001... but we have to truncate it down to ~7 digits of sig values. And you always take the largest sig values resulting in 745,672,190,000.

    Also these 2 things come into play together. If you have 15.6 + 0.01. Well we already know that .6 is a repeating number, and 0.01 is a repeating number. But there also out of sig range of one another. Even though 0.01 is stored as ~0.0099998, we're going to lose some of that when we sume it to the 15.6 since so much sig value is store in that 15 of the 15.6. Our result definitely won't be 15.61, but it will be even off from if we had just set a variable to 15.6 rather than summed our way there.

    ...

    You are currently running into the binary/decimal conversion error of floats. And there's NOTHING you can do about it. Just like you accept that 0.33333 is close enough to 1/3. You need to just accept that 0.59999 is close enough to 0.6 for your maths. You're not performing rocket science here, it's just a video game, and that tiny difference isn't going to kill anyone.

    Just know that because this float error exists you should NEVER do float comparisons. Never check if some float x == some float y. Because even if they're reeeeaaaaalllly close, they're not exactly the same. You seldom should ever need to do a float comparison, but if you find yourself needing too, use Mathf.Approximately. But usually there's another way around it. I never really find myself needing to check if a value IS some other value... rather instead I usually find myself needing to check if a value is larger than or less than another value.
     
    Last edited: Dec 28, 2019
    konurhan, Souka, tsapper and 7 others like this.
  6. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,997
    To pull out one thing there: 1/10th in binary is a repeating decimal, so that 0.3 ending introduces rounding error. If you only use fractions with power of 2 denominators (0.25 or 0.125 or 0.625) I'd suspect that you would stop seeing those funny values.

    The most common trick is finding a formula which lets storage be an integer. If your weights only use 1 decimal place, multiply them by 10 -- a weight 6 rock becomes 60, while a weight 0.2 worm becomes 2. As a bonus, it (probably) uses less memory.
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    This is a work around.

    This is called a "fixed point number" (as opposed to the floating point). In a fixed point you don't store an exponent (the exponent is what allows moving the radix/point... hence 'floating' point). In a fixed point number, the radix always has the same location.

    You still have precision issues. But you can forego the conversion problem if you define your radix as being decimal rather than binary (note saying "decimal point" is a bit of a misnomer in binary since decimal implies base 10. This is why 'radix' is the correct term). Only problem is you have to implement the fixed point arithmetic (you can't just multiply/divide fixed points as if they're integers 50 * 1 and 5 * 0.1 are very different numbers). You won't won't save much memory though... an int is 32-bit just like a float is 32-bit. You'd save memory if you used a short... but then we get into even shorter precision AND how C#/.net pads memory in many cases for types that are smaller than a 32-bit word.

    You can also have a floating point number without the conversion issue. And C# has it built in. It's the 'decimal' type:
    https://docs.microsoft.com/en-us/dotnet/api/system.decimal?view=netframework-4.8

    Again, you still have precision issues. Though you get a whopping 96-bits of precision. So about 28-29 decimal digits of precision. That's even more than double which only has 53-bits of precision. And is why it's considered the preferred type for monetary calculations (though every damn enterprise job I've worked where we write accounting software... I've always had to explain to everyone including the boss what float error is and the usefulness of decimal). One big downside though is that C# always leans towards double... so if you say try to divide a decimal by a double, it'll do so as a double. So you need to be vigilent about typing your numbers. When working with decimals always stick an 'm' on the end to be sure. value / 5, value / 5.0, and value / 5m (where value is a decimal). All divide differently.

    Downside is none of these types are used within Unity as it prefers floats. So you'd need to convert and suffer any conversion lossiness in the process. But at least you can control when you convert.
     
    Last edited: Dec 28, 2019
  8. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,997
    I think you're wanting to think about how floats are stored so much that you're missing the trick -- it's all ints, all the time. Say you want to use 20th's of a kilo as the basic unit. 4.35 kilos is entered as 87. Then there will never be a 4.35 after that (except as a string, when we display the weight for the player, using modulo 20). You probably won't even do the conversion in your head -- it will be easier to think in terms of the integers once you have reference values (a typical helmet is 60 units (3 kilos), and so on).
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    To what effect are you saying?

    You said that a fixed point will save more memory. But if you compare an int and a float they're the same size. As you said, in the end they're all ints.

    I already said that fixed points are useful. The main issue is that OP would have to implement the arithmetic at the end of the day, and ensure they understand the precision limitations. And yes, doing this in the sense of using a defined fixed point units as 1/20th.. that's fine. You're just demonstrating the conversion of the stored value to the represented value.

    My point is to highlight HOW a fixed point works and what comes with that. I'm not saying they're bad or disagreeing. Just rather highlighting those aspects. Because at the end of the day... how it's stored is the problem OP is having. Case in point... now 4.35 isn't stored in floating point format, or in decimal scientific notation. But now it's stored as 87.

    The arithmetic problem is still there though.

    0.5 in a 1/20th scale is 10u (where u denotes a number in our unit scale of 1/20th). 10u * 10u is 100u. 100u converted back out of 1/20th scale is 5. Not the 0.25 we expect.

    And your choice of saying kilo highlights this. 10 kilo * 10 kilo doesn't make 100 kilo. It makes 100 kilos square (which doesn't make sense in terms of kilos, but if you described it in say meters, it would).

    So how you'd get the real result lets look at the arithmetic in fractional notation.
    10u * 10u = 10/20 * 10/20 = 100/400 = 1/4 = 0.25
    BUT that gives it to us in decimal. We multiply it back by 20 to put it back into our 1/20 format. But we could have done that in our multiplication.
    10u * 10u = 10/20 * 10/20 * 20 = 10 * 10/20 = 100/20 = 5u

    So if we created this special 1/20th type, we'd overload the multiplier to perform the integer arithmetic of (a * b / scale).
     
    Last edited: Dec 29, 2019
    Antypodish likes this.
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    For OP's benefit. Here's how a fixed point number with a scale of 1/20th would be implemented.

    Code (csharp):
    1.  
    2.         public struct Fixed20
    3.         {
    4.  
    5.             private int _value;
    6.  
    7.  
    8.             public override string ToString()
    9.             {
    10.                 return ((float)_value / 20f).ToString("0.00");
    11.             }
    12.  
    13.             public static Fixed20 operator +(Fixed20 a, Fixed20 b)
    14.             {
    15.                 return new Fixed20()
    16.                 {
    17.                     _value = a._value + b._value
    18.                 };
    19.             }
    20.  
    21.             public static Fixed20 operator -(Fixed20 a, Fixed20 b)
    22.             {
    23.                 return new Fixed20()
    24.                 {
    25.                     _value = a._value - b._value
    26.                 };
    27.             }
    28.  
    29.             public static Fixed20 operator *(Fixed20 a, Fixed20 b)
    30.             {
    31.                 return new Fixed20()
    32.                 {
    33.                     _value = (a._value * b._value) / 20
    34.                 };
    35.             }
    36.  
    37.             public static Fixed20 operator /(Fixed20 a, Fixed20 b)
    38.             {
    39.                 return new Fixed20()
    40.                 {
    41.                     _value = (a._value * 20) / b._value
    42.                 };
    43.             }
    44.  
    45.             public static bool operator ==(Fixed20 a, Fixed20 b)
    46.             {
    47.                 return a._value == b._value;
    48.             }
    49.  
    50.             public static bool operator !=(Fixed20 a, Fixed20 b)
    51.             {
    52.                 return a._value != b._value;
    53.             }
    54.  
    55.             public static implicit operator Fixed20(float f)
    56.             {
    57.                 return new Fixed20() {
    58.                     _value = (int)(f * 20)
    59.                 };
    60.             }
    61.  
    62.             public static implicit operator Fixed20(double f)
    63.             {
    64.                 return new Fixed20()
    65.                 {
    66.                     _value = (int)(f * 20)
    67.                 };
    68.             }
    69.  
    70.             public static implicit operator Fixed20(int i)
    71.             {
    72.                 return new Fixed20()
    73.                 {
    74.                     _value = i * 20
    75.                 };
    76.             }
    77.  
    78.             public static explicit operator float(Fixed20 v)
    79.             {
    80.                 return (float)v._value / 20f;
    81.             }
    82.  
    83.             public static explicit operator double(Fixed20 v)
    84.             {
    85.                 return (double)v._value / 20d;
    86.             }
    87.  
    88.             public static explicit operator int(Fixed20 v)
    89.             {
    90.                 return (int)(v._value / 20d);
    91.             }
    92.  
    93.         }
    94.  
    I also include implicit conversion operators for float, double and int so you can just easily write them down like so:
    Code (csharp):
    1.  
    2. Fixed20 a = 1.6;
    3. Fixed20 b = 3;
    4. Fixed20 c = a * b; //should be 4.8
    5.  
    But honestly, I don't even bother. I use decimal if this is important. It behaves the way we expect decimal numbers to behave. It has tons of precision. And any speed inflicted by the arithmetic operations are also part of any fixed point implementation you used since even if you're going to want to either implement them as distinct funcitons (like I have here), OR you have to remember to inline them every single time you do them (here's where C++'s inline'ing capabilities are super beneficial if we were using that language to implement a fixed point... though in theory newer C# can inline, but I am skeptical of how that'd work out here in Unity).
     
    Last edited: Dec 29, 2019