前言

從這篇開始難度陡然提高了不少,不過雖然在三角剖分這塊略微複雜一些,但也不算很難,主要就是計算繁瑣。大家可以在紙上畫一下方便理解。

本篇原文地址:Hex Map 3


本篇難度:☆☆☆

海拔高度與階梯連接

此教程是HexMap的第三部分,這次會添加一個方法讓單元格處於不同的海拔高度,並用一個特殊的方法去連接它們。

單元格的高度與階梯化的連接

1.單元格的高度

我們之前在平坦的區域把地圖分成了不同的六邊形單元格,現在要給每個單元格加上高度變化。我們將用高度等級來表示,在HexCell裏新建一個整數欄位表示這個值。

高度等級每一級可以是任何值,在HexMetrics裏另外定義一個常量表示。這裡用5標準單位作為每層的高度值,變化看起來會比較明顯,如果是實際遊戲中可以適當減小每層高度。

1.1編輯單元格

之前我們只能編輯單元格的顏色,而現在要添加編輯高度的功能,所以現在HexGrid.ColorCell方法已經不夠用了。而且考慮到以後可能給每個單元格添加更多的可編輯選項,這需要一個新的編輯方法。

把ColorCell重命名為GetCell,並把編輯顏色的功能變為返回一個指定位置的單元格。現在不再作任何改變操作,也不再直接三角化每個單元格。

現在由編輯器自己來修改單元格,之後在重新三角化一次。為此添加一個公共方法HexGrid.Refresh()去處理。

修改HexMapEditor裏的代碼讓其調用新方法,添加一個EditCell的新方法負責單元格的編輯,然後刷新網格。

可以簡單地為正在編輯的單元格分配一個選定的高度等級來調整單元格的海拔高度。

和顏色一樣,需要添加一個方法去設置有效的高度等級,並與UI相關聯。在這裡用UI裏的Slider組件去調整高度等級。由於Slider組件接收的是float類型的值,所以我們的方法也需要一個float類型的參數,然後再轉換為整數。

在Canvas上添加一個Slider組件(GameObject / Create / Slider)並調整位置到調色板的下面。設置其為豎直方向,視覺上比較吻合調整高度的組件。限制它的取值上下範圍在一個合理的區間,比如說0-6。接著把OnValueChanged事件與HexMapEditor裏的SetElevation方法相連。

編輯高度的滑動條組件

1.2高度可視化

現在編輯單元格可以同時改變顏色和高度等級了,你可以在Insperctor裏看到高度確實改變了,但是三角化時並未應用。

現在需要在高度變化時修改垂直方向的本地坐標,為了讓這一步更簡化一些,把HexCell.elevation設為私有類型同時添加一個HexCell.Elevation公共欄位。

現在就能在編輯高度時修改垂直坐標。

當然別忘了修改HexMapEditor.EditCell裏的賦值。

不同高度的單元格

MeshCollider是否能吻合新的高度地圖?

舊版本的Unity需要在設置相同的Mesh信息之前先把MeshCollider設為null,它只是假設Mesh的數據不會改改變,所以只有不同的Mesh(或者null)能觸發碰撞器的刷新,現在已經沒這個必要了。所以我們現在的方法:在三角化之後重新分配碰撞器的Mesh信息,這是可行的。

現在單元格的高度就明顯可見了,但是還有兩個問題:

第一:單元格顯示坐標的標籤在被升高的單元格下面消失了。

第二:單元格的連接沒有考慮到高度。

現在我們就來解決這兩個問題。

1.3 單元格坐標標籤複位

就當前情況下,單元格的坐標UI標籤只有在開始的時候創建和定位了.然後就沒有管了。要更新標籤的垂直坐標就得獲取引用,所以給每個HexCell一個自己UI標籤的RectTransform的引用,以便接下來去更新它。

在HexGrid.CreateCell結尾時給它們賦值。

現在可以擴展HexCell.Elevation的set方法,讓其同時校準坐標UI標籤的位置。因為Canvas組件之前我們旋轉過,所以應該是在Z軸的負方向移動而不是Y軸。

坐標標籤與單元格的高度同步

1.4 創建傾斜連接

下面把平坦的連接變為傾斜的連接,這一步的方法已經在HexMesh.TriangulateConnection裏完成了。在邊界相連的情況下需要覆蓋高度坐標到連接終點兩端。

在角相連的情況下則需要對每個相連接的單元格做一樣的操作。

不同高度單元格之間的連接

這樣不同海拔高度之間的連接就完成了,可以看到所有的傾斜面都用正確的方式連接在一起,但這還不算完,繼續讓連接方式變得更有意思一些。


2.階梯狀的邊界連接

筆直的傾斜連接看起來沒什麼意思,我們可以讓它分成階梯狀的好幾段。《無盡傳說》就是其中一個這麼做的遊戲。

例如我們可以在一個傾斜面上插入兩段階梯,結果就是一個完整的大斜坡被分為三個小斜坡,中間用平坦的部分相連,這需要我們把連接分為五個部分來看。

一個斜坡中插入兩段梯形

我們可以在HexMetrics裏定義每個斜坡中插入梯形的數量,並由此計算出分成部分的數量。

理想情況下,我們可以簡單地沿著斜率對每一步進行插值。這並不完全是不重要的,因為Y坐標只能在奇數步上變化,而不能在偶數步上變化。否則我們就得不到平坦的類似梯田的形狀。讓我們在HexMetrics中添加一個特殊的插值方法來解決這個問題。

水平方向的插值是筆直向前的,只要我們知道插值的步長就行。

兩個值之間的插值是怎麼計算的?

兩個值 ab 之間的插值需要第三個插入值 t ,當 t 是0,結果就是 a 。當 t 是1時結果就是 b 。當 t 在0和1之間的某個點時, ab 是一個混合比例。所以插值的公式是 (1-t)a+tb 這個公式可以變成 (1-t)a+tb=a-ta+tb=a+t(b-a) 。插值的第三個值相當於向量 (b-a) 上從 ab 的移動,這需要的乘法計算量更少。

為了只在奇數步修改Y的坐標,我們可以使用 frac{step+1}{2} 計算。如果我們只取商的整數部分,相當於把1,2,3,4的序列變為1,1,2,2。

同樣添加顏色的階梯插值計算方法,僅當連接處是平的時候計算插值。

2.1 三角剖分

因為邊界連接處的三角剖分更複雜了,所以把HexMesh.TriangulateConnection中的相關代碼提取出來放到一個新方法中,把原先代碼注釋掉以便等會作為參考。

先處理第一步,用我們的特殊插值方法創建第一個四邊形,會得到一個比原來斜率更陡的短斜坡。

階梯化的第一步

現在直接跳過其他步驟到最後一步,儘管此時還不是正確形狀,但先完成邊界連接。

階梯化的最後一步

中間跳過的步驟可以放在一個循環中,每一步中上一次計算的最後兩個頂點作為這一步開始的兩個頂點,顏色賦值也是同樣的方法。這樣一來新的向量和顏色就計算出來了,另外幾個四邊形也添加進去了。

單元格之間的所有階梯化步驟

現在每個單元格的邊界連接都有兩次階梯變換,或者你也可以在HexMetrice.TerracesPerSlope中修改變換的次數。當然,我們還沒有對角落連接進行階梯化處理,這部分放到後面來完成。

階梯化連接所有的邊界

3 連接類型

把所有的邊界連接都進行階梯化處理似乎不太妥當,當兩個相鄰單元格之間的高度相差不大的情況下看起來還行,但差距較大時這樣處理會產生一個狹長的階梯狀大跳躍,這不太好看。另外還有平坦連接情況下就更不需要進行階梯化處理了。

我們把不同高度的連接情況抽象為三種類型:Flat(平坦),Slope(傾斜),Cliff(陡峭),並為此創建一個新的枚舉類型。

為了確認是哪種連接類型,可以在HexMetrice裏添加一個方法,基於連個高度等級去獲取連接類型。

如果高度是一樣的,那自然就是平坦的。

如果高度等級的差值剛好為1,就是傾斜類型。除此之外,無論是大於1還是小於-1,就只能是陡峭類型了。

同樣也在HexCell裏添加一個便於獲取邊界類型的方法。

是否需要在所有方向上都要檢查存不存在相鄰單元格?

你可能最終會在恰好位於地圖邊界上的方向獲取邊緣類型。在那種情況下是沒有相鄰單元格的,然後我們也會得到一個空引用異常。當然可以在方法內就檢查這個,然後如果發生這種情況拋出某種異常。但是那種情況下,異常就已經發生了,所以這是多此一舉,除非你想拋出一個自定義的異常類型。需要明確的是,我們只會在我們知道不是處理地圖邊界的時候才會使用這個方法,如果在某些地方出錯了,我們肯定會得到一個空引用異常。

3.1限制只讓傾斜類型階梯化

現在已經能確認,哪種連接類型才需要階梯化。修改一下HexMesh.TriangulateConnection讓它只在處理傾斜類型時候階梯化。

這裡可以重新啟用之前注釋的代碼,負責平坦和陡峭類型的連接情況。

只有傾斜類型會進行階梯化處理

4.角落連接的階梯化

角落連接比邊界連接更為複雜,因為它連接了三個不同單元格,而不是僅僅兩個。每個角連接的三個邊界類型可能是任意一種情況,所以可能性很多。因此最好另外添加一個角落專用的階梯化方法到HexMesh裏。

新的方法需要角落三角形的三個頂點和連接的單元格。為了便於管理,我們按順序整理找到最低高度的單元格,然後從底部開始從左往右階梯化。

單元格角落上的連接

現在TriangulateConnection裏需要確認哪一個是高度最低的單元格。首先檢查將被三角化的單元格是否高度低於其相鄰單元格,或者就是最低的單元格。在這種情況下我們可以把它當做底部單元格使用。

如果最裡面的檢測是false, 說明另一個相鄰單元格是最低高度的單元格。這時需要逆時針轉動三角形的參數到正確的方向。

如果第一次高度檢測就是false,情況就變成了分辨出兩個相鄰單元格誰纔是最低的。如果邊界上的相鄰單元格是最低的,就要順時針旋轉參數順序,否則逆時針旋轉。

注:這裡刪去底下原本直接添加三角形的代碼
逆時針旋轉,不旋轉,和順時針旋轉

4.1傾斜連接的三角剖分

在去想如何三角化角落連接區域時,先需要知道處理的是哪種邊界類型。為簡化這一步,在HexCell裏添加一個新方法,獲取任意兩個單元格連接方式。

在HexMesh.TriangulateCorner裏使用新方法去確認左右的邊界類型。

如果兩邊都是傾斜類型,就需要在左右兩邊都進行階梯化。因為底部單元格是最低的,所以這些傾斜類型都是向上的。此外這也意味著左右兩邊單元格有相同的高度,所以上端的邊界連接類型是平坦的。我們可以定義這種情況叫slope-slope-flat,或者簡稱SSF。

兩個傾斜和一個陡峭類型,SSF

檢測是否是這種情況,如果是就調用新方法TriangulateCornerTerraces,然後直接跳出方法。把檢測代碼放到默認的三角化代碼之前,這樣就會替代默認的的三角化。

只要沒在TriangulateCornerTerraces()裏做些什麼,這個連接角就會變成一個空洞。是否會變成洞取決於哪個單元格最終成為底部單元格。

出現了一個洞

要填充這個洞我們得穿過缺口連接左右的階梯,這與邊界連接基本是一樣的,除了內部添加的雙色四邊形變成了三色三角形。我們還是首先從第一個三角形開始。

第一步的三角形

再一次直接跳到最後一步,這是一個梯形狀的四邊形。與邊界連接最後一步不同的是,這裡四個頂點都是不同的顏色。

最後一步的四邊形

在此之間的步驟添加的也是四邊形。

所有的步驟

4.2 雙傾斜連接類型的變體

雙傾斜連接類型在不同的朝向上有兩個變體,取決於哪一個是底部單元格。我們可以通過檢查單元格到左右兩個相鄰單元格的連接類型是不是傾斜-平坦,和平坦-傾斜類型來找到。

如果右邊的邊界連是平坦類型,我們就從左邊開始階梯化處理而不是底部。如果左邊是平坦類型,就從右邊開始。

這樣一來階梯就能沒有中斷的環繞每個單元格,除非碰到陡峭連接或者地圖邊界。

連續環繞的階梯化連接

5.傾斜和陡峭類型的融合

所以當傾斜類型和陡峭類型之間該怎麼連接起來?如果我們知道左邊的邊界連接類型是傾斜類型而右邊是陡峭類型,那最上端的單元格的邊界連接是什麼類型?它肯定不可能是平坦的,但有可能是傾斜和陡峭類型中的任意一個。

SCS和SCC類型

添加一個新方法同時處理這兩種情況。

它應當在左邊邊界連接是傾斜類型的情況下,在右邊連接最後一個可能選項中被調用。

所以這塊該如何三角化?這個問題要分成兩個部分:底部和頂部。

5.1 底部部分

底部部分的左邊已經階梯化了,而右邊是直接連接的陡峭類型。我們要合併它們最簡單的方法就是把階梯化部分向右上角摺疊,這樣會讓階梯部分向上越來越細。

摺疊階梯化部分

但事實上並不想這樣讓它在右邊的角落交匯,因為這樣可能會干擾可能存在的頂部到其他單元格的階梯化部分。並且在處理非常高的陡峭連接時,這麼做還會產生非常高和薄的三角形,這樣會很不好看。所以我們把它摺疊到陡峭到傾斜連接的分界點上。

摺疊到分界點上

把分界點定位在比底部單元格高一級的海拔高度上,我們可以根據海拔高度差的插值得到。

驗證一下邊界點是否正確,用一個三角形覆蓋它們。

較低的三角形

分界點定位準確時就可以開始三角化階梯部分了。還是從第一個三角形開始。

摺疊的第一個步驟

這一次最後一個步驟也是一個三角形。

摺疊的最後一個步驟

之間的步驟也是三角形。

階梯化的摺疊

不能保持階梯的高度不變麼?

通過在起始點和分界點之間插值計算我們的確能保持階梯部分平坦,而不是總是使用邊界點。這就需要使用傾斜四邊形填充這個部分。但是這些傾斜四邊形並不是在一個平坦的平面上,因為它們左右相連的邊的斜率並不一樣。最後結果看起來會很亂。

5.2 完成相連角

底部部分就完成了,現在來看看頂部部分怎麼處理。如果頂部單元格與相鄰單元格的邊緣連接時傾斜類型,我們就又得去連接階梯化部分和陡峭部分。為了代碼能復用,這裡單獨抽成一個方法,並把先前的代碼移動到它自己的方法中。

現在完成頂部連接就簡單許多了,如果頂部邊緣連接時傾斜的,就改變參數順序,添加旋轉過的分界點三角形,否則就直接一個簡單的三角形就足夠了。

完成這兩種情況的三角化

5.3 鏡像情況

現在我們已經解決了傾斜-陡峭情況的階梯化合併.還有兩種鏡像的情況,這時它們陡峭連接的部分在左邊。

方法和之前是一樣的,只在方向問題上有些許不同。複製TriangulateCornerTerracesCliff並修改相應的部分,這裡只標註了不同的地方。

在TriangulateCorner裏去處理這些情況。

CSS和CSC的三角化也完成了

5.4 兩個陡峭連接類型的情況

剩餘的沒有平坦連接的情況就是底部單元格兩邊的邊緣連接都是陡峭類型,這樣一來頂部的兩個相鄰單元格的連接有可能是平坦,傾斜,陡峭中的任意情況。我們只對頂部連接是傾斜時這種情況感興趣(C-C-S),只有這種情況下要處理連接角的階梯化。

實際上有兩個不同的CCS版本,根據哪邊更高來決定,它們互為鏡像。把這兩種情況定義為CCSR和CCSL。

CCSR和CCSL

我們可以通過不同的參數順序調用TriangulateCorner裏先前寫好的TriangulateCornerCliffTerraces和TriangulateCornerTerracesCliff這兩個方法去解決這兩種情況。

然而這會產生一個奇怪的三角剖分。發生這種情況是因為我們現在是從上至下在進行三角化,這樣分界點的插值計算就得到了一個負值。解決方案是保證插值計算的正確性。

CCSR和CCSL三角化完成

5.5 清理

現在能處理所有特殊的連接情況,並且所有的階梯都有正確的三角剖分。

所有情況下的三角化都完成了

我們可以清理一下TriangulateCorner裏的代碼,用else來替代return。

最後一個else裏包含了所有之前沒提到的連接情況,分別是FFF,CCF,CCCR,CCCL。它們的角連接都可以用一個三角形表示。

所有一個三角形能表示的情況

下一篇教程是Irregularity。

本期工程地址tank1018702/Hex-Map-Learning

有想系統學習遊戲開發的童鞋,歡迎訪問levelpp.com/


推薦閱讀:
相關文章