Skip to content

Instantly share code, notes, and snippets.

@inca
Last active April 28, 2024 06:42
Show Gist options
  • Save inca/0b3d194b755e2efd9d7d8e3665e6cfbb to your computer and use it in GitHub Desktop.
Save inca/0b3d194b755e2efd9d7d8e3665e6cfbb to your computer and use it in GitHub Desktop.
[Unity] Simple Hex Grid System

Simple Hex Grid System

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 :)

using System.Collections;
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 {
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 Vector2(1f / 2, - Mathf.Sqrt(3) / 6);
public static Vector2 R_INV = new Vector2(0, Mathf.Sqrt(3) / 3);
public static Hex[] AXIAL_DIRECTIONS = new Hex[] {
new Hex(1, 0), // 0
new Hex(0, 1), // 1
new Hex(-1, 1), // 2
new Hex(-1, 0), // 3
new Hex(0, -1), // 4
new Hex(1, -1), // 5
};
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 Hex operator +(Hex a, Hex b) {
return new Hex(a.q + b.q, a.r + b.r);
}
public static Hex operator -(Hex a, Hex b) {
return new Hex(a.q - b.q, a.r - b.r);
}
public static Hex zero = new Hex(0, 0);
public static IEnumerable<Hex> Ring(Hex center, int radius) {
Hex current = center + new Hex(0, -radius);
foreach (Hex dir in AXIAL_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<Hex> FloodFill(IEnumerable<Hex> startFrom) {
HashSet<Hex> visited = new HashSet<Hex>();
Queue<Hex> frontier = new Queue<Hex>(startFrom);
while (frontier.Count > 0) {
Hex current = frontier.Dequeue();
yield return current;
foreach (Hex next in current.Neighbours()) {
if (visited.Contains(next)) {
continue;
}
visited.Add(next);
frontier.Enqueue(next);
}
}
}
public int q;
public int r;
public Hex(float q, float r) : this(Mathf.RoundToInt(q), Mathf.RoundToInt(r)) {
}
public Hex(int q, int r) {
this.q = q;
this.r = r;
}
public Vector2 ToPlanar() {
return Q_BASIS * q + R_BASIS * r;
}
public Vector3 ToWorld(float y = 0f) {
return ToPlanar().PlanarToWorld(y);
}
public IEnumerable<Hex> Neighbours() {
foreach (Hex dir in AXIAL_DIRECTIONS) {
yield return this + dir;
}
}
public Hex GetNeighbour(int dir) {
Hex incr = AXIAL_DIRECTIONS[dir % AXIAL_DIRECTIONS.Length];
return this + incr;
}
public int DistanceTo(Hex to) {
return (Mathf.Abs(q - to.q)
+ Mathf.Abs(q + r - to.q - to.r)
+ Mathf.Abs(r - to.r)) / 2;
}
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 + ")";
}
}
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