Skip to content

Instantly share code, notes, and snippets.

@GeorgiyRyaposov
Last active March 11, 2025 10:59
Show Gist options
  • Save GeorgiyRyaposov/953824e30cfe3f5ee611e281510eae08 to your computer and use it in GitHub Desktop.
Save GeorgiyRyaposov/953824e30cfe3f5ee611e281510eae08 to your computer and use it in GitHub Desktop.
Unity hexagon grid helpers
namespace Code.Scripts.Data.Hexes
{
public enum HexDirection
{
NE,
E,
SE,
SW,
W,
NW
}
/// <summary>
/// Extension methods for <see cref="HexDirection"/>.
/// </summary>
public static class HexDirectionExtensions
{
/// <summary>
/// Get the opposite of a hex direction.
/// </summary>
/// <param name="direction">A given direction.</param>
/// <returns>The opposite direction.</returns>
public static HexDirection Opposite(this HexDirection direction) =>
(int)direction < 3 ? (direction + 3) : (direction - 3);
/// <summary>
/// Get the previous direction, rotating counter-clockwise.
/// </summary>
/// <param name="direction">A given direction.</param>
/// <returns>A direction rotated one step counter-clockwise.</returns>
public static HexDirection Previous(this HexDirection direction) =>
direction == HexDirection.NE ? HexDirection.NW : (direction - 1);
/// <summary>
/// Get the next direction, rotating clockwise.
/// </summary>
/// <param name="direction">A given direction.</param>
/// <returns>A direction rotated one step clockwise.</returns>
public static HexDirection Next(this HexDirection direction) =>
direction == HexDirection.NW ? HexDirection.NE : (direction + 1);
/// <summary>
/// Get the same result as invoking <see cref="Previous"/> twice.
/// </summary>
/// <param name="direction">A given direction.</param>
/// <returns>A direction rotated two steps counter-clockwise.</returns>
public static HexDirection Previous2(this HexDirection direction)
{
direction -= 2;
return direction >= HexDirection.NE ? direction : (direction + 6);
}
/// <summary>
/// Get the same result as invoking <see cref="Next"/> twice.
/// </summary>
/// <param name="direction">A given direction.</param>
/// <returns>A direction rotated two steps clockwise.</returns>
public static HexDirection Next2(this HexDirection direction)
{
direction += 2;
return direction <= HexDirection.NW ? direction : (direction - 6);
}
public static float RotationAngle(this HexDirection direction, bool flatOrientation)
{
var angle = (int)direction * 60f;
if (!flatOrientation)
{
angle += 30f;
}
return angle;
}
}
}
using System.Collections.Generic;
using UnityEngine;
namespace Code.Scripts.Data.Hexes
{
public class HexGridMetrics
{
private readonly Orientation _orientation;
private readonly bool _flatOrientation;
public HexGridMetrics(float size, bool flatOrientation)
{
_flatOrientation = flatOrientation;
_orientation = HexOrientationFabric.CreateOrientation(size, flatOrientation);
}
public float GetDistanceBetweenTwoCubes(Vector3Int a, Vector3Int b)
{
return Mathf
.Max(Mathf.Abs(a.x - b.x),
Mathf.Abs(a.y - b.y),
Mathf.Abs(a.z - b.z));
}
public float GetRotationAngle(HexDirection direction)
{
return direction.RotationAngle(_flatOrientation);
}
public Vector3Int GetNeighbor(Vector3Int from, HexDirection direction, int size)
{
return GetNeighbor(from, (int)direction, size);
}
/// <summary>
/// Calculates adjacent cube coordinate for a given direction and distance
/// </summary>
/// <param name="cube">Vector3 origin</param>
/// <param name="direction">direction of neighbor (0-5 is valid)</param>
/// <param name="distance">distance from origin (>=1 is valid)</param>
/// <returns>Vector3 cube coordinate</returns>
public Vector3Int GetNeighbor(Vector3Int cube, int direction, int distance)
{
return cube + _orientation.Directions[direction] * distance;
}
/// <summary>
/// Calculates diagonally adjacent cube coordinate for a given direction and distance
/// </summary>
/// <param name="origin">Vector3 origin</param>
/// <param name="direction">direction of neighbor (0-5 is valid)</param>
/// <param name="distance">distance from origin (>=1 is valid)</param>
/// <returns>Vector3 cube coordinate</returns>
public Vector3Int GetDiagonalNeighbor(Vector3Int origin, int direction, int distance)
{
return origin + _orientation.Diagonals[direction] * distance;
}
/// <summary>
/// Calculates adjacent cube coordinates over a number of steps
/// </summary>
/// <param name="origin">Vector3 origin</param>
/// <param name="steps">steps from origin (>=1 is valid)</param>
/// <returns>List of Vector3 cube coordinates</returns>
public List<Vector3Int> GetNeighbors(Vector3Int origin, int steps)
{
var results = new List<Vector3Int>();
for (var x = (origin.x - steps); x <= (origin.x + steps); x++)
for (var y = (origin.y - steps); y <= (origin.y + steps); y++)
for (var z = (origin.z - steps); z <= (origin.z + steps); z++)
if ((x + y + z) == 0) results.Add(new Vector3Int(x, y, z));
return results;
}
public Vector3 ConvertCubeToWorldPosition(Vector3Int cube)
{
return _orientation.ConvertCubeToWorldPosition(cube);
}
public Vector3Int ConvertWorldPositionToCube(Vector3 position)
{
return ConvertAxialToCube(ConvertWorldPositionToAxial(position));
}
private Vector3Int ConvertAxialToCube(Vector2Int axial)
{
return new Vector3Int(axial.x, (-axial.x - axial.y), axial.y);
}
private Vector3 ConvertAxialToCube(Vector2 axial)
{
return new Vector3(axial.x, (-axial.x - axial.y), axial.y);
}
private static Vector2Int ConvertCubeToAxial(Vector3Int cube)
{
return new Vector2Int(cube.x, cube.z);
}
private Vector2Int ConvertWorldPositionToAxial(Vector3 position)
{
var axial = _orientation.ConvertWorldPositionToAxial(position);
return RoundAxial(axial);
}
/// <summary>
/// Rounds a calculated (may not be valid) axial coordinate to the nearest valid Axial coordinate
/// </summary>
/// <param name="axial">Vector2 axial coordinate</param>
/// <returns>Vector2 axial coordinate</returns>
private Vector2Int RoundAxial(Vector2 axial)
{
var cube = RoundCube(ConvertAxialToCube(axial));
return ConvertCubeToAxial(cube);
}
/// <summary>
/// Rounds a provided Vector3 to the nearest valid cube coordinate
/// </summary>
/// <param name="cube">Vector3 input cube coordinate (does not have to be valid)</param>
/// <returns>Vector3 cube coordinate</returns>
private static Vector3Int RoundCube(Vector3 cube)
{
var rx = Mathf.RoundToInt(cube.x);
var ry = Mathf.RoundToInt(cube.y);
var rz = Mathf.RoundToInt(cube.z);
var xDiff = Mathf.Abs(rx - cube.x);
var yDiff = Mathf.Abs(ry - cube.y);
var zDiff = Mathf.Abs(rz - cube.z);
if (xDiff > yDiff && xDiff > zDiff)
rx = -ry - rz;
else if (yDiff > zDiff)
ry = -rx - rz;
else
rz = -rx - ry;
return new Vector3Int(rx, ry, rz);
}
#region Debug
public void DrawHexGrid(int mapWidth, int mapHeight)
{
foreach (var cube in GetNeighbors(Vector3Int.zero, mapWidth * mapHeight))
{
var corners = HexCorners(cube);
DrawHex(corners);
}
}
public void DrawCell(Vector3 worldPosition, Color color)
{
var corners = HexCorners(worldPosition);
DrawHex(corners, color);
}
public void DrawCell(Vector3Int cube, Color color)
{
var corners = HexCorners(cube);
DrawHex(corners, color);
}
private Vector3[] HexCorners(Vector3 position)
{
var cube = ConvertWorldPositionToCube(position);
return HexCorners(cube);
}
private Vector3[] HexCorners(Vector3Int cube)
{
return _orientation.HexCorners(cube);
}
private void DrawHex(Vector3[] corners)
{
DrawHex(corners, Color.white);
}
private void DrawHex(Vector3[] corners, Color color)
{
if (corners == null || corners.Length == 0)
{
return;
}
var startCorner = corners[0];
for (int i = 1; i < corners.Length; i++)
{
var corner = corners[i];
var lineStart = startCorner;
var lineEnd = corner;
Debug.DrawLine(lineStart, lineEnd, color);
startCorner = corner;
}
Debug.DrawLine(startCorner, corners[0]);
}
#endregion //debug
}
}
using UnityEngine;
namespace Code.Scripts.Data.Hexes
{
public static class HexOrientationFabric
{
public static Orientation CreateOrientation(float size, bool flatOrientation = false)
{
return flatOrientation
? CreateFlatOrientation(size)
: CreatePointyOrientation(size);
}
private static Orientation CreateFlatOrientation(float size)
{
return new FlatOrientation(size);
}
private static Orientation CreatePointyOrientation(float size)
{
return new PointyOrientation(size);
}
}
public abstract class Orientation
{
protected float Size;
public float SpacingX;
public float SpacingZ;
protected const float Sqrt = 1.73205080757f;// Mathf.Sqrt(3) comes from sin(60°).
protected const float OuterRadiusNormalized = 1f;
protected const float RelationOuterToInnerNormalized = Sqrt * 0.5f * OuterRadiusNormalized;
protected const float InnerRadiusNormalized = OuterRadiusNormalized * RelationOuterToInnerNormalized;
public abstract Vector3Int[] Directions { get; }
public abstract Vector3Int[] Diagonals { get; }
public abstract Vector3[] Corners { get; }
public abstract Vector2 ConvertWorldPositionToAxial(Vector3 position);
public abstract Vector3 ConvertCubeToWorldPosition(Vector3Int cube);
public Vector3[] HexCorners(Vector3Int cube)
{
var center = ConvertCubeToWorldPosition(cube);
var corners = new Vector3[Corners.Length];
for (int i = 0; i < Corners.Length; i++)
{
corners[i] = center + Corners[i] * Size;
}
return corners;
}
}
public class FlatOrientation : Orientation
{
public override Vector3Int[] Directions { get; } =
{
new(1, -1, 0),
new(1, 0, -1),
new(0, 1, -1),
new(-1, 1, 0),
new(-1, 0, 1),
new(0, -1, 1)
};
public override Vector3Int[] Diagonals { get; } =
{
new(1, -2, 1),
new(2, -1, -1),
new(1, 1, -2),
new(-1, 2, -1),
new(-2, 1, 1),
new(-1, -1, 2),
};
public override Vector3[] Corners { get; } = {
new(0.5f * OuterRadiusNormalized, 0f, InnerRadiusNormalized),
new(OuterRadiusNormalized, 0f, 0),
new(0.5f * OuterRadiusNormalized, 0f, -InnerRadiusNormalized),
new(-0.5f * OuterRadiusNormalized, 0f, -InnerRadiusNormalized),
new(-OuterRadiusNormalized, 0f, 0),
new(-0.5f * OuterRadiusNormalized, 0f, InnerRadiusNormalized),
new(0.5f * OuterRadiusNormalized, 0f, InnerRadiusNormalized),
};
public FlatOrientation(float size)
{
Size = size;
SpacingX = size * 1.5f; // 3/2 * size
SpacingZ = ((Mathf.Sqrt(3) / 2.0f) * (size * 2) / 2); //Mathf.Sqrt(3) * size
}
public override Vector2 ConvertWorldPositionToAxial(Vector3 position)
{
var q = (position.x * (2.0f / 3.0f)) / Size;
var r = ((-position.x / 3.0f) + ((Mathf.Sqrt(3) / 3.0f) * position.z)) / Size;
return new Vector2(q, r);
}
public override Vector3 ConvertCubeToWorldPosition(Vector3Int cube)
{
return new Vector3(cube.x * SpacingX,
0f,
cube.x * SpacingZ + cube.z * SpacingZ * 2f);
}
}
public class PointyOrientation : Orientation
{
public override Vector3Int[] Directions { get; } =
{
new(0, -1, 1),
new(1, -1, 0),
new(1, 0, -1),
new(0, 1, -1),
new(-1, 1, 0),
new(-1, 0, 1),
};
public override Vector3Int[] Diagonals { get; } =
{
new(1, -2, 1),
new(2, -1, -1),
new(1, 1, -2),
new(-1, 2, -1),
new(-2, 1, 1),
new(-1, -1, 2),
};
public override Vector3[] Corners { get; } = {
new(0f, 0f, OuterRadiusNormalized),
new(InnerRadiusNormalized, 0f, 0.5f * OuterRadiusNormalized),
new(InnerRadiusNormalized, 0f, -0.5f * OuterRadiusNormalized),
new(0f, 0f, -OuterRadiusNormalized),
new(-InnerRadiusNormalized, 0f, -0.5f * OuterRadiusNormalized),
new(-InnerRadiusNormalized, 0f, 0.5f * OuterRadiusNormalized),
new(0f, 0f, OuterRadiusNormalized)
};
public PointyOrientation(float size)
{
Size = size;
SpacingX = ((Mathf.Sqrt(3) / 2.0f) * (size * 2) / 2);
SpacingZ = size * 1.5f;
}
public override Vector2 ConvertWorldPositionToAxial(Vector3 position)
{
var q = ((-position.z / 3.0f) + ((Mathf.Sqrt(3) / 3.0f) * position.x)) / Size;
var r = (position.z * (2.0f / 3.0f)) / Size;
return new Vector2(q, r);
}
public override Vector3 ConvertCubeToWorldPosition(Vector3Int cube)
{
return new Vector3(cube.z * SpacingX + cube.x * SpacingX * 2f,
0f,
cube.z * SpacingZ);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment