Skip to content

Instantly share code, notes, and snippets.

@onewinter
Forked from inca/Hex.cs
Last active May 31, 2023 09:17
Show Gist options
  • Save onewinter/023c78c2c3d3d98c92a49725e53ef2a8 to your computer and use it in GitHub Desktop.
Save onewinter/023c78c2c3d3d98c92a49725e53ef2a8 to your computer and use it in GitHub Desktop.
[Unity] Simple Hex Grid System

Simple Hex Grid System (forked)

The Hex.cs above has been filled in with more of the example code from https://www.redblobgames.com/grids/hexagons/; in addition, the code now works better with floating point hexagons. Much thanks to @inca whose original gist helped me finally understand & use hex grids!

Unity doesn't include C#'s PriorityQueue class yet; you can use this direct port from the official C# lib as a drop-in replacement: https://github.com/FyiurAmron/PriorityQueue/blob/main/PriorityQueue.cs

Original Readme

These two classes are the essential building blocks for building all sorts of hex-based grids, including but not limited to puzzles, tower defense, strategy and tactics games.

Important! Even though just copy/pasting the code will give you what you want, you won't be able to go much further unless you understand the bare essentials. I strongly recommend taking a cup of tea/coffee and reading the first half of https://www.redblobgames.com/grids/hexagons/ which explains pretty much everything you need to know about hex grids.

Usage & Beyond

Copy and paste into your project. Hex is "model", i.e. it is a plain struct which doesn't derive from MonoBehaviour. Its purpose is to encapsulate a pair of integer coordinates in hex grid space (in skewed coordinate system) and to perform conversions to/from world space, alongside some basic traversal methods.

The Node class inherits from MonoBehaviour and therefore can be attached to any game object. When you do so, said object automagically begins to snap to a logical hexagonal floor, when you move it in your editor. Please note that by "floor" we mean world XZ plane by default; changing that is trivial by tweaking WorldToPlanar and PlanarToWorld static methods.

The dimensions of grid cell are chosen in a way that Unity's unit sphere (with radius 0.5f) in inscibed into a unit hexagon of grid. Which means, the inradius of hexagonal grid is 0.5f. It is relatively easy to modify this: you only need to modify 4 basis vectors (Q_BASIS, R_BASIS, Q_INV, R_INV) which are responsible for converting the coordinates.

Node also exposes a way to snap rotations to hexagonal dimensions, by using integer multiples of 60 degrees. By convention adopted from RedBlobGames the dir integer is constrained to [0, 5] range and is a universal way to address neighbours and all direction-related problems such as line of sight, traversal, pathfinding, etc. Our Hex Laser Puzzle game makes extensive use of that :)

// https://gist.github.com/inca/0b3d194b755e2efd9d7d8e3665e6cfbb
// math from https://www.redblobgames.com/grids/hexagons/
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public static class HexVectorExtensions {
public static Vector2 WorldToPlanar(this Vector3 world) {
return new Vector2(world.x, world.z);
}
public static Vector3 PlanarToWorld(this Vector2 planar, float y = 0f) {
return new Vector3(planar.x, y, planar.y);
}
public static Hex ToHex(this Vector3 world) {
return Hex.FromWorld(world);
}
public static Hex ToHex(this Vector2 planar) {
return Hex.FromPlanar(planar);
}
}
[System.Serializable]
public struct Hex {
#region Static Members
public static float RADIUS = 0.5f;
public static Vector2 Q_BASIS = new Vector2(2f, 0) * RADIUS;
public static Vector2 R_BASIS = new Vector2(1f, Mathf.Sqrt(3)) * RADIUS;
public static Vector2 Q_INV = new(1f / 2, -Mathf.Sqrt(3) / 6);
public static Vector2 R_INV = new(0, Mathf.Sqrt(3) / 3);
public static Hex zero = new(0, 0);
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 Hex operator *(Hex a, int k) => new Hex(a.q * k, a.r * k);
public static Hex operator /(Hex a, int k) => new Hex(a.q / k, a.r / k);
public enum HexDirections
{ // pointy top orientation
East = 0, Southeast = 1, Southwest = 2, West = 3, Northwest = 4, Northeast = 5
}
public static Hex[] PRIMARY_DIRECTIONS =
{
new(1, 0), // 0
new(0, 1), // 1
new(-1, 1), // 2
new(-1, 0), // 3
new(0, -1), // 4
new(1, -1), // 5
};
public static Hex HexDirection(HexDirections direction)
{
return HexDirection((int)direction);
}
public static Hex HexDirection(int direction)
{
//Hex incr = PRIMARY_DIRECTIONS[direction % PRIMARY_DIRECTIONS.Length];
//return this + incr;
return PRIMARY_DIRECTIONS[direction];
}
public static Hex[] DIAGONAL_DIRECTIONS =
{
new(2, -1),
new(1, 1),
new(-1, 2),
new(-2, 1),
new(-1, -1),
new(1, -2)
};
public static Hex FromPlanar(Vector2 planar) {
float q = Vector2.Dot(planar, Q_INV) / RADIUS;
float r = Vector2.Dot(planar, R_INV) / RADIUS;
return new Hex(q, r);
}
public static Hex FromWorld(Vector3 world) {
return FromPlanar(world.WorldToPlanar());
}
public static IEnumerable<Hex> Ring(Hex center, int radius) {
Hex current = center + new Hex(0, -radius);
foreach (Hex dir in PRIMARY_DIRECTIONS) {
for (int i = 0; i < radius; i++) {
yield return current;
current = current + dir;
}
}
}
public static IEnumerable<Hex> Spiral(Hex center, int minRadius, int maxRadius) {
if (minRadius == 0) {
yield return center;
minRadius += 1;
}
for (int r = minRadius; r <= maxRadius; r++) {
var ring = Ring(center, r);
foreach (Hex hex in ring) {
yield return hex;
}
}
}
public static IEnumerable<Vector3> AStarSearchWorld(Hex start, Hex goal, IEnumerable<Hex> obstacles = null, IEnumerable<Hex> grid = null)
{
return AStarSearch(start, goal, obstacles, grid).Select(hex => hex.ToWorld());
}
public static IEnumerable<Hex> AStarSearch(Hex start, Hex goal, IEnumerable<Hex> obstacles = null, IEnumerable<Hex> grid = null)
{
var cameFrom = new Dictionary<Hex, Hex>();
var costSoFar = new Dictionary<Hex, double>();
var frontier = new Utils.PriorityQueue<Hex, double>();
frontier.Enqueue(start, 0);
cameFrom[start] = start;
costSoFar[start] = 0;
while (frontier.Count > 0)
{
var current = frontier.Dequeue();
if (current.Equals(goal))
{
break;
}
var search = current.Neighbours();
if (obstacles != null) search = search.Except(obstacles);
if (grid != null) search = search.Where(tile=>grid.Contains(tile));
foreach (var next in search)
{
double newCost = costSoFar[current];
if ((!costSoFar.ContainsKey(next) || newCost < costSoFar[next]))
{
costSoFar[next] = newCost;
double priority = newCost + next.DistanceTo(goal);
frontier.Enqueue(next, priority);
cameFrom[next] = current;
}
}
}
// return the path we calculated
var current2 = goal;
while (!current2.Equals(start))
{
yield return current2;
current2 = cameFrom[current2];
}
yield return start;
}
public static IEnumerable<Hex> DrawLine(Hex start, Hex end)
{
var n = start.DistanceTo(end);
for (int i = 0; i <= n; i++)
{
yield return LerpHex(start, end, (1f / n) * i);
}
}
static Hex LerpHex(Hex a, Hex b, float t)
{
return new Hex(Mathf.Lerp(a.q, b.q, t), Mathf.Lerp(a.r, b.r, t));
}
#endregion
#region Main Class
public int q;
public int r;
public int s => -q - r;
public Hex(int q, int r) {
this.q = q;
this.r = r;
}
public Hex(float floatQ, float floatR)
{
var floatS = -floatQ - floatR;
this = new Hex(floatQ, floatR, floatS);
}
public Hex(float floatQ, float floatR, float floatS)
{
var intQ = Mathf.RoundToInt(floatQ);
var intR = Mathf.RoundToInt(floatR);
var intS = Mathf.RoundToInt(floatS);
var q_diff = Mathf.Abs(intQ - floatQ);
var r_diff = Mathf.Abs(intR - floatR);
var s_diff = Mathf.Abs(intS - floatS);
if (q_diff > r_diff && q_diff > s_diff)
intQ = -intR - intS;
else if (r_diff > s_diff)
intR = -intQ - intS;
q = intQ;
r = intR;
}
public Vector2 ToPlanar() {
return Q_BASIS * q + R_BASIS * r;
}
public Vector3 ToWorld(float y = 0f) {
return ToPlanar().PlanarToWorld(y);
}
public Hex RotateLeft() => new(-s, -q, -r);
public Hex RotateRight() => new(-r, -s, -q);
public IEnumerable<Hex> Neighbours() {
foreach (var dir in PRIMARY_DIRECTIONS) {
yield return this + dir;
}
}
public Hex GetNeighbor(int direction, bool diagonal = false)
{
return diagonal ? this + DIAGONAL_DIRECTIONS[direction] : this + HexDirection(direction);
}
public IEnumerable<Hex> DrawLine(Hex end)
{
var n = DistanceTo(end);
for (int i = 0; i <= n; i++)
{
yield return LerpHex(this, end, (1f / n) * i);
}
}
public int Length()
{
return (Mathf.Abs(q) + Mathf.Abs(r) + Mathf.Abs(s)) / 2;
}
public int DistanceTo(Hex to)
{
//return (Mathf.Abs(q - to.q) + Mathf.Abs(r - to.r) + Mathf.Abs(s - to.s)) / 2;
return (this - to).Length();
}
#endregion
#region Overrides
public override bool Equals(System.Object obj) {
Hex hex = (Hex)obj;
return (q == hex.q) && (r == hex.r);
}
public override int GetHashCode() {
return 23 + 31 * q + 37 * r;
}
public override string ToString() {
return "(" + q + ";" + r + ")";
}
#endregion
}
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[ExecuteInEditMode]
public class Node : MonoBehaviour {
[Range(0, 5)]
public int dir;
public bool randomizeDir = false;
public bool lockY = false;
public Hex hex {
get {
return transform.position.ToHex();
}
}
public Hex localHex {
get {
return transform.localPosition.ToHex();
}
}
public void ApplyTransform() {
if (randomizeDir) {
Hex hex = this.hex;
int i = hex.q * 100 + hex.r;
dir = ((i % 6) + 6) % 6;
}
float y = lockY ? 0f : transform.localPosition.y;
Vector3 newPos = this.localHex.ToWorld(y);
transform.localPosition = newPos;
transform.localRotation = Quaternion.Euler(0, -60f * dir, 0);
}
#if UNITY_EDITOR
protected virtual void Update() {
if (!Application.isPlaying) {
ApplyTransform();
// Hack to never re-apply dir to instances
this.dir += 1;
UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(this);
this.dir = (dir - 1) % 6;
UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
}
void OnDrawGizmosSelected() {
UnityEditor.Handles.Label(transform.position, hex.ToString());
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment