Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Resolved Round numbers to 5 tenths?

Discussion in 'Scripting' started by jlorenzi, Jun 23, 2023.

  1. jlorenzi

    jlorenzi

    Joined:
    May 2, 2021
    Posts:
    270
    I'm making a grid based building game and I want to align blocks to the grid by every 0.5m.
    For example:
    1.3 > 1.5
    1.2 > 1
    1.6 > 2

    I wrote this code which works with positive numbers, but not negative
    Code (CSharp):
    1.     float RoundTo5Tenths(float num)
    2.     {  
    3.         float decimals = num % 1; // get decimal part of number
    4.  
    5.         if ( decimals > 0.25f )
    6.         {
    7.             return Mathf.Floor(num) + 0.5f;
    8.         }
    9.  
    10.         else
    11.         {
    12.             return Mathf.Floor(num);
    13.         }
    14.     }
    I feel like there has to be a better way than making your own rounding function. Any ideas?
     
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Code (csharp):
    1. static float HalfRound(float num) => MathF.Round(num * 2f) / 2f;
    This is guaranteed not to overflow if num < 1,073,741,824 (Edit: nah, scratch this, I was thinking about the integer version, in this case it follows the natural sloppiness of floating numbers, there's is no overflow opportunity, and it's perfectly safe in this regard.)

    (It works with negative numbers just as well.)
     
    Last edited: Jun 23, 2023
    Bunny83 and StarBornMoonBeam like this.
  3. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209
    Sorry wrong answer :) Orion has it
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,538
    Those examples are not really consistent. Are you sure your last line of those 3 is what you want? The closest 0.5 value for 1.6 would be 1.5 and not 2 unless you always want to round up (ceil) the number. However in that case your second example would be wrong. Rounding usually happens in between two desired values. So since you want a multiple of 0.5 the actual points where you decide to round up or down would be 0.25, 0.75, 1.25, 1.75, ....

    So values between 0.25 and 0.75 would round to 0.5 while values between 0.75 and 1.25 round to 1.0

    The solution that @orionsyndrome posted does exactly that. If you want to always round up use Ceil instead of Round. If you want to always round down, use Floor instead. The logic stays the same, however the point around which is rounded changes

    Code (CSharp):
    1.     v   | Round(v*2)/2 | Floor(v*2)/2 | Ceil(v*2)/2
    2. -----------------------------------------------------
    3.   0.0   |     0.0      |     0.0      |    0.0
    4.   0.1   |     0.0      |     0.0      |    0.5
    5.   0.2   |     0.0      |     0.0      |    0.5
    6.   0.3   |     0.5      |     0.0      |    0.5
    7.   0.4   |     0.5      |     0.0      |    0.5
    8.   0.5   |     0.5      |     0.0      |    0.5
    9.   0.6   |     0.5      |     0.0      |    1.0
    10.   0.7   |     0.5      |     0.0      |    1.0
    11.   0.8   |     1.0      |     0.0      |    1.0
    12.   0.9   |     1.0      |     0.0      |    1.0
    13.   1.0   |     1.0      |     1.0      |    1.0
    14.   1.1   |     1.0      |     1.0      |    1.5
    15.   1.4   |     1.5      |     1.0      |    1.5
    16.  
     
  5. jlorenzi

    jlorenzi

    Joined:
    May 2, 2021
    Posts:
    270
    Oops hahaha, don't mind me forgetting basic math for a minute... but yeah 1.6 should go to 1.5 my bad.
     
    Bunny83 likes this.
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    I considered that to be a typo. I subconsciously assumed you were after nearest rounding because you mentioned negative numbers. Floors and Ceils aren't that useful in the context of negatives, given that they perceptually flip around.

    This is completely gratuitous but if you ever need the floor to behave as "flooring toward zero" and not "flooring toward negative infinity" you can (Edit: Skip this to the next "Edit")
    Code (csharp):
    1. static float FloorTowardZero(float n) => MathF.Sign(n) * MathF.Abs(MathF.Floor(n));
    However even though this is branchless, this optimization is better
    Code (csharp):
    1. static float FloorTowardZero(float n) => n >= 0f? MathF.Floor(n) : -MathF.Abs(MathF.Floor(n));
    Now that you have Floor, Ceiling is just
    Code (csharp):
    1. static float CeilTowardZero(float n) => MathF.Sign(n) + FloorTowardZero(n);
    However this doesn't work properly and I'm too lazy to describe why (yeah lol).
    (Edit: it didn't work properly because of typos, but also because the approach was wrong.)

    Instead the second version reveals you could simply introduce symmetry by doing
    Code (csharp):
    1. static float FloorTowardZero(float n) => n >= 0f? MathF.Floor(n) : MathF.Ceiling(n);
    This works. And why is this useful? Well, you get the nearest integer interval out of it!
    If you do something like this
    Code (csharp):
    1. static void test() {
    2.   for(int i = 0; i < 40; i++) {
    3.     var n = i / 5f - 3f;
    4.     Console.WriteLine(string.Format("{0,-12:0.00}\t{1,-12}\t{2,-12}\t{3,-12}", n, HalfRound(n), FloorTowardZero(n), CeilTowardZero(n)));
    5.   }
    6. }
    7.  
    8. static float HalfRound(float num) => MathF.Round(num * 2f) / 2f;      
    9. static float FloorTowardZero(float n) => n >= 0f? MathF.Floor(n) : MathF.Ceiling(n);
    10. static float CeilTowardZero(float n) => MathF.Sign(n) + FloorTowardZero(n);
    You'll get this result
    Code (csharp):
    1. // n            HalfRound       FloorTowardZero CeilTowardZero
    2. -3.00           -3              -3              -4      
    3. -2.80           -3              -2              -3      
    4. -2.60           -2.5            -2              -3      
    5. -2.40           -2.5            -2              -3      
    6. -2.20           -2              -2              -3      
    7. -2.00           -2              -2              -3      
    8. -1.80           -2              -1              -2      
    9. -1.60           -1.5            -1              -2      
    10. -1.40           -1.5            -1              -2      
    11. -1.20           -1              -1              -2      
    12. -1.00           -1              -1              -2      
    13. -0.80           -1              -0              -1      
    14. -0.60           -0.5            -0              -1      
    15. -0.40           -0.5            -0              -1      
    16. -0.20           -0              -0              -1      
    17. 0.00            0               0               0      
    18. 0.20            0               0               1      
    19. 0.40            0.5             0               1      
    20. 0.60            0.5             0               1      
    21. 0.80            1               0               1      
    22. 1.00            1               1               2      
    23. 1.20            1               1               2      
    24. 1.40            1.5             1               2      
    25. 1.60            1.5             1               2      
    26. 1.80            2               1               2      
    27. 2.00            2               2               3      
    28. 2.20            2               2               3      
    29. 2.40            2.5             2               3      
    30. 2.60            2.5             2               3      
    31. 2.80            3               2               3      
    32. 3.00            3               3               4      
    33. 3.20            3               3               4      
    34. 3.40            3.5             3               4      
    35. 3.60            3.5             3               4      
    36. 3.80            4               3               4      
    37. 4.00            4               4               5      
    38. 4.20            4               4               5      
    39. 4.40            4.5             4               5      
    40. 4.60            4.5             4               5      
    41. 4.80            5               4               5
    I'm not claiming it's groundbreakingly useful, but it has some nice properties.
     
    Last edited: Jun 24, 2023
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,538
    Why would you ever use such methods like your "FloorTowardZero"? It suffers from the same singularity around 0 as integer division / integer modulo since it creates a twice as large gap around 0 because of that. The usual floor and ceil work consistently and shifting the numbers up or down doesn't change its behaviour. Floor always rounds down to the closest whole number that is smaller or equal to the original. Ceil always rounds up to the closest number that is larger or equal to the original. Floor, Round and Ceil work pretty much the same, just the threshold is different.

    Integer division always rounds towards zero as it essentially treats the division like a positive division. That's why the modulo of negative numbers can return negative values so that this holds true:

    Code (CSharp):
    1. d = a / b
    2. m = a % b
    3.  
    4. a == d * b + m
    In mathematics the euclidean division is defined to be consistent, however the result are a bit counter-intuitive. So here an integer division that involves negatives may be a bit surprising because the above mentioned relationship also should hold true. Though that means we get this:

    Code (CSharp):
    1. // euclidean division
    2.   a  /   b  ==  d         a  %   b  ==  m
    3. -----------------------------------------
    4. ( 8) / ( 3) ==  2       ( 8) % ( 3) ==  2
    5. ( 8) / (-3) == -2       ( 8) % (-3) ==  2
    6. (-8) / ( 3) == -3       (-8) % ( 3) ==  1
    7. (-8) / (-3) ==  3       (-8) % (-3) ==  1
    8.  
    9.  
    However in C# we have negative modulo results and integer division always rounds towards zero which actually causes a lot of issues since the area -1 to 1 is a range of length 2 but it "rounds" to 0. I've seen people on YT which made a minecraft clone from scratch and the world generation had a gap around the origin because of that ^^. So you can not just do an integer division to get the chunk coordinates for a block as the 4 chunks around 0 would all map to 0. That's why either floor or ceil is the way to go in such cases. Floor is the most common approach.


    What nice properties do you have in mind? The only real case would be when you take the absolute value anyways. However for any kind of coordinate system that goes into negative values that gap around zero is usually more an issue than something you want to have ^^.

    ps:
    The first and second "FloorTowardZero" method literally does nothing and just returns what MathF.Floor returns anyways as you just take the absolute value and then add the sign back on. Your final version of FloorTowardZero indeed does round towards 0. But I still don't get where you would need that? It just means you're loosing one interval. Floor is consistent and using floor and ceil give you the upper and lower bounds of the integer interval the number is in with the only exception of perfect integer values where both floor and ceil give you the same number as the number is already an integer, so no need to round up or down.
     
    orionsyndrome likes this.
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Wow you're giving this too much thought and weight.

    Yes you're right, and the explanation is simple: it's like that because I made a mistake hahah.
    But I never got back to fix it (or even notice it), because I've moved on to a better idea that actually satisfies the initial constraint.

    It should've been
    Code (csharp):
    1. static float FloorTowardZero(float n) => MathF.Sign(n) * MathF.Abs(MathF.Floor(n) + 1f);
    But then I figured out that I'm messing with the consistency of it, so I moved onto the branching variant, because this is only true for the negatives. And so, yes, I agree, getting abs and bringing back the sign is really a poor idea altogether, but this was the order in which I worked this out in 6 minutes or so, and I didn't want to remove any of it.

    Normally floor/ceil pair travels along the number line constrained with the following rule:
    F(n) >= n > C(n)

    I can't name any particular engineering reason for why would anyone do this, but the motivation was completely mathematical -> producing a symmetric floor/ceiling pair, with a different constraint:
    abs(FZero(n)) >= abs(n) > abs(CZero(n))

    I haven't really spend too much time on this, however, and I'm sorry for the errors in the argument build up. I was kind of aiming for the final output.

    Actually if you look at the output (which is why I provided one), you do not lose an interval, you gain one! The solution actually introduces a degenerate interval of 0..0 dedicated to pure 0. Which is what you would expect from a sign-agnostic floor/ceil pair.

    There are some benefits to be had:
    - the solution is truly symmetric: +1[.xxx] is bound by 1 and 2, just like -1[.xxx] which is bound by -1 and -2
    - pure zero is excluded from the intervals
    - you are guaranteed that the floor is always that one bound closer to the zero (order matters),
    - you get a sign-agnostic round-toward-zero (you always know it's floor), or round-away-from-zero (you always know it's ceil)
    - it could be something that is useful to someone, sometimes?

    As I said, it has nice properties. And it could be really useful for some natural quantization (where you keep signs for semantics such as giving/taking), such as boardgame logic, or anything that uses fuzzy dynamics over discrete sets. But I would have to think this through to come up with a proper example.
     
    Last edited: Jun 24, 2023
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    In short: it makes sense to use this function, if and only if, you find yourself in a situation where you'd have to check for the sign before using floor or ceil. It doesn't supplant or replace anything and I certainly don't claim it's "better".
     
  10. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    A proper way to do true modulo 4 is not
    n % 4
    but
    n & 3
    . This plays well with the negatives because of 2s complement. A generalized true modulo when the modulus is a power of 2 is
    n & ((1 << power) - 1)


    Whoever uses anything but shifts and ands for tiles and chunks is doing it wrong, imho.
     
    Last edited: Jun 24, 2023
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,538
    Very true :) That's the main reason Minecraft uses 16x16 chunks and regions of 512x512. Powers of two are just great
     
    orionsyndrome likes this.