Alternatively best described as "how can floor(4) be 3 and how to fix it".
The problem
Let's suppose you are incrementing a value by 0.05 in iterations or over time.
var val = 0; repeat (80) val += 0.05; show_debug_message(val); show_debug_message(floor(val)); show_debug_message(ceil(val));
Contrary expectations, the output is 4.00, 3, and 4.
This is the result of a round-off error - 0.05 cannot be precisely represented in floating-point, and thus adding 0.05 to a value 80 times doesn't quite add up to 4. This can be tested using
show_debug_message(string_format(val, 0, 15));
which shows that val is, in fact, 3.999999999999994.
Solutions
Smarter floor/ceil functions
First, let's take a look at the code:
/// smart_floor(value) /// @param value var v = argument0; if (sign(v) < 0) { if (frac(v) == 0) return ceil(v); } else { if (frac(v) == 1) return ceil(v); } return floor(v);
/// smart_ceil(value) /// @param value var v = argument0; if (sign(v) < 0) { if (frac(v) == -1) return floor(v); } else { if (frac(v) == 0) return floor(v); } return ceil(v);
Evidently, it works, but what does it mean?
The way this works is that GameMaker implements epsilon - when you do (a == b), what really happens is (a >= b - eps && a <= b + eps). This allows us to do a seemingly nonsensical frac(v) == 1 comparison that would really mean v > 1 - eps. sign(v) is used for the same reason - v < 0 alone would be false for values that are negative but are within epsilon-range.
If we wanted to do this in a language that does not have epsilon built-in - say, JavaScript, we'd do everything ourselves:
var eps = 1/1000000; function smartFloor(v) { if (v < 0) { if (v % 1 > -eps) return Math.ceil(v); } else if (v % 1 > 1-eps) return Math.ceil(v); return Math.floor(v); } function smartCeil(v) { if (v < 0) { if (v % 1 < eps-1) return Math.floor(v); } else if (v % 1 < eps) return Math.floor(v); return Math.ceil(v); }
round()
Depending on your circumstances, you may be able to get away with using round to snap the value to the nearest integer - since the function uses bankers' rounding, it's not going to fail at X±eps, although you should watch out for values close to .5 instead.
Avoiding round-off errors
The best way to avoid round-off errors is not to store your increment-able values as floating-point more often than you should. So, for example, with the initial sample, if you were to add an integer and then multiply by step,
var counter = 0; for (var i = 0; i < 80; i++) counter++; var val = counter * 0.05;
this would work as intended:
show_debug_message(val); // 4 show_debug_message(floor(val)); // 4 show_debug_message(ceil(val)); // 4
even for least-usual values like dividing by 3.
And that's about it.
Thanks for this. I found this several months ago and it fixed my issue. However, another similar issue came up where this script failed. Using smart_floor, this below:
“`
frac(.999999999999773) == 1
“`
Ends up equaling 1, therefore ceiling the value instead of flooring. I read up a bit more and change epsilon in game maker to a smaller value, which made this work. But then, if I am changing it to a smaller value, why not use the floor / ceil functions built in to Game Maker? Also, are you aware of any consequences using really small values of epsilon?
The precise purpose of smart_floor() is to return 1 for values sufficiently close to 1. Else you’d be using “regular” floor().
Ran into that once. It was not fun.
Good blog post!
This is destroying my brain. Luckily I haven’t run into the problem yet