Hex Map Editor

I got the inspiration to make this tool by playing games such Civilization V and Endless Legends, I am very impressed by the great result the games maps achieved and I wanted to learn how I could do that myself.

So I started this project with the aim to both learn as well as create a tool that fellow developers could use to create strategy or non-strategy games with a hexagonal tile based grid.


This is still an ongoing project, I will keep updating the page as i progress with the development.

I started by creating a mesh composed by hexagon, so i dreaw out the triangles i needed, and place each hexagon in the fitting position. 

After doing some research I realized that I had the need to farther divide the mesh to be able to have a smoother transition once the elevation was implemented. so i shrank the hexagons, and filled the gaps in between them.

This gives the entire mesh more flexibility.

          
  public class HexGrid : MonoBehaviour
{
    /*GRID SIZE*/
    public int width = 6;
    public int height = 6;

    /*CELL*/
    public HexCell cellPrefab = null;
    private HexCell[] _cells;

    /*UI*/
    public Text cellTextPrefab = null;
    private Canvas gridCanvas;
    
    /*HEX MESH*/
    private HexMesh hexMesh;
    
    /*GRID COLORING*/
    public Color defaultColor = Color.white;

    private void Awake()
    {
        gridCanvas = GetComponentInChildren< Canvas>();
        hexMesh = GetComponentInChildren< HexMesh>();
        
        _cells = new HexCell[height * width];
        for (int z = 0, i =0 ; z < height ; z++)
        {
            for (int x = 0; x < width; x++)
            {
                CreateCell(x, z, i++);
            }
        }
    }

    private void Start()
    {
        hexMesh.Triangulate(_cells);
    }

    public HexCell GetCell (Vector3 hitPoint)
    {
        hitPoint = transform.InverseTransformPoint(hitPoint);
        HexCoordinates coords = HexCoordinates.FromPosition(hitPoint);
        int index = coords.X + coords.Z * width + coords.Z / 2;
        return _cells[index];
        //HexCell cell = _cells[index];
        //cell.color = color;
        //hexMesh.Triangulate(_cells);
        //Debug.Log("clicked on cell at: " + coords);
    }

    public void Refresh()
    {
        hexMesh.Triangulate(_cells);
    }

    private void CreateCell(int x, int z, int i)
    {
        Vector3 position;
        position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);
        position.y = 0f;
        position.z = z * (HexMetrics.outerRadius * 1.5f);

        HexCell cell = _cells[i] = Instantiate< HexCell>(cellPrefab);
        cell.transform.SetParent(transform, false);
        cell.transform.localPosition = position;
        cell.coords = HexCoordinates.FromOffsetCoordinates(x, z);
        cell.color = defaultColor;
        

        if (x > 0)
        {
            cell.SetNeighbor(HexDirection.W, _cells[i - 1]);
        }

        if (z > 0)
        {
            if ((z & 1) == 0)
            {
                cell.SetNeighbor(HexDirection.SE, _cells[i -width]);
                if (x > 0)
                {
                    cell.SetNeighbor(HexDirection.SW, _cells[i -width -1]);
                }
            }
            else
            {
                cell.SetNeighbor(HexDirection.SW, _cells[i -width]);
                if (x < width -1)
                {
                    cell.SetNeighbor(HexDirection.SE, _cells[i - width + 1]);
                }
            }
        }
        //Text label = Instantiate< Text>(cellTextPrefab);
        //label.rectTransform.SetParent(gridCanvas.transform, false);
        //label.rectTransform.anchoredPosition = new Vector2(position.x, position.z);
        //label.text = x + "\n" + z;
        //label.text = cell.coords.ToStringOnSeparateLines();
        //cell.uiLabelTransform = label.rectTransform;
    }
}
    
            
        
          
  /*MESH*/
    private Mesh hexMesh;
    private List< Vector3> vertices;
    private List< int> triangles;
    private MeshCollider meshCollider;
    
    /*COLOR*/
    private List< Color> meshColors;
    private void Awake()
    {
        GetComponent< MeshFilter>().mesh = hexMesh = new Mesh();
        meshCollider = gameObject.AddComponent< MeshCollider>();
        hexMesh.name = "HEX Mesh";
        vertices = new List< Vector3>();
        triangles = new List< int>();
        meshColors = new List< Color>();
    }

    public void Triangulate(HexCell[] cells)
    {
        hexMesh.Clear();
        vertices.Clear();
        triangles.Clear();
        meshColors.Clear();

        for (int i = 0; i < cells.Length; i++)
        {
            Triangulate(cells[i]);
        }

        hexMesh.vertices = vertices.ToArray();
        hexMesh.triangles = triangles.ToArray();
        hexMesh.colors = meshColors.ToArray(); 
        hexMesh.RecalculateNormals();
        meshCollider.sharedMesh = hexMesh;
    }

    private void Triangulate(HexCell cell)
    {
        for (HexDirection dir = HexDirection.NE; dir <=HexDirection.NW; dir++)
        {
            Triangulate(dir, cell);
        }
    }

    private void Triangulate(HexDirection direction, HexCell cell)
    {
        Vector3 center = cell.transform.localPosition;
        Vector3 vertex1 = center + HexMetrics.GetFirstSolidCorner(direction);
        Vector3 vertex2 = center + HexMetrics.GetSecondSolidCorner(direction);

        AddTriangle(center, vertex1, vertex2);
        AddTriangleColor(cell.color);
        /*ADDING A BRIDGE IN BETWEEN HEXAGONS*/
        if (direction <= HexDirection.SE)
        {
            TriangulateConnections(direction, cell, vertex1, vertex2);
        }
    }
     /*CREATE TRIANGLES FOR EACH HEXAGON*/
    void AddTriangle(Vector3 vertex1, Vector3 vertex2, Vector3 vertex3)
    {
        int vertexIndex = vertices.Count;
        vertices.Add(vertex1);
        vertices.Add(vertex2);
        vertices.Add(vertex3);
        triangles.Add(vertexIndex);
        triangles.Add(vertexIndex +1);
        triangles.Add(vertexIndex +2);
    }

    void AddTriangleColor(Color color)
     {
         meshColors.Add(color);
         meshColors.Add(color);
         meshColors.Add(color);
     }
    
    void AddTriangleColor(Color color1, Color color2, Color color3)
    {
        meshColors.Add(color1);
        meshColors.Add(color2);
        meshColors.Add(color3);
    }
    
    /*CREATE QUADS TO CLOSE GAP IN BETWEEN EACH HEX*/

    private void AddQuad(Vector3 vertex1, Vector3 vertex2, Vector3 vertex3, Vector3 vertex4)
    {
        int vertexindex = vertices.Count;
        vertices.Add(vertex1);
        vertices.Add(vertex2);
        vertices.Add(vertex3);
        vertices.Add(vertex4);
        triangles.Add(vertexindex);
        triangles.Add(vertexindex + 2);
        triangles.Add(vertexindex + 1);
        triangles.Add(vertexindex + 1);
        triangles.Add(vertexindex + 2);
        triangles.Add(vertexindex + 3);
    }

    private void AddQuadColor(Color color1, Color color2, Color color3, Color color4)
    {
        meshColors.Add(color1);
        meshColors.Add(color2);
        meshColors.Add(color3);
        meshColors.Add(color4);
    }

    void AddQuadColor(Color color1, Color color2)
    {
        meshColors.Add(color1);
        meshColors.Add(color1);
        meshColors.Add(color2);
        meshColors.Add(color2);

    }
    
            
        

Once the mesh was set up, I preceeded experimenting and researching how to elevate the mesh in a good and esthetically pleasing way. 

I chosed an Endless Legens approach to how the slopes in between different elevation should look.

After that was sorted out i proceded in filling in all the gaps in the mesh and fitting the edges to the slopes steps. 

          
void TriangulateConnections(HexDirection direction, HexCell cell, Vector3 vertex1, Vector3 vertex2)
    {
        
        HexCell currentNeighbor = cell.GetNeighbor(direction);
        
        if (!currentNeighbor)
            return;
        
        Vector3 bridge = HexMetrics.GetBridge(direction);
        Vector3 vertex3 = vertex1 + bridge;
        Vector3 vertex4 = vertex2 + bridge;
        /*CREATING A SLOPE*/
        vertex3.y = vertex4.y = currentNeighbor.Elevation * HexMetrics.elevationStep;
        /*TERRACING ONLY ONE HEIGHT DIFFERENCE CONNECTIONS*/
        if (cell.GetEdgeType(direction) == HexEdgeType.Slope)
        {
            TriangulateEdgeTerraces(vertex1, vertex2, cell, vertex3, vertex4, currentNeighbor);
        }
        else
        {
            AddQuad(vertex1, vertex2, vertex3, vertex4);
            AddQuadColor(cell.color, currentNeighbor.color);
        }
        
        HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
        if (direction <= HexDirection.E && nextNeighbor)
        {
            /*SLOPE CONNECTIONS*/
            Vector3 vertex5 = vertex2 + HexMetrics.GetBridge(direction.Next());
            vertex5.y = nextNeighbor.Elevation * HexMetrics.elevationStep;
            //AddTriangle(vertex2, vertex4, vertex5);
            //AddTriangleColor(cell.color, currentNeighbor.color, nextNeighbor.color);
            if (cell.Elevation <= currentNeighbor.Elevation)
            {
                if (cell.Elevation <= nextNeighbor.Elevation)
                {
                    TriangulateCorner(vertex2, cell, vertex4, currentNeighbor, vertex5, nextNeighbor);
                }
                else
                {
                    TriangulateCorner(vertex5, nextNeighbor, vertex2, cell, vertex4, currentNeighbor);
                }
            }
            else if (currentNeighbor.Elevation <= nextNeighbor.Elevation)
            {
                TriangulateCorner(vertex4, currentNeighbor, vertex5, nextNeighbor, vertex2, cell);
            }
            else
            {
                TriangulateCorner(vertex5, nextNeighbor, vertex2, cell, vertex4, currentNeighbor);
            }
        }

    }

    void TriangulateEdgeTerraces(Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft,
        Vector3 endRight, HexCell endCell)
    {
        Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1);
        Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1);
        Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1);
        
        AddQuad(beginLeft, beginRight, v3, v4);
        AddQuadColor(beginCell.color, c2);
        for (int i = 2; i < HexMetrics.terracesSteps; i++)
        {
            Vector3 v1 = v3;
            Vector3 v2 = v4;
            Color c1 = c2;

            v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i);
            v4 = HexMetrics.TerraceLerp(beginRight, endRight, i);
            c1 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i);
            
            AddQuad(v1, v2, v3, v4);
            AddQuadColor(c1, c2);
        }
        AddQuad(v3, v4, endLeft, endRight);
        AddQuadColor(c2, endCell.color);
    }

    void TriangulateCorner(Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right,
        HexCell rightCell)
    {
        HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell);
        HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell);

        if (leftEdgeType == HexEdgeType.Slope)
        {
            if (rightEdgeType == HexEdgeType.Slope)
            {
                TriangulateCornerTerraces(bottom, bottomCell, left, leftCell, right, rightCell);
                return;
            }

            if (rightEdgeType == HexEdgeType.Flat)
            {
                TriangulateCornerTerraces(left, leftCell, right, rightCell,  bottom, bottomCell);
                return;
            }
            TriangulateCornerTerracesCliffL(bottom, bottomCell, left, leftCell, right, rightCell);
            return;
        }

        if (rightEdgeType == HexEdgeType.Slope)
        {
            if (leftEdgeType == HexEdgeType.Flat)
            {
                TriangulateCornerTerraces(right, rightCell, bottom, bottomCell, left, leftCell);
                return;
            }
            TriangulateCornerTerracesCliffR(bottom, bottomCell, left, leftCell, right, rightCell);
            return;
        }
        AddTriangle(bottom, left, right);
        AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color);
    }

    void TriangulateCornerTerraces(Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell)
    {
        Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1);
        Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1);
        Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1);
        Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1);
        
        AddTriangle(begin, v3, v4);
        AddTriangleColor(beginCell.color, c3, c4);

        for (int i = 0; i < HexMetrics.terracesSteps; i++)
        {
            Vector3 v1 = v3;
            Vector3 v2 = v4;
            Color c1 = c3;
            Color c2 = c4;
            v3 = HexMetrics.TerraceLerp(begin, left, i);
            v4 = HexMetrics.TerraceLerp(begin, right, i);
            c1 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i);
            c2 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i);
            AddQuad(v1, v2, v3, v4);
            AddQuadColor(c1, c2, c3, c4);
        }
        AddQuad(v3, v4,left, right);
        AddQuadColor(c3, c4, leftCell.color, rightCell.color);
    }

    void TriangulateCornerTerracesCliffL(Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right,
        HexCell rightCell)
    {
        float b = 1f / (rightCell.Elevation - beginCell.Elevation);
        Vector3 boundary = Vector3.Lerp(begin, right, b);
        Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b);
        TriangulateBoundaryTriangle(begin, beginCell, left, leftCell, boundary, boundaryColor);

        if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope)
        {
            TriangulateBoundaryTriangle(left, leftCell, right, rightCell, boundary, boundaryColor);
        }
        else
        {
            AddTriangle(left, right, boundary);
            AddTriangleColor(leftCell.color, rightCell.color, boundaryColor);
        }
    }
    void TriangulateCornerTerracesCliffR(Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right,
        HexCell rightCell)
    {
        float b = 1f / (leftCell.Elevation - beginCell.Elevation);
        Vector3 boundary = Vector3.Lerp(begin, left, b);
        Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b);
        TriangulateBoundaryTriangle(right, rightCell, begin, beginCell, boundary, boundaryColor);

        if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope)
        {
            TriangulateBoundaryTriangle(left, leftCell, right, rightCell, boundary, boundaryColor);
        }
        else
        {
            AddTriangle(left, right, boundary);
            AddTriangleColor(leftCell.color, rightCell.color, boundaryColor);
        }
    }
    
    void TriangulateBoundaryTriangle(Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary,
        Color boundaryColor)
    {
        Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1);
        Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1);
        
        AddTriangle(begin, v2, boundary);
        AddTriangleColor(beginCell.color, c2, boundaryColor);
        for (int i = 0; i < HexMetrics.terracesSteps; i++)
        {
            Vector3 v1 = v2;
            Color c1 = c2;
            v2 = HexMetrics.TerraceLerp(begin, left, i);
            c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i);
            AddTriangle(v1, v2, boundary);
            AddTriangleColor(c1, c2, boundaryColor);
        }
        AddTriangle(v2, left, boundary);
        AddTriangleColor(c2, leftCell.color, boundaryColor);
    }
            
        
          
public enum HexEdgeType
{
    Flat, Slope, Cliff
}
public class HexMetrics
{
    /*HEX BASE CONSTRUCTION*/
    public const float outerRadius = 10f;
    public const float innerRadius = outerRadius * 0.866025404f;

    
    private static Vector3[] corners =
    {
        new Vector3(0f, 0f, outerRadius),
        new Vector3(innerRadius, 0f, 0.5f * outerRadius),
        new Vector3(innerRadius, 0f, -0.5f * outerRadius),
        new Vector3(0f, 0f, -outerRadius),
        new Vector3(-innerRadius, 0f, -0.5f * outerRadius),
        new Vector3(-innerRadius, 0f, 0.5f * outerRadius),
        new Vector3(0f, 0f, outerRadius), 

    };
    
    public static Vector3 GetFirstCorner(HexDirection direction) => corners[(int) direction];
    public static Vector3 GetSecondCorner(HexDirection direction) => corners[(int) direction + 1];
    
    /*COLOR BLENDING*/
    public const float solidFactor = 0.75f;
    public const float blendFactor = 1f - solidFactor;

    public static Vector3 GetFirstSolidCorner(HexDirection direction) => corners[(int) direction] * solidFactor;
    public static Vector3 GetSecondSolidCorner(HexDirection direction) => corners[(int) direction + 1] * solidFactor;

    public static Vector3 GetBridge(HexDirection direction) =>
        (corners[(int) direction] + corners[(int) direction + 1]) * blendFactor;

    
    /*ELEVATION*/
    public const float elevationStep = 5f;
    /*TERRACING */
    public const int terracesPerSlope = 2;
    public const int terracesSteps = terracesPerSlope * 2 + 1;
    public const float horizontalTerraceStepSize = 1f / terracesSteps;
    public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1);
    
    public static Vector3 TerraceLerp(Vector3 a, Vector3 b, int step)
    {
        float h = step * HexMetrics.horizontalTerraceStepSize;
        a.x += (b.x - a.x) * h;
        a.z += (b.z - a.z) * h;
        float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize;
        a.y += (b.y - a.y) * v;
        return a;
    }

    public static Color TerraceLerp(Color a, Color b, int step)
    {
        float h = step * HexMetrics.horizontalTerraceStepSize;
        return Color.Lerp(a, b, h);
    }
    
    public static HexEdgeType GetEdgeType(int firstElevation, int secondElevation)
    {
        if (firstElevation == secondElevation)
        {
            return HexEdgeType.Flat;
        }
        
        int delta = secondElevation - firstElevation;
        if (delta == 1 || delta == -1)
        {
            return HexEdgeType.Slope;
        }

        return HexEdgeType.Cliff;
    }