六邊形地圖相較四方地圖的優(yōu)勢:只有6個鄰居而且每個鄰居到中心的距離都是一樣的榔幸。而四方地圖有8個鄰居包含2種情況奏路,一種是邊鄰居爱榕,一種是角鄰居阵幸,難以統(tǒng)一處理花履。
開始之前先確定一下六邊形的大小。假定邊長是10個單位挚赊。因為六邊形由6個等邊三角形組成诡壁,所以外徑就是邊長。內(nèi)徑就是三角的高荠割,√3/2*10=5√3妹卿,這些值用靜態(tài)變量存起來旺矾。
using UnityEngine;
public static class HexMetrics {
public const float outerRadius = 10f;
public const float innerRadius = outerRadius * 0.866025404f;
}
接下來確定6個點相對中心的位置。注意到有2種擺放六邊形的方式夺克,角朝上或者邊朝上箕宙。我們選擇角朝上。從這個角開始铺纽,其它角順時針擺放柬帕。順著XZ平面擺放,六邊形們就能貼著地面方向了狡门。
public 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)
};
- 網(wǎng)格構(gòu)造
按最簡單的方式來陷寝,創(chuàng)建一個默認的plane,把cell組件加上去其馏,然后做成prefab凤跑。
using UnityEngine;
public class HexCell : MonoBehaviour
{
}
然后來做網(wǎng)格。創(chuàng)建一個空對象把HexGrid組件給它叛复。
using UnityEngine;
public class HexGrid : MonoBehaviour
{
public int width = 6;
public int height = 6;
public HexCell cellPrefab;
}
我們從一個常規(guī)的方形網(wǎng)格開始仔引。把單元存在數(shù)組里方便訪問。
默認的單元大小是10X10致扯,把每個格子依次加上偏移量肤寝。
HexCell[] cells;
void Awake()
{
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++);
}
}
}
void CreateCell(int x, int z, int i)
{
Vector3 position;
position.x = x * 10f;
position.y = 0f;
position.z = z * 10f;
HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
cell.transform.SetParent(transform, false);
cell.transform.localPosition = position;
}
一個嚴(yán)絲合縫的10X10網(wǎng)格。但是看不出哪是哪抖僵。
2.1顯示坐標(biāo)
創(chuàng)建一個Canvas然后將他變成Grid的子對象鲤看。渲染模式變成World Space,繞X軸旋轉(zhuǎn)90度讓Canvas躺在地上。
為了顯示坐標(biāo)耍群,創(chuàng)建一個文本對象通過GameObject/ui/text然后變成prefab义桂。
在HexGrid里創(chuàng)建一個變量CellLablePrefab
void CreateCell(int x, int z, int i)
{
…
Text label = Instantiate<Text>(cellLabelPrefab);
label.rectTransform.SetParent(gridCanvas.transform, false);
label.rectTransform.anchoredPosition = new Vector2(position.x, position.z);
label.text = x.ToString() + "\n" + z.ToString();
}
2.2六邊形位置
現(xiàn)在我們可以可視地定位每個格子了,來擺放它們吧蹈垢!我們知道相鄰格子間的沿X軸的距離等于內(nèi)徑的2倍慷吊。而距離下一行的距離是1.5倍的外徑。
position.x = x * (HexMetrics.innerRadius * 2f);
position.y = 0f;
position.z = z * (HexMetrics.outerRadius * 1.5f);
當(dāng)然格子們不是直直的放成一行行的而是交錯放的溉瓶,每行的在X軸的偏移量是內(nèi)徑
position.x = (x +(z % 2)*0.5f) * (HexMetrics.innerRadius * 2f);
3.渲染六邊形
我們用一個Mesh去畫整個網(wǎng)格。創(chuàng)建一個HexMesh組件來創(chuàng)建Mesh谤民。需要一個mesh filter, mesh renderer, mesh堰酿,有頂點和三角面列表。
using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class HexMesh : MonoBehaviour
{
Mesh hexMesh;
List<Vector3> vertices;
List<int> triangles;
void Awake()
{
GetComponent<MeshFilter>().mesh = hexMesh = new Mesh();
hexMesh.name = "Hex Mesh";
vertices = new List<Vector3>();
triangles = new List<int>();
}
}
創(chuàng)建一個新的子對象綁上HexMesh张足。然后給一個默認材質(zhì)触创。
public void Triangulate(HexCell[] cells)
{
hexMesh.Clear();
vertices.Clear();
triangles.Clear();
for (int i = 0; i < cells.Length; I++)
{
Triangulate(cells[I]);
}
hexMesh.vertices = vertices.ToArray();
hexMesh.triangles = triangles.ToArray();
hexMesh.RecalculateNormals();
}
void Triangulate(HexCell cell)
{
}
既然六邊形是用三角面組成的,那就創(chuàng)建一個快捷方法給定三個頂點就能添加三角面为牍。注意第一個點的索引就是添加點之前點列表的長度哼绑,先存起來岩馍。
void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3)
{
int vertexIndex = vertices.Count;
vertices.Add(v1);
vertices.Add(v2);
vertices.Add(v3);
triangles.Add(vertexIndex);
triangles.Add(vertexIndex + 1);
triangles.Add(vertexIndex + 2);
}
來畫第一個三角形
void Triangulate(HexCell cell)
{
Vector3 center = cell.transform.localPosition;
AddTriangle(
center,
center + HexMetrics.corners[0],
center + HexMetrics.corners[1]
);
}
循環(huán)畫6個,但是i+1會超抖韩。所以存corner的時候復(fù)制第一個元素在最后面蛀恩,就省得判斷有沒有超出了。
Vector3 center = cell.transform.localPosition;
for (int i = 0; i< 6; i++) {
AddTriangle(
center,
center + HexMetrics.corners[I],
center + HexMetrics.corners[i + 1]
);
}
六邊形坐標(biāo)
上面那張圖帽蝶,Z軸表現(xiàn)得挺好的赦肋,但是 X軸彎彎曲曲的。
我們來添加一個六邊形坐標(biāo)系結(jié)構(gòu)用來轉(zhuǎn)換不同的坐標(biāo)系
using UnityEngine;
[System.Serializable]
public struct HexCoordinates
{
public int X { get; private set; }
public int Z { get; private set; }
public HexCoordinates(int x, int z)
{
X = x;
Z = z;
}
X軸的偏移。EX:(0,2)原本會在(0,0)隔一行的正上方囱井,而正確的位置應(yīng)該是往右偏一格驹尼。以此類推,偶行+Z/2(注意Z是整型庞呕,結(jié)果會取整)
public static HexCoordinates FromOffsetCoordinates(int x, int z)
{
return new HexCoordinates(x - z / 2, z);
}
}
二維坐標(biāo)可以很明確的用來描述6個方向中的4個新翎。變化X就是X軸方向的變化 ,變化Z就是左下到右上的位置住练。這意味著我們需要第三維地啰,只要把X坐標(biāo)取反就得到了Y坐標(biāo)
由于X,Y軸是對方的鏡像讲逛,所以保持Z不變亏吝,把坐標(biāo)加起來總是得到同一個值。實際上盏混,如果把所有坐標(biāo)都相加會得到0.如果你增加一個坐標(biāo)蔚鸥,就要減少另外一個。這個屬性非常像立方體坐標(biāo)系许赃,同樣都是3個維度止喷,拓撲結(jié)構(gòu)類似于立方體。
因為總共為0混聊,所以得知其中兩個就能推算出剩下那個弹谁,所以Y不必存。
public int Y
{
get
{
return -X - Z;
}
}
public override string ToString()
{
return "(" +
X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")";
}
public string ToStringOnSeparateLines()
{
return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString();
}
4.1 Inspector里的坐標(biāo)
定義一個屬性繪制器句喜。創(chuàng)建HexCoordinatesDrawer
腳本然后放在Editor文件夾下预愤。類繼承自 UnityEditor.PropertyDrawer
而且需要UnityEditor.CustomPropertyDrawer
把它關(guān)聯(lián)到正確的類型上。
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(HexCoordinates))]
public class HexCoordinatesDrawer : PropertyDrawer
{
}
Property drawers通過OnGUI
渲染其內(nèi)容藤滥。提供了屏幕坐標(biāo)鳖粟、系列化屬性和屬性名稱標(biāo)簽。
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
HexCoordinates coordinates = new HexCoordinates(
property.FindPropertyRelative("x").intValue,
property.FindPropertyRelative("z").intValue
);
position = EditorGUI.PrefixLabel(position, label);
GUI.Label(position, coordinates.ToString());
}
EditorGUI.PrefixLabel
用來加前綴標(biāo)簽拙绊,并且返回新的位置
- 觸摸格子
用Physics.Raycast
實現(xiàn)向图,前提是給HexMesh
一個mesh collider泳秀。
MeshCollider meshCollider;
void Awake()
{
GetComponent<MeshFilter>().mesh = hexMesh = new Mesh();
meshCollider = gameObject.AddComponent<MeshCollider>();
…
}
在 triangulating后把mesh給collider
public void Triangulate(HexCell[] cells)
{
…
meshCollider.sharedMesh = hexMesh;
}
現(xiàn)在需要確定點擊到的是哪個格子。在HexCoordinates
定義一個FromPosition
進行轉(zhuǎn)化榄攀。
public void TouchCell(Vector3 position)
{
position = transform.InverseTransformPoint(position);
HexCoordinates coordinates = HexCoordinates.FromPosition(position);
Debug.Log("touched at " + coordinates.ToString());
}
如果Z等于0的話嗜傅,X,Y互為相反數(shù)。
public static HexCoordinates FromPosition(Vector3 position)
{
float x = position.x / (HexMetrics.innerRadius * 2f);
float y = -x;
}
沿著z軸移動檩赢,每二行就出現(xiàn)X-1,Y-1的情況吕嘀。
float offset = position.z / (HexMetrics.outerRadius * 3f);
x -= offset;
y -= offset;
取整
int iX = Mathf.RoundToInt(x);
int iY = Mathf.RoundToInt(y);
int iZ = Mathf.RoundToInt(-x - y);
return new HexCoordinates(iX, iZ);
加個LOG驗證
if (iX + iY + iZ != 0) {
Debug.LogWarning("rounding error!");
}
return new HexCoordinates(iX, iZ)
發(fā)生問題的是當(dāng)點是靠近在格子邊緣的時候(注:X和Z算行數(shù)的時候2行之前是有交疊的部分的),取整導(dǎo)致的問題贞瞒。離格子中心越遠則誤差越大偶房。那可以認為誤最大的那個方向是錯的道媚。
if (iX + iY + iZ != 0)
{
float dX = Mathf.Abs(x - iX);
float dY = Mathf.Abs(y - iY);
float dZ = Mathf.Abs(-x - y - iZ);
if (dX > dY && dX > dZ) {
iX = -iY - iZ;
}
else if (dZ > dY) {
iZ = -iX - iY;
}
}
5.1給六邊形著色
給HexGrid
一個默認色和點擊色
public Color defaultColor = Color.white;
public Color touchedColor = Color.magenta;
給HexCell
一個顏色字段酪我。并在創(chuàng)建格子的時候把默認色給它
public class HexCell : MonoBehaviour
{
public HexCoordinates coordinates;
public Color color;
…
void CreateCell(int x, int z, int i)
{
…
cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z);
cell.color = defaultColor;
…
}
}
還得把顏色信息給HexMesh
List<Color> colors;
void Awake()
{
…
vertices = new List<Vector3>();
colors = new List<Color>();
…
}
public void Triangulate(HexCell[] cells)
{
hexMesh.Clear();
vertices.Clear();
colors.Clear();
…
hexMesh.vertices = vertices.ToArray();
hexMesh.colors = colors.ToArray();
…
}
在triangulating的時候。我們順便把顏色信息設(shè)置了逗扒。另外寫個方法AddTriangleColor來處理這事
void Triangulate(HexCell cell)
{
Vector3 center = cell.transform.localPosition;
for (int i = 0; i < 6; i++)
{
AddTriangle(
center,
center + HexMetrics.corners[i],
center + HexMetrics.corners[i + 1]
);
AddTriangleColor(cell.color);
}
}
void AddTriangleColor(Color color)
{
colors.Add(color);
colors.Add(color);
colors.Add(color);
}
回到HexGrid.TouchCell
乒融。首頁找到格子坐標(biāo)在數(shù)組里的正確索引掰盘,如果是個方形的地圖,那就應(yīng)該是(X+Z)*WIDTH赞季。但我們這種情況愧捕,還需要加上半個Z的偏移。然后取出相應(yīng)的格子申钩,改變顏色 次绘。再重新triangulate一遍。其實也并不用重新triangulate典蜕,之后優(yōu)化的教程會說到
public void TouchCell(Vector3 position)
{
position = transform.InverseTransformPoint(position);
HexCoordinates coordinates = HexCoordinates.FromPosition(position);
int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2;
HexCell cell = cells[index];
cell.color = touchedColor;
hexMesh.Triangulate(cells);
}
雖然改變了顏色 断盛,但是并沒有看到效果。因為默認的shader不會用的頂點顏色愉舔。創(chuàng)建 Assets / Create / Shader / Default Surface Shader钢猛。改兩個地方: input加上color屬性,albedo* color轩缤。只關(guān)心RGB命迈,因為我們是不透明的。然后新建個材質(zhì)用這個shader火的。
Shader "Custom/VertexColors" {
Properties {
_Color("Color", Color) = (1,1,1,1)
_MainTex("Albedo (RGB)", 2D) = "white" {}
_Glossiness("Smoothness", Range(0,1)) = 0.5
_Metallic("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
float4 color : COLOR; //這里加上顏色
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf(Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb * IN.color; //改這里
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
注:如果陰影扭曲或者動來動去的話壶愤,是因為Z值沖突。調(diào)整方向光的shadow bias 就能解決馏鹤。