HexMap學習筆記(五)——更大的地圖
HexMap學習筆記(二)——單元格顏色混合
HexMap學習筆記(三)——海拔高度與階梯連線
HexMap學習筆記(四)——不規則化
HexMap學習筆記(五)——更大的地圖
前言
這篇教程為地圖新增了更多的編輯功能,是其更像一個六邊形地圖編輯器了。程式碼與操作均不難,簡單過一遍就行。
本篇原文地址:Hex Map 5
本篇難度:★☆☆☆☆
這個教程是HexMap系列的第五篇,之前都是在一張很小的地圖上進行編輯,這次將會把地圖放大一些。
是時候把地圖擴大一些了
1.地圖網格塊
我們不能把地圖網格設定地太大,因為單個mesh的容量是有限的(注:Unity中Mesh陣列最大能儲存65000個頂點)。解決方案是使用多個mesh拼接,這樣就能把網格分成若干塊,這裡將使用一個固定大小的矩形塊。
把網格分為三乘三大小的網格塊的樣子
先設定網格塊塊的大小為五乘五,所以每一個網格塊是25個單元格,在HexMetrics裡定義。
網格塊的大小設定成多大比較合適?
視情況而定,較大的網格塊意味著數量少但較大的mesh,這會使draw calls較少。但是較小的網格塊會在視錐體裁剪剔除時效率較高,繪製的三角形較少。實際方法就是先設定一個預估的大小然後再進行微調。
現在我們不能再使用網格本身的尺寸,而是用網格塊的倍數的尺寸。修改HexGrid讓其以塊的形式定義網格的大小。預設先設定為4乘3個網格塊,這樣就是12個網格塊和300個單元格,這對於地圖測試是個較為合適的大小。
我們仍然要使用width和height這兩個變數,但它們應該變成私有型別。然後重新命名為cellCountX和cellCountZ。使用IDE的快捷功能可以一次重新命名所有出現這些變數的位置。這樣在處理地圖塊或者單元格的個數時就會很清楚。
指定地圖塊的尺寸
修改Awake方法,這樣在需要它之前單元格的數量就能用地圖塊的數量推算出來。把單元格的建立放到它們自己的方法中,讓Awake保持整潔。
1.1地圖塊預製體
我們需要一個新的元件指令碼來表示地圖塊。
接下來建立一個地圖塊的預製體,複製HexGrid物件並重新命名為HexGridChunk。刪除上面的HexGrid指令碼並用HexGridChunk代替,然後把其建立為預製體並在場景中刪除。
地圖塊的預製體,與其自己的畫布元件和HexMesh物件
因為要在HexGrid裡例項化這些網格塊,給其預製體一個引用。
新增地圖塊的欄位
地圖塊的例項化看起來很像是單元格的例項化,為之後方便使用,用陣列把它們存起來並用雙重迴圈去填充。
HexGridChunk裡的例項化與之前例項化六邊形網格相似,在Awake裡設定資料並在Start裡三角化。它需要canvas和mesh的引用和一個儲存自身單元格的陣列。不過這裡並不會建立單元格,仍然把建立步驟放在HexGrid中完成。
1.3把單元格賦值到地圖塊中
HexGrid仍然負責建立所有的單元格,這是對的。現在要做的是新增單元格到它所連線的地圖塊中,而不是設定它們的mesh和canvas。
我們可以通過對X和Z按地圖塊的尺寸進行整數分割來找到正確的地圖塊。
通過整數結果還可以確認每個單元格在其所在地圖塊中的下標,有了這個下標就可以把單元格新增到地圖塊中。
之後HexGridChunk.AddCell就可以把單元格放到它自己的陣列裡,再設定單元格和其UI的父節點。
1.3清理程式碼
現在HexGrid能清除掉它的canvas和mesh子物體物件,還有與之相關的程式碼。
因為我們刪除了Refresh方法.現在HexMapEditor不能再使用它了。
清理後的HexGrid
當點選執行後,地圖看起來一樣,但是場景內物件的層級會發生改變。HexGrid現在會生成地圖塊的子物體,包含其單元格連同mesh和canvas。
執行模式下的地圖塊子物體
單元格的座標顯示標籤有點問題:我們一開始設定的標籤寬度是5,這足夠顯示兩個字元。在較小的地圖上剛好用完,但現在我們可以獲取到“-10”這樣有三個字元的座標,這樣一來字元無法相匹配並有斷層。把標籤的寬度增加到10或更多來解決這個問題。
更寬一些的單元格標籤
現在我們能建立大得多的地圖!當開始生成整個網格地圖時,如果地圖較大可能會花上一點時間。但當一次完成之後,你就有更大的區域可以玩了。
1.4修復編輯功能
現在地圖編輯功能用不了,因為網格的重新整理方法刪除了。現在需要重新整理的是單獨的地圖塊,所以在HexGridChunk裡新增Refresh方法。
那應該在什麼時候呼叫這個方法?之前是每時每刻都在重新整理,因為那時只有一個mesh,但現在我們有很多的地圖塊,就不能每個地圖塊都一直重新整理,僅當地圖塊被改變時再重新整理效率會更高,否則編輯較大地圖時會感覺很卡。
那問題就變成了如何知道哪個地圖塊需要重新整理。一個比較簡單的方法是確保每個單元格都知道它是屬於哪一個地圖塊,這樣單元格就能在其被改變時重新整理它所在的地圖塊,所以給HexCell一個地圖塊的引用。
當新增單元格時HexGridChunk可以直接把自己賦值給它。
當這些連線建立之後,在HexCell裡建立一個Refresh方法,單元格重新整理時就同步重新整理自己所在的地圖塊。
我們不需要把HexCell.Refresh()設定成公共方法,因為只有單元格自己清楚它什麼時候發生了變化。例如,在高度改變之後。
實際上只有在高度被設定成了一個不同的值時才需要重新整理,甚至都不需要在賦了一個相同的高度值後重新計算,所以這種情況下可以在set屬性的一開始就跳出。
然而這會跳過第一次設定高度為0時的計算,因為0是網格的預設高度,為預防這一點,確保初始值是你永遠都不會用到的值。
什麼是int.MinValue?
這是int所能表示的最小值,在C#中int是一個32位的數字,它有2的32次方種可能的整數值,分成正值和負值和0,其中一位用來指出這個值是不是負的。
最小值是負的2的31次方=-2147483648,我們永遠不會使用這個高度等級。
最大值是2的31次方減1=2147483647,比2的31次方少1是因為還有0存在。
為了檢測顏色是否被改變,我們也要把顏色變成一個屬性。重新命名成首字母大寫的Color,接著改成屬性並使用私有的color變數。顏色的預設值是標準黑色,就這個不用改了。
在執行模式下會報空引用異常,這是因為在把單元格賦值給它所在的地圖塊之前就設定了預設的顏色和高度。最好的辦法是在這裡先不重新整理,因為我們會在初始化完成之後三角化它們。換句話說就是隻有在地圖塊被賦值完成後才進行重新整理。
現在又能編輯地圖了!然而還是有個問題,這似乎會在跨越地圖塊邊界編輯顏色時出現。
地圖塊邊界之間出現了問題
這個問題很好理解,因為一個單元格發生變化後所有它連線的相鄰單元格也會發生改變,而這些相鄰單元格有可能在不同的地圖塊中。最簡單的解決方案是當單元格與其相鄰單元格不在一個地圖塊是也重新整理一下相鄰單元格的地圖塊。
這雖然可行,但我們要重新整理單個地圖塊多次,一旦我們在一次繪製橫跨多個單元格時,情況就更糟糕了。
我們沒必要在地圖塊重新整理資訊時直接三角化,我們可以通知這個地圖塊需要重新整理,然後在編輯完成時一次性三角化。
因為HexGridChunk沒有用來做其它的事情,我們可以用指令碼的enable狀態作為需要重新整理的訊號,當開始重新整理時,給指令碼設定enable狀態,就算多次設定也沒關係,不會有變化。稍後指令碼更新時,我們就在這裡進行三角化,然後再次設定狀態為disable。
我們使用LateUpdate代替Update,這樣就能確保三角化發生在當前幀編輯完成之後。
Update與LateUpdate有什麼區別?
每一幀中,所有enabled狀態的元件中的update會在隨機時候呼叫.在這結束之後,LateUpdate方法也是同樣的邏輯.所以這是兩個更新步驟,一個早一些一個晚一些。
因為指令碼元件預設狀態就是enabled,所以我們不再需要在Start裡三角化,現在就能刪了這個方法。
20乘20的地圖塊尺寸,包含10000個單元格
1.5共享列表
儘管現在三角化網格地圖的方式有了較大的改變,但HexMesh裡的工作還是一樣的,它只需要一個單元格陣列就能幹活,無論是一個還是多個mesh都沒有關係。之前我們都沒有考慮過使用多個mesh,或許這裡能有優化的地方?
HexMesh裡使用的列表實際上是一個臨時資料快取,它只在三角化的過程中使用。然而現在地圖塊的三角化也是一次性完成的,所以還是隻需要設定一次列表的資料而不是每個mesh三角化時都設定一次,我們可以把列表設定為靜態型別來實現這個改動。
使用靜態型別的列表會有很大的效率提升麼?
這只是一個說明該如何使用列表的簡單改動,雖然提升不大但值得這麼去做,即使我們現在不用太過擔心它的效率問題。
這樣改動後效率會有些微提升,因為列表共享以後所需要的記憶體分配要少上一些。當使用20乘20的地圖塊時,節省的記憶體差不多剛超過100MB。
2.攝像機控制
地圖變大是件好事,但是有的地方看不見就很捉急。為了能看到整張地圖的全貌,需要攝像機能夠四處移動,焦距變化功能也應該是必須的。接下來就實現一個有這些功能的相機。
新建一個空物件命名為HexMapCamera,重置它的transform元件。為其新建一個子物件並命名為Swivel,然後在Swivel下建立一個子物件Sticlk。把主相機設定為Stick的子物體,然後重置其transform元件。
攝像機的層級
Swivel的工作時控制攝像機看向的角度,把它的預設旋轉設定為(45,0,0)。Stick則是用來控制攝像機的遠近,設定預設座標為(0,0,-45)。
現在我們需要一個指令碼來控制這個組合裝置,在根節點新增這個指令碼並新增Swivel和Stick的引用,在Awake的時候獲取它們。
攝像機的指令碼
2.1攝像機遠近控制
第一個要實現的功能就是攝像機的遠近視距變化,我們可以用一個float變數記錄當前的視距,值為0表示相機拉到最遠,值為1表示相機拉到最近,把初始值設為1即最近。
攝像機變距功能通常都是用滑鼠滾輪或者類似的輸入方法控制,我們可以使用Unity預設的MouseScrollWheel輸入軸,然後在Update方法裡檢查是否有輸入增量,如果有再呼叫方法調整視距。
要調整視距幅度就簡單的加上輸入增量,然後將值限制在0-1之間。
當我們改變視距的值時,攝像機的距離也應該相應的改變,可以通過調整Stick的Z軸座標來實現。新增兩個公共型別的float變數,設定Stick的最大和最小距離。由於我們建立的地圖相對較小,先暫時設定為-250和-45。
視距改變之後,應該基於這個新的視距線性插值計算這兩個值,然後更新Stick的位置。
現在能調整了,但還不太好用。通常遊戲中的攝像機會在焦距拉遠的時候過渡到從上至下的俯視角。我們可以用過旋轉Swivel來實現,所以也同樣為Swivel新增最大和最小的旋轉角度標量,預設設定為90和45。
就像計算Stick的座標一樣,插值計算出合適的攝像機角度,然後給Swivel的旋轉賦值。
Swivel的最小值和最大值
可以通過調整滑鼠滾輪的靈敏度設定來調整視距的變化速度,在Edit/ProjectSettings/Input裡可以找到,例如可以把靈敏度預設值0.1改為0.025來獲得更為平滑的視距變化感覺。
2.2攝像機移動
下一步是攝像機的移動,我們要在Update中檢測X和Z方向的移動。和調整視距類似,可以使用預設的水平和垂直的輸入軸,這允許我們用WASD和方向鍵來移動攝像機。
最直接的方法是獲取攝像機當前的座標,加上X和Z軸上的輸入增量,然後將結果賦值到攝像機的座標值上。
現在可以按住方向鍵或者WASD來控制攝像機移動了,但速度不是恆定的,它取決於幀率。為了能確定移動的距離,需要使用時間增量以及期望的移動速度,所以新增一個公共型別的變數moveSpeed並設定為100,接著把他和時間增量作為變數因素新增到座標的位移增量中。
移動速度
現在也能在X或者Z軸上以恆定速度移動了,但如果沿著對角線在兩個方向上同時移動會快一些,所以需要把移動速度的向量標準化,當做一個方向來使用。
對角線的速度也修正了,但有點出乎意料的是當鬆開按鍵後相機依然會持續移動一段時間。這是因為輸入軸按下按鍵是不會立即跳轉到它的極值上,而是會有一個過渡時間,鬆開按鍵時也是一樣。又因為我們把輸入的向量標準化了,所以在這一段時間內一直會維持在最大速度上。
現在我們可以修正輸入設定去除它的延遲過渡,但是帶來的操作平滑的感覺卻很值得保留,所以我們可以把當前輸入的最大值作為移動的阻尼係數。
現在攝像機的移動功能在焦距拉近時沒什麼問題,但當拉遠時又感覺太慢了,我們需要在攝像機拉遠時提高速度。可以把之前單個moveSpeed分成兩個針對最大和最小焦距的移動速度,然後插值計算它們。先分別設定為400和100。
根據視距變化的相機移動速度
現在攝像機可以在地圖中自如的移動了!實習上現在能移動出地圖的邊界,這並不合理,攝像機應該只能在地圖中移動。為修正這個問題,首先要知道地圖的範圍。所以在指令碼中獲取HexGrid的引用。
需要獲取網格的尺寸
用一個新方法限制提取出來的座標。
座標的X最小值為0,最大值則由地圖的大小決定。
座標的Z值也是一樣。
實際上這稍微有點不精確,基準線應該是在單元格的中心而不是左邊,我們希望攝像機最終會停在右邊單元格的中心上,因此需要在X的最大值上減去半個單元格寬度。
基於同樣的原因Z的最大值也要減去一些,因為度量標準有點不一樣,所以這裡要減去整個單元格的寬度。
現在移動功能就完成了,除了一個小細節。有的時候UI會響應方向鍵,其結果就是在移動攝像機的時候UI上的滑動條也會跟著移動。當你點選UI並把滑鼠停留在上面,UI就會認為自己處於啟用狀態,這時就會發生這種情況。
可以取消選擇EventSystem上的Send Navigation Event選項來禁止UI監聽按鍵事件。
取消選中Send Navigation event
2.3攝像機旋轉
想看看山崖後面是什麼東西?如果能旋轉攝像機就能很方便的看到!所以同樣要新增這個功能。
旋轉功能與焦距沒有什麼關係,所以定義一個速度變數就足夠了。新增一個公共型別變數rotationSpeed並設定為180。在Update裡通過對輸入軸"Rotation"取值來獲取旋轉增量,並在需要時調整旋轉。
旋轉速度
事實上預設輸入軸裡並沒有"Rotation",我們要自己去建立一個。在輸入設定中複製最上面的"Vertical"然後把名字改成"Rotation",按鍵分別改成Q,E,逗號和點。
自設的旋轉軸
我下載了Unity的工程包,為什麼裡面沒有這個input設定?
輸入設定是專案範圍的設定,它不包括在Unity的工程包中。幸運地是你很容易自己新增一個,如果沒有新增就會報一個輸入軸丟失的異常。
在AdjustRotation裡記錄並修正旋轉角度,需要進行旋轉的是包括支架在內的整個攝像機裝置物件。(即根節點)
因為旋轉一整圈是360度,所以限制旋轉角的範圍在0-360之間。
旋轉操作
旋轉功能就完成了,不過當你試著轉動時你會發現移動方向是基於世界座標系的絕對座標。所以當旋轉180度之後移動方向會與預期的完全相反,而移動方向如果是相對於攝像機的視角則會更易於使用,所以我們把當前攝像機的旋轉與移動相乘。
相對自身座標系的移動
3.高階編輯功能
現在我們有了更大地圖,是時候更新一下編輯工具了。一次編輯一個單元格太過侷限了,所以使用更大的筆刷是一個不錯的注意。一次只編輯顏色或者是高度中其中一項,而讓其他部分保持不變,這同樣也是一個很實用的功能。
3.1顏色和高度可選功能
我們可以通過新增一個切換(toggle)組來實現顏色選擇功能。複製一個顏色選項卡並將標籤名改為“---”或者別的什麼能代表這不是一個可選顏色的字元。然後設定其OnValueChanged事件傳遞引數為-1。
無效顏色陣列下標
當然這對於我們的顏色陣列來說是一個無效的下標,我們可與以此確定是否對單元格應用顏色修改功能。
高度變化使用的是一個滑動條(Slider)元件,所以我們沒辦法在這裡面建立一個切換開關。於是用一個分離的切換選項卡(toggle)表示是否應用高度編輯,預設設定為開啟狀態。
新增這個新高度開關到UI上,把所有內容都放到一個新的UI皮膚上,把高度滑動條設定為水平好讓UI看起來更整潔一些。
為了讓這個開關起效,需要一個新的方法並與UI相關聯。
當你掛載方法的時候確保是使用的方法列表頂端的dynamic bool method。正確的版本不會在檢視皮膚中顯示檢驗框。
傳遞高度選中狀態到選項卡中
現在你可以選擇是修改顏色還是高度,或者像之前一樣同時修改。甚至可以兩個都不選,儘管現在這個功能沒什麼用。
編輯高度和編輯顏色的切換
為什麼當我選擇一個顏色後自動取消選擇高度了?
這個情況發生在你把所有的選項都放在一個選項組時,你可能複製了一份顏色選項卡然後修改成高度選項卡,但沒有清理它的選項組。
3.2筆刷尺寸
要實現一個可以變化的筆刷尺寸,新增一個整數變數brushSize和一個在UI中設定它的方法。因為要使用滑動條,所以再一次把float引數轉換為整數。
筆刷尺寸滑動條
你可以通過複製高度的滑動條來新增一個新的滑動條,修改它的最大值為4並關聯正確的方法,這裡還同時給它新增了一個標籤。
滑動條的設定
現在需要用EditCells方法編輯多個單元格,它負責呼叫所有受影響的單元格的EditCell方法,原本選擇的單元格將作為筆刷的中心。
筆刷的尺寸定義了我們的編輯範圍的半徑,當半徑為0時,就僅包含中心單元格。當半徑為1時,包含中心單元格與其所有相鄰單元格。當半徑為2時,還包含相鄰單元格的直接相鄰單元格,以此類推。
半徑為3的尺寸
要編輯所有的受影響單元格就需要迴圈遍歷它們,首先需要中心單元格的X和Z座標。(之前自定義的coordinates結構體)
通過減去半徑得到了最小的Z座標,即第0行的位置。從這一行開始迴圈直至找到中心行。
底部行的第一個單元格與中心單元格具有相同的X座標,這個座標隨著行數的增加而減少。最後一個單元格的X座標總是等於中心單元格的X座標加上半徑。現在可以相對於單元格的座標來迴圈每一行。
現在指令碼里還沒有一個傳入座標引數的HexGrid.GetCell方法,所以新增一個並轉換成偏移座標獲取單元格。
尺寸為2的下半部分
剩下的部分可以通過從最上面一行迴圈到中心行得到,這裡除了排除中間行外剩餘邏輯是對稱的。
尺寸為2的完整筆刷大小
功能基本是正確的,除了當筆刷延伸到地圖邊界之外時。而當其發生時會報一個陣列越界異常。為了防止這種情況發生,在HexGrid.GetCell裡檢測邊界並且當獲取不存在的單元格時返回null。
為了防止報空引用異常,HexMapEditor需要在編輯前檢查是否真的存在這個單元格。
使用多種筆刷尺寸
3.3單元格標籤顯示切換
大多數情況下可能不需要顯示單元格的標籤,所以我們做一個切換是否顯示的功能。由於每一個地圖塊的管理器都有它自己的canvas,所以在HexGridChunk中新增一個ShowUI方法,當UI需要顯示時啟用它,否則就關閉它。
在Awake時預設隱藏標籤UI。
由於是切換整張地圖的標籤UI顯示,同樣在HexGrid中新增一個ShowUI方法,它就簡單的把切換請求傳遞到所有地圖塊中。
HexMapEditor也同樣建立呼叫這個方法,把切換請求轉發到HexGrid中。
最後新增一個toggle到UI上並繫結這個方法。
標籤選項
作者:沈琰
專欄地址:https://zhuanlan.zhihu.com/p/57029837
相關文章
- HexMap學習筆記(六)——河流筆記
- HexMap學習筆記(七)——道路筆記
- HexMap學習筆記(八)——水體筆記
- HexMap學習筆記(九)——地形特徵筆記特徵
- HexMap學習筆記(四)——不規則化筆記
- HexMap學習筆記(一)——建立六邊形網格筆記
- HexMap學習筆記(二)——單元格顏色混合筆記
- iOS學習筆記20 地圖(二)MapKit框架iOS筆記地圖APK框架
- JVM 學習筆記(五)JVM筆記
- cmake學習筆記(五)筆記
- DP學習筆記(五)(2024.11.16)筆記
- c++學習筆記(五)C++筆記
- Kubernetes學習筆記(五):卷筆記
- 字典--Python學習筆記(五)Python筆記
- 弦圖 學習筆記筆記
- Jenkinsant介紹(學習筆記五)Jenkins筆記
- Qt學習筆記(五)QString 字串QT筆記字串
- 飛機的 PHP 學習筆記五:陣列PHP筆記陣列
- 第五天學習Java的筆記Java筆記
- hive學習筆記之五:分桶Hive筆記
- Netty學習筆記(五)NioEventLoop啟動Netty筆記OOP
- TypeScript學習筆記之五類(Class)TypeScript筆記
- springcloud學習筆記(五)Spring Cloud ActuatorSpringGCCloud筆記
- numpy的學習筆記\pandas學習筆記筆記
- 計網學習筆記五 wireless && mobile networks筆記
- Perl學習筆記(五)——關聯陣列筆記陣列
- Java IO學習筆記五:BIO到NIOJava筆記
- Spring 學習筆記(五)執行時注入Spring筆記
- systemtap和火焰圖學習筆記筆記
- JVM學習筆記五--虛擬機器棧JVM筆記虛擬機
- AD學習筆記----原理圖設計筆記
- Python學習筆記 - 下載圖片Python筆記
- Java設計模式學習筆記(五) 單例模式Java設計模式筆記單例
- Redis 學習筆記(五)高可用之主從模式Redis筆記模式
- C++學習筆記-五大基本概念C++筆記
- Dubbo 學習筆記(五) 開發環境常用技巧筆記開發環境
- 圖論進階學習筆記(四)(2024.10.4)圖論筆記
- [學習筆記] 樹上差分 - 圖論筆記圖論