GameMaker: smart floor/ceil functions

An image showing that what seems like a "4" may be a "3.999999999999994", causing unexpected results with floor/ceil functions.

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.

Related posts:

2 thoughts on “GameMaker: smart floor/ceil functions

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.