Skip to content

Instantly share code, notes, and snippets.

@wallstop
Created June 13, 2025 16:12
Show Gist options
  • Save wallstop/dc277fc6823e75ff4a39c647103c3702 to your computer and use it in GitHub Desktop.
Save wallstop/dc277fc6823e75ff4a39c647103c3702 to your computer and use it in GitHub Desktop.
Hex - GridOffset
namespace Core.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using DataStructure.Adapters;
using UnityEngine;
public sealed class GridOffset
{
public const int NeighborCount = 6;
public enum GridDirection
{
UpRight,
Up,
UpLeft,
DownLeft,
Down,
DownRight,
}
public static FastVector2Int GetNeighboringGridCellInDirection(FastVector2Int cell, Vector2 direction)
{
GridDirection gridDirection = direction.AsDirection() switch
{
Direction.North => GridDirection.Up,
Direction.NorthEast => GridDirection.UpRight,
Direction.NorthWest => GridDirection.UpLeft,
Direction.South => GridDirection.Down,
Direction.SouthEast => GridDirection.DownRight,
Direction.SouthWest => GridDirection.DownLeft,
Direction.East => direction.y >= 0 ? GridDirection.UpRight : GridDirection.DownRight,
Direction.West => direction.y >= 0 ? GridDirection.UpLeft : GridDirection.DownLeft,
_ => GridDirection.Up,
};
return (cell.y % 2 == 0 ? EvenYOffsetLookup[gridDirection] : OddYOffsetLookup[gridDirection]) + cell;
}
public static FastVector2Int GetNeighboringGridCellInDirection(FastVector2Int cell, Direction direction)
{
if (direction == Direction.East)
{
return (cell.y % 2 == 0
? EvenYOffsetLookup[GridDirection.UpRight]
: OddYOffsetLookup[GridDirection.DownRight]) + cell;
}
if (direction == Direction.West)
{
return (cell.y % 2 == 0
? EvenYOffsetLookup[GridDirection.UpLeft]
: OddYOffsetLookup[GridDirection.DownLeft]) + cell;
}
GridDirection gridDirection = direction switch
{
Direction.North => GridDirection.Up,
Direction.NorthEast => GridDirection.UpRight,
Direction.NorthWest => GridDirection.UpLeft,
Direction.South => GridDirection.Down,
Direction.SouthEast => GridDirection.DownRight,
Direction.SouthWest => GridDirection.DownLeft,
_ => GridDirection.Up,
};
return (cell.y % 2 == 0 ? EvenYOffsetLookup[gridDirection] : OddYOffsetLookup[gridDirection]) + cell;
}
public static Vector3Int GetNeighboringGridCellInDirection(Vector3Int cell, GridDirection direction, int range = 1)
{
FastVector2Int offset = (cell.y % 2 == 0 ? EvenYOffsetLookup[direction] : OddYOffsetLookup[direction]);
return new Vector3Int(cell.x + (offset.x * range), cell.y + (offset.y * range), cell.z);
}
public static FastVector3Int GetNeighboringGridCellInDirection(FastVector3Int cell, GridDirection direction, int range = 1)
{
FastVector2Int offset = (cell.y % 2 == 0 ? EvenYOffsetLookup[direction] : OddYOffsetLookup[direction]);
return new FastVector3Int(cell.x + (offset.x * range), cell.y + (offset.y * range), cell.z);
}
/*
Cell position mutations to use to find hex cell neighbors.
*/
public readonly FastVector2Int[] Offsets;
/*
AdditionalUnblocked offsets are hexes linked by the "hex lines"
*/
public readonly Dictionary<FastVector2Int, List<FastVector2Int>> AdditionalUnblockedOffsets;
private GridOffset(FastVector2Int[] offsets, Dictionary<FastVector2Int, List<FastVector2Int>> additionalUnblockedOffsets)
{
Offsets = offsets ?? throw new ArgumentNullException(nameof(offsets));
AdditionalUnblockedOffsets = additionalUnblockedOffsets ?? throw new ArgumentNullException(nameof(additionalUnblockedOffsets));
}
public static readonly Dictionary<GridDirection, FastVector2Int> EvenYOffsetLookup = new(NeighborCount)
{
{ GridDirection.UpRight, new FastVector2Int(0, 1) },
{ GridDirection.Up, new FastVector2Int(1, 0) },
{ GridDirection.UpLeft, new FastVector2Int(0, -1) },
{ GridDirection.DownLeft, new FastVector2Int(-1, -1) },
{ GridDirection.Down, new FastVector2Int(-1, 0) },
{ GridDirection.DownRight, new FastVector2Int(-1, 1) },
};
private static readonly Dictionary<FastVector2Int, GridDirection> InverseEvenYOffsetLookup = new(NeighborCount)
{
{ new FastVector2Int(0, 1), GridDirection.UpRight },
{ new FastVector2Int(1, 0), GridDirection.Up },
{ new FastVector2Int(0, -1), GridDirection.UpLeft },
{ new FastVector2Int(-1, -1), GridDirection.DownLeft },
{ new FastVector2Int(-1, 0), GridDirection.Down },
{ new FastVector2Int(-1, 1), GridDirection.DownRight },
};
public static readonly Dictionary<GridDirection, FastVector2Int> AdditionalEvenYOffsetLookup = new(4)
{
{ GridDirection.UpLeft, new FastVector2Int(1, -1) },
{ GridDirection.UpRight, new FastVector2Int(1, 1) },
{ GridDirection.DownRight, new FastVector2Int(-2, 1) },
{ GridDirection.DownLeft, new FastVector2Int(-2, -1) }
};
private static readonly Dictionary<FastVector2Int, GridDirection> InverseAdditionalEvenYOffsetLookup = new(4)
{
{ new FastVector2Int(1, -1), GridDirection.UpLeft },
{ new FastVector2Int(1, 1), GridDirection.UpRight },
{ new FastVector2Int(-2, 1), GridDirection.DownRight },
{ new FastVector2Int(-2, -1), GridDirection.DownLeft },
};
public static readonly Dictionary<GridDirection, FastVector2Int> OddYOffsetLookup = new(NeighborCount)
{
{ GridDirection.DownRight, new FastVector2Int(0, 1) },
{ GridDirection.UpRight, new FastVector2Int(1, 1) },
{ GridDirection.Up, new FastVector2Int(1, 0) },
{ GridDirection.UpLeft, new FastVector2Int(1, -1) },
{ GridDirection.DownLeft, new FastVector2Int(0, -1) },
{ GridDirection.Down, new FastVector2Int(-1, 0) },
};
private static readonly Dictionary<FastVector2Int, GridDirection> InverseOddYOffsetLookup = new(NeighborCount)
{
{ new FastVector2Int(0, 1), GridDirection.DownRight },
{ new FastVector2Int(1, 1), GridDirection.UpRight },
{ new FastVector2Int(1, 0), GridDirection.Up },
{ new FastVector2Int(1, -1), GridDirection.UpLeft },
{ new FastVector2Int(0, -1) , GridDirection.DownLeft },
{ new FastVector2Int(-1, 0), GridDirection.Down },
};
public static readonly Dictionary<GridDirection, FastVector2Int> AdditionalYOddOffsetLookup = new(4)
{
{ GridDirection.UpLeft, new FastVector2Int(2, -1) },
{ GridDirection.UpRight, new FastVector2Int(2, 1)} ,
{ GridDirection.DownRight, new FastVector2Int(-1, 1) },
{ GridDirection.DownLeft, new FastVector2Int(-1, -1) }
};
private static readonly Dictionary<FastVector2Int, GridDirection> InverseAdditionalYOddOffsetLookup = new(4)
{
{ new FastVector2Int(2, -1), GridDirection.UpLeft },
{ new FastVector2Int(2, 1), GridDirection.UpRight },
{ new FastVector2Int(-1, 1), GridDirection.DownRight },
{ new FastVector2Int(-1, -1), GridDirection.DownLeft }
};
public static FastVector2Int EvenToOdd(FastVector2Int offset)
{
return TransformOffset(offset, InverseEvenYOffsetLookup, InverseAdditionalEvenYOffsetLookup, OddYOffsetLookup, AdditionalYOddOffsetLookup);
}
public static FastVector2Int OddToEven(FastVector2Int offset)
{
return TransformOffset(offset, InverseOddYOffsetLookup, InverseAdditionalYOddOffsetLookup, EvenYOffsetLookup, AdditionalEvenYOffsetLookup);
}
private static Vector2Int TransformOffset(FastVector2Int offset, Dictionary<FastVector2Int, GridDirection> fromLookup, Dictionary<FastVector2Int, GridDirection> additionalFromLookup, Dictionary<GridDirection, FastVector2Int> toLookup, Dictionary<GridDirection, FastVector2Int> additionalToLookup)
{
FastVector2Int modOffset = new(offset.x % 2, offset.y % 2);
if (fromLookup.TryGetValue(modOffset, out GridDirection direction))
{
FastVector2Int newOffset = toLookup[direction];
return offset + newOffset - modOffset;
}
if (additionalFromLookup.TryGetValue(modOffset, out direction))
{
FastVector2Int newOffset = additionalToLookup[direction];
return offset + newOffset - modOffset;
}
return offset;
}
/*
Use these offsets to find neighbors for hexes with an even y (position.y % 2 == 0)
to find neighboring tiles.
*/
public static readonly GridOffset EvenYOffset = new GridOffset(new[]
{
new FastVector2Int(0, 1), // UpRight
new FastVector2Int(1, 0), // Up
new FastVector2Int(0, -1), // UpLeft
new FastVector2Int(-1, -1), // DownLeft
new FastVector2Int(-1, 0), // Down
new FastVector2Int(-1, 1) // DownRight
}, new Dictionary<FastVector2Int, List<FastVector2Int>>(NeighborCount)
{
[new FastVector2Int(0, -2)] = new List<FastVector2Int>(2) { new FastVector2Int(0, -1), new FastVector2Int(-1, -1) }, // Left
[new FastVector2Int(1, -1)] = new List<FastVector2Int>(2) { new FastVector2Int(0, -1), new FastVector2Int(1, 0) }, // UpLeft
[new FastVector2Int(1, 1)] = new List<FastVector2Int>(2) { new FastVector2Int(1, 0), new FastVector2Int(0, 1) }, // UpRight
[new FastVector2Int(0, 2)] = new List<FastVector2Int>(2) { new FastVector2Int(0, 1), new FastVector2Int(-1, 1) }, // Right
[new FastVector2Int(-2, 1)] = new List<FastVector2Int>(2) { new FastVector2Int(-1, 1), new FastVector2Int(-1, 0) }, // DownRight
[new FastVector2Int(-2, -1)] = new List<FastVector2Int>(2) { new FastVector2Int(-1, 0), new FastVector2Int(-1, -1) }// DownLeft
});
public static readonly GridOffset OddYOffset = new GridOffset(new[]
{
new FastVector2Int(0, 1), // DownRight
new FastVector2Int(1, 1), // UpRight
new FastVector2Int(1, 0), // Up
new FastVector2Int(1, -1), // UpLeft
new FastVector2Int(0, -1), // DownLeft
new FastVector2Int(-1, 0), // Down
}, new Dictionary<FastVector2Int, List<FastVector2Int>>(NeighborCount)
{
[new FastVector2Int(2, 1)] = new List<FastVector2Int>(2) { new FastVector2Int(1, 1), new FastVector2Int(1, 0) }, // UpRight
[new FastVector2Int(0, 2)] = new List<FastVector2Int>(2) { new FastVector2Int(1, 1), new FastVector2Int(0, 1) }, // Right
[new FastVector2Int(-1, 1)] = new List<FastVector2Int>(2) { new FastVector2Int(0, 1), new FastVector2Int(-1, 0) }, // DownRight
[new FastVector2Int(-1, -1)] = new List<FastVector2Int>(2) { new FastVector2Int(0, -1), new FastVector2Int(-1, 0) }, // DownLeft
[new FastVector2Int(0, -2)] = new List<FastVector2Int>(2) { new FastVector2Int(0, -1), new FastVector2Int(1, -1) }, // Left
[new FastVector2Int(2, -1)] = new List<FastVector2Int>(2) { new FastVector2Int(1, -1), new FastVector2Int(1, 0) }, // UpLeft
});
}
public static class GridOffsetExtensions
{
public static GridOffset.GridDirection AsGridDirection(this Vector2 vector)
{
if (vector is { x: 0, y: 0 })
{
return GridOffset.GridDirection.Up;
}
float angle;
if (vector.x < 0)
{
angle = 360 - Mathf.Atan2(vector.x, vector.y) * Mathf.Rad2Deg * -1;
}
else
{
angle = Mathf.Atan2(vector.x, vector.y) * Mathf.Rad2Deg;
}
if (315 <= angle || angle < 45)
{
return GridOffset.GridDirection.Up;
}
if (45 <= angle && angle < 90)
{
return GridOffset.GridDirection.UpRight;
}
if (90 <= angle && angle < 135)
{
return GridOffset.GridDirection.DownRight;
}
if (135 <= angle && angle < 225)
{
return GridOffset.GridDirection.Down;
}
if (225 <= angle && angle < 270)
{
return GridOffset.GridDirection.DownLeft;
}
if (270 <= angle && angle < 315)
{
return GridOffset.GridDirection.UpLeft;
}
throw new Exception($"Invalid angle {angle:0.00}");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment