本文主要参考这篇文章hexagons,作者对六边形坐标系统进行了深入的研究和总结,相当具有研究价值。
重要
本文研究的六边形默认是正六边形
一、六边形几何概念
1.六边形单元的几何描述
六边形按照放置方式,可以分为Flat-top和Point-top。

六边形的大小可以使用内接圆和外接圆的半径描述,这里以外接圆的半径代表六边形的大小。
通过可以进一步表示六边形的水平尺寸和竖直尺寸。
2.六边形单元间的间距

对于Flat-top形式:

对于Point-top形式:
二、六边形坐标系统
对于方形网格,建立坐标系相当容易。但对于六边形网格有多种建立坐标系的方法,而且每种坐标系都有自己独特的优缺点。
1.偏置坐标系(Offset coordinates)
偏置坐标系会尝试像方形网格坐标系那样,给予坐标系中每个单元唯一的坐标。但是需要将奇数或偶数行/列的单元,在世界空间上向坐标轴正向偏移。

对于Flat-top形式,只能偏移列col(q),分别记为偏移奇数列odd-q和偶数列even-q

对于Point-top形式,只能偏移行row(r),分别记为偏移奇数行odd-r和偶数行even-r
2.立方坐标系(Cube coordinates)

假设空间中铺满了正立方体,使用的平面将整个空间对半切开,就能看到正六边形的切面。
 每个六边形都对应一个正方体,因此我们可以使用三个轴来表示每个六边形的唯一位置。且。

3.轴向坐标(Axial coordinates)
在轴向坐标系中,我们默认,那么在存储上就没有必要存储三个值。因此轴向坐标使用两个轴表示六边形的坐标。
三、坐标系变换
提示
原文论证了在数学运算上,立方坐标系/轴向坐标系具有数学上的一致性,更适合作为顶层数学运算。
 而偏置坐标系更符合我们大脑的认知,适合作为上层表示。
 因此下文重点介绍轴向坐标系和偏置坐标系
目前已知有偏置坐标系四种形式,他们与其他坐标系的转换也是不一致的。
1.偏置坐标系与轴向坐标系
a. 偏置坐标系=>轴向坐标系
function axial_to_oddr(hex):
    var parity = hex.r&1
    var col = hex.q + (hex.r - parity) / 2
    var row = hex.r
    return OffsetCoord(col, row)
function oddr_to_axial(hex):
    var parity = hex.row&1
    var q = hex.col - (hex.row - parity) / 2
    var r = hex.row
    return Hex(q, r)function axial_to_evenr(hex):
    var parity = hex.r&1
    var col = hex.q + (hex.r + parity) / 2
    var row = hex.r
    return OffsetCoord(col, row)
function evenr_to_axial(hex):
    var parity = hex.row&1
    var q = hex.col - (hex.row + parity) / 2
    var r = hex.row
    return Hex(q, r)function axial_to_oddq(hex):
    var parity = hex.q&1
    var col = hex.q
    var row = hex.r + (hex.q - parity) / 2
    return OffsetCoord(col, row)
function oddq_to_axial(hex):
    var parity = hex.col&1
    var q = hex.col
    var r = hex.row - (hex.col - parity) / 2
    return Hex(q, r)function axial_to_evenq(hex):
    var parity = hex.q&1
    var col = hex.q
    var row = hex.r + (hex.q + parity) / 2
    return OffsetCoord(col, row)
function evenq_to_axial(hex):
    var parity = hex.col&1
    var q = hex.col
    var r = hex.row - (hex.col + parity) / 2
    return Hex(q, r)2.轴向坐标系和世界坐标系
a. 轴向坐标系=>世界坐标系
function flat_hex_to_pixel(hex):
    // hex to cartesian
    var x = (     3./2 * hex.q                    )
    var y = (sqrt(3)/2 * hex.q  +  sqrt(3) * hex.r)
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)function pointy_hex_to_pixel(hex):
    // hex to cartesian
    var x = (sqrt(3) * hex.q  +  sqrt(3)/2 * hex.r)
    var y = (                         3./2 * hex.r)
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)b. 世界坐标系=>轴向坐标系
function pixel_to_flat_hex(point):
    // invert the scaling
    var x = point.x / size
    var y = point.y / size
    // cartesian to hex
    var q = ( 2./3 * x                  )
    var r = (-1./3 * x  +  sqrt(3)/3 * y)
    return axial_round(Hex(q, r))function pixel_to_pointy_hex(point):
    // invert the scaling
    var x = point.x / size
    var y = point.y / size
    // cartesian to hex
    var q = (sqrt(3)/3 * x  -  1./3 * y)
    var r = (                  2./3 * y)
    return axial_round(Hex(q, r))3.偏置坐标系和世界坐标系
a. 偏置坐标系=>世界坐标系
function oddr_offset_to_pixel(hex):
    // hex to cartesian
    var x = sqrt(3) * (hex.col + 0.5 * (hex.row&1))
    var y =    3./2 * hex.row
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)function evenr_offset_to_pixel(hex):
    // hex to cartesian
    var x = sqrt(3) * (hex.col - 0.5 * (hex.row&1))
    var y =    3./2 * hex.row
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)function oddq_offset_to_pixel(hex):
    // hex to cartesian
    var x =    3./2 * hex.col
    var y = sqrt(3) * (hex.row + 0.5 * (hex.col&1))
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)function evenq_offset_to_pixel(hex):
    // hex to cartesian
    var x =    3./2 * hex.col
    var y = sqrt(3) * (hex.row - 0.5 * (hex.col&1))
    // scale cartesian coordinates
    x = x * size
    y = y * size
    return Point(x, y)四、几何运算
1.获取邻居
提示
在这里你会发现轴向坐标系在数学上是多么的优雅
a.轴向坐标系
var axial_direction_vectors = [
    Hex(+1, 0), Hex(+1, -1), Hex(0, -1), 
    Hex(-1, 0), Hex(-1, +1), Hex(0, +1), 
]
function axial_direction(direction):
    return axial_direction_vectors[direction]
function axial_add(hex, vec):
    return Hex(hex.q + vec.q, hex.r + vec.r)
function axial_neighbor(hex, direction):
    return axial_add(hex, axial_direction(direction))b.偏置坐标系
var oddr_direction_differences = [
    // even rows 
    [[+1,  0], [ 0, -1], [-1, -1], 
     [-1,  0], [-1, +1], [ 0, +1]],
    // odd rows 
    [[+1,  0], [+1, -1], [ 0, -1], 
     [-1,  0], [ 0, +1], [+1, +1]],
]
function oddr_offset_neighbor(hex, direction):
    var parity = hex.row & 1
    var diff = oddr_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])var evenr_direction_differences = [
    // even rows 
    [[+1,  0], [+1, -1], [ 0, -1], 
     [-1,  0], [ 0, +1], [+1, +1]],
    // odd rows 
    [[+1,  0], [ 0, -1], [-1, -1], 
     [-1,  0], [-1, +1], [ 0, +1]],
]
function evenr_offset_neighbor(hex, direction):
    var parity = hex.row & 1
    var diff = evenr_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])var oddq_direction_differences = [
    // even cols 
    [[+1,  0], [+1, -1], [ 0, -1], 
     [-1, -1], [-1,  0], [ 0, +1]],
    // odd cols 
    [[+1, +1], [+1,  0], [ 0, -1], 
     [-1,  0], [-1, +1], [ 0, +1]],
]
function oddq_offset_neighbor(hex, direction):
    var parity = hex.col & 1
    var diff = oddq_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])var evenq_direction_differences = [
    // even cols 
    [[+1, +1], [+1,  0], [ 0, -1], 
     [-1,  0], [-1, +1], [ 0, +1]],
    // odd cols 
    [[+1,  0], [+1, -1], [ 0, -1], 
     [-1, -1], [-1,  0], [ 0, +1]],
]
function evenq_offset_neighbor(hex, direction):
    var parity = hex.col & 1
    var diff = evenq_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])目前只使用了这么多,其余待补充
五、Unity版本的实现
- struct Hex:底层用于数学运算的轴向坐标系坐标 
- struct OffsetCoordData:上层用于呈现的偏置坐标系 
- interface IOffsetCoordType:四种偏置坐标系与轴向坐标系、世界坐标系之间的转换器 
- struct OffsetCoord<T>:持有一个静态的坐标转换器,真正的六边形单元坐标系,拥有一个Hex和一个OffsetCoordData对象 
using System;
using UnityEngine;
namespace CatchIt2.Math
{
    public struct Hex:IEquatable<Hex>
    {
        public int q;
        public int r;
        public Hex(int q = 0, int r = 0)
        {
            this.q = q;
            this.r = r;
        }
        public bool Equals(Hex other)
        {
            return q == other.q && r == other.r;
        }
        public override bool Equals(object obj) => obj is Hex other && Equals(other);
        public override int GetHashCode() => HashCode.Combine(q, r);
        public override string ToString() => $"Hex({q},{r})";
        public static Hex operator +(Hex a, Hex b) => new Hex(a.q + b.q, a.r + b.r);
        public static Hex operator -(Hex a, Hex b) => new Hex(a.q - b.q, a.r - b.r);
        public static readonly Hex[] Directions = new Hex[6]
        {
            new Hex(+1, 0), new Hex(+1, -1), new Hex(0, -1),
            new Hex(-1, 0), new Hex(-1, +1), new Hex(0, +1),
        };
        public Hex GetNeighbor(int direction)
        {
            if (direction < 0 || direction >= Directions.Length)
                throw new ArgumentOutOfRangeException(nameof(direction));
            return this + Directions[direction];
        }
    }
    // 用 struct 以减少 GC 压力
    public struct OffsetCoordData
    {
        public int col, row;
        public OffsetCoordData(int col = 0, int row = 0)
        {
            this.col = col;
            this.row = row;
        }
    }
    public interface IOffsetCoordType
    {
        Vector2 HexToPoint(Hex hex, float size = 1);
        Hex ToHex(OffsetCoordData data);
        OffsetCoordData FromHex(Hex hex);
    }
    public struct OddR : IOffsetCoordType
    {
        public Vector2 HexToPoint(Hex hex, float size = 1)
        {
            // hex to cartesian
            return new Vector2(
                       Mathf.Sqrt(3) * hex.q + Mathf.Sqrt(3) / 2 * hex.r,
                       3.0f / 2 * hex.r)
                   * size;
        }
        public Hex ToHex(OffsetCoordData data)
        {
            var parity = data.row & 1;
            var q = data.col - (data.row - parity) / 2;
            var r = data.row;
            return new Hex(q, r);
        }
        public OffsetCoordData FromHex(Hex hex)
        {
            var parity = hex.r & 1;
            int col = hex.q + (hex.r - parity) / 2;
            int row = hex.r;
            return new OffsetCoordData(col,row);
        }
    }
    public struct EvenR : IOffsetCoordType
    {
        public Vector2 HexToPoint(Hex hex, float size = 1)
        {
            // hex to cartesian
            return new Vector2(
                       Mathf.Sqrt(3) * hex.q + Mathf.Sqrt(3) / 2 * hex.r,
                       3.0f / 2 * hex.r)
                   * size;
        }
        public Hex ToHex(OffsetCoordData data)
        {
            var parity = data.row & 1;
            var q = data.col - (data.row + parity) / 2;
            var r = data.row;
            return new Hex(q, r);
        }
        public OffsetCoordData FromHex(Hex hex)
        {
            var parity = hex.r & 1;
            int col = hex.q + (hex.r + parity) / 2;
            int row = hex.r;
            return new OffsetCoordData(col,row);
        }
    }
    public struct OddQ : IOffsetCoordType
    {
        public Vector2 HexToPoint(Hex hex, float size = 1)
        {
            // hex to cartesian
            return new Vector2(
                       3.0f / 2 * hex.q,
                       Mathf.Sqrt(3) / 2 * hex.q + Mathf.Sqrt(3) * hex.r)
                   * size;
        }
        public Hex ToHex(OffsetCoordData data)
        {
            var parity = data.col & 1;
            var q = data.col;
            var r = data.row - (data.col - parity) / 2;
            return new Hex(q, r);
        }
        public OffsetCoordData FromHex(Hex hex)
        {
            var parity = hex.q & 1;
            int col = hex.q;
            int row = hex.r + (hex.q - parity) / 2;
            return new OffsetCoordData(col,row);
        }
    }
    public struct EvenQ : IOffsetCoordType
    {
        public Vector2 HexToPoint(Hex hex, float size = 1)
        {
            // hex to cartesian
            return new Vector2(
                       3.0f / 2 * hex.q,
                       Mathf.Sqrt(3) / 2 * hex.q + Mathf.Sqrt(3) * hex.r)
                   * size;
        }
        public Hex ToHex(OffsetCoordData data)
        {
            var parity = data.col & 1;
            var q = data.col;
            var r = data.row - (data.col + parity) / 2;
            return new Hex(q, r);
        }
        public OffsetCoordData FromHex(Hex hex)
        {
            var parity = hex.q & 1;
            int col = hex.q;
            int row = hex.r + (hex.q + parity) / 2;
            return new OffsetCoordData(col,row);
        }
    }
    public struct OffsetCoord<T>:IEquatable<OffsetCoord<T>> where T:IOffsetCoordType,new()
    {
        private static T coord2Type = new T();
        private Hex hexData;
        private OffsetCoordData data;
        public int Col
        {
            get => data.col;
        }
        public int Row
        {
            get => data.row;
        }
        public void SetCoord(int col, int row)
        {
            data.col = col;
            data.row = row;
            hexData = coord2Type.ToHex(data);
        }
        public OffsetCoord(int col = 0, int row = 0)
        {
            data = new OffsetCoordData(col, row);
            hexData = new Hex();
            SetCoord(col, row);
        }
        public OffsetCoord(Hex hex)
        {
            hexData = hex;
            data = coord2Type.FromHex(hex);
            SetCoord(data.col, data.row);
        }
        public OffsetCoord(OffsetCoordData data)
        {
            this.data = data;
            hexData = new Hex();
            SetCoord(data.col, data.row);
        }
        public OffsetCoord(OffsetCoord<T> other)
        {
            data = other.data;
            hexData = other.hexData;
            //SetCoord(other.Col, other.Row);
        }
        public Vector2 ToPoint(float size = 1.0f) => coord2Type.HexToPoint(hexData,size);
        public Vector3 ToPointXZ(float size = 1.0f)
        {
            Vector2 vec2 = ToPoint(size);
            return new Vector3(vec2.x, 0, vec2.y);
        }
        public OffsetCoord<T> GetNeighbor(int direction)
        {
            Hex neighbor = hexData.GetNeighbor(direction);
            return new OffsetCoord<T>(coord2Type.FromHex(neighbor));
        }
        public bool Equals(OffsetCoord<T> other)
        {
            return other.Col == Col && other.Row == Row;
        }
        public override string ToString() => $"OffsetCoord<{typeof(T).Name}>({Col},{Row})";
    }
}