HexMap學習筆記(九)——地形特徵

遊資網發表於2019-06-06
HexMap學習筆記(九)——地形特徵

前言

這期內容為地形新增了一些裝飾,但坦白說,使用預設形狀拼接的效果並不太好看,大家可以用別的軟體製作一些更精細的模型作為替代。

本期原文地址:HexMap9

這篇教程為HexMap系列的第九篇,這部分內容是為地形表面新增一些特徵相關的東西,像是建築和樹木之類的。

HexMap學習筆記(九)——地形特徵
樹木,農地和城市建築的交融

1新增地形特徵功能支援

雖然地形的形狀有變化,但幅度不大且看起來了無生氣,要使其看起來有活力一些,需要新增一些類似樹和建築的地形特徵。這些東西並不屬於地形mesh部分,它們是獨立的物體,但這不妨礙我們也在三角化地形時新增它們。

HexGridChunk同樣也不關心mesh是如何工作的,它只是簡單的令其中一個掛有HexMesh指令碼的子物體新增三角形或是四邊形,那同樣也能新增另一個子物體來負責地形特徵物的放置。

1.1地形特徵管理器

建立一個指令碼HexFeatureManager來負責單獨一個地圖塊上地形特徵物的管理。使用與HexMesh相同的運作方式,新增Clear()、Apply()和AddFeature()的方法。由於物體要放置在一個具體的地方,所以AddFeature方法裡要傳入一個座標引數。

我們先不做任何實際的事,把程式碼結構搭建起來。

HexMap學習筆記(九)——地形特徵

現在可以在HexGridChunk裡新增對其的引用,並與其他HexMesh的子物體一樣,在三角化處理時包含其方法。

HexMap學習筆記(九)——地形特徵

從往每個單元格的中心新增地形特徵物開始。

HexMap學習筆記(九)——地形特徵

現在我們需要實際的管理器物件,新增另一個子物體到HexGridChunk的預製體中並掛載HexFeatureManager指令碼,接著拖入指令碼中關聯起來。

HexMap學習筆記(九)——地形特徵
特徵物管理器,新增到chunk的預製體裡

1.2地形特徵物的預製體

應該建立何種型別的地形特徵物?首次測試先使用預設的方塊。建立一個較大的方塊,設定縮放為(3,3,3),然後轉換為預製體,再為其新建一個預設顏色為紅色的材質球。刪除其碰撞器,因為我們用不到。

HexMap學習筆記(九)——地形特徵
特徵物方塊預製體

我們的地形特徵管理器需要獲取這個預製體的引用,向HexFeatureManager裡新增一個,然後關聯起來。座標資訊需要訪問Transform元件獲取,所以使用其作為引用型別。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
將預製體拖入管理器的皮膚中

1.3例項化地形特徵物

設定完成,可以開始新增地形特徵物了,即簡單的在HexFeatureManager.AddFeature裡例項化預製體並設定位置。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
特徵物例項

現在地形會被方塊填充,至少是一半的方塊。由於Unity裡的Cube座標原點在方塊的中心,所以底部半邊的方塊會處於地形表面之下,要把方塊整個放在地形頂部就需要向上移動方塊高度的一半。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
位於地形的頂部

如果使用不同的mesh呢?

這個方法是針對Unity預設方塊的,如果要使用自定義mesh,有一個比較好的辦法是在建模時將座標原點直接設定在底部,這樣就再也不用去修正座標了。

當然,別忘了單元格的座標是被噪聲擾動過的,所以地形特徵物的座標也需要如此處理一下,這樣就去除了網格個規律性。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵

1.4刪除地形特徵物

每次地圖塊重新整理時都會建立一個地形特徵物,這意味著我們現在一直在同一個座標上建立越來越多的地形特徵物。所以為了防止重複,在地圖塊被清理時也要刪除舊的地形特徵物。

一個比較快的方法是建立一個容器物體,並把所有的地形特徵物體都設定為其子物體。當Clear()被呼叫時就刪除這個容器物體並建立一個新的,這個容器物體將是管理器的子物體。

HexMap學習筆記(九)——地形特徵

一直在建立和銷燬物體,效率不是會很低麼?

可能感覺是這樣的,但現在還沒到考慮這個問題的時候。首先要關心的是地形特徵物放置座標的正確性,一旦解決了這個問題,而效能問題成為了瓶頸,接下來就能用相對聰明的方法來解決效率問題。那時可能會在HexFeatureManager.Apply方法裡解決,不過那將是之後的教程了。不過就算保持現在這樣,效率也沒那麼糟糕,別忘了我們已經把整個地圖分塊了。

2地形特徵物的擺放

目前把地形特徵物放在每個單元格中心的位置,這對於其他空著的單元格可行,但是對於包含河流、道路或者是水下的單元格,看起來就不太對。

HexMap學習筆記(九)——地形特徵
特徵物可以任意位置放置

所以在新增特徵物之前先確認單元格是否乾淨。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
限制放置位置

2.1每個方向一個特徵物

每個單元格中只有一個特徵物看起來不夠,單元格內還有更多的空間。讓我們在單元格每個方向上三角形的中心位置都新增一個特徵物。

當知道當前方向不是河流部分時在另一個三角化方法裡做這件事,但是仍要檢測是否處於水下或者是否存在道路,但在這種情況下,只需要關心道路是否穿過當前方向。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
特徵物沒有與河流毗鄰

這就生成了更多的地形特徵物,它們出現在道路的旁邊但還是遠離河流。要讓特徵物能順著河流出現,我們還需要在TriangulateAdjacentToRiver裡新增它們,這一次要求這部分單元格不在水下和不在道路的頂部。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
同樣也與河流毗鄰了

我們能渲染這麼多物體麼?

更多的特徵物會帶來更多的drawCalls,但是Unity的動態批處理會幫我們擺脫這個問題。由於特徵物都不大,它們的Mesh只會有很少的頂點,這使得它們中的許多可以在一次批處理中組合起來。但如果這會成為一個效能瓶頸,我們就會在之後處理。也可以使用例項化,這類似於使用許多小mesh時的動態批處理。

3特徵物的樣式

現在所有的地形特徵物的朝向都是一樣的,這看起來很死板,所以我們要設定隨機朝向。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
隨機朝向

雖然這產生了很多樣式型別,但是每當地圖塊重新整理,特徵物都會獲得一個新的隨機朝向。編輯特徵物是不應該還要注意是否會讓附近的特徵物的朝向反覆變化,所以我們要換一個方法。

我們有一張噪聲紋理圖,它是一直相同的,但是其包含的柏林梯度噪聲是區域性連貫的,這在我們擾動單元格頂點時是需要的,但對旋轉來說就不需要連貫的值,所有的旋轉都應該是等概率。所以需要的是一個非梯度隨機值的紋理,並且不使用雙線性過濾取樣。這實際上就是一個雜湊值的網格表,梯度噪聲紋理的基本形式。

3.1建立雜湊表

我們可以使用一個floats型別的陣列建立雜湊表並用隨機值填充,這樣就不再需要紋理圖了。在HexMetrics裡新增它,長度設定成256乘256,應該大致夠用。

HexMap學習筆記(九)——地形特徵

通過數學公式生成的隨機值會保持相同的結果,獲得的結果值的序列取決於一個隨機種子,它預設為當前時間,這就是為什麼每次執行時得到的結果序列都不一樣。

為了每次執行時特徵物在相同位置的旋轉相同,我們必須在初始化方法中新增一個seed引數。

HexMap學習筆記(九)——地形特徵

現在已經初始化了隨機數流,將始終得到相同的序列。因此所有在地圖生成之後的其他隨機事件也是一樣的。可以在初始化隨機數發生器之前儲存它的狀態來防止這種情況的發生。在初始化完成之後將恢復到原來的狀態。

HexMap學習筆記(九)——地形特徵

雜湊表的初始化由HexGrid完成,它同時也負責分配噪聲紋理。所以在HexGrid.start裡和HexGrid.Awake裡都新增上初始化,並做一個檢測確保不會過多生成。

HexMap學習筆記(九)——地形特徵

公共的種子欄位可以設定任何值,這裡設定的1234。

HexMap學習筆記(九)——地形特徵
選擇一個隨機種子

3.2使用雜湊表

在HexMetrics裡新增取樣方法來使用雜湊表,與噪聲紋理的取樣相同,使用XZ位置的座標來獲取相對的值。雜湊表的索引是通過把座標值取整,再除以雜湊表的大小獲得餘數得到。

HexMap學習筆記(九)——地形特徵

%是什麼?

這是取模操作符,它計算的是除法中的餘數,在我們的情況中是整數除法。舉個例子,序列-4,-3,-2,-1,0,1,2,3,4分別對3取模,得到的結果就是-1,0,-2,-1,0,1,2,0,1。

當座標為正值的時候沒問題,但是如果座標值為負時,得到的餘數也是負的。可以通過為負值結果加上雜湊表尺寸來修正這個問題。

HexMap學習筆記(九)——地形特徵

現在為每平方單位生成了一個不同的值,但是我們的特徵物其實並沒有這麼密集,它們之間的座標差距更大。所以在計算下標之前可以通過縮小位置座標來放大雜湊表,每4乘4的單位取一個唯一隨機值就夠了。

HexMap學習筆記(九)——地形特徵

現在回到HexFeatureManager.AddFeature裡,使用座標在雜湊表中取樣一個值並用它來設定特徵物的旋轉。如此一來當我們編輯地形時,地貌物的朝向將保持靜止。

HexMap學習筆記(九)——地形特徵

3.3特徵物的放置閾值

現在特徵物的旋轉變化明顯,但是放置位置的模式依然不變,每個單元格都有7個特徵物塞在一起。這裡可以通過省略一些特徵物來產生變化,通過訪問另一個隨機值來確認是否放置特徵物。

所以現在需要兩個雜湊值而不是一個,把雜湊表的陣列型別由float改為vector2也行,不過向量運算對於雜湊值來說沒有意義。所以為此建立一個特殊的結構體,所需要的就是兩個float值,探後新增一個靜態方法來建立兩個隨機值。

HexMap學習筆記(九)——地形特徵

它不需要序列化麼?

我們只在雜湊表中儲存這些結構,雜湊表也是靜態的,因此在重編譯時不會被Unity序列化,所以這個結構體也不需要序列化。

在HexMetrics裡修改程式碼,使用這個新的結構體。

HexMap學習筆記(九)——地形特徵

現在HexFeatureManager.AddFeature裡可以訪問到兩個雜湊值,使用第一個來確認是否新增特徵物或者跳過。當值大於等於0.5f的時候就直接跳出,這樣大約能排除一半的特徵物。第二個值就像之前一樣用來決定特徵物的旋轉。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
特徵物出現概率平均減少了50%

4繪製地形特徵物

我們來讓特徵物的出現位置可編輯,而不是像現在這樣每個單元格都放一點。但是我們並不是來繪製單獨的特徵物,而是給沒個單元格新增一個等級的屬性,這個屬性控制著地形特徵物出現的可能性。其預設值為0,代表特徵物體不會出現。

由於紅色的方塊實在不像是自然產生的地形特徵物,所以就將其定義為建築。它們代表著城市開發區域,新增城市等級的屬性到HexCell裡。

HexMap學習筆記(九)——地形特徵

可能覺得需要確保水下的單元格城市等級為0,但這沒有必要。因為已經在水下單元格三角化的時候跳過生成特徵物的步驟了。而且之後可能新增一些處於水下的特徵物,比如說碼頭或者船塢之類的。

4.1密度滑動條

在HexMapEditor裡新增另一個滑動條元件,讓城市等級可編輯。

HexMap學習筆記(九)——地形特徵

新增另一個滑動條到UI上並與相應的方法相連,這裡把它放在螢幕右邊的一個新的UI皮膚上,防止左邊的皮膚塞得太滿。

最大等級設定成多少比較合適?這裡還是使用4個等級,分別表示零,低,中,高密度的城市開發區。

HexMap學習筆記(九)——地形特徵
表示城市密度的滑動條

4.2閾值調整

現在城市等級可以編輯了,使用這個值來確認是否需要放置地形特徵物,為此需要將城市等級作為額外的引數新增到HexFeatureManager.AddFeature裡.。這裡我們更進一步,把單元格本身當做引數傳遞,這樣在之後會更方便一些。

一個快速使用城市等級的方式是把它乘上0.25並使用這個值作為新的特徵物出現閾值,這樣一來城市等級每提高一級,地形特徵物的出現概率就增加25%。

HexMap學習筆記(九)——地形特徵

在HexGridChunk裡傳遞單元格引數。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
根據密度放置特徵物

5多種地形特徵物預製體

光是特徵物的出現概率還不足以很明顯的區別不同的城市等級,有些單元格里的建築物數量甚至比預期都少。可以使用不同的預製體來表示不同城市等級,使區別更加明顯。

把HexFeatureManager裡的featurePerfab欄位的型別改成預製體的陣列,用城市等級減一的值作為陣列的下標。

HexMap學習筆記(九)——地形特徵

建立兩個額外的特徵物預製體並重新命名來表示三種不同的城市等級。等級1是低密度,就使用標準尺寸的方塊代表小平房。把等級2的預製體縮放設定為(1.5,2,1.5)表示更大一些的雙層建築。等級3則使用(2,5,2)的縮放表示大樓或高層建築。

HexMap學習筆記(九)——地形特徵
為每個城市等級使用不同的預製體

5.1混合預製體

我們不需要嚴格限制建築物型別分離,可以像真實世界中的一樣稍微混合一下。每個城市等級使用三個閾值表示每種建築物的出現機率。

對於等級1,設定小平房的出現概率是40%,而其他兩種建築不會出現。所以這組閾值是(0.4,0,0)。

對於等級2,把40%概率出現的建築型別由小平房改為雙層建築,並額外給予20%的概率出現小平房,大樓依然不會出現,這時的閾值組是(0.2,0.4,0)。

對於等級3,把雙層建築升級為大樓,小平房升級為雙層建築,再給予20%的概率出現小平房。

這裡的想法是,隨著城市等級的提高,會對現有建築升級並有概率在空地上新建新的建築。如果要替換現有建築,就需要使用相同的雜湊值範圍。比如雜湊值在0-0.4之間是1級小平房的話,那麼在這個雜湊值範圍的建築在城市等級為3時會變成大樓。

具體來說就是,在城市等級為3時,大樓的雜湊值範圍為0-0.4,雙層建築為0.4-0.6,小平房為0.6-0.8,如果從高到低來檢測,等級3的閾值組就是(0.4,0.6,0.8)。以此類推等級2就變成(0,0.4,0.6),等級1為(0,0,0.4)。

在HexMetrics裡用一個二維陣列儲存這些閾值組,然後新建一個獲取特定等級閾值組的方法。由於我們只關心與特徵物型別相關聯的等級,所以這裡忽略了等級0。

HexMap學習筆記(九)——地形特徵

接著在HexFeatureManager裡新增一個使用城市等級與雜湊值來選擇預製體的方法。當前城市等級大於0時使用其值減1作為閾值組陣列的下標,然後迴圈遍歷閾值組,直到其中一個超過閾值組的雜湊值為止,這就表示我們找到了一個預製體,如果沒找到就返回null。

HexMap學習筆記(九)——地形特徵

這就需要我們重新對預製體的引用進行排列,所以它們是從高密度到低密度排列的。

HexMap學習筆記(九)——地形特徵

使用新的方法去獲取預製體,如果最後都沒找到就跳出方法,否則就像之前一樣例項化然後繼續。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
預製體混合

5.2每級的變體

現在建築混合效果已經很不錯了,但建築本身依然只是三個不同樣子。我們可以把預製體集合與每級城市密度關聯起來增加更多的變化,然後在其中隨機選擇一個。這樣就需要用到一個新的隨機值,所以在HexHash裡新增第三個隨機值。

HexMap學習筆記(九)——地形特徵

把HexFeatureManager.urbanPrefabs的型別改為陣列型的陣列,然後在PickPrefab方法中新增一個choice引數,使用它將巢狀陣列與該陣列的長度相乘並轉換為整數,從而為該陣列建立下標。

HexMap學習筆記(九)——地形特徵

使用第二個雜湊值,即b來做出這個選擇,相應的旋轉的雜湊值由b變為c。

HexMap學習筆記(九)——地形特徵

在繼續往下之前,需要意識到一個問題。Random.value是有機率出現1這個數的,這樣就會出現陣列越界的問題。為確保這種問題不會發生,把最終得到的雜湊值減小一些。

HexMap學習筆記(九)——地形特徵

不幸地是Inspector裡無法顯示陣列中的陣列,所以我們也沒辦法在Inspector裡用拖入的方式賦值。要解決這個問題就需要建立一個封裝巢狀陣列的可序列化結構,給它一個專用方法處理引數到索引的轉換,並返回預製體。

HexMap學習筆記(九)——地形特徵

在HexFeatureManager裡使用這些封裝結構替代巢狀陣列。

HexMap學習筆記(九)——地形特徵

現在可以為每個密度級別定義多種建築物。因為它們是獨立的,所以不需要在每個組裡新增相同的數量。這裡在每個組裡都額外新增了一個較長較低的建築變體。其縮放分別設定為(3.5,3,2),(2.75,1.5,1.5)和(1.75,1,1)。

HexMap學習筆記(九)——地形特徵

6其他地形特徵物型別

使用目前的設定,可以生成一些看起來還行的城市。但地形特徵物並不僅僅包含建築物,還有植物和農場之類的,讓我們把這些也新增到HexCell中。它們並非只能單獨存在,是可以混合在一起的。

HexMap學習筆記(九)——地形特徵

當然也要在HexMapEditor裡新增額外的滑動條元件。

HexMap學習筆記(九)——地形特徵

新增到UI皮膚上並與相應的方法關聯。

HexMap學習筆記(九)——地形特徵
三個滑動條

HexFeatureManager裡也需要新增新的容器。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵

這裡也在每個密度等級給農場和植被設定了兩個型別預製體,用的也都是預設的方塊。其中農場使用的是淺綠色的材質球,植被使用的深綠色材質球。

農場的方塊高度設定為0.1單位,來表示矩形的農田,其縮放分別為第三級(2.5,0.1,2.5)和(3.5,0.1,2),第二級(1.75,0.1,1.75)和(2.5,0.1,1.25),第一級則是(1,0.1,1)和(1.5,0.1,0.75)。

而植物的預製體代表高大的樹木和大型灌木,第三級為(1.25,4.5,1.25)和(1.5,3,1.5)。第二級(0.75,3,0.75)和)(1,1.5,1),第一級為(0.5,1.5,0.5)和(0.75,1,0.75)。

6.1地形特徵物的選擇

每一種特徵物型別都要給其自己的雜湊值,所以它們有不同的生成模式,這樣就可以混合這些特徵物了。新增兩個額外的值到HexHash中。

HexMap學習筆記(九)——地形特徵

HexFeatureManager.PickPrefab現在使用不同的容器工作,新增一個引數以示區分。再一次修改預製體選擇的雜湊值為D,旋轉的值為E。

HexMap學習筆記(九)——地形特徵

之前AddFeature裡只選擇了一個建築的預製體,現在我們有更多選擇。在農田預製體裡再選擇一個,使用B作為其出現機率的雜湊值,型別選擇則是使用D。

HexMap學習筆記(九)——地形特徵

最終要例項化的預製體是哪個?如果其中一個預製體為null,那麼選擇不言而喻。但當兩個預製體都不為null時,使用雜湊值較小的那個。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
農場與城市的混合

接下來使用雜湊值C對植被的預製體做相同操作。

HexMap學習筆記(九)——地形特徵

但是這裡不能簡單的將程式碼複製貼上,當我們最終的選擇是農田而不是建築時,接下來應該將植被的雜湊值與農田的進行比較,而不是建築的雜湊。因此我得注意最終使用哪個雜湊值,並與使用的那個進行比較。

HexMap學習筆記(九)——地形特徵

HexMap學習筆記(九)——地形特徵
所有特徵物的混合

下一篇教程是Walls

本期工程地址:Hex-Map-Learning/tree/TerrainFeatures

有意向學習遊戲開發線下課程的童鞋,歡迎訪問http://levelpp.com/。

系列文章
HexMap學習筆記(一)——建立六邊形網格
HexMap學習筆記(二)——單元格顏色混合
HexMap學習筆記(三)——海拔高度與階梯連線

HexMap學習筆記(四)——不規則化
HexMap學習筆記(五)——更大的地圖
HexMap學習筆記(六)——河流
HexMap學習筆記(七)——道路

HexMap學習筆記(八)——水體
HexMap學習筆記(九)——地形特徵
HexMap學習筆記(十)——城牆
HexMap學習筆記(十一)——更多種地形特徵物

作者:沈琰
專欄地址:https://zhuanlan.zhihu.com/p/61853469

相關文章