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去做六角网格地图的。
如果有更好的改进建议,欢迎交流。
转载注明出处:)