I found a simple, alternative approach that uses the same logic as a regular checkerboard. It creates a snap-to-grid effect with points at the center of every tile and at every vertex (by creating a tighter grid and ignoring alternating points).
This approach works well for games like Catan, where players interact with tiles and vertices, but is not suited to games where players only interact with tiles, as it returns which center point or vertex the coordinates are closest to, rather than which hexagonal tile the coordinates are within.
The Geometry
If you place points in a grid with columns that are quarter the width of a tile, and rows that are half the height of a tile, you get this pattern:
If you then modify the code to skip over every second dot in a checkerboard pattern (skip if column % 2 + row % 2 == 1
), you end up with this pattern:
Implementation
With that geometry in mind, you can create a 2D array (just like you would with a square grid), storing the x, y
coordinates for each point in the grid (from the first diagram) - something like this:
points = []
for x in numberOfColumns
points.push([])
for y in numberOfRows
points[x].push({x: x * widthOfColumn, y: y * heightOfRow})
Note: As normal, when you are creating a grid around the points (rather than placing dots at the points themselves), you need to offset the origin (subtracting half the width of a column from x
and half the height of a row from y
).
Now that you have your 2D array (points
) initialized, you can find the nearest point to the mouse just like you would on a square grid, only having to ignore every other point to produce the pattern in the second diagram:
column, row = floor(mouse.x / columnWidth), floor(mouse.y / rowHeight)
point = null if column % 2 + row % 2 != 1 else points[column][row]
That will work, but the coordinates are being rounded to the nearest point (or no point) based on which invisible rectangle the pointer is within. You really want a circular zone around the point (so the snap-range is equal in every direction). Now that you know which point to check, you can easily find the distance (using Pythagoras' Theorem). The implied circle would still have to fit inside the original bounding rectangle, limiting its maximum diameter to the width of a column (quarter the width of a tile), but that is still large enough to work well in practice.