# Staggered Isometric Snap

Get help using Construct 2

### » Sat May 14, 2016 11:33 pm

TL;DR somebody with the maths, looks at my capx and tell me why it doesn't snap right. please and thanks!

I'm making an isometric tile editor (using sprites) and I just want my sprites to snap on the staggered isometric grid.
I know there are lots of ways to do this. But I feel like there has to be a simple, 1-2 line way to do it, and that's what I'm looking for.

I figure if
set tile position: (mouse.x-mouse.x%128)
snaps it on a square grid, then I just need to take it one step further and have it check if the Y is on an even or odd row and then add the stagger (64px=half a tile) or don't. And then vice versa for the Y.

I keep coming up with something that looks kinda like this:
set tile position:
X: ((mouse.x-mouse.x%128)+(64*(round(mouse.y/64)%2)),
Y: ((mouse.y-mouse.y%64)+(32*(round(mouse.x/128)%2))

so the first half sets it on a square grid, and the second half checks if the opposite axis is odd or even (0 or 1 from the %2) and then moves it over the extra pixels if necessary.
At least that's what I think it should do. It almost does, but not really. heh.

Can anyone tell me where I'm going wrong, or why it doesn't work, or something?
Thanks!
B
88
S
43
G
71
Posts: 601
Reputation: 43,669

### » Sun May 15, 2016 7:18 am

Hey @spacedoubt,

So, if I understand correctly, I believe the short answer would be this:

snapX = ( round( mouse.y / 32 ) % 2 = 0 ) ? ( round( mouse.x / 128 ) * 128 ) : ( ( round( ( mouse.x + 64 ) / 128 ) * 128 ) - 64 )
snapY = round( mouse.y / 32 ) * 32

That said, I've also included an explanation of this solution below:

The math
What it sounds like you want to do is snap to a diamond shaped grid; basically a rectangular grid with every odd row shifted half a cell length.

Let's start by thinking of the rectangular un-shifted version of the grid.
From your capx, it looks like you want a horizontal snap distance of 128, and a vertical snap distance of 32. Now 32 might sound too small, but remember that as soon as we shift every odd row, it will suddenly look like the vertical snap distance is 64, because the vertical separation between tiles will appear to skip a row. But it really isn't skipping a row, and it really isn't 64, it's still 32.

Code: Select all
`Unshifted#---#---#---##---#---#---##---#---#---##---#---#---#Shifted#---#---#---#..#---#---#---##---#---#---#..#---#---#---#`

Now if we shift every odd row by half a cell width, we'll have a diamond grid that will snap 128 x 64 pixel tiles.

Vertical snap
So lets start with the vertical snapping since it will be the same regardless of the row.
We need to snap to steps of 32 pixels.

tileY = round( mouse.y / 32 ) * 32

We are squeezing the 32 pixel tile size down to the size of integers, then using round() to chop off the decimal part, then expanding the rounded result back to the 32 pixel tile size.

Horizontal snap
Next lets do the horizontal snapping, but for simplicity, our first version here will not account for the shifting on odd rows.
It's basically the same as the vertical snapping, but with bigger steps.

tileY = round( mouse.x / 128 ) * 128

Same deal, we squeeze, round, and expand back to normal.
Now that formula works fine for the even rows, but what about the shifted odd rows?
Well lets make a similar, but slightly adjusted version of the formula for the odd rows.
We want to shift the tiles over by half the horizontal step width of 128. So we'll shift by 64.

tileY = ( round( ( mouse.x + 64 ) / 128 ) * 128 ) - 64

So it's basically exactly the same as the even-row formula, except we add a 64 offset to the input "mouse.x", and we subtract it back off after we end.
Why? Well remember that the heart of our snapping system is the round() function. When we shift everything by half a step width (64), and then divide by an entire step width (128), as far as the round() is concerned we are shifting by "0.5"; and since round() will snap to the nearest integer, it will be snapping exactly halfway between the spots we'd get from the even-row formula.

But wait, even though we shifted the input, round() is still just snapping to integers, so shouldn't it just expand back to the same snap points that the even-row formula gave us? Yes, it would except, remember that we also un-offset the result by half a step width, which is the "- 64" at the very end.

Picking Even or Odd formula
Okay, so we have two formulas, but now we have to choose one or the other depending on the row.
We'll need a formula that tells us if we're on an odd or even row.

round( mouse.y / 32 ) % 2

We squeeze the Y coord from counting pixels down to counting rows by dividing, then use round() to chop off the decimal fluff, leaving us with the integer row number. We then mod the row number by 2, meaning we'll get "0" if the row is even, and "1" if the row is odd.

Now we just need to say, IF ( row is even ) THEN ( even formula ) ELSE (odd formula).
You could do this with events, and that would be just fine, but there is a slightly more compact way to handle the IF / THEN / ELSE case, and it's called the "ternary" operator, sometimes called the one-line-IF-statement.

The ternary operator looks like this when used:

( name = "world" ) ? ( "hello world" ) : ( "hello person who is suspiciously not world" )

and means this:

IF ( name = "world" ) THEN ( "hello world" ) ELSE ( "hello person who is suspiciously not world" )

(Note: The "ternary" operator is named for the fact that it takes three arguments, where most operators (like "+" and "-") take only two.)
This is exactly what we want to do.
We just need to plug in the parts we want to use.

IF ( round( mouse.y / 32 ) % 2 = 0 )
THEN ( round( mouse.x / 128 ) * 128 )
ELSE ( ( round( ( mouse.x + 64 ) / 128 ) * 128 ) - 64 )

Using the ternary operator we get this:

( round( mouse.y / 32 ) % 2 = 0 ) ? ( round( mouse.x / 128 ) * 128 ) : ( ( round( ( mouse.x + 64 ) / 128 ) * 128 ) - 64 )

Final X &Y formulas
snapX = ( round( mouse.y / 32 ) % 2 = 0 ) ? ( round( mouse.x / 128 ) * 128 ) : ( ( round( ( mouse.x + 64 ) / 128 ) * 128 ) - 64 )
snapY = round( mouse.y / 32 ) * 32

Modulo operator
The JavaScript Modulo operator (%) is ... tricky, to put it diplomatically.

Unlike the more common operators like plus and minus, there's actually no universal standard definition for exactly how modulo is supposed to work, and so there are a few very subtly different versions of it. The version that JavaScript uses (and by extension Construct 2) is one of the weird ones, which does not behave uniformly for positive and negative numbers.
There's a nice visual comparison of the mod variations on the modulus wiki page.

This is especially a problem when writing snapping formulas that make use of JS's modulo, as it means the snapping behavior will likewise not behave consistently across positive and negative input coordinates. If you are absolutely certain that you will never feed in a negative value then it's safe-ish to use the JS modulo, though if you inadvertently change that later in a project without realizing it, you may introduce some very subtle bugs as a result of its weird behavior. I personally never use it for anything. I instead use an alternate version of the mod function described below.

There is a version of the modulo operator that does behave uniformly, and that's the "floored division" version.
Here is the formula for the "floored division" modulo.

mod( val , div ) ... = ... val - ( floor( val / div ) * div )

So if you just used substitution to modify the original even-odd-detector formula we made, we'd go from this,
( round( mouse.y / 32 ) % 2 = 0 )
to this,
( ( round( mouse.y / 32 ) - ( floor( round( mouse.y / 32 ) / 2 ) * 2 ) ) = 0 )

Not very pretty, but it will work. it would be nicer to be able to simply use a floored division mod function like any other function.
Well, you can use C2's Function object to create a custom "floored division" style mod function.
I actually explained how to do this a while back in another post if you're interested, in the subsection Getting "floored division" mod into C2 (towards the end of the post).

Hope that helps out.
B
30
S
20
G
8
Posts: 350
Reputation: 6,479

### » Sun May 15, 2016 7:33 am

@fisholith Myyyyy hero!

This is so awesome.
I really REALLY appreciate the solution and the explanation. It makes perfect sense.

Wish I had more to say. Just.. Thanks!
B
88
S
43
G
71
Posts: 601
Reputation: 43,669