六邊形地圖

六邊形地圖相較四方地圖的優(yōu)勢:只有6個鄰居而且每個鄰居到中心的距離都是一樣的。而四方地圖有8個鄰居包含2種情況,一種是邊鄰居,一種是角鄰居,難以統(tǒng)一處理。


六邊形地圖和他的鄰居

開始之前先確定一下六邊形的大小。假定邊長是10個單位。因為六邊形由6個等邊三角形組成,所以外徑就是邊長。內(nèi)徑就是三角的高,√3/2*10=5√3,這些值用靜態(tài)變量存起來。


六邊形的內(nèi)徑和外徑.png
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)
    };
  1. 網(wǎng)格構(gòu)造
    按最簡單的方式來,創(chuàng)建一個默認的plane,把cell組件加上去,然后做成prefab。
using UnityEngine;

public class HexCell : MonoBehaviour
{
}
用一個plane來做六邊形prefab

然后來做網(wǎng)格。創(chuàng)建一個空對象把HexGrid組件給它。

using UnityEngine;

public class HexGrid : MonoBehaviour
{

    public int width = 6;
    public int height = 6;

    public HexCell cellPrefab;

}
HexGrid對象

我們從一個常規(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;
}
方塊網(wǎng)格

一個嚴絲合縫的10X10網(wǎng)格。但是看不出哪是哪。

2.1顯示坐標
創(chuàng)建一個Canvas然后將他變成Grid的子對象。渲染模式變成World Space,繞X軸旋轉(zhuǎn)90度讓Canvas躺在地上。


顯示坐標用的canvas

為了顯示坐標,創(chuàng)建一個文本對象通過GameObject/ui/text然后變成prefab。

單元格標簽prefab

在HexGrid里創(chuàng)建一個變量CellLablePrefab


把標簽預設(shè)關(guān)聯(lián)給腳本
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);
使用六邊形距離,不加偏移量

當然格子們不是直直的放成一行行的而是交錯放的,每行的在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ì)。


HexMesh對象
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的時候復制第一個元素在最后面,就省得判斷有沒有超出了。

Vector3 center = cell.transform.localPosition;
        for (int i = 0; i< 6; i++) {
            AddTriangle(
                center,
                center + HexMetrics.corners[I],
                center + HexMetrics.corners[i + 1]
            );
        }
完成的六邊形

六邊形坐標
上面那張圖,Z軸表現(xiàn)得挺好的,但是 X軸彎彎曲曲的。

坐標偏移,高亮第0行

我們來添加一個六邊形坐標系結(jié)構(gòu)用來轉(zhuǎn)換不同的坐標系

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);
    }
}
軸坐標

二維坐標可以很明確的用來描述6個方向中的4個。變化X就是X軸方向的變化 ,變化Z就是左下到右上的位置。這意味著我們需要第三維,只要把X坐標取反就得到了Y坐標

紅色代表X,綠色代表Y

由于X,Y軸是對方的鏡像,所以保持Z不變,把坐標加起來總是得到同一個值。實際上,如果把所有坐標都相加會得到0.如果你增加一個坐標,就要減少另外一個。這個屬性非常像立方體坐標系,同樣都是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里的坐標
定義一個屬性繪制器。創(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)容。提供了屏幕坐標、系列化屬性和屬性名稱標簽。

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用來加前綴標簽,并且返回新的位置

  1. 觸摸格子

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ā)生問題的是當點是靠近在格子邊緣的時候(注:X和Z算行數(shù)的時候2行之前是有交疊的部分的),取整導致的問題。離格子中心越遠則誤差越大。那可以認為誤最大的那個方向是錯的。

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;
選擇顏色設(shè)置.png

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。首頁找到格子坐標在數(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 就能解決。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374