Unity 生成六角網格地圖:矩形地圖以及矩形地圖內隨機


 

 

 

unity生成六角網格地圖:矩形地圖以及矩形地圖內隨機

本文某些概念是參考國外大神的文章去做的,讀者可能需要理解其中某些概念才能了解本文的一些做法

參考鏈接:https://www.redblobgames.com/grids/hexagons/

 

用到的地塊貼圖如下:

 

先放上六角網格地圖效果圖:

 

 

 

 前兩個分別是是固定尺寸豎六邊形和固定尺寸矩形,后兩個是在前面兩個的形狀下,在里面隨機生成。

 

開始之前需要定義一個結構體,用於建立六邊形地圖的坐標系,方便以后做距離判斷和攻擊范圍判斷,詳細坐標系的介紹請查看鏈接中Coordinate Systems的Cube coordinates部分,后面的描述稱之為立方體坐標

 

    public struct CubeCoordinate
    {
        public int q;
        public int r;
        public int s;

        public CubeCoordinate(int q, int r, int s)
        {
            this.q = q;
            this.r = r;
            this.s = s;
        }

        public CubeCoordinate CubePositionAdd(CubeCoordinate offset)
        {
            return new CubeCoordinate(q + offset.q, r + offset.r, s + offset.s);
        }
    }

在結構體中定義CubePositionAdd是為了方便做坐標相加運算。

 

另外定義一個靜態List用於存放坐標偏移量,分別為左下、右下、下、右上、上、左上。 

 

    private static readonly List<CubeCoordinate> hexDirectionOffset = new List<CubeCoordinate> {
            new CubeCoordinate(-1, 0, 1), new CubeCoordinate(1, -1, 0), new CubeCoordinate(0, -1, 1),
            new CubeCoordinate(1, 0, -1), new CubeCoordinate(0, 1, -1), new CubeCoordinate(-1, 1, 0)
    };

 

另外定義兩個List用於存放所有地塊的立方體坐標和世界坐標。

 

 

    private List<CubeCoordinate> cubePosList;
    private List<Vector2> worldPosList;

 

定義兩個float用於存放六邊形之間的寬距離和高距離,參考見鏈接中Geometry的Size and Spacing部分。

    private float widthDistance;
    private float heightDistance;

    private void InitHexsDistance()
    {
        widthDistance = hexSize * 0.75f * 0.01f;
        heightDistance = Mathf.Sqrt(3f) * hexSize * 0.5f * 0.01f;
    }

 

生成固定尺寸豎六邊形的函數如下:

    private void CreateHexagonalMap()
    {
        Queue<CubeCoordinate> cubePosQueue_BFS = new Queue<CubeCoordinate>();

        CubeCoordinate currentCubePos;
        CubeCoordinate nextCubePos;
        Vector2 currentWorldPos;
        Vector2 nextWorldPos;

        cubePosList.Add(new CubeCoordinate(0, 0, 0));
        worldPosList.Add(Vector2.zero);

        cubePosQueue_BFS.Enqueue(cubePosList[0]);

        Instantiate(landBlockPrefab, worldPosList[0], Quaternion.identity, transform);

        while (cubePosQueue_BFS.Count > 0)
        {
            currentCubePos = cubePosQueue_BFS.Dequeue();
            currentWorldPos = worldPosList[cubePosList.IndexOf(currentCubePos)];

            for (int j = 0; j < 3; j++)
            {
                nextCubePos = currentCubePos.CubePositionAdd(hexDirectionOffset[j]);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextCubePos.q >= -mapSize &&
                    nextCubePos.q <= mapSize &&
                    nextCubePos.r >= -mapSize * 2 &&
                    nextCubePos.s <= mapSize * 2)
                {
                    nextWorldPos = currentWorldPos + hexPositionOffset[j];

                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    cubePosQueue_BFS.Enqueue(nextCubePos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }

因為地塊貼圖的左下、下、右下是有邊緣的樣式,所以要隨機,又要不讓非邊緣的地塊不露餡,需要按下面的順序去生成才不會導致0顯示在3的上面,所以上面hexDirectionOffset沒有按照左下、下、右下、右上、上、左上而是按照左下、右下、下、右上、上、左上的原因。

 

另外,邊界條件的判斷,參考下圖的示意,x軸管左右兩邊,y軸管右下、左上,z軸管左下、右上。 

 

為方便生成固定尺寸矩形,需要參閱鏈接中Coordinate Systems的Offset coordinates部分,以及Coordinate conversion的Offset coordinates部分Odd-q,下文稱之為偏移坐標系。

 

以下是偏移坐標系轉換立方體坐標系的方法。

    private CubeCoordinate OffsetToCube_Oddq(int col, int row)
    {
        int x = col;
        int z = row - (col - (col & 1)) / 2;

        int y = -x - z;

        return new CubeCoordinate(x, y, z);
    }

 

以下是生成固定尺寸矩形的函數,需要注意的是為了保證地塊的疊加順序,所以生成是按下圖所示,一行一行生成的,01一行生成完,接23一行,最后45一行。

 

    private void CreateRectangleMap()
    {
        for (int i = 0; i < rectangleHeight * 2; i++)
        {
            for (int j = i % 2; j < rectangleWidth; j += 2)
            {
                cubePosList.Add(OffsetToCube_Oddq(i, j));

                Instantiate(landBlockPrefab, new Vector2(j * widthDistance, -heightDistance * 0.5f * i), Quaternion.identity, transform);
            }
        }
    }

 

有了上面的兩個坐標系的建立,后面兩個地圖的隨機就很好做了。

 

大豎六邊形的隨機,主要是在左下、右下其中的一個或兩個方向生成,邊界判斷同上文,。

    private void CreateRandomHexagonalMap()
    {
        Queue<CubeCoordinate> cubePosQueue_BFS = new Queue<CubeCoordinate>();

        CubeCoordinate currentCubePos;
        CubeCoordinate nextCubePos;

        Vector2 nextWorldPos;

        int times = 1;
        int curentDirection = -1;

        cubePosQueue_BFS.Enqueue(new CubeCoordinate(0, 0, 0));
        cubePosList.Add(new CubeCoordinate(0, 0, 0));
        worldPosList.Add(Vector2.zero);

        Instantiate(landBlockPrefab, Vector2.zero, Quaternion.identity, transform);

        while (cubePosQueue_BFS.Count > 0)
        {
            times = Random.Range(1, 3);

            currentCubePos = cubePosQueue_BFS.Dequeue();

            for (int i = 0; i < times; i++)
            {
                if (times == 1)
                {
                    curentDirection = Random.Range(0, 2);
                }
                else
                {
                    curentDirection = i;
                }

                nextCubePos = currentCubePos.CubePositionAdd(hexDirectionOffset[curentDirection]);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextCubePos.q >= -mapSize &&
                    nextCubePos.q <= mapSize &&
                    nextCubePos.r >= -mapSize * 2 &&
                    nextCubePos.s <= mapSize * 2)
                {
                    nextWorldPos = worldPosList[cubePosList.IndexOf(currentCubePos)] + hexPositionOffset[curentDirection];

                    cubePosQueue_BFS.Enqueue(nextCubePos);
                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }

 

 矩形內隨機地圖生成函數,如下。隨機也是在左下、右下其中的一個或兩個方向生成,有了偏移坐標系,邊界判斷就變得簡單了。當然,為了疊加順序,同樣按照上面的生成順序,一行by一行。

 

    private void CreateRandomRectangleMap()
    {
        Queue<OffsetCoordinate> offsetPosQueue_BFS = new Queue<OffsetCoordinate>();

        OffsetCoordinate currentOffsetPos;
        OffsetCoordinate nextOffsetPos;
        CubeCoordinate nextCubePos;

        Vector2 nextWorldPos;

        int[] direction = new int[2] { -1, 1 };

        int times = 1;
        int curentDirection = -1;

        for (int i = 0; i <= rectangleWidth; i += 2)
        {
            offsetPosQueue_BFS.Enqueue(new OffsetCoordinate(i, 0));
            cubePosList.Add(OffsetToCube_Oddq(i, 0));
            worldPosList.Add(new Vector2(i * widthDistance, 0));

            Instantiate(landBlockPrefab, new Vector2(i * widthDistance, 0), Quaternion.identity, transform);
        }

        while (offsetPosQueue_BFS.Count > 0)
        {
            times = Random.Range(1, 3);

            currentOffsetPos = offsetPosQueue_BFS.Dequeue();

            for (int i = 0; i < times; i++)
            {
                if (times == 1)
                {
                    curentDirection = direction[Random.Range(0, 2)];
                }
                else
                {
                    curentDirection = direction[i];
                }

                if (currentOffsetPos.col % 2 == 0)
                {
                    nextOffsetPos = currentOffsetPos.CubePositionAdd(new OffsetCoordinate(curentDirection, 0));
                }
                else
                {
                    nextOffsetPos = currentOffsetPos.CubePositionAdd(new OffsetCoordinate(curentDirection, 1));
                }
                
                nextCubePos = OffsetToCube_Oddq(nextOffsetPos.col, nextOffsetPos.row);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextOffsetPos.col >= 0 &&
                    nextOffsetPos.col <= rectangleWidth &&
                    nextOffsetPos.row >= 0 &&
                    nextOffsetPos.row <= rectangleHeight)
                {
                    if (nextOffsetPos.col % 2 == 0)
                    {
                        nextWorldPos = new Vector2(nextOffsetPos.col * widthDistance, -heightDistance * nextOffsetPos.row);
                    }
                    else
                    {
                        nextWorldPos = new Vector2(nextOffsetPos.col * widthDistance, -heightDistance * 0.5f - heightDistance * nextOffsetPos.row);
                    }

                    offsetPosQueue_BFS.Enqueue(nextOffsetPos);
                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }

 

下面貼出完整的代碼:

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

public class MapBehaviour : MonoBehaviour
{
    public struct CubeCoordinate
    {
        public int q;
        public int r;
        public int s;

        public CubeCoordinate(int q, int r, int s)
        {
            this.q = q;
            this.r = r;
            this.s = s;
        }

        public CubeCoordinate CubePositionAdd(CubeCoordinate offset)
        {
            return new CubeCoordinate(q + offset.q, r + offset.r, s + offset.s);
        }
    }

    public struct OffsetCoordinate
    {
        public int col;
        public int row;

        public OffsetCoordinate(int col, int row)
        {
            this.col = col;
            this.row = row;
        }

        public OffsetCoordinate CubePositionAdd(OffsetCoordinate offset)
        {
            return new OffsetCoordinate(col + offset.col, row + offset.row);
        }
    }

    public GameObject landBlockPrefab;

    public int hexSize;
    public int mapSize;

    public int rectangleWidth;
    public int rectangleHeight;

    private static readonly List<CubeCoordinate> hexDirectionOffset = new List<CubeCoordinate> {
            new CubeCoordinate(-1, 0, 1), new CubeCoordinate(1, -1, 0), new CubeCoordinate(0, -1, 1),
            new CubeCoordinate(1, 0, -1), new CubeCoordinate(0, 1, -1), new CubeCoordinate(-1, 1, 0)
    };

    private List<Vector2> hexPositionOffset;

    private List<CubeCoordinate> cubePosList;
    private List<Vector2> worldPosList;

    private float widthDistance;
    private float heightDistance;

    // Use this for initialization
    void Start()
    {
        InitHexsDistance();
        InitHexPosOffset();

        cubePosList = new List<CubeCoordinate>();
        worldPosList = new List<Vector2>();

        CreateHexagonalMap();
        //CreateRectangleMap();
        //CreateRandomHexagonalMap();
        //CreateRandomRectangleMap();
    }

    private void InitHexsDistance()
    {
        widthDistance = hexSize * 0.75f * 0.01f;
        heightDistance = Mathf.Sqrt(3f) * hexSize * 0.5f * 0.01f;
    }

    private void InitHexPosOffset()
    {
        hexPositionOffset = new List<Vector2> {
            new Vector2(-widthDistance, -heightDistance*0.5f),new Vector2(widthDistance, -heightDistance*0.5f), new Vector2(0,-heightDistance),
            new Vector2(widthDistance, heightDistance*0.5f),new Vector2(0, heightDistance), new Vector2(-widthDistance,heightDistance*0.5f)
        };
    }

    public float GetTwoHexDistance(CubeCoordinate a, CubeCoordinate b)
    {
        return (Mathf.Abs(a.q - b.q) + Mathf.Abs(a.r - b.r) + Mathf.Abs(a.s - b.s)) / 2;
    }

    private void CreateHexagonalMap()
    {
        Queue<CubeCoordinate> cubePosQueue_BFS = new Queue<CubeCoordinate>();

        CubeCoordinate currentCubePos;
        CubeCoordinate nextCubePos;
        Vector2 currentWorldPos;
        Vector2 nextWorldPos;

        cubePosList.Add(new CubeCoordinate(0, 0, 0));
        worldPosList.Add(Vector2.zero);

        cubePosQueue_BFS.Enqueue(cubePosList[0]);

        Instantiate(landBlockPrefab, worldPosList[0], Quaternion.identity, transform);

        while (cubePosQueue_BFS.Count > 0)
        {
            currentCubePos = cubePosQueue_BFS.Dequeue();
            currentWorldPos = worldPosList[cubePosList.IndexOf(currentCubePos)];

            for (int j = 0; j < 3; j++)
            {
                nextCubePos = currentCubePos.CubePositionAdd(hexDirectionOffset[j]);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextCubePos.q >= -mapSize &&
                    nextCubePos.q <= mapSize &&
                    nextCubePos.r >= -mapSize * 2 &&
                    nextCubePos.s <= mapSize * 2)
                {
                    nextWorldPos = currentWorldPos + hexPositionOffset[j];

                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    cubePosQueue_BFS.Enqueue(nextCubePos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }

    private void CreateRectangleMap()
    {
        for (int i = 0; i < rectangleHeight * 2; i++)
        {
            for (int j = i % 2; j < rectangleWidth; j += 2)
            {
                cubePosList.Add(OffsetToCube_Oddq(i, j));

                Instantiate(landBlockPrefab, new Vector2(j * widthDistance, -heightDistance * 0.5f * i), Quaternion.identity, transform);
            }
        }
    }

    private CubeCoordinate OffsetToCube_Oddq(int col, int row)
    {
        int x = col;
        int z = row - (col - (col & 1)) / 2;

        int y = -x - z;

        return new CubeCoordinate(x, y, z);
    }

    private void CreateRandomHexagonalMap()
    {
        Queue<CubeCoordinate> cubePosQueue_BFS = new Queue<CubeCoordinate>();

        CubeCoordinate currentCubePos;
        CubeCoordinate nextCubePos;

        Vector2 nextWorldPos;

        int times = 1;
        int curentDirection = -1;

        cubePosQueue_BFS.Enqueue(new CubeCoordinate(0, 0, 0));
        cubePosList.Add(new CubeCoordinate(0, 0, 0));
        worldPosList.Add(Vector2.zero);

        Instantiate(landBlockPrefab, Vector2.zero, Quaternion.identity, transform);

        while (cubePosQueue_BFS.Count > 0)
        {
            times = Random.Range(1, 3);

            currentCubePos = cubePosQueue_BFS.Dequeue();

            for (int i = 0; i < times; i++)
            {
                if (times == 1)
                {
                    curentDirection = Random.Range(0, 2);
                }
                else
                {
                    curentDirection = i;
                }

                nextCubePos = currentCubePos.CubePositionAdd(hexDirectionOffset[curentDirection]);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextCubePos.q >= -mapSize &&
                    nextCubePos.q <= mapSize &&
                    nextCubePos.r >= -mapSize * 2 &&
                    nextCubePos.s <= mapSize * 2)
                {
                    nextWorldPos = worldPosList[cubePosList.IndexOf(currentCubePos)] + hexPositionOffset[curentDirection];

                    cubePosQueue_BFS.Enqueue(nextCubePos);
                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }

    private void CreateRandomRectangleMap()
    {
        Queue<OffsetCoordinate> offsetPosQueue_BFS = new Queue<OffsetCoordinate>();

        OffsetCoordinate currentOffsetPos;
        OffsetCoordinate nextOffsetPos;
        CubeCoordinate nextCubePos;

        Vector2 nextWorldPos;

        int[] direction = new int[2] { -1, 1 };

        int times = 1;
        int curentDirection = -1;

        for (int i = 0; i <= rectangleWidth; i += 2)
        {
            offsetPosQueue_BFS.Enqueue(new OffsetCoordinate(i, 0));
            cubePosList.Add(OffsetToCube_Oddq(i, 0));
            worldPosList.Add(new Vector2(i * widthDistance, 0));

            Instantiate(landBlockPrefab, new Vector2(i * widthDistance, 0), Quaternion.identity, transform);
        }

        while (offsetPosQueue_BFS.Count > 0)
        {
            times = Random.Range(1, 3);

            currentOffsetPos = offsetPosQueue_BFS.Dequeue();

            for (int i = 0; i < times; i++)
            {
                if (times == 1)
                {
                    curentDirection = direction[Random.Range(0, 2)];
                }
                else
                {
                    curentDirection = direction[i];
                }

                if (currentOffsetPos.col % 2 == 0)
                {
                    nextOffsetPos = currentOffsetPos.CubePositionAdd(new OffsetCoordinate(curentDirection, 0));
                }
                else
                {
                    nextOffsetPos = currentOffsetPos.CubePositionAdd(new OffsetCoordinate(curentDirection, 1));
                }

                nextCubePos = OffsetToCube_Oddq(nextOffsetPos.col, nextOffsetPos.row);

                if (!cubePosList.Contains(nextCubePos) &&
                    nextOffsetPos.col >= 0 &&
                    nextOffsetPos.col <= rectangleWidth &&
                    nextOffsetPos.row >= 0 &&
                    nextOffsetPos.row <= rectangleHeight)
                {
                    if (nextOffsetPos.col % 2 == 0)
                    {
                        nextWorldPos = new Vector2(nextOffsetPos.col * widthDistance, -heightDistance * nextOffsetPos.row);
                    }
                    else
                    {
                        nextWorldPos = new Vector2(nextOffsetPos.col * widthDistance, -heightDistance * 0.5f - heightDistance * nextOffsetPos.row);
                    }

                    offsetPosQueue_BFS.Enqueue(nextOffsetPos);
                    cubePosList.Add(nextCubePos);
                    worldPosList.Add(nextWorldPos);

                    Instantiate(landBlockPrefab, nextWorldPos, Quaternion.identity, transform);
                }
            }
        }
    }
}

獲取兩個六邊形地塊之間的距離,在GetTwoHexDistance函數中,因為立方體坐標系的存在而變得很簡單了。:)

 

后面有時間,會出些講基於六角網格地圖的攻擊范圍、視線、尋路等等,還有unity的tilemap去做六角網格地圖的。

 

如果有更好的改進建議,歡迎交流。

 

轉載注明出處:)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM