Advertisement

Error in remainder of integer? (scaling offset)

Started by March 24, 2018 03:13 PM
11 comments, last by suliman 6 years, 8 months ago

Hi!

I have a two floats (pan.x and pan.y) which allows the player to move the view in my tile-based 2d-gameworld. I also have a "float scale" for zooming the world in and out.

For drawing most stuff it works fine. I just (where pos is the tile-position of something and #define TILE 20 is the size of a gametile at scale = 1):


drawStuff(pos.x * scale * TILE, pos.y * scale * TILE);

When I change scale it draws to the correct position on screen. But when i draw the lines that shows the grid (all the tiles) they do not offset correctly. It works when scale = 1, but when I lower scale they become off. I tried so many variations but I've got stuck. What am I doing wrong?


int tile2 = TILE * scale;
int offY = ((int)pan.y) % tile2;

// draw horizontal lines for the grid
for (int i = 0; i < h; i++)
	gfx.lineRelative(0, TILE*i*scale + offY, gfx.screenX, 0, col);

 

floating point is not exact, it's an approximation of the real value. Mathematical operations with different values subtly pick different rounding of the last bit.  For example, 1 *n + 1*n != 2*n, for many float values n.

Read about "what every programmer should know about floating point".

One solution is to eliminate all floating point, and do purely integer arithmetic. Another option is to do a bit of floating point, but not there were rounding matters. For example, compute the tile size as integer, and use that everywhere to compute positions of tiles.

Advertisement

Hmm yes. But changing the "pan" variables to int doesnt solve it. And scale needs to be a float.

I realised the error goes away if the remainder would be without decimals anyway, for example:
unscaled tile-size is always 20 pixels, so if "scale" is exactly 0.5 or 0.75 (making scaled tiles exactly 10 or 15 pixels) all is fine.

But how do i fix this? I need the scaling to be continuous so i need a float for it. And why does it work for the images ("drawstuff") but not when i draw the lines?

16 hours ago, suliman said:

But when i draw the lines that shows the grid (all the tiles) they do not offset correctly. It works when scale = 1, but when I lower scale they become off.

What exactly do you mean by "become off"?

The most likely thing I see in your code is that you fix your offset to an integer.  The line: int offY = ((int)pan.y) % tile2; will assign a value that probably isn't what you want.  If the value was 10.5 then you'd need a step of 10 pixels, then 11 pixels, then 10, then 11.  If you had a value of 4.75 you'd step 4 pixels, then they'd step at +4, +5, +5, +5, +4, +5, +5, +5, +4, ...  To correct for that you will need to use the correct position and round to the nearest integer pixel value.

Inside your loop you'd need to compute that value each time. I'm not completely sure without the surrounding code, but it looks like the offset would be your row number times the regular tile size times the scale, all cast to an int.  Or:  int( i * scale * TILE ).  

 

If you mean something else by "become off", you'll need a different description or images or something.

int tile2 = TILE * scale;

int offY = ((int)pan.y) % tile2;

 

Is this allowed in C++ ? Just curious since int=int*float would throw an error for me.

 

Also scale could be and int  1-100 and divide by 100 before casting , might get better results.

 

And 4th you are passing a float as a potion argument to a 2d draw function ?


TILE*i*scale + offY

 

EDIT:

As to why it offsets , the reason seems the usage of float that round to int and loose information. Example scale 0.26:

tile2=5(5.2 rounded to 5) - take in consideration this is the same as scale 0.25 ;

offY=a value X derived from % 5 instead 5.2;

you are basically offsetting by a value of scale 0.25 for a scale of 0.26 , could be the reason of your issue, but would need more data to clarify.

I would try to first maybe cast tile2 as float and let off as int.

17 minutes ago, CrazyApplesStudio said:

 Is it a requirement of your game to zoom/pan like this , instead of say using the Camera.transform to pan and Orthographic.size to zoom in ?

Not everyone is using an engine with a built-in Camera class that handles all this for you!

Advertisement
Just now, Kylotan said:

Not everyone is using an engine with a built-in Camera class that handles all this for you!

I know i saw to late , i apologize for my mistake.

2 hours ago, frob said:

Inside your loop you'd need to compute that value each time

Why? The offset is the same for all the lines. Basically it tells me where the top of the screen is in the gameworld. And for the 30 lines or whatever fits in the screen they all have the SAME wrong offset.

With wrong offset i mean the lines do not match the tiles drawn for buildings and other objects in the world. So ALL the lines have the same wrong offset, which is far more than a pixel, so im not sure what's going on and why.

If i skip the remainder (% operator) all together and calc which would be the first tile shown (top left corner of the game-window) this works fine:


for (int i = startY; i < startY + h; i++) // hori
	gfx.lineRelative(0, i * TILE*scale + pan.y, gfx.screenX, 0, col);

 

1 hour ago, suliman said:

Why? The offset is the same for all the lines. Basically it tells me where the top of the screen is in the gameworld. And for the 30 lines or whatever fits in the screen they all have the SAME wrong offset.

With wrong offset i mean the lines do not match the tiles drawn for buildings and other objects in the world. So ALL the lines have the same wrong offset, which is far more than a pixel, so im not sure what's going on and why.

If i skip the remainder (% operator) all together and calc which would be the first tile shown (top left corner of the game-window) this works fine:



for (int i = startY; i < startY + h; i++) // hori
	gfx.lineRelative(0, i * TILE*scale + pan.y, gfx.screenX, 0, col);

 

See my suggestion :

Example scale 0.26:

tile2=5(5.2 rounded to 5) - take in consideration this is the same as scale 0.25 ;

offY=a value X derived from % 5 instead 5.2;

you are basically offsetting by a value of scale 0.25 for a scale of 0.26 , could be the reason of your issue, but would need more data to clarify.

I would try to first maybe cast tile2 as float and let off as int.

Small aside: floats are less magical, than many people make them to be. They can be counterintuitive to unwary, but you can, in fact, ensure some things with certainty.

21 hours ago, Alberth said:

floating point is not exact, it's an approximation of the real value

I'm really not fond of that particular wording, as it conflates several issues (and in some ways is untrue).

21 hours ago, Alberth said:

For example, 1 *n + 1*n != 2*n, for many float values n.

As far as I can tell 1*n+1*n==2*n holds for all non-NaN floats.

 

Back to the topic at hand. Some clarifications would be welcome (e. g. drawStuff does not mention pan at all), but there seems to be enough information now to piece the problem together.

First off, if

3 hours ago, suliman said:

If i skip the remainder (% operator) all together and calc which would be the first tile shown (top left corner of the game-window) this works fine:

you may simply go with that.

Now, as to why the original code doesn't work. The reason have already been mentioned by few people, but I'll elaborate.

The lines are at


i * TILE*scale + pan.y

on the y-axis.

The first (least y) line that fits on the screen is at i=startY such that


0 <= startY * TILE*scale + pan.y < TILE*scale

-pan.y <= startY * TILE*scale < TILE*scale - pan.y

startY = ceil(- pan.y / (TILE*scale)) {assuming scale>0}

It's offset is


ceil(- pan.y / (TILE*scale)) * (TILE*scale) + pan.y

which is


-floor(pan.y / (TILE*scale)) * (TILE*scale) + pan.y

that is mod(pan.y, TILE*scale), assuming modulo is defined to be always non-negative, which std::fmod isn't.

This is different from mod((int)pan.y, (int)(TILE*scale)) which you are doing (once more, disregarding negatives).

In fact, difference can be estimated as frac(TILE*scale)*startY (not always true, since it can wrap-around), which may be quite large. This explains why integer TILE*scale work fine.

So the original code can be (I think) fixed as follows:


float oy=fmodf(pan.y,TILE*scale);
int offY = (int)(oy<0.0f?TILE*scale+oy:oy);

This topic is closed to new replies.

Advertisement