HexMap學(xué)習(xí)筆記(九)——地形特征

作者:沈琰
前言
這期內(nèi)容為地形添加了一些裝飾,但坦白說(shuō),使用默認(rèn)形狀拼接的效果并不太好看,大家可以用別的軟件制作一些更精細(xì)的模型作為替代。
本期原文地址:https://catlikecoding.com/unity/tutorials/hex-map/part-9/
這篇教程為HexMap系列的第九篇,這部分內(nèi)容是為地形表面添加一些特征相關(guān)的東西,像是建筑和樹(shù)木之類(lèi)的。

1 添加地形特征功能支持
雖然地形的形狀有變化,但幅度不大且看起來(lái)了無(wú)生氣,要使其看起來(lái)有活力一些,需要添加一些類(lèi)似樹(shù)和建筑的地形特征。這些東西并不屬于地形mesh部分,它們是獨(dú)立的物體,但這不妨礙我們也在三角化地形時(shí)添加它們。
HexGridChunk同樣也不關(guān)心mesh是如何工作的,它只是簡(jiǎn)單的令其中一個(gè)掛有HexMesh腳本的子物體添加三角形或是四邊形,那同樣也能添加另一個(gè)子物體來(lái)負(fù)責(zé)地形特征物的放置。
1.1 地形特征管理器
創(chuàng)建一個(gè)腳本HexFeatureManager來(lái)負(fù)責(zé)單獨(dú)一個(gè)地圖塊上地形特征物的管理。使用與HexMesh相同的運(yùn)作方式,添加Clear()、Apply()和AddFeature()的方法。由于物體要放置在一個(gè)具體的地方,所以AddFeature方法里要傳入一個(gè)坐標(biāo)參數(shù)。
我們先不做任何實(shí)際的事,把代碼結(jié)構(gòu)搭建起來(lái)。

現(xiàn)在可以在HexGridChunk里添加對(duì)其的引用,并與其他HexMesh的子物體一樣,在三角化處理時(shí)包含其方法。

從往每個(gè)單元格的中心添加地形特征物開(kāi)始。

現(xiàn)在我們需要實(shí)際的管理器對(duì)象,添加另一個(gè)子物體到HexGridChunk的預(yù)制體中并掛載HexFeatureManager腳本,接著拖入腳本中關(guān)聯(lián)起來(lái)。

1.2 地形特征物的預(yù)制體
應(yīng)該創(chuàng)建何種類(lèi)型的地形特征物?首次測(cè)試先使用默認(rèn)的方塊。創(chuàng)建一個(gè)較大的方塊,設(shè)置縮放為(3,3,3),然后轉(zhuǎn)換為預(yù)制體,再為其新建一個(gè)默認(rèn)顏色為紅色的材質(zhì)球。刪除其碰撞器,因?yàn)槲覀冇貌坏健?/p>
我們的地形特征管理器需要獲取這個(gè)預(yù)制體的引用,向HexFeatureManager里添加一個(gè),然后關(guān)聯(lián)起來(lái)。坐標(biāo)信息需要訪(fǎng)問(wèn)Transform組件獲取,所以使用其作為引用類(lèi)型。

1.3 實(shí)例化地形特征物
設(shè)置完成,可以開(kāi)始添加地形特征物了,即簡(jiǎn)單的在HexFeatureManager.AddFeature里實(shí)例化預(yù)制體并設(shè)置位置。



1.4 刪除地形特征物
每次地圖塊刷新時(shí)都會(huì)創(chuàng)建一個(gè)地形特征物,這意味著我們現(xiàn)在一直在同一個(gè)坐標(biāo)上創(chuàng)建越來(lái)越多的地形特征物。所以為了防止重復(fù),在地圖塊被清理時(shí)也要?jiǎng)h除舊的地形特征物。
一個(gè)比較快的方法是創(chuàng)建一個(gè)容器物體,并把所有的地形特征物體都設(shè)置為其子物體。當(dāng)Clear()被調(diào)用時(shí)就刪除這個(gè)容器物體并創(chuàng)建一個(gè)新的,這個(gè)容器物體將是管理器的子物體。

一直在創(chuàng)建和銷(xiāo)毀物體,效率不是會(huì)很低么?
可能感覺(jué)是這樣的,但現(xiàn)在還沒(méi)到考慮這個(gè)問(wèn)題的時(shí)候。首先要關(guān)心的是地形特征物放置坐標(biāo)的正確性,一旦解決了這個(gè)問(wèn)題,而性能問(wèn)題成為了瓶頸,接下來(lái)就能用相對(duì)聰明的方法來(lái)解決效率問(wèn)題。那時(shí)可能會(huì)在HexFeatureManager.Apply方法里解決,不過(guò)那將是之后的教程了。不過(guò)就算保持現(xiàn)在這樣,效率也沒(méi)那么糟糕,別忘了我們已經(jīng)把整個(gè)地圖分塊了。
2 地形特征物的擺放
目前把地形特征物放在每個(gè)單元格中心的位置,這對(duì)于其他空著的單元格可行,但是對(duì)于包含河流、道路或者是水下的單元格,看起來(lái)就不太對(duì)。

所以在添加特征物之前先確認(rèn)單元格是否干凈。

2.1 每個(gè)方向一個(gè)特征物
每個(gè)單元格中只有一個(gè)特征物看起來(lái)不夠,單元格內(nèi)還有更多的空間。讓我們?cè)趩卧衩總€(gè)方向上三角形的中心位置都添加一個(gè)特征物。
當(dāng)知道當(dāng)前方向不是河流部分時(shí)在另一個(gè)三角化方法里做這件事,但是仍要檢測(cè)是否處于水下或者是否存在道路,但在這種情況下,只需要關(guān)心道路是否穿過(guò)當(dāng)前方向。


這就生成了更多的地形特征物,它們出現(xiàn)在道路的旁邊但還是遠(yuǎn)離河流。要讓特征物能順著河流出現(xiàn),我們還需要在TriangulateAdjacentToRiver里添加它們,這一次要求這部分單元格不在水下和不在道路的頂部。

我們能渲染這么多物體么?
更多的特征物會(huì)帶來(lái)更多的drawCalls,但是Unity的動(dòng)態(tài)批處理會(huì)幫我們擺脫這個(gè)問(wèn)題。由于特征物都不大,它們的Mesh只會(huì)有很少的頂點(diǎn),這使得它們中的許多可以在一次批處理中組合起來(lái)。但如果這會(huì)成為一個(gè)性能瓶頸,我們就會(huì)在之后處理。也可以使用實(shí)例化,這類(lèi)似于使用許多小mesh時(shí)的動(dòng)態(tài)批處理。
3 特征物的樣式
現(xiàn)在所有的地形特征物的朝向都是一樣的,這看起來(lái)很死板,所以我們要設(shè)置隨機(jī)朝向。


雖然這產(chǎn)生了很多樣式類(lèi)型,但是每當(dāng)?shù)貓D塊刷新,特征物都會(huì)獲得一個(gè)新的隨機(jī)朝向。編輯特征物是不應(yīng)該還要注意是否會(huì)讓附近的特征物的朝向反復(fù)變化,所以我們要換一個(gè)方法。
我們有一張?jiān)肼暭y理圖,它是一直相同的,但是其包含的柏林梯度噪聲是局部連貫的,這在我們擾動(dòng)單元格頂點(diǎn)時(shí)是需要的,但對(duì)旋轉(zhuǎn)來(lái)說(shuō)就不需要連貫的值,所有的旋轉(zhuǎn)都應(yīng)該是等概率。所以需要的是一個(gè)非梯度隨機(jī)值的紋理,并且不使用雙線(xiàn)性過(guò)濾采樣。這實(shí)際上就是一個(gè)散列值的網(wǎng)格表,梯度噪聲紋理的基本形式。
3.1 創(chuàng)建散列表
我們可以使用一個(gè)floats類(lèi)型的數(shù)組創(chuàng)建散列表并用隨機(jī)值填充,這樣就不再需要紋理圖了。在HexMetrics里添加它,長(zhǎng)度設(shè)置成256乘256,應(yīng)該大致夠用。

通過(guò)數(shù)學(xué)公式生成的隨機(jī)值會(huì)保持相同的結(jié)果,獲得的結(jié)果值的序列取決于一個(gè)隨機(jī)種子,它默認(rèn)為當(dāng)前時(shí)間,這就是為什么每次運(yùn)行時(shí)得到的結(jié)果序列都不一樣。
為了每次運(yùn)行時(shí)特征物在相同位置的旋轉(zhuǎn)相同,我們必須在初始化方法中添加一個(gè)seed參數(shù)。

散列表的初始化由HexGrid完成,它同時(shí)也負(fù)責(zé)分配噪聲紋理。所以在HexGrid.start里和HexGrid.Awake里都添加上初始化,并做一個(gè)檢測(cè)確保不會(huì)過(guò)多生成。

公共的種子字段可以設(shè)置任何值,這里設(shè)置的1234。

3.2 使用散列表
在HexMetrics里添加采樣方法來(lái)使用散列表,與噪聲紋理的采樣相同,使用XZ位置的坐標(biāo)來(lái)獲取相對(duì)的值。散列表的索引是通過(guò)把坐標(biāo)值取整,再除以散列表的大小獲得余數(shù)得到。

%是什么?
這是取模操作符,它計(jì)算的是除法中的余數(shù),在我們的情況中是整數(shù)除法。舉個(gè)例子,序列-4,-3,-2,-1,0, 1,2, 3,4分別對(duì)3取模,得到的結(jié)果就是-1, 0,-2, -1, 0,1, 2, 0, 1。
當(dāng)坐標(biāo)為正值的時(shí)候沒(méi)問(wèn)題,但是如果坐標(biāo)值為負(fù)時(shí),得到的余數(shù)也是負(fù)的。可以通過(guò)為負(fù)值結(jié)果加上散列表尺寸來(lái)修正這個(gè)問(wèn)題。

現(xiàn)在為每平方單位生成了一個(gè)不同的值,但是我們的特征物其實(shí)并沒(méi)有這么密集,它們之間的坐標(biāo)差距更大。所以在計(jì)算下標(biāo)之前可以通過(guò)縮小位置坐標(biāo)來(lái)放大散列表,每4乘4的單位取一個(gè)唯一隨機(jī)值就夠了。

現(xiàn)在回到HexFeatureManager.AddFeature里,使用坐標(biāo)在散列表中采樣一個(gè)值并用它來(lái)設(shè)置特征物的旋轉(zhuǎn)。如此一來(lái)當(dāng)我們編輯地形時(shí),地貌物的朝向?qū)⒈3朱o止。

3.3 特征物的放置閾值
現(xiàn)在特征物的旋轉(zhuǎn)變化明顯,但是放置位置的模式依然不變,每個(gè)單元格都有7個(gè)特征物塞在一起。這里可以通過(guò)省略一些特征物來(lái)產(chǎn)生變化,通過(guò)訪(fǎng)問(wèn)另一個(gè)隨機(jī)值來(lái)確認(rèn)是否放置特征物。
所以現(xiàn)在需要兩個(gè)哈希值而不是一個(gè),把散列表的數(shù)組類(lèi)型由float改為vector2也行,不過(guò)向量運(yùn)算對(duì)于哈希值來(lái)說(shuō)沒(méi)有意義。所以為此創(chuàng)建一個(gè)特殊的結(jié)構(gòu)體,所需要的就是兩個(gè)float值,探后添加一個(gè)靜態(tài)方法來(lái)創(chuàng)建兩個(gè)隨機(jī)值。

它不需要序列化么?
我們只在散列表中存儲(chǔ)這些結(jié)構(gòu),散列表也是靜態(tài)的,因此在重編譯時(shí)不會(huì)被Unity序列化,所以這個(gè)結(jié)構(gòu)體也不需要序列化。
在HexMetrics里修改代碼,使用這個(gè)新的結(jié)構(gòu)體。

現(xiàn)在HexFeatureManager.AddFeature里可以訪(fǎng)問(wèn)到兩個(gè)哈希值,使用第一個(gè)來(lái)確認(rèn)是否添加特征物或者跳過(guò)。當(dāng)值大于等于0.5f的時(shí)候就直接跳出,這樣大約能排除一半的特征物。第二個(gè)值就像之前一樣用來(lái)決定特征物的旋轉(zhuǎn)。


4 繪制地形特征物
我們來(lái)讓特征物的出現(xiàn)位置可編輯,而不是像現(xiàn)在這樣每個(gè)單元格都放一點(diǎn)。但是我們并不是來(lái)繪制單獨(dú)的特征物,而是給沒(méi)個(gè)單元格添加一個(gè)等級(jí)的屬性,這個(gè)屬性控制著地形特征物出現(xiàn)的可能性。其默認(rèn)值為0,代表特征物體不會(huì)出現(xiàn)。
由于紅色的方塊實(shí)在不像是自然產(chǎn)生的地形特征物,所以就將其定義為建筑。它們代表著城市開(kāi)發(fā)區(qū)域,添加城市等級(jí)的屬性到HexCell里。

可能覺(jué)得需要確保水下的單元格城市等級(jí)為0,但這沒(méi)有必要。因?yàn)橐呀?jīng)在水下單元格三角化的時(shí)候跳過(guò)生成特征物的步驟了。而且之后可能添加一些處于水下的特征物,比如說(shuō)碼頭或者船塢之類(lèi)的。
4.1 密度滑動(dòng)條
在HexMapEditor里添加另一個(gè)滑動(dòng)條組件,讓城市等級(jí)可編輯。

添加另一個(gè)滑動(dòng)條到UI上并與相應(yīng)的方法相連,這里把它放在屏幕右邊的一個(gè)新的UI面板上,防止左邊的面板塞得太滿(mǎn)。
最大等級(jí)設(shè)置成多少比較合適?這里還是使用4個(gè)等級(jí),分別表示零,低,中,高密度的城市開(kāi)發(fā)區(qū)。

4.2 閾值調(diào)整
現(xiàn)在城市等級(jí)可以編輯了,使用這個(gè)值來(lái)確認(rèn)是否需要放置地形特征物,為此需要將城市等級(jí)作為額外的參數(shù)添加到HexFeatureManager.AddFeature里.。這里我們更進(jìn)一步,把單元格本身當(dāng)做參數(shù)傳遞,這樣在之后會(huì)更方便一些。
一個(gè)快速使用城市等級(jí)的方式是把它乘上0.25并使用這個(gè)值作為新的特征物出現(xiàn)閾值,這樣一來(lái)城市等級(jí)每提高一級(jí),地形特征物的出現(xiàn)概率就增加25%。

在HexGridChunk里傳遞單元格參數(shù)。


5 多種地形特征物預(yù)制體
光是特征物的出現(xiàn)概率還不足以很明顯的區(qū)別不同的城市等級(jí),有些單元格里的建筑物數(shù)量甚至比預(yù)期都少??梢允褂貌煌念A(yù)制體來(lái)表示不同城市等級(jí),使區(qū)別更加明顯。
把HexFeatureManager里的featurePerfab字段的類(lèi)型改成預(yù)制體的數(shù)組,用城市等級(jí)減一的值作為數(shù)組的下標(biāo)。

創(chuàng)建兩個(gè)額外的特征物預(yù)制體并重命名來(lái)表示三種不同的城市等級(jí)。等級(jí)1是低密度,就使用標(biāo)準(zhǔn)尺寸的方塊代表小平房。把等級(jí)2的預(yù)制體縮放設(shè)置為(1.5, 2,1.5)表示更大一些的雙層建筑。等級(jí)3則使用(2, 5,2)的縮放表示大樓或高層建筑。

5.1 混合預(yù)制體
我們不需要嚴(yán)格限制建筑物類(lèi)型分離,可以像真實(shí)世界中的一樣稍微混合一下。每個(gè)城市等級(jí)使用三個(gè)閾值表示每種建筑物的出現(xiàn)幾率。
對(duì)于等級(jí)1,設(shè)置小平房的出現(xiàn)概率是40%,而其他兩種建筑不會(huì)出現(xiàn)。所以這組閾值是(0.4 , 0,0)。
對(duì)于等級(jí)2,把40%概率出現(xiàn)的建筑類(lèi)型由小平房改為雙層建筑,并額外給予20%的概率出現(xiàn)小平房,大樓依然不會(huì)出現(xiàn),這時(shí)的閾值組是(0.2 , 0.4 , 0)。
對(duì)于等級(jí)3,把雙層建筑升級(jí)為大樓,小平房升級(jí)為雙層建筑,再給予20%的概率出現(xiàn)小平房。
這里的想法是,隨著城市等級(jí)的提高,會(huì)對(duì)現(xiàn)有建筑升級(jí)并有概率在空地上新建新的建筑。如果要替換現(xiàn)有建筑,就需要使用相同的哈希值范圍。比如哈希值在0-0.4之間是1級(jí)小平房的話(huà),那么在這個(gè)哈希值范圍的建筑在城市等級(jí)為3時(shí)會(huì)變成大樓。
具體來(lái)說(shuō)就是,在城市等級(jí)為3時(shí),大樓的哈希值范圍為0-0.4,雙層建筑為0.4-0.6,小平房為0.6-0.8,如果從高到低來(lái)檢測(cè),等級(jí)3的閾值組就是(0.4,0.6,0.8)。以此類(lèi)推等級(jí)2就變成(0,0.4,0.6),等級(jí)1為(0,0,0.4)。
在HexMetrics里用一個(gè)二維數(shù)組保存這些閾值組,然后新建一個(gè)獲取特定等級(jí)閾值組的方法。由于我們只關(guān)心與特征物類(lèi)型相關(guān)聯(lián)的等級(jí),所以這里忽略了等級(jí)0。

接著在HexFeatureManager里添加一個(gè)使用城市等級(jí)與哈希值來(lái)選擇預(yù)制體的方法。當(dāng)前城市等級(jí)大于0時(shí)使用其值減1作為閾值組數(shù)組的下標(biāo),然后循環(huán)遍歷閾值組,直到其中一個(gè)超過(guò)閾值組的哈希值為止,這就表示我們找到了一個(gè)預(yù)制體,如果沒(méi)找到就返回null。

這就需要我們重新對(duì)預(yù)制體的引用進(jìn)行排列,所以它們是從高密度到低密度排列的。

使用新的方法去獲取預(yù)制體,如果最后都沒(méi)找到就跳出方法,否則就像之前一樣實(shí)例化然后繼續(xù)。


5.2 每級(jí)的變體
現(xiàn)在建筑混合效果已經(jīng)很不錯(cuò)了,但建筑本身依然只是三個(gè)不同樣子。我們可以把預(yù)制體集合與每級(jí)城市密度關(guān)聯(lián)起來(lái)增加更多的變化,然后在其中隨機(jī)選擇一個(gè)。這樣就需要用到一個(gè)新的隨機(jī)值,所以在HexHash里添加第三個(gè)隨機(jī)值。

把HexFeatureManager.urbanPrefabs的類(lèi)型改為數(shù)組型的數(shù)組,然后在PickPrefab方法中添加一個(gè)choice參數(shù),使用它將嵌套數(shù)組與該數(shù)組的長(zhǎng)度相乘并轉(zhuǎn)換為整數(shù),從而為該數(shù)組建立下標(biāo)。

使用第二個(gè)哈希值,即 b來(lái)做出這個(gè)選擇,相應(yīng)的旋轉(zhuǎn)的哈希值由b變?yōu)閏。

在繼續(xù)往下之前,需要意識(shí)到一個(gè)問(wèn)題。Random.value是有幾率出現(xiàn)1這個(gè)數(shù)的,這樣就會(huì)出現(xiàn)數(shù)組越界的問(wèn)題。為確保這種問(wèn)題不會(huì)發(fā)生,把最終得到的哈希值減小一些。

不幸地是Inspector里無(wú)法顯示數(shù)組中的數(shù)組,所以我們也沒(méi)辦法在Inspector里用拖入的方式賦值。要解決這個(gè)問(wèn)題就需要?jiǎng)?chuàng)建一個(gè)封裝嵌套數(shù)組的可序列化結(jié)構(gòu),給它一個(gè)專(zhuān)用方法處理參數(shù)到索引的轉(zhuǎn)換,并返回預(yù)制體。

在HexFeatureManager里使用這些封裝結(jié)構(gòu)替代嵌套數(shù)組。

現(xiàn)在可以為每個(gè)密度級(jí)別定義多種建筑物。因?yàn)樗鼈兪仟?dú)立的,所以不需要在每個(gè)組里添加相同的數(shù)量。這里在每個(gè)組里都額外添加了一個(gè)較長(zhǎng)較低的建筑變體。其縮放分別設(shè)置為(3.5,3,2),(2.75,1.5,1.5)和(1.75,1,1)。

6 其他地形特征物類(lèi)型
使用目前的設(shè)置,可以生成一些看起來(lái)還行的城市。但地形特征物并不僅僅包含建筑物,還有植物和農(nóng)場(chǎng)之類(lèi)的,讓我們把這些也添加到HexCell中。它們并非只能單獨(dú)存在,是可以混合在一起的。

當(dāng)然也要在HexMapEditor里添加額外的滑動(dòng)條組件。

添加到UI面板上并與相應(yīng)的方法關(guān)聯(lián)。

HexFeatureManager里也需要添加新的容器。


這里也在每個(gè)密度等級(jí)給農(nóng)場(chǎng)和植被設(shè)置了兩個(gè)類(lèi)型預(yù)制體,用的也都是默認(rèn)的方塊。其中農(nóng)場(chǎng)使用的是淺綠色的材質(zhì)球,植被使用的深綠色材質(zhì)球。
農(nóng)場(chǎng)的方塊高度設(shè)置為0.1單位,來(lái)表示矩形的農(nóng)田,其縮放分別為第三級(jí)(2.5,0.1,2.5)和(3.5,0.1,2) ,第二級(jí)(1.75,0.1,1.75)和(2.5,0.1,1.25) ,第一級(jí)則是(1,0.1,1)和(1.5,0.1,0.75)。
而植物的預(yù)制體代表高大的樹(shù)木和大型灌木,第三級(jí)為(1.25,4.5,1.25)和(1.5,3,1.5)。第二級(jí)(0.75,3,0.75)和)(1,1.5,1),第一級(jí)為(0.5,1.5,0.5)和(0.75,1,0.75)。
6.1 地形特征物的選擇
每一種特征物類(lèi)型都要給其自己的哈希值,所以它們有不同的生成模式,這樣就可以混合這些特征物了。添加兩個(gè)額外的值到HexHash中。

HexFeatureManager.PickPrefab現(xiàn)在使用不同的容器工作,添加一個(gè)參數(shù)以示區(qū)分。再一次修改預(yù)制體選擇的哈希值為D,旋轉(zhuǎn)的值為E。

之前AddFeature里只選擇了一個(gè)建筑的預(yù)制體,現(xiàn)在我們有更多選擇。在農(nóng)田預(yù)制體里再選擇一個(gè),使用B作為其出現(xiàn)幾率的哈希值,類(lèi)型選擇則是使用D。

最終要實(shí)例化的預(yù)制體是哪個(gè)?如果其中一個(gè)預(yù)制體為null,那么選擇不言而喻。但當(dāng)兩個(gè)預(yù)制體都不為null時(shí),使用哈希值較小的那個(gè)。


接下來(lái)使用哈希值C對(duì)植被的預(yù)制體做相同操作。

但是這里不能簡(jiǎn)單的將代碼復(fù)制粘貼,當(dāng)我們最終的選擇是農(nóng)田而不是建筑時(shí),接下來(lái)應(yīng)該將植被的哈希值與農(nóng)田的進(jìn)行比較,而不是建筑的哈希。因此我得注意最終使用哪個(gè)哈希值,并與使用的那個(gè)進(jìn)行比較。


下一篇教程是:https://catlikecoding.com/unity/tutorials/hex-map/part-10/
本期工程地址 :?https://github.com/tank1018702/Hex-Map-Learning/tree/TerrainFeatures
想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪(fǎng)問(wèn):http://levelpp.com/??
另有專(zhuān)業(yè)開(kāi)發(fā)交(gao)流(ji)群等待大家強(qiáng)勢(shì)插入:869551769