HexMap學習筆記(一)——建立六邊形網格
HexMap學習筆記(一)——建立六邊形網格
HexMap學習筆記(二)——單元格顏色混合
HexMap學習筆記(三)——海拔高度與階梯連線
HexMap學習筆記(四)——不規則化
HexMap學習筆記(五)——更大的地圖
前言
事情源於前不久偶然翻到了Jasper Flick大佬在catlikecoding上的Unity部落格教程。

大致讀了一下教程,非常佩服這位作者。這應該是見過最細緻的教程,並沒有把大段的程式碼和效果圖一股腦丟給你看,而是詳細地說明每一個步驟的思路和原理,甚至會在一段程式碼上反覆改動只為讀者能完全弄明白。

極少有教程會像這樣去一點點的教你作者的思路,內容也是由淺顯到深幷包含許多綜合性的內容,非常具有學習價值。

在學習大有收穫之餘,我也希望如此精彩的教程能幫助到更多同學。在此選取HexMap這個系列教程整理翻譯。

關於選擇HexMap系列的原因
這個系列從簡單到複雜,有著很明顯的難度階梯曲線,比較適合有一定基礎的初學者入手學習。此教程包含的內容從演算法、3D數學到Unity的特性、C#語法等,都有涉及,內容相當豐富且綜合。並且地圖生成本身也是非常有趣和實用的內容。

完全體的漂亮地圖
當然,雖然我會盡己所能,但限於水準難免翻譯會有疏漏和詞不達意的位置。因此還是推薦有能力的同學直接學習英文原版。
另外,因為原版教程中程式碼的增刪改動會以黃色背景色表示,而知乎的程式碼編輯模組不具備這種功能,為了能最大限度保留作者原意,就直接以截圖的方式附上程式碼。
此係列教程的資源包從Unity5.3.1到Unity2017.3.0p3橫跨數個版本,為防止匯入出現版本衝突問題,我會統一使用Unity2018.3.0b12實現一遍,並在結尾附上當前教程的工程檔案。
原版教程地址:Unity C#and Shader Tutorials
本期原文地址:Hex Map 1
本篇難度:★☆☆☆☆
建立六邊形網格
此教程是六邊形地圖系列的第一部分。許多遊戲,尤其是戰略遊戲,都常使用六邊形的網格地圖。包括《奇蹟時代3》,《文明5》和《無盡傳奇》等等。我們將從簡單的基礎開始逐步新增功能,直到最終得到一個基於六邊形網格結構的複雜地圖。
此教程的知識基礎是假設你已經學完了以Procedural Grid開始的Mesh程式設計系列教程。工程由Unity5.3.1建立,中間橫跨多個Unity版本,最後一部分是用Unity2017.3.0p3完成。

一張基本的六邊形地圖
1.六邊形相關知識
為什麼要用六邊形?如果需要一個網格地圖,使用正方形應該更合理一點。正方形很容易繪製和定位座標,不過也有一個缺點。
觀察一下下圖中網格里的正方形和它的相鄰正方形:

正方形網格與其相鄰單元格
可以看到中間的正方形一共有八個相鄰正方形,其中四個在邊上相鄰,另外四個在角上相鄰。
假如正方形邊長為1,那麼邊上相鄰的正方形距離中間的正方形長度為1,而角上相鄰的正方形距離為√2。
這種距離的差別會在計算移動時導致很多問題。對於這種問題,不同的遊戲有不同的解決方式。其中一個就是用六邊形網格來代替正方形網格。

六邊形網格與其相鄰單元格
相比於正方形網格,六邊形網格的相鄰格由八個變成了六個,並且都是在邊上相鄰。也就是說每個相鄰格對於中間的單元格距離是一致的,這無疑簡化了很多事情。不過六邊形網格的構造比正方形要複雜一些,而這篇教程就是用來解決這個問題的。
在開始之間,我們先定義六邊形的大小。如下圖,以邊長10作為基準,所以圓心到每個角的距離也是10,因為六邊形是由六個等邊三角形組成的。我們把圓心到角的距離稱作六邊形的外徑。

六邊形的內徑和外徑
有外徑自然就有內徑,就是圓心到每條邊中心的距離。這個常量很重要,因為相鄰六邊形之間的距離剛好是這個值的兩倍。
內徑的長度等於外徑的倍,所以在當前邊長下內徑的值就是。把這些常量放在一個靜態類中方便取用。

內徑是如何計算的?
取組成六邊形的六個等邊三角形中的一個,內徑就是這個三角形的高,把這個三角形中中間分開成兩個直角三角形,即可用勾股定理求得。
對於邊長e,內徑為:

此外還需要定位中心的六邊形單元格。六邊形有兩種定位方式,要麼是尖的朝上,要麼是平的朝上。這裡我們選前一種方式,並把朝上的第一個角作為起點,然後順時針新增其餘角的頂點,在世界座標系的XZ平面構建六邊形。

可能的朝向

2網格構建
建立一個六邊形的網格就需要定義每一個單元格。為此建立一個HexCell指令碼。目前就空著,暫時還不需要任何單元格資料。

一開始很簡單,建立一個預設的Plane,把HexCell指令碼掛上去,並建立成預製體。

使用一個Plane作為單元格的預製體
下面輪到建立HexGrid指令碼,並定義寬度、高度和單元格預製體這幾個公共變數。在場景中新建一個空物件掛載這個指令碼。


六邊形網格物件
先從建立一個標準的正方形網格開始,並把單元格儲存在陣列裡,以便之後訪問。
預設的Plane是10乘10的大小,所以以此為基準調整每個單元格的位置。


Plane構成的正方形網格
如此一來就生成了一個嚴絲合縫的正方形網格,但同時也不好識別每個單元格分別的位置。對於正方形網格也許還比較容易推算出來,但在六邊形網格中就不好辦了。如果這時能分別看到所有單元格的座標就很方便了。
2.1顯示單元格座標
在場景中新增一個canvas元件(GameObject/UI/canvas)並使其成為HexGrid的子物體。因為這是一個純用來顯示資訊的canvas,所以可以刪除上面的附帶的raycaster元件。基於同樣的理由,自動新增的EventSystem也可以刪了,暫時都用不到。
把canvas的渲染模式設定為World Space,繞X軸旋轉90度。將其軸心和座標全部歸零,並給予一個輕微的垂直向上偏移量,這樣顯示的內容就會出現在網格上面。canvas的寬度和高度都不重要,我們自己會定位顯示內容,你可以全部歸零來消除場景中canvas的矩形預覽框。
最後把Dynamic Pixels Per Unit屬性修改為10,確保文字有一個合適的字型紋理解析度。

六邊形網格座標的Canvas
建立一個Text元件物件(GameObject/UI/text)並設為預製體,用來顯示座標。確保其錨點和軸心居中,大小設定為5x15。文字對齊方式也設定為水平和處置居中,字型大小設定為4。
最後,刪除預設顯示的文字也不啟用Rich Text。是否啟用RaycastTarger也不重要,canvas不會呼叫。

單元格座標標籤的預製體
現在HexGrid指令碼中需要引用canvas和text的預製體,新增UnityEngine.UI的頭部引用就能訪問這些型別。text預製體可以通過公共變數靜態引用,canvas就直接呼叫GetComponentInChildren()找到。


與標籤建立連線關係
指令碼中獲取text預製體的引用後就可以例項化並顯示每個單元格的座標,在X和Z之間新增一個換行符,這樣就能顯示在不同的行間。


座標可見
2.2六邊形的位置
現在我們可以直觀地識別每個單元格了,開始對單元格的位置進行移動。通過之前的分析我們可以知道在X軸方向上,每個六邊形單元格的相對距離是其內徑的2倍,同時每行之間的距離應該是其外徑的1.5倍。

六邊形相鄰的幾何關係


沒有偏移並使用六邊形網格每個單元格的距離切分開
當然,連續的六邊形單元格位置並不是在彼此的上下方,而應該在X軸上以內徑為基準偏移。我們可以把Z軸偏移的一半加到X軸上然後乘以內徑的兩倍。

與六邊形位置相符的菱形網格
現在的網格的形狀是是個菱形而不是矩形,由於使用矩形更為方便,我們強制讓Z軸上的單元格回到直線上。通過減去部分X軸上的偏移量來實現這個目的,每隔一行所有的X軸上的單元格應該都後退一點,具體的值通過相乘之前減去Z軸偏移的整數倍除以2就行了。


矩形區域內六邊形化的間隔
3.渲染六邊形
當位置確定後就可以把工作轉移到渲染實際的六邊形上了。首先移除預設的Plane,HexCell預製體上除了指令碼其它元件全部刪除。

不再使用Plane
就像在Mesh Basics系列教程中所做的一樣,我們用單個Mesh來呈現整個網格。但這一次不打算預先確定需要多少個頂點和三角形了,將使用列表來代替陣列作為儲存容器。
建立一個新的指令碼HexMesh來管理Mesh,它需要MeshFilter和MeshRenderer元件,一個Mesh物件與儲存其頂點和三角形的列表。

在HexGrid上建立一個空的子物件並掛載此指令碼,會自動新增MeshFilter和MeshRenderer元件。然後再新增一個預設的材質球。

六邊形網格物件
現在HexGrid指令碼可以像獲取canvas元件一樣獲取HexMesh。

當HexGrid呼叫Awake()獲取HexMesh後,HexMesh才能三角化單元格,所以這裡要確保呼叫順序在Awake()之後。在MonoBehaviour的生命週期裡Start()是在Awake()之後的,所以在這裡執行。

HexMesh.Triangulate()可能在任何時候呼叫,即使在之前已經對單元格三角化的情況下也是如此。所以首先從清理舊資料開始,然後迴圈遍歷所有單元格分別對它們進行三角剖分。這一步進行完之後將生成的頂點和三角形資料賦值給Mesh,最後重新計算Mesh的法線。

因為六邊形是三角形組成的,比較直觀的方式是通過給定的三個頂點建立新增三角形的方法,按順序往列表裡新增頂點並把這些頂點的下標相連形成三角形。第一個頂點的下標等於新增新頂點之前頂點列表的長度,所以在新增頂點之前先儲存它。

現在可以從第一個三角形開始試試效果,它的第一個頂點應該是六邊形的中心點,另外兩個頂點是相對於中心的第一個和第二個角的頂點。


每個單元格的第一個三角形
可以看到沒問題,接下來就迴圈六次渲染剩下的的三角形。

我們不能共用頂點麼?
當然可以,甚至可以做的更好,只使用四個,而不是六個三角形來組成一個六邊形。但不這麼做會讓接下來的工作更簡單,也許現在優化頂點數是個不錯的點子,但隨著教程的進展事情反而會變得更復雜。現在優化頂點和三角形只會礙事。
不過這樣寫會產生一個陣列越界的異常,這是因為最後一個三角形會試圖獲取不存在的第七個角。這時候應該繞回到第一個角作為它的最終頂點。但在不改變程式碼邏輯的情況下也可以用點小技巧,在HexMetrics.corners中複製第一個角作為第七個角,這樣就不用擔心陣列越界問題了。


完成六邊形
4.六邊形的座標
現在來觀察一下六邊形網格里每一個單元格的座標,Z軸方向的座標看起來沒問題,但是X軸方向的座標看起來是呈鋸齒狀的。這是我們為了讓網格整體呈現一個矩形而強行偏移每一行座標產生的副作用。

座標偏移,高亮顯示零座標軸線
直接處理六邊形網格的座標偏移不太方便,讓我們新增一個HexCoordinates結構體用來在不同的座標系中轉換。將其序列化以便Unity在執行模式也能識別它,並且在屬性中只公開get確保座標不可改動。

提前先建立一個轉換成常規偏移座標的靜態轉換方法,具體的等會寫,現在先直接返回引數的值。

再新增一個字串轉換方法。預設的ToString()方法返回的是結構體的型別名,這顯然沒法用。所以過載這個方法讓其返回當前結構的座標值。同樣的再新增一個分成兩行顯示座標的方法,因為現在的顯示用的就是這樣的格式。

現在可以給HexCell宣告一個座標了。

這時候就可以修改HexGrid.CreateCell,用新的座標獲取方法。

現在我們修改X軸上的座標,令其在一條直線上對齊,通過去掉水平偏移實現。



軸向座標
目前這個二維座標系可以讓我們在四個方向上描述移動和偏移量,但是在一個單元格中可是有六個相鄰單元格,剩下的兩個方向去哪了?這表明了其實還存在第三個維度,事實上水平翻轉一下X軸就可以得到那條缺失的Y軸。

Y軸出現
當X軸和Y軸互為映象的情況下,如果Z軸不變,那麼三個座標相加應該總會得到一樣的結果。事實上當你這麼做時會發現這個結果總是0。如果你在一個軸向上增加座標,另一個軸上就會減少,這就產生了六個可能的移動方向。這些座標通常稱為立方體座標,因為它是三維的,其拓撲結構類似於立方體。
因為所有的座標加起來一定等於0,所以你總是可以從其他兩個座標中得到另一個座標。我們目前已經儲存了X和Z的座標,就不需要儲存Y的座標了。可以建立一個屬性包含計算它的方法,並在ToString()方法中使用。


立體座標
4.1在inspector上顯示座標。
執行模式中選中網格中的一個單元格,只有HexCell.coordinates的字首名而並沒有顯示具體座標值。

Inspector上沒有顯示座標
雖然這不是什麼大問題,但如果能顯示座標值會顯得整潔美觀。現在沒有顯示是因為座標值沒有標記成序列化欄位,為此需要顯示定義一下。


難看並且可編輯
現在顯示倒是顯示出來了,但是是可編輯狀態的。我們並不想這樣,因為按道理來說座標是固定不變的,而且顯示在下面也不好看。
我們可以通過為HexCoordinates制定一個自定義的特性來讓它做得更好。建立一個HexCoordinatesDrawer指令碼放在Editor資料夾下,這是一個只用於編輯器的指令碼。這個類應該擴充套件自UnityEditor.PropertyDrawer,並需要UnityEditor.CustomPropertyDrawer特性來讓其正確關聯起來。

PropertyDrawers通過OnGUI()方法顯示其內容,該方法提供了要在其中繪製的螢幕矩形、序列化的屬性資料以及它所屬的欄位的標籤。

從屬性中提取X和Z的值並用它們新建一個座標,然後用過載的ToString()方法在指定位置繪製GUI標籤。


沒有標籤名的座標
這樣座標就顯示出來了,但是型別名丟失了。名字之類的通常使用EditorGUI.PrefoxLabel()方法繪製。有個額外的好處是它返回一個經過調整的矩形,與該標籤名右側的空間剛好匹配。


有標籤名的座標
5.點選單元格
如果不能互動那這個六邊形的網格也沒什麼意思。最基本的互動方式就是點選定位單元格,接下來我們就試著去實現它。現在先把這部分程式碼寫在HexGrid裡,一旦一切正常,就把這段程式碼搬到別的位置去。
實現這個功能可以通過滑鼠向場景打射線的方式,和Mesh Deformation教程中的使用方式一樣。

這些程式碼現在什麼都幹不了,我們需要現在網格上新增碰撞盒好讓射線能檢測到。

並在三角化之後給Collider的Mesh賦值。

不能就用BoxCollider麼?
可以,但是沒法精確吻合Mesh的輪廓。而且我們的Mesh不會一直都是平的,不過那是之後教程的部分了。
現在能點選網格了,但是不知道具體點選的是哪一個。為了弄清楚這個,需要從滑鼠點選的位置轉換成六邊形的座標。這是HexCoordinates的工作,所以在其中宣告一個靜態方法FromPosition()。

這個方法該怎麼計算選中的是哪個六邊形?可以同過X值除以六邊形的水平寬度,然後因為Y是X的映象,通過負的X得到Y。

不過這隻在Z為零的時候才是對的,Z的座標遞增時需要向左移動單元格。

現在X和Y計算完成,因為需要的是每個單元格中心的整數座標,所以四捨五入一下,Z的值也能推匯出來,然後構築最後的輸出座標。

看起來似乎沒問題,但這些座標都正確麼?仔細研究一下你會發現,有可能所有座標加起來不等於0。在這種情況下報一個警告確保真的發生了。

事實上得也到警告資訊了,似乎只發生在滑鼠點選的位置接近六邊形的邊界的時候。所以是四捨五入那出了問題,因為離單元格的中心越遠,四捨五入捨去的值就越多,所以我們做一個合理的假設:捨去值更大的座標是錯誤的。
最後解決方法就變成了廢棄具有最大舍去增量的座標值,然後用其它的兩個座標去重新構建它。這裡我們只需要去重建X和Z,不用為Y費神,因為Y本來就是由X和Z求得的。

5.1為單元格著色
顯現點選單元格的座標是正確的,在此基礎上做一些真正的互動:改變點選單元格的顏色。在HexGrid宣告一個預設的顏色和點選變化的顏色。


單元格顏色選擇
在HexCell中新增一個公共的顏色欄位:

在HexGrid.CreateCell()中賦值預設的顏色:

同樣的還要在HexMesh中新增顏色資訊:

當三角化時,我們也需要給每個三角形新增顏色。另外分出一個方法去實現這個功能。

回到HexGrid.TouchCell裡。整個流程就是先轉換單元格座標為儲存單元格的陣列下標。在四邊形網格中就是X+Z乘以寬度,但在這裡還需要加上一半的Z軸偏移。獲取到點選的單元格後改變其顏色,然後再次三角化整個Mesh。
我們真的需要重新三角化整個Mesh麼?
這裡可以玩點花樣,但現在還不是進行此類優化的時候。在後面的教程中Mesh會變得越來越複雜,現在做的任何假設和玩的花樣過些時候都會無效。但是重新三角化整個Mesh這種野蠻的方式無論在什時候都是有效的。

儘管現在顏色已經改變了,但我們什麼都沒看到。這是因為預設的著色器沒有應用頂點顏色。我們現在需要建立一個自定義的著色器(Assets/Create/Shader/Default Surface Shader),它只需要做兩處改動:
1.在輸入結構中新增顏色資訊
2.輸出時讓反射率與顏色相乘。
當材質不透明時只用關心RGB通道。

新建一個材質球應用這個自定義的著色器,然後替換原來的材質球,單元格的顏色就能顯示出來。

單元格著色效果
我得到了一些奇怪的陰影效果!
在某些Unity版本中,自定義表面著色器會遇到陰影問題。如果你遇到了陰影抖動或帶狀陰影問題,說明Z軸發生了衝突。校準方向光的陰影偏斜應該能解決這個問題。
6.地圖編輯
現在我們把顏色編輯功能做成一個專門的編輯器,這就超出了HexGrid的功能範圍了。所以把TouchCell()方法設為公共並新增額外的引數。同樣要刪除touchedColor欄位。

建立一個HexMapEditor指令碼,然後把Update和HandleInput方法移動到這裡,新增一個公共欄位引用HexGrid,一個儲存顏色的陣列,一個私有欄位保記錄當前顏色。最後新增一個公共方法選擇顏色,並確保當前選擇的顏色是陣列中的第一個。

另外新建一個canvas,這一次保持預設的設定,把HexMapEditor指令碼新增到這裡,新增若干顏色並把HexGrid拖入欄位中。這一次需要EventSystem元件了,並且它也會隨著新建canvas再次自動建立。

擁有4種顏色的地圖編輯器
在canvas上新增一個用來選擇顏色的皮膚,根據顏色數量新增若干toggle元件(Components/UI/toggle),放在螢幕的一角。

用Toggle Group實現調色盤
為每個toggle指定顏色,我們現在不需要花哨的UI,只要它足夠清楚易用就行。

一個Toggle一顏色
確保只有第一個toggle被選中,並確保都在一個toggle group中,這樣一次只能有一個toggle被選中。最後在編輯器中把他們與SelectColor()方法關聯起來。

第一個Toggle
這個事件在每次選擇變化時提供一個布林參數列明這個toggle是否被選中。但是我們不需要關心這個。相反我們需要手動提供一個符合顏色陣列下標的我們希望選中的整數型的引數。所以設定第一個為0,第二個為1,以此類推。
toggle的事件方法是什麼時候呼叫的?
當每次選中的toggle變化時,就會呼叫這個方法。如果這個方法有一個布林型別的引數,它會告訴我們這個toggle是否被選中。
當我們的toggle在一個組中,選中一個不同的toggle首先會取消選中當前被選中的toggle,然後選中現在選中的。這意味著SelectColor()方法會呼叫兩次。這是對的,因為第二次的呼叫才是我們需要關心的。

多種顏色著色
雖然UI只是一個功能性的元件,但是會有一個煩人的細節。移動UI皮膚使其覆蓋到六邊形網格上。在選擇顏色時同時也會改變UI下的單元格的顏色。同時進行UI和場景中的互動是衝突的。
可以通過詢問事件系統是否檢測到滑鼠位於某個物件之上來解決這個問題。因為它只能檢測UI物件,這表明我們在與UI互動。所以我們應該只在這種情況下自己處理輸入。

本期工程檔案:tank1018702/Hex-Map-Learning
有想系統學習遊戲開發的童鞋,歡迎訪問http://levelpp.com/。
原地址:https://zhuanlan.zhihu.com/p/54640839
相關文章
- HexMap學習筆記(六)——河流筆記
- HexMap學習筆記(二)——單元格顏色混合筆記
- HexMap學習筆記(七)——道路筆記
- HexMap學習筆記(八)——水體筆記
- HexMap學習筆記(九)——地形特徵筆記特徵
- HexMap學習筆記(四)——不規則化筆記
- HexMap學習筆記(五)——更大的地圖筆記地圖
- Laravel學習筆記七-建立部落格Laravel筆記
- 從零開始做一個SLG遊戲(一):六邊形網格遊戲
- 計網學習筆記六 Network Layer Overview筆記View
- (Django)18.3建立網頁:學習筆記主頁Django網頁筆記
- 網路學習筆記(一):TCP連線的建立與關閉筆記TCP
- 不規則圖形背景排版高階技巧 -- 酷炫的六邊形網格背景圖
- 六邊形架構架構
- vue學習筆記(六) ----- vue元件Vue筆記元件
- python學習筆記(六)——函式Python筆記函式
- 如何利用CSS寫一個六邊形?CSS
- orientDB學習筆記(一)六度分隔理論筆記
- Networking && Internet 計網學習筆記一筆記
- 2394 輸出六邊形
- springcloud學習筆記(六)Spring Cloud ZuulSpringGCCloud筆記Zuul
- Vue學習筆記(六) 長樂未央Vue筆記
- 學習筆記(二十四):ArkUi-網格 (Grid/GridItem)筆記UI
- 學習筆記(一)筆記
- IDL建立泰森多邊形
- MySQL學習筆記——建立與約束MySql筆記
- 圖形學學習筆記二:觀測變換筆記
- ES6學習筆記(六)【promise,Generator】筆記Promise
- Vue學習筆記(六):監視屬性Vue筆記
- hive學習筆記之六:HiveQL基礎Hive筆記
- domain-driven-hexagon: 領域驅動六邊形架構學習資料AIGo架構
- Mudo C++網路庫第六章學習筆記C++筆記
- kitten 學習教程(一) 學習筆記筆記
- Angular 學習筆記(一)Angular筆記
- React 學習筆記【一】React筆記
- vue學習筆記一Vue筆記
- Canvas學習筆記(一)Canvas筆記
- Jquery學習筆記(一)jQuery筆記