HexMap學(xué)習(xí)筆記(十)——城墻

作者:沈琰
前言
第十篇的內(nèi)容與上一篇從效果上來看差不多,都是增添一類地形特征。但從實現(xiàn)方式上來看,前者是擴展原有邏輯,后者則是嵌入原有邏輯,在此基礎(chǔ)上修改。所以從難度上來說還是比上一篇高不少的。
本期原文地址:https://catlikecoding.com/unity/tutorials/hex-map/part-10/
這篇教程是HexMap系列的第十篇,這一次將在單元格之間添加城墻。

1 編輯城墻
要實現(xiàn)城墻編輯的功能,首先要知道把城墻放哪,這里把城墻放在單元格的邊緣連接部分上。由于已經(jīng)把其他地形特征物放在了單元格的內(nèi)部區(qū)域,所以不用擔心這兩部分的物體會相互穿模干擾。

城墻也算是一種地形特征物,盡管比較大。所以像其他特征物一樣我們不直接編輯城墻的位置,而是編輯單元格。并非放置單獨的城墻段落,而是為整個單元格添加城墻。
1.1 標記單元格是否有城墻屬性
在HexCell里添加個Walled屬性來表示單元格是否被城墻包裹。用一個簡單的toggle來控制,由于城墻是放置在單元格之間的,所以這里需要刷新包含自身在內(nèi)的所有相鄰單元格。

1.2 編輯Toggle
在HexMapEditor里添加一個toggle來控制單元格是否被城墻包圍,同樣添加一個方法設(shè)置這個狀態(tài)。

與道路和河流不同的是,城墻不會從一個單元格穿過到另一個單元格,而是處于連接部分上。所以不用擔心鼠標拖拽產(chǎn)生錯誤的結(jié)果。當城墻的Toggle是選中狀態(tài)時就直接根據(jù)值來改變單元格的城墻狀態(tài)。

復(fù)制一份之前有的UI面板,修改名字個調(diào)用方法使其控制單元格的城墻狀態(tài),這里把這個新UI面板跟其它地形特征放在一起。

2 構(gòu)筑城墻
由于城墻是根據(jù)單元格的輪廓放置的,它沒有固定形狀,所以不能像其他地形特征物一樣用一個簡單的預(yù)制體來保存。相應(yīng)的我們需要一個相對于地形的mesh結(jié)構(gòu),這表示我們的地圖塊預(yù)制體需要添加另一個HexMesh子物體。復(fù)制一份其他的子物體,并確保其開啟了陰影投射。它不需要任何除頂點和三角形之外的數(shù)據(jù),所以HexMesh上的其他選項全都不勾選。

城墻是屬于城市的地形特征物,所以這里使用城市的材質(zhì)球。
2.1 城墻管理
由于城墻也是地形特征物,所以它是由HexFeatureManager負責(zé)管理。在腳本里添加城墻對象的引用,并調(diào)用其Clear和Apply方法。

城墻不應(yīng)該是地形特征物的子物體么?
當然也可以這么設(shè)置,但并不是必要的。因為在預(yù)制體的層級界面中只會顯示根物體的直接子物體,所以這里選擇把城墻設(shè)置為HexGridChunk的子物體,為的是方便查看。
現(xiàn)在要在管理器中添加方法去構(gòu)筑城墻。由于城墻處于單元格的邊緣連接處上,所以它需要知道相關(guān)的連接處頂點和單元格。HexGridChunk會通過TriangulateConnection調(diào)用這個方法,這時單元格才開始三角化,參數(shù)里的另一個單元格是其相鄰單元格。從這里來看,當前單元格為較近的單元格,而另一個單元格為較遠的。

在連接工作完成后,處理角落三角形之前這個時間點,在HexGridChunk.TriangualteConnection里調(diào)用這個新方法,然后交由HexFeatureManager來最終決定是否確實需要放置城墻。

2.2 構(gòu)筑單段城墻
整個城墻會如蛇形蜿蜒一般穿過多個單元格的連接處,段段連接處中就有城墻里的單獨一段。從相鄰單元格的角度來看,這段城墻從邊緣連接處的左邊開始到右邊結(jié)束。在HexFeatureManager里創(chuàng)建一個單獨的方法,基于邊緣連接處的四個頂點來構(gòu)筑城墻段。


AddWall方法里可以通過邊緣連接處的第一個和最后一個頂點來調(diào)用這個方法。不過城墻應(yīng)該只能在有城墻和沒城墻的兩個單元格之間才能添加,不用考慮誰在外面誰在里面,只要狀態(tài)不同就行。

現(xiàn)在就可以編輯城墻了,它會顯示為一個四邊形條狀。但是你不是總能看到城墻,每個四邊形只有一邊可見并且可見的面是朝向被標記為有城墻的單元格。

通過添加向外朝向的四邊形來快速解決這個問題。


現(xiàn)在整段城墻都是可見的了,盡管在三個單元格交界處是空缺的。稍后就會填充這部分。
2.3 城墻加厚
雖然現(xiàn)在城墻兩面可見,但是它們沒有厚度。現(xiàn)在的城墻薄的像紙一樣,使其在某些角度幾乎看不見,現(xiàn)在我們來增加城墻的厚度來使其更像實體。在HexMetrics里定義城墻的厚度,這里定義厚度為0.75,一個比較合適的量。



現(xiàn)在四邊形的位置就被移動了,盡管不太明顯,但從影子上還是看的出來。
城墻的厚度是一致的么?
如果遠近的偏移向量都指向一個方向,那就是的。由于單元格周圍呈曲線,情況顯然并非如此.這些點的向量要么彼此遠離,要么指向?qū)Ψ?因此城墻段的頂點移動是基于一個梯形而不是矩形,最終的結(jié)果會比我們之前設(shè)置的值要薄一些.此外,由于單元格受到噪聲圖的擾動,向量之間的角度會發(fā)生變化,導(dǎo)致厚度不均勻,我們之后會對其進行改進.
2.4 填充城墻頂部
要讓城墻的厚度從上可見就需要在城墻頂部添加一個四邊形,最簡單的辦法就是記錄第一個四邊形的頂部兩個頂點并與第二個四邊形的頂部頂點相連,組成一個新的四邊形。


2.5 角落連接處的城墻
城墻剩余的空缺部分在單元格的角落連接處,所以現(xiàn)在要這個部分添加城墻。每個角連接處連接著三個單元格,而每個單元格都可以是有或者沒有城墻的狀態(tài),所以這就產(chǎn)生了8種可能的結(jié)構(gòu)。

我們只在不同城墻狀態(tài)的單元格之間擺放城墻,所以排除不用放置城墻的情況,這樣相關(guān)的結(jié)構(gòu)就減少到了6個。這些結(jié)構(gòu)中都有一個單元格在城墻曲線的內(nèi)側(cè),我們把這個單元格看做是使城墻彎曲的軸心,然后從左往右構(gòu)筑城墻。

新建一個AddWallSegment方法,以相關(guān)聯(lián)的三個單元格與三角形的三個頂點為參數(shù)。盡管我們可以直接就在這個方法里三角化,但其實這個方法是另一個AddWallSegment方法的特殊情況。作為軸心的單元格同時扮演兩個較近頂點的角色。

下一步新建一個AddWall方法的變體,傳入三個單元格與三個頂點作為參數(shù)。這個方法的作用是在六種可能的情況中找出哪一個單元格是軸心。因此它要檢測所有8種情況,并在其中的六種中調(diào)用AddWallSegment方法。

在HexGridChunk.TriangulateCorner方法的最后調(diào)用這個方法來添加這段城墻。


2.6 填充縫隙
現(xiàn)在城墻上還有縫隙,這是因為每段城墻的高度不一致。沿著邊緣連接處的城墻有固定高度,而角上的城墻位于兩個不同的邊緣連接處之間。因為每條邊都有不同的高度,所以角上就會出現(xiàn)間隙。
要解決這個問題就要修改AddWallSegment方法,使其左右頂部頂點的Y坐標分開。


現(xiàn)在城墻是閉合了,但是好像還能在陰影中看到縫隙,這是由于方向光陰影設(shè)置里的法線偏移在搞鬼。當其值大于0時,三角形的陰影投射會向著表面法線方向偏移。這是為了防止自投影的現(xiàn)象,但也同時在三角形的朝向位置之間產(chǎn)生間隙。這就會在較薄的幾何圖形上產(chǎn)生可見的陰影間隙,就像我們的城墻一樣。
我們可以通過把法線偏移減少到零來解決這個問題,或者也可以修改城墻mesh渲染器的的陰影投射模式為Two Sided,這樣就使陰影投射會同時渲染兩邊的陰影從而覆蓋間隙。

3 城墻階梯化
現(xiàn)在的城墻是相對筆直的,這在平坦的地形上看著沒問題,但在于階梯連接處貼合時就顯得很奇怪。在單元格有一級的高度差的城墻兩側(cè)就會出現(xiàn)這種情況。

3.1 城墻與邊緣連接處的貼合
我們的解決方法是在邊緣的每一個分段處都構(gòu)建一段城墻,而不是只用一段城墻覆蓋整個單元格邊緣連接處。在邊緣處使用的AddWallSegment方法里調(diào)用4次AddWall方法。


現(xiàn)在城墻的形狀與單元格個邊緣相吻合了,看起來比之前好多了。這同樣在平坦的地形上產(chǎn)生了更多城墻的變化。
3.2 城墻與地面的貼合
仔細觀察階梯連接處上的城墻,發(fā)現(xiàn)了一個問題,城墻是漂浮在地面上的。對于傾斜的平面其實城墻也是漂浮的,不過那種情況下通常不太明顯。

要解決這個問題最簡單的方法就是把城墻往下移動,使城墻在地形較高的那邊陷到地形之下,得到我們想要的效果。
要把城墻下移首先是要確認城墻較近邊還是較遠邊的高度更低。我們可以使用較低邊的單元格高度作為修正高度,但其實不必這么低??梢园迅叩瓦叺腨坐標進行0.5的插值算出偏移高度。由于我們的城墻在極少情況下厚度會超出階梯連接處每格的寬度,所以可以直接用階梯每格的垂直高度作為城墻的偏移。如果城墻的厚度不同,偏移值可能就需要修改一下。

我們在HexMetrics里添加一個WallLerp方法來負責(zé)計算這個插值,它基于TerraceLerp方法并額外對遠近頂點的X和Z坐標求平均值。
在HexFeatureManager里使用這個方法確認左右頂點。


3.3 城墻頂點擾動修正
現(xiàn)在城墻在不同高度的單元格之間的顯示正常了,但還是不能精確吻合擾動頂點后的邊緣連接處,盡管由于城墻封閉的原因不大看得出來。這是由于我們是先計算的城墻頂點,后進行的頂點擾動。當這些頂點位于近遠邊緣頂點之間時,再進行擾動的結(jié)果就會有些微不同。
城墻沒有完全吻合邊緣不是問題,但對城墻的頂點擾動會影響其相對均勻厚度。如果我們使用擾動后的頂點來定位城墻,然后添加非擾動的四邊形,城墻的厚度就應(yīng)該不會有太大變化。


使用這種方法后,城墻不再像以前一樣貼合著階梯連接處了。但反過來看,城墻的鋸齒感覺更少了,厚度也更加一致了。

4 城墻開口
目前我們沒有考慮河流或者道路穿過城墻的情況,當這種情況發(fā)生時,應(yīng)該在城墻上開一個缺口,這樣河流和道路就可以通過了。
向AddWall方法內(nèi)添加兩個布爾參數(shù)來表示是否有河流或者道路穿過邊緣。雖然我們也可以用不同的方法處理,但現(xiàn)在在這兩種情況下都只用刪除中間的部分的兩段城墻,更方便一些。

現(xiàn)在HexGridChunk.TriangulateConnection方法來提供相應(yīng)的參數(shù)。由于之前這個方法里已經(jīng)用到了,所以我們將這些參數(shù)緩存成布爾值,并在調(diào)用時傳遞。


4.1 城墻封邊
城墻的末端出現(xiàn)了新的缺口,我們現(xiàn)在要用四邊形封住這些缺口。為此在HexFeatureManager中新建一個AddWallCap方法,它的工作原理類似于AddWallSegment,但它只需要一對近-遠處頂點,并用其添加一個四邊形。

當在AddWall里確認需要一個封邊時,在第二和第四組頂點之間添加一個四邊形封蓋。別忘了調(diào)換第四組頂點的參數(shù)順序,否則這個四邊形將是從里可見的。


地圖邊緣的缺口怎么解決??
你也可以額外寫點代碼在那里蓋上城墻。就我個人而言,我會避免把墻放在地圖的邊緣。通常也不會希望游戲玩法發(fā)生在離地圖邊緣太近的地方。
5 讓城墻避開懸崖和水體
最后來考慮一下懸崖和水體上的單元格邊緣連接處。懸崖實際上就是巨大的墻,所以在上面再建一堵城墻也沒什么意義,并且看起來很難看。此外水下的城墻也沒有意義,把海岸線圍起來也不太好看。

我們可以在AddWall方法中通過額外的檢測來清除這些不合適的城墻。當兩個單元格都不在水下,和他們共享的邊緣連接處不是懸崖時。


5.1 清除角落連接處的城墻段
清除角上的城墻需要更多步驟。最容易想到的情況是當軸心上的單元格在水下時,這時就沒有任何相鄰單元格的角上需要連接。


現(xiàn)在再來看其余兩個單元格。如果其中一個在水下或者通過懸崖與軸心上的單元格相連,那么沿著這條邊上就不會有城墻。當其中至少有一個成立時,那么這三個單元格的角連接處就不會有城墻段。
使用兩個bool變量緩存左邊和右邊的城墻是否存在的結(jié)果,這樣推理起來更清晰。


5.2 角落連接處的城墻封邊
當左右兩邊的單元格上都沒有城墻時就算完成了,但如果其中一個方向上有城墻就意味著城墻上會新開一個缺口,所以我們得封住它。


5.3 城墻與相鄰懸崖的融合
現(xiàn)在有一種情況下我們的城墻看起來不太對,就是當城墻處于懸崖底部時。因為懸崖不是完全垂直的,所以城墻和懸崖之間留下了一個很窄的縫隙,這個問題當城墻處于懸崖頂部上是不存在的。

如果城墻一直延伸到懸崖邊且不留縫隙看起來就更好一些。我們可以通過在當前城墻的末端和懸崖所在單元格的角落連接處之間添加一段額外的城墻來實現(xiàn)這一點。由于懸崖這邊的城墻大部分都是隱藏在懸崖里的,因此可以把這一段的懸崖邊上的城墻厚度減少到零,只需要創(chuàng)建一個楔形--兩個四邊形合于一個點上,一個三角形在上面。為此新建一個AddWallWedge方法,大部分都可以復(fù)制AddWallCap方法,然后加入楔形點的參數(shù)。這里已經(jīng)把代碼不同的位置標注出來了。

在角落連接處的AddWallSegment方法中,當只有一個方向有城墻,而且城墻所在單元格的海拔高度低于另一邊時就調(diào)用此方法。那時就是遇到懸崖的時候。


下一篇教程是:https://catlikecoding.com/unity/tutorials/hex-map/part-11/
本期工程地址:https://link.zhihu.com/?target=https%3A//github.com/tank1018702/Hex-Map-Learning/tree/TerrainFeatures
有意向?qū)W習(xí)游戲開發(fā)線下課程的童鞋,歡迎訪問http://levelpp.com/。