Hexagonal Grids 六边形 Grid 编程全解
Geometry
Angles
Coordinate Systems
Now let’s assemble hexagons into a grid. With square grids, there’s one obvious way to do it. With hexagons, there are multiple approaches. I recommend using cube coordinates as the primary representation. Use either axial or offset coordinates for map storage, and displaying coordinates to the user.
Offset coordinates
The most common approach is to offset every other column or row. Columns are named col
or q
. Rows are named row
or r
. You can either offset the odd or the even column/rows, so the horizontal and
vertical hexagons each have two variants.
Cube coordinates
The cube coordinates are a reasonable choice for a hex grid coordinate system. The constraint is that x + y + z =
0
so the algorithms must preserve that. The constraint also ensures that there’s a canonical coordinate for each hex.
There are many different valid cube hex coordinate systems. Some of them have constraints other than x + y + z = 0
. I’ve shown only one of the many systems. You can also construct cube coordinates with x-y, y-z, z-x
,
and that has its own set of interesting properties, which I don’t explore here.
“But Amit!” you say, “I don’t want to store 3 numbers for coordinates. I don’t know how to store a map that way.”
Axes
Offset coordinates are the first thing people think of, because they match the standard cartesian coordinates used with square grids. Unfortunately one of the two axes has to go “against the grain”, and ends up complicating things. The cube and axial systems go “with the grain” and have simpler algorithms, but slightly more complicated map storage. There’s also another system, called interlaced or doubled, that I haven’t explored here; some people say it’s easier to work with than cube/axial.
Offset Cube AxialThe axis is the direction in which that coordinate increases. Perpendicular to the axis is a line where that coordinate stays constant. The grid diagrams above show the perpendicular line.
Coordinate conversion
It is likely that you will use axial or offset coordinates in your project, but many algorithms are simpler to express in cube coordinates. Therefore you need to be able to convert back and forth.
Axial coordinates
Axial coordinates are closely connected to cube coordinates. Axial discards the third coordinate and cube calculates the third coordinate from the other two:
function cube_to_axial(cube):
var q = cube.x
var r = cube.z
return Hex(q, r)
function axial_to_cube(hex):
var x = hex.q
var z = hex.r
var y = -x-z
return Cube(x, y, z)
Offset coordinates
Determine which type of offset system you use; *-r are pointy top; *-q are flat top:
- odd-r shoves odd rows by +½ column
- even-r shoves even rows by +½ column
- odd-q shoves odd columns by +½ row
- even-q shoves even columns by +½ row
function cube_to_oddr(cube):
col = cube.x + (cube.z - (cube.z&1)) / 2
row = cube.z
return Hex(col, row)
function oddr_to_cube(hex):
x = hex.col - (hex.row - (hex.row&1)) / 2
z = hex.row
y = -x-z
return Cube(x, y, z)
Implementation note: I use a&1
(bitwise and) instead of a%2
(modulo) to detect whether something
is even (0) or odd (1), because it works with negative numbers too. See a longer explanation on my implementation notes page.
Neighbors
Given a hex, which 6 hexes are neighboring it? As you might expect, the answer is simplest with cube coordinates, still pretty simple with axial coordinates, and slightly trickier with offset coordinates. We might also want to calculate the 6 “diagonal” hexes.
Offset coordinates
With offset coordinates, the change depends on where in the grid we are. If we’re on an offset column/row then the rule is different than if we’re on a non-offset column/row.
As above, we’ll build a table of the numbers we need to add to col
and row
. However this time there will be two arrays, one for odd columns/rows and one for even columns/rows. Look at (1,1)
on the grid map above
and see how col
and row
change as you move in each of the six directions. Then do this again for (2,2)
. The tables and code are different for each of the four offset grid types, so pick a grid type to see the corresponding code.
Using the above lookup tables is the easiest way to to calculate neighbors. It’s also possible to derive these numbers, for those of you who are curious.
Diagonals
Distances
Cube coordinates
Axial coordinates
In the axial system, the third coordinate is implicit. Let’s convert axial to cube to calculate distance:
function hex_distance(a, b):
var ac = axial_to_cube(a)
var bc = axial_to_cube(b)
return cube_distance(ac, bc)
If your compiler inlines axial_to_cube
and cube_distance
, it will generate this code:
function hex_distance(a, b):
return (abs(a.q - b.q)
+ abs(a.q + a.r - b.q - b.r)
+ abs(a.r - b.r)) / 2
There are lots of different ways to write hex distance in axial coordinates, but no matter which way you write it, axial hex distance is derived from the Mahattan distance on cubes. For example, the “difference of differences” described
here results from writing a.q + a.r - b.q - b.r
as a.q - b.q + a.r - b.r
, and using “max” form instead of the “divide by two” form of cube_distance
. They’re all equivalent once you see the connection
to cube coordinates.
Offset coordinates
As with axial coordinates, we’ll convert offset coordinates to cube coordinates, then use cube distance.
function offset_distance(a, b):
var ac = offset_to_cube(a)
var bc = offset_to_cube(b)
return cube_distance(ac, bc)
We’ll use the same pattern for many of the algorithms: convert hex to cube, run the cube version of the algorithm, and convert any cube results back to hex coordinates (whether axial or offset).
Line drawing
Putting these together to draw a line from A
to B
:
function lerp(a, b, t): // for floats
return a + (b - a) * t
function cube_lerp(a, b, t): // for hexes
return Cube(lerp(a.x, b.x, t),
lerp(a.y, b.y, t),
lerp(a.z, b.z, t))
function cube_linedraw(a, b):
var N = cube_distance(a, b)
var results = []
for each 0 ≤ i ≤ N:
results.append(cube_round(cube_lerp(a, b, 1.0/N * i)))
return results
More notes:
- There are times when
cube_lerp
will return a point that’s just on the edge between two hexes. Thencube_round
will push it one way or the other. The lines will look better if it’s always pushed in the same direction. You can do this by adding an “epsilon” hexCube(1e-6, 1e-6, -2e-6)
to one or both of the endpoints before starting the loop. This will “nudge” the line in one direction to avoid landing on edge boundaries. - The DDA Algorithm on square grids sets
N
to the max of the distance along each axis. We do the same in cube space, which happens to be the same as the hex grid distance. - The
cube_lerp
function needs to return a cube with float coordinates. If you’re working in a statically typed language, you won’t be able to use theCube
type but instead could defineFloatCube
, or inline the function into the line drawing code if you want to avoid defining another type. - You can optimize the code by inlining
cube_lerp
, and then calculatingB.x-A.x
,B.x-A.y
, and1.0/N
outside the loop. Multiplication can be turned into repeated addition. You’ll end up with something like the DDA algorithm. - I use axial or cube coordinates for line drawing, but if you want something for offset coordinates, take a look at this article.
- There are many variants of line drawing. Sometimes you’ll want “super cover”. Someone sent me hex super cover line drawing code but I haven’t studied it yet.
Movement Range
Coordinate range
Intersecting ranges
Obstacles
Rotation
Rings
Field of view
There are many different ways to define what’s “visible”. Do you want to be able to see the center of the other hex from the center of the starting hex? Do you want to see any part of the other hex from the center of the starting point? Maybe any part of the other hex from any part of the starting point? Are there obstacles that occupy less than a complete hex? Field of view turns out to be trickier and more varied than it might seem at first. Start with the simplest algorithm, but expect that it may not compute exactly the answer you want for your project. There are even situations where the simple algorithm produces results that are illogical.
Clark Verbrugge’s guide describes a “start at center and move outwards” algorithm to calculate field of view. Also see the Duelo project, which has an an online demo of directional field of view and code on Github. Also see my article on 2d visibility calculation for an algorithm that works on polygons, including hexagons. For grids, the roguelike community has a nice set of algorithms for square grids (see this and this and this); some of them might be adapted for hex grids.
Hex to pixel
For hex to pixel, it’s useful to review the size and spacing diagram at the top of the page.
Axial coordinates
If you have a matrix library, this is a simple matrix multiplication; however I’ll write the code out without matrices here. For the x = q, z = r
axial grid I use in this guide, the conversion is:
function hex_to_pixel(hex):
x = size * sqrt(3) * (hex.q + hex.r/2)
y = size * 3/2 * hex.r
return Point(x, y)
Axial coordinates: flat top or pointy top
The matrix approach will come in handy later when we want to convert pixel coordinates back to hex coordinates. All we will have to do is invert the matrix. For cube coordinates, you can either use cube basis vectors (x, y, z), or you can convert to axial first and then use axial basis vectors (q, r).
Offset coordinates
For offset coordinates, we need to offset either the column or row number (it will no longer be an integer).
function oddr_offset_to_pixel(hex):
x = size * sqrt(3) * (hex.col + 0.5 * (hex.row&1))
y = size * 3/2 * hex.row
return Point(x, y)
Offset coordinates: odd-r even-r odd-q even-q
Unfortunately offset coordinates don’t have basis vectors that we can use with a matrix. This is one reason pixel-to-hex conversions are harder with offset coordinates.
Another approach is to convert the offset coordinates into cube/axial coordinates, then use the cube/axial to pixel conversion. By inlining the conversion code then optimizing, it will end up being the same as above.
Pixel to Hex
One of the most common questions is, how do I take a pixel location (such as a mouse click) and convert it into a hex grid coordinate? I’ll show how to do this for axial or cube coordinates. For offset coordinates, the simplest thing to do is to convert the cube to offset at the end.
-606-615-624-633-642-651-660-5-16-505-514-523-532-541-550-56-1-4-26-4-15-404-413-422-431-440-45-1-46-2-3-36-3-25-3-14-303-312-321-330-34-1-35-2-36-3-2-46-2-35-2-24-2-13-202-211-220-23-1-24-2-25-3-26-4-1-56-1-45-1-34-1-23-1-12-101-110-12-1-13-2-14-3-15-4-16-50-660-550-440-330-220-11xyz01-102-203-304-405-506-61-651-541-431-321-211-1010-111-212-313-414-515-62-642-532-422-312-202-1-120-221-322-423-524-63-633-523-413-303-2-13-1-230-331-432-533-64-624-514-404-3-14-2-24-1-340-441-542-65-615-505-4-15-3-25-2-35-1-450-551-66-606-5-16-4-26-3-36-2-46-1-560-6- First we invert the hex to pixel conversion. This will give us a fractional hex coordinate, shown as a small blue circle in the diagram.
- Then we find the hex containing the fractional hex coordinate, shown as the highlighted hex in the diagram.
There are many other ways to convert pixel to hex; see this page for the ones I know of.
Rounding to nearest hex
Sometimes we'll end up with a floating-point cube coordinate (x, y, z)
, and we’ll want to know which hex it should be in. This comes up in line drawing and pixel to hex.
Converting a floating point value to an integer value is called rounding so I call this algorithm cube_round
.
With cube coordinates, x + y + z = 0
, even with floating point cube coordinates. So let’s round each component to the nearest integer, (rx, ry, rz)
. However, although x + y + z = 0
, after rounding we do not have a guarantee that rx + ry + rz = 0
. So we reset the component with the largest change back to what the constraint rx + ry + rz = 0
requires. For example, if the y-change abs(ry-y)
is larger than
abs(rx-x)
and abs(rz-z)
, then we reset ry = -rx-rz
. This guarantees that rx + ry + rz = 0
. Here’s the algorithm:
function cube_round(cube):
var rx = round(cube.x)
var ry = round(cube.y)
var rz = round(cube.z)
var x_diff = abs(rx - cube.x)
var y_diff = abs(ry - cube.y)
var z_diff = abs(rz - cube.z)
if x_diff > y_diff and x_diff > z_diff:
rx = -ry-rz
else if y_diff > z_diff:
ry = -rx-rz
else:
rz = -rx-ry
return Cube(rx, ry, rz)
For non-cube coordinates, the simplest thing to do is to convert to cube coordinates, use the rounding algorithm, then convert back:
function hex_round(hex):
return cube_to_axial(cube_round(axial_to_cube(hex)))
The same would work if you have oddr
, evenr
, oddq
, or evenq
instead of axial
.
Implementation note: cube_round
and hex_round
take float coordinates instead of int coordinates. If you’ve written a Cube and Hex class, they’ll work fine in dynamically languages where you can pass in floats
instead of ints, and they’ll also work fine in statically typed languages with a unified number type. However, in most statically typed languages, you’ll need a separate class/struct type for float coordinates, and cube_round
will
have type FloatCube → Cube
. If you also need hex_round
, it will be FloatHex → Hex
, using helper function floatcube_to_floathex
instead of cube_to_hex
. In languages with parameterized
types (C++, Haskell, etc.) you might define Cube<T>
where T
is either int
or float
. Alternatively, you could write cube_round
to take three floats as inputs instead of defining
a new type just for this function.
Map storage in axial coordinates
One of the common complaints about the axial coordinate system is that it leads to wasted space when using a rectangular map; that's one reason to favor an offset coordinate system. However all the hex coordinate systems lead to wasted space when using a triangular or hexagonal map. We can use the same strategies for storing all of them.
Notice in the diagram that the wasted space is on the left and right sides of each row (except for rhombus maps) This gives us three strategies for storing the map:
- Ignore the problem. Use a dense array with nulls or some other sentinel at the unused spaces. At most there’s a factor of two for these common shapes; it may not be worth using a more complicated solution.
- Use a hash table instead of dense array. This allows arbitrarily shaped maps, including ones with holes. When you want to access the hex at
q,r
you accesshash_table(hash(q,r))
instead. Encapsulate this into the getter/setter in the grid class so that the rest of the game doesn’t need to know about it. - Slide the rows to the left, and use variable sized rows. In many languages a 2D array is an array of arrays; there’s no reason the arrays have to be the same length. This removes the waste on the right. In addition, if you start
the arrays at the leftmost column instead of at 0, you remove the waste on the left.
To do this for arbitrary convex shaped maps, you’d keep an additional array of “first columns”. When you want to access the hex at
q,r
you accessarray[r][q - first_column[r]]
instead. Encapsulate this into the getter/setter in the grid class so that the rest of the game doesn’t need to know about it. In the diagramfirst_column
is shown on the left side of each row.If your maps are fixed shapes, the “first columns” can be calculated on the fly instead of being stored in an array.
- For rectangle shaped maps,
first_column[r] == -floor(r/2)
, and you’d end up accessingarray[r][q + r/2]
, which turns out to be equivalent to converting the coordinates into offset grid coordinates. - For triangle shaped maps as shown here,
first_column[r] == 0
, so you’d accessarray[r][q]
— how convenient! For triangle shaped maps that are oriented the other way (not shown in the diagram), it’sarray[r][q+r]
. - For hexagon shaped maps of radius
N
, whereN = max(abs(x), abs(y), abs(z)
, we havefirst_column[r] == -N - min(0, r)
. You’d accessarray[r][q + N + min(0, r)]
. However, since we’re starting with some values ofr < 0
, we also have to offset the row, and usearray[r + N][q + N + min(0, r)].
- For rhombus shaped maps, everything matches nicely, so you can use
array[r][q]
.
- For rectangle shaped maps,
Wraparound maps
Pathfinding
More
I have an guide to implementing your own hex grid library, including sample code in C++, Java, C#, Javascript, Haxe, and Python.
- In my Guide to Grids, I cover axial coordinate systems to address square, triangle, and hexagon edges and corners, and algorithms for the relationships among tiles, edges, and corners. I also show how square and hex grids are related.
- The best early guide I saw to the axial coordinate system was Clark Verbrugge’s guide, written in 1996.
- The first time I saw the cube coordinate system was from Charles Fu’s posting to rec.games.programmer in 1994.
- DevMag has a nice visual overview of hex math including how to represent areas such as half-planes, triangles, and quadrangles. There’s a PDF version here that goes into more detail. Highly reocmmended! The GameLogic Grids library implements these and many other grid types in Unity.
- James McNeill has a nice visual explanation of grid transformations.
- Overview of hex coordinate types: staggered (offset), interlaced, 3d (cube), and trapezoidal (axial).
- The Rot.js library has a list of hex coordinate systems: non-orthogonal (axial), odd shift (offset), double width (interlaced), cube.
- Range for cube coordinates: given a distance, which hexagons are that distance from the given one?
- Distances on hex grids using cube coordinates, and reasons to use cube coordinates instead of offset.
- This guide explains the basics of measuring and drawing hexagons, using an offset grid.
- Convert cube hex coordinates to pixel coordinates.
- This thread explains how to generate rings.
- The HexPart system uses both hexes and rectangles to make some of the algorithms easier to work with.
- Are there pros and cons of “pointy topped” and “flat topped” hexagons?
- Line of sight in a hex grid with offset coordinates, splitting hexes into triangles
- Hexnet explains how the correspondence between hexagons and cubes goes much deeper than what I described on this page, generalizing to higher dimensions.
- I used the PDF hex grids from this page while working out some of the algorithms.
- Generalized Balanced Ternary for hex coordinates seems interesting; I haven’t studied it.
- Hex-Grid Utilities is a C# library for hex grid math, with neighbors, grids, range finding, path finding, field of view. Open source, MIT license.
- The Reddit discussion and Hacker News discussion and MetaFilter discussion have more comments and links.
The code that powers this page is written in a mixture of Haxe and Javascript: Cube.hx, Hex.hx, Grid.hx, ScreenCoordinate.hx, ui.js, and cubegrid.js (for the cube/hex animation). However, if you are looking to write your own hex grid library, I suggest instead looking at my guide to implementation.
There are more things I want to do for this guide. I’m keeping a list on Trello. Do you have suggestions for things to change or add? Comment below.
转载自:https://juejin.cn/post/6844903497234645006