现在steam上面有很多下棋类/经营类的游戏都是用六边形的地形,比较美观而且实用,去年在版本末期我也自己尝试做了一个绘制六边体的demo,一年没接触unity竟然都要忘光了,赶紧在这边记录一下。
想cv代码可以直接拉到代码章节

功能

能够动态生成一系列可以“挖空中心”的六边形。指定innerWidth为0也可以生成实心的六边体。

unity绘制矩形GGiwos_图形渲染


unity绘制矩形GGiwos_图形渲染_02


能够生成平铺/直铺的六边形群,调整之间距离

unity绘制矩形GGiwos_unity_03

unity绘制矩形GGiwos_图形渲染_04

unity绘制矩形GGiwos_List_05

绘制思路

将绘制一个六边形看成六个下面这种等腰体,绕中心旋转60度之后合并成一个。

unity绘制矩形GGiwos_数据_06


unity绘制矩形GGiwos_unity_07


一个这种等腰体又可以看成绘制四个面:上面的等腰梯形,内测的长方形,下面的等腰梯形,外侧的长方形,两边无需绘制,因为合并之后不会显示出来。

所以只需要通过三角函数计算出我们所需的所有点->拼出一个面->合成一个等腰体->合成一个六边体。

组件

我们需要一个MeshFilter来设置mesh,一个MeshRenderer来设置mesh的材质。同时需要对mesh所需的内置成员变量有些了解。

unity绘制矩形GGiwos_unity绘制矩形GGiwos_08

m_meshFilter = GetComponent<MeshFilter>();
        m_meshRenderer = GetComponent<MeshRenderer>();

        m_mesh = new Mesh();
        m_mesh.name = "HexMesh";

        m_meshFilter.mesh = m_mesh;
        m_meshRenderer.material = m_material;
		
		//最终数据传入
		m_mesh.vertices = verticles.ToArray();
        m_mesh.triangles = tris.ToArray();
        m_mesh.uv = uvs.ToArray();
        m_mesh.RecalculateNormals();

具体计算

绘制某个点

根据前面需要绘制的等腰梯形,设A是梯形长边的点,B是梯形短边的点,易得平面内某个点的计算方式

unity绘制矩形GGiwos_图形渲染_09


定义一个CreatePoint接口,根据width和y轴高度height来生成某个点的三维向量,(注意unity下生成图中y轴实际上是三维空间的z轴)

private Vector3 CreatePoint(float distance, float height, float angle)
    {
        float rad = angle * Mathf.Deg2Rad; //Mathf接收的参数需要是弧度制
        return new Vector3(distance * Mathf.Cos(rad), height, distance * Mathf.Sin(rad));
    }

生成面所需的数据

上文提到的等腰体四个不同面实际上都是四个顶点组成的,并且都是两个点组成的平行的线段,所以我们可以提供一个接口,只需指定高度和半径,就可以画出这四种不同的面,同时存在上下和内外两侧面的朝向是相反的,所以提供reverse接口来进行反向。

/// <summary>
    /// 上下底面的单独一个等腰梯形
    /// </summary>
    /// <param name="innerRad">内径</param>
    /// <param name="outerRad">外径</param>
    /// <param name="heightA">外高</param>
    /// <param name="heightB">内高</param>
    /// <param name="point">顺序</param>
    /// <param name="reverse">连接方向</param>
    /// <returns></returns>
    private Face CreateFace(float innerRad, float outerRad, float heightA, float heightB, int point, bool reverse = false)
    {
        float angle1 = point * 60;
        float angle2 = angle1 + 60;
        if (!isFlat){ //竖着排布,初始角度是-30
            angle1 -= 30;
            angle2 -= 30;
        }
        List<Vector3> verticals = new List<Vector3>();
        //.......C.
        //..B.......
        //..........
        //...A......D
        verticals.Add(CreatePoint(innerRad, heightA, angle1));
        verticals.Add(CreatePoint(innerRad, heightA, angle2));
        verticals.Add(CreatePoint(outerRad, heightB, angle2));
        verticals.Add(CreatePoint(outerRad, heightB, angle1));
        List<int> tris = new List<int> { 0, 1, 2, 2, 3, 0};
        List<Vector2> uv = new List<Vector2> { new Vector2(0, 0),new Vector2(1,0),new Vector2(1,1),new Vector2(0,1) };
        //vertical顺序颠倒,就会按照顺时针绘制。
        if(reverse)
        {
            verticals.Reverse();
        }
        return new Face(verticals, tris, uv);
    }

这里有一些关于mesh的基础知识,首先是三个顶点能够组成一个面,从上往下看如果点之间是逆时针顺序的话,就是面向我们的。这里我们添加了四个点。tirs指定其顺序,每三个一组将会连成一个面,uvs代表是渲染的时候的uv坐标,这里如果六边体有规范的话,就需要根据需求设置对应的uv值,这里就不关注这个了。

List<int> tris = new List<int> { 0, 1, 2, 2, 3, 0};
       List<Vector2> uv = new List<Vector2> { new Vector2(0, 0),new Vector2(1,0),new Vector2(1,1),new Vector2(0,1) };
public struct Face
{
    //顶点位置数组
    public List<Vector3> verticles { get; private set; }
    //三角形顶点索引数组,按给定的顺序连接顶点,为顺时针三个一组的顺序
    public List<int> triangles { get; private set; }
    public List<Vector2> uvs { get; private set; }

    public Face(List<Vector3> verticles, List<int> triangles, List<Vector2> uvs)
    {
        this.verticles = verticles;
        this.triangles = triangles;
        this.uvs = uvs;
    }
}

这样能够生产出一个面,接下来我们批量生产所需的面,只需要不断让角度偏移60度(忘记了可以去看上面计算A点坐标),重复刚才的步骤,将所有的面的数据都生成

private void DrawFaces()
    {
        m_faces = new List<Face>();

        //上表面
        for(int point = 0; point < 6; point ++)
        {
            m_faces.Add(CreateFace(innerWidth, outerWidth, height / 2, height / 2, point));
        }
        //下表面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerWidth, outerWidth,- height / 2, -height / 2, point,true));
        }
        //侧面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(outerWidth, outerWidth, height / 2, -height / 2, point));
        }
        //里侧面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerWidth, innerWidth, height / 2, -height / 2, point,true));
        }
    }

组装

刚才我们将数据填入Face,但是Face是不能直接使用的,我们要将刚才生成的顶点信息,uv信息,三角形信息等一次灌入Mesh中,
Mesh提供了成员变量来接收这些数据。
顶点和uv直接添加就可以,注意三角形数据需要根据顶点数据来加下标。

private void CombineFaces()
    {
        List<Vector3> verticles = new List<Vector3>();
        List<int> tris = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        for(int i = 0; i < m_faces.Count; i++)
        {
            verticles.AddRange(m_faces[i].verticles); //AddRange方法可以把list中所有数据从头到尾添加到新的list
            uvs.AddRange(m_faces[i].uvs);

            //注意:这里需要依次指定指定所有顶点在最终mesh的三角形顺序,由于每个face里面包括四个顶点,每次+4
            int offset = (4 * i);
            foreach(int triangle in m_faces[i].triangles)
            {
                tris.Add(triangle + offset);
            }
        }

        m_mesh.vertices = verticles.ToArray();
        m_mesh.triangles = tris.ToArray();
        m_mesh.uv = uvs.ToArray();
        m_mesh.RecalculateNormals();
    }

排布

要让游戏能玩,肯定需要一系列整齐布局的六边形,所以我们需要一个动态创建六边形的管理器。

纵向排布

unity绘制矩形GGiwos_unity_10


前面我们生成面的时候发现有个isFlat变量,这个变量就是控制了第一个面的生成角度,所以横向的时候能保证六边形是横着的。

private Face CreateFace(float innerRad, float outerRad, float heightA, float heightB, int point, bool reverse = false)
    {
        float angle1 = point * 60;
        float angle2 = angle1 + 60;
        if (!isFlat){ //竖着排布,初始角度是-30
            angle1 -= 30;
            angle2 -= 30;
        }
        ......

问题是如何计算出每个六边形的中心点在哪。这里用三角函数也非常容易看出来

下面是六边体“直立“”情况下,设两个六边形之间间隔为d,六边形中心到外顶点的距离为L

可以发现Y轴方向每个六边形之间距离为(L * cos(30°) * 2 + d)* sin60°

X轴方向每个六边形之间距离为(L*(cos(30°)*2 + d)
同时注意距离偶数行的X轴要添加一个(L * cos(30°) * 2 + d)*sin30°的偏移

具体计算就初中级别的数学,就不一步步画图了

unity绘制矩形GGiwos_数据_11

横向排布

同理横向布局也很好计算

可以发现Y轴方向每个六边形之间距离为(L * cos(30°) * 2 + d)
X轴方向每个六边形之间距离为(L*(cos(30°)*2 + d) *sin60°
同时注意距离偶数行的Y轴要添加一个(L * cos(30°) * 2 + d)*sin30°的偏移

unity绘制矩形GGiwos_unity_12


万事具备,我们只需要计算每一行每列的点即可生成蜂窝了。

public void SetInterval()
    {
        centerDistance = outterWidth * 2 * Mathf.Sin(60 * Mathf.Deg2Rad) + interval;
    }
	 private void UpdateGrid(GameObject[][] girds)
 {
     if (girds.Length <= 0) return;
     bool shouldOffset = false;
     for (int j = 0; j < heightCount; j++)
     {
         if (!isFlat)
         {
             shouldOffset = j % 2 != 0;
         }
         for (int i = 0; i < widthCount; i++)
         {
             if (isFlat)
             {
                 shouldOffset = i % 2 != 0;
             }
             HexagonRenderer render = girds[i][j].GetComponent<HexagonRenderer>();
             //计算六边形位置
             Vector3 pos = Getpos(i, j, shouldOffset);
             Debug.Log(pos);
             render.SetAtrributes(innerWidth, outterWidth, height, pos, matrial, isFlat);
             render.DrawMesh();
         }
     }
 }

 private Vector3 Getpos(int i, int j, bool shouldOffset)
 {
     float angle60 = 60 * Mathf.Deg2Rad;
     float angle30 = 30 * Mathf.Deg2Rad;
     if (isFlat)
     {
         if (shouldOffset)
         {
             return new Vector3(i * centerDistance * Mathf.Sin(angle60) , transform.position.y, j * centerDistance +centerDistance * Mathf.Sin(angle30));
         }
         else
         {
             return new Vector3(i * centerDistance * Mathf.Sin(angle60), transform.position.y, j * centerDistance);
         }
     }
     else
     {
         if (shouldOffset)
         {
             return new Vector3(i * centerDistance + centerDistance * Mathf.Sin(angle30), transform.position.y, j * centerDistance * Mathf.Sin(angle60));
         }
         else
         {
             return new Vector3(i * centerDistance, transform.position.y, j * centerDistance * Mathf.Sin(angle60));
         }
     }

 }

完整代码

在场景中创建一个空物体,将GenerateMap.cs挂载在其身上即可,将会自动生成一系列身上挂载HexagonRenderer.cs的物体

GenerateMap.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GenerateMap : MonoBehaviour
{
    [Header("Grid Settings")]
    public int widthCount;
    public int heightCount;

    [Header("Layout Settings")]
    public float innerWidth;
    public float outterWidth;
    public float height;
    public bool isFlat;
    public Material matrial;
    /// <summary>
    /// 六边形之间的间隔
    /// </summary>
    public float interval;
    private float centerDistance;


    /// <summary>
    /// 存储所有的六边形
    /// </summary>
    private GameObject[][] girds;
    private bool hasGenerate = false;
    public void Start()
    {
        girds = new GameObject[widthCount][];
        for (int i = 0; i < girds.Length; i++)
        {
            girds[i] = new GameObject[heightCount];
        }
        SetInterval();
        GenerateGrid();
        LayoutGrid();
    }

    public void SetInterval()
    {
        centerDistance = outterWidth * 2 * Mathf.Sin(60 * Mathf.Deg2Rad) + interval;
    }
    /// <summary>
    /// 设置六边形布局,从左下角生成
    /// </summary>
    private void LayoutGrid()
    {
        UpdateGrid(girds);
    }

    private void GenerateGrid()
    {
        if (hasGenerate == true) return;
        for (int j = 0; j < heightCount; j++)
        {
            for (int i = 0; i < widthCount; i++)
            {
                GameObject single = new GameObject($"HEX:({i},{j})", typeof(HexagonRenderer)); //$代表string.format
                girds[i][j] = single;
                single.transform.SetParent(transform, true);
            }
        }
        hasGenerate = true;
    }

    private void UpdateGrid(GameObject[][] girds)
    {
        if (girds.Length <= 0) return;
        bool shouldOffset = false;
        for (int j = 0; j < heightCount; j++)
        {
            if (!isFlat)
            {
                shouldOffset = j % 2 != 0;
            }
            for (int i = 0; i < widthCount; i++)
            {
                if (isFlat)
                {
                    shouldOffset = i % 2 != 0;
                }
                HexagonRenderer render = girds[i][j].GetComponent<HexagonRenderer>();
                //计算六边形位置
                Vector3 pos = Getpos(i, j, shouldOffset);
                Debug.Log(pos);
                render.SetAtrributes(innerWidth, outterWidth, height, pos, matrial, isFlat);
                render.DrawMesh();
            }
        }
    }

    private Vector3 Getpos(int i, int j, bool shouldOffset)
    {
        float angle60 = 60 * Mathf.Deg2Rad;
        float angle30 = 30 * Mathf.Deg2Rad;
        if (isFlat)
        {
            if (shouldOffset)
            {
                return new Vector3(i * centerDistance * Mathf.Sin(angle60) , transform.position.y, j * centerDistance +centerDistance * Mathf.Sin(angle30));
            }
            else
            {
                return new Vector3(i * centerDistance * Mathf.Sin(angle60), transform.position.y, j * centerDistance);
            }
        }
        else
        {
            if (shouldOffset)
            {
                return new Vector3(i * centerDistance + centerDistance * Mathf.Sin(angle30), transform.position.y, j * centerDistance * Mathf.Sin(angle60));
            }
            else
            {
                return new Vector3(i * centerDistance, transform.position.y, j * centerDistance * Mathf.Sin(angle60));
            }
        }

    }
}

HexagonRenderer.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public struct Face
{
    //顶点位置数组
    public List<Vector3> verticles { get; private set; }
    //三角形顶点索引数组,按给定的顺序连接顶点,为顺时针三个一组的顺序
    public List<int> triangles { get; private set; }
    public List<Vector2> uvs { get; private set; }

    public Face(List<Vector3> verticles, List<int> triangles, List<Vector2> uvs)
    {
        this.verticles = verticles;
        this.triangles = triangles;
        this.uvs = uvs;
    }
}
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]

public class HexagonRenderer : MonoBehaviour
{
    private Mesh m_mesh;
    private MeshFilter m_meshFilter;
    private MeshRenderer m_meshRenderer;

    private List<Face> m_faces;

    private bool isFlat = true;

    public Material m_material;
    public float innerWidth;
    public float outerWidth;
    public float height;
    private void Awake()
    {
        m_meshFilter = GetComponent<MeshFilter>();
        m_meshRenderer = GetComponent<MeshRenderer>();

        m_mesh = new Mesh();
        m_mesh.name = "HexMesh";

        m_meshFilter.mesh = m_mesh;
        m_meshRenderer.material = m_material;
    }
    public void SetAtrributes(float innerWidth, float outerWidth, float height, Vector3 position, Material material, bool isFlat)
    {
        this.innerWidth = innerWidth;
        this.outerWidth = outerWidth;
        this.isFlat = isFlat;
        this.height = height;
        transform.position = position;
        m_material = material;
        m_meshRenderer.material = m_material;

        DrawMesh();
    }
    private void OnEnable()
    {
        DrawMesh();
    }

    //渲染整个六边形体
    public void DrawMesh()
    {
        DrawFaces();
        CombineFaces();
    }

    private void OnValidate()
    {
    }

    private void DrawFaces()
    {
        m_faces = new List<Face>();

        //上表面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerWidth, outerWidth, height / 2, height / 2, point));
        }
        //下表面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerWidth, outerWidth, -height / 2, -height / 2, point, true));
        }
        //侧面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(outerWidth, outerWidth, height / 2, -height / 2, point));
        }
        //里侧面
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerWidth, innerWidth, height / 2, -height / 2, point, true));
        }
    }
    private void CombineFaces()
    {
        List<Vector3> verticles = new List<Vector3>();
        List<int> tris = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        for (int i = 0; i < m_faces.Count; i++)
        {
            verticles.AddRange(m_faces[i].verticles);AddRange方法可以把list中所有数据从头到尾添加到新的list
            uvs.AddRange(m_faces[i].uvs);

            //注意:这里需要依次指定指定所有顶点在最终mesh的三角形顺序,由于每个face里面包括四个顶点,每次+4
            int offset = (4 * i);
            foreach (int triangle in m_faces[i].triangles)
            {
                tris.Add(triangle + offset);
            }
        }

        m_mesh.vertices = verticles.ToArray();
        m_mesh.triangles = tris.ToArray();
        m_mesh.uv = uvs.ToArray();
        m_mesh.RecalculateNormals();
    }
    /// <summary>
    /// 上下底面的单独一个等腰梯形
    /// </summary>
    /// <param name="innerRad">内径</param>
    /// <param name="outerRad">外径</param>
    /// <param name="heightA">外高</param>
    /// <param name="heightB">内高</param>
    /// <param name="point">顺序</param>
    /// <param name="reverse">连接方向</param>
    /// <returns></returns>
    private Face CreateFace(float innerRad, float outerRad, float heightA, float heightB, int point, bool reverse = false)
    {
        float angle1 = point * 60;
        float angle2 = angle1 + 60;
        if (!isFlat)
        {
            angle1 -= 30;
            angle2 -= 30;
        }
        List<Vector3> verticals = new List<Vector3>();
        //.......C.
        //..B.......
        //..........
        //...A......D
        verticals.Add(CreatePoint(innerRad, heightA, angle1));
        verticals.Add(CreatePoint(innerRad, heightA, angle2));
        verticals.Add(CreatePoint(outerRad, heightB, angle2));
        verticals.Add(CreatePoint(outerRad, heightB, angle1));
        List<int> tris = new List<int> { 0, 1, 2, 2, 3, 0 };
        List<Vector2> uv = new List<Vector2> { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) };

        if (reverse)
        {
            verticals.Reverse();
        }
        return new Face(verticals, tris, uv);
    }
    /// <summary>
    /// 创造一个顶点
    /// </summary>
    /// <param name="distance">距离坐标原点距离</param>
    /// <param name="height">y轴高度</param>
    /// <param name="angle">和坐标轴所成夹角</param>
    /// <returns></returns>

    private Vector3 CreatePoint(float distance, float height, float angle)
    {
        float rad = angle * Mathf.Deg2Rad;
        return new Vector3(distance * Mathf.Cos(rad), height, distance * Mathf.Sin(rad));
    }
}