The world at your fingertips — 天涯明月刀幕後16(資源管理1)
來自專欄遊戲開發隨筆
71 人贊了文章
差不多是時候可以體系的討論一下遊戲中的資源問題。
引擎雖然是整個遊戲的核心,但真正海量的開發內容,都在素材。整個引擎、工具團隊的存在意義,就是製作流程,讓更多的策劃、美術、邏輯程序等能高效製作遊戲的內容。這些數據需要被妥善的管理。除去基本的貼圖格式,其他數據的格式,一般都是自定義格式。用什麼形式表現,都在程序員的一念之間。如何組織數據,方便多人工作,如何提高工作效率或者載入效率,如何讓數據有更強的表現能力,都是引擎需要考慮的。
首先考慮的是,數據是否對合併(Merge)友好。
這是一個大型多人遊戲中非常重要的因素。大型開發團隊必然有很多人需要同時工作在一個版本中,編輯修改文件中的造成衝突是在所難免的。程序員們常用的編程語言,一般都是對Merge友好的,但資源文件就不一定了。
Merge友好有幾個先決條件。最基本的自然是Diff友好。所謂的Diff友好,就是一個文件的兩個不同版本,我們要可以方便的知道它們有沒有不同,有什麼地方不同。如果兩個版本的文件,都沒有辦法Diff,自然就無法Merge了。大多數文本文件都能比較好的滿足這個條件,基於文本行之間的差異,就能方便的做到上述要求。
但光Diff友好還不夠,還需要對編輯友好。比如圖像文件,通過一些輔助工具,也可以做到圖像Diff,通過圖形化的界面顯示出兩幅圖像的差別,用高亮的形式把像素差別顯示在了圖像上。這類文件,雖然可以Diff了,但並沒有太容易的方法可以輕易地編輯它,來合併大家的不同改動,所以本質上還是Merge不友好的。只有文本文件,才是編輯友好的,顯示出不同,可以方便的合併和修改,解決多人工作中的修改衝突。
最後需要的,是可讀性,或者說,是理解友好。舉個例子,一個Mesh文件,如果用文本表達,它既是Diff友好,又是編輯友好的,但整個文件裡面大段大段都是頂點坐標。即使有了Diff,你也很容易編輯它,但由於你無法輕易理解這個頂點的含義,對你來說,Merge這個文件無異於碰運氣。
有了Diff友好、編輯友好,和理解友好,我們就可以解決第一個問題,合併友好問題了。
下一個要考慮的問題,是編輯和運行時刻的效率問題。
編輯效率問題比較簡單,如果文件有好的工具可以修改,效率就比較高。配合合併友好性問題一起考慮,那麼我們還需要加上一個維度,即這個資源文件是否有利於合併。即使我們可以高效的修改,但如果不能高效的合併,我們依然會在工作文件有編輯衝突時面臨相當大的困境。總體來說,引擎要提供高質量的編輯工具,以及保證文件的可合併性。
載入效率問題是另一個需要考慮的問題。遊戲運行時必然需要高效的載入文件,減少Loading時間。載入效率也分成IO效率以及解析效率兩個環節。
所謂的IO效率,就是文件被讀進引擎的速度有多快。IO一直是現代電腦的最大瓶頸,現代電腦在CPU、內存、網路速度都有成百上千倍提升,但IO速度始終是系統瓶頸。直到SSD硬碟的出現,IO才有了巨大的提升,但即使是SSD硬碟,依然需要開發者認真考慮IO問題,因為它比內存速度,依然有數量級上的差距。
回到我們的問題,IO效率需要考慮文件大小,這個通常不是最大的問題,因為IO系統最大的問題在零星小文件的讀取,不在於持續讀取速度。只要讀取文件沒有數量級上的差異,多幾百KB並不從根本上影響IO效率。
另一個問題便是尋道問題,這個需要重點解決。零散小文件是IO效率的殺手。以前做主機遊戲的時候,這個問題更突出,因為那些遊戲使用光碟來讀取文件,而不是硬碟,隨機尋道基本是不可能的事情。一般通過一些稱之為Linear loading的手段加以解決,即有相當多的文件總是按照指定的順序被讀取的,如果我們能合理的分組,將它們一次全部讀入,就可以大大減少尋道時間。說起來容易,做起來非常瑣碎,因為這個改動很可能會影響上層的載入邏輯。當然有很多巧妙地辦法可以解決這個問題,比如上層載入的時候,都通過統一的管理介面。管理介面底層其實是順序讀文件的,但在上層看來是透明的,上層只管不停的提出載入需求即可。這樣的系統通常會有一個預先的記錄環節,在正常載入過程中,錄下所有需要讀取的小文件,把這些小文件順序輸出到一個大文件中。然後把這個大文件燒錄進DVD版本。後續正式載入遊戲的時候,高層邏輯正常載入這些小文件,但底層的IO介面,其實是順序讀那個預先生成的大文件。如果上層和下層的讀文件順序嚴格一致,那就可以非常快速的讀完這個文件。用這類方法,如果用在隨機尋道比較多的遊戲引擎中,比如Unreal,可以提升讀盤速度10-100倍,非常驚人。更進一步可以做的,就是在最後DVD刻錄的時候,把這些大文件,盡量放在DVD的外道,因為外道的讀盤速度會更快。
在我們的PC硬碟上,這個解決方法依然是非常有效的,但這個方法有幾個缺點。一是它犧牲了讀盤的安全性,我們的錄製讀盤順序過程,和實際載入遊戲過程中,讀盤順序必須完美一致,如果有不一致,遊戲通常就會有不可知問題,至少也會crash。二是這個模式的開發流程更複雜,預先錄製讀盤順序,需要遍歷每個地圖,耗時很長,稍有邏輯改動,又要重新做一遍,嚴重影響出版本的效率。三是這個模式應用受限,無法用在可變順序的載入上。比如我們網遊隨時要按照地圖塊來進行streaming,我們很難預測玩家會如何移動,需要載入怎麼樣的場景塊,這就大大限制了他的應用範圍。
當然整個思路還是可用的,如果我們能把載入數據分割成更小的塊,通過合適的管理,也能在效率和靈活性上達成一定的妥協。
IO效率只是載入效率的一部分問題。另一個就是解析處理的效率。
對於文本類型的數據文件,比如XML、Json,讀進來只是很小的一部分工作,當內容到了內存,就要考慮如何處理。這些文件都要在內存中被處理,建立DOM樹,不同的處理庫的快慢,顯然對效率有極大的影響。
簡單的庫處理解析內容,對每一個需要生成的節點,都會用動態內存分配空間,或是動態分配字元串內容。這雖然簡單,但並不高效。高端一點的庫,比如Rapidxml,大量用了in placement的new,直接在需要解析內容的內存塊上直接構建內容,對程序員來說,需要了解,這塊內存已經被DOM樹使用了,內存管理也特別小心,某種意義上,整個實現變得更dirty,高層也需要知道這些內存管理的細節。但效率的確極高。遊戲開發,往往需要打破常規,做更多違反良好軟體工程實踐的優化,提高效率。
Milo受到了Rapidxml的啟發,在周末寫了一個RapidJson,速度極快,號稱只需要strlen的一倍時間就可以解析完同等長度的Json文件。封裝成公共組件後給大家用,有些同學會抱怨說這個API用起來不太順手,需要先分配好內存才能在內存上構建這個DOM樹,無法直接扔進去stream,從而得到新生成的DOM樹。然而這是沒有辦法的選擇,in placement的生成內容,自然需要上層幫助管理這塊內存,為了運行效率,自然帶來了額外的開發複雜度。
解析完對象,還需要實際在引擎中生成相應的對象。根據不同的引擎,生成效率也各不相同。最簡單的方法可以在多線程載入的時候直接生成相關的對象,管理邏輯非常簡單清晰。但這個方式有幾個缺點,多線程的時候,生成一個新對象未必是線程安全的,需要很多保護,有些時候甚至是不可能做到的。另一個原因是不容易控制生成的效率,如果某一刻突然有大量的對象需要生成,就會block住載入邏輯非常久。
好一點的做法是把所有需要生成的對象放進一個隊列。然後另外在主線程寫處理代碼,在線程安全的時候從隊列裡面拿出對象一一處理。這樣我們就解決了線程安全問題。至於另一個生成效率問題,既然我們已經把對象解析和生成的邏輯分開了,那麼什麼時候生成對象已經變得相當靈活,我們完全可以給定一個固定的時間budget,每一幀只花2ms生成對象,所有來不及處理的對象放到下一幀處理即可。分割問題到更可控的小規模,是解決問題百試不爽的好辦法。
下一回我們聊聊資源的組織管理方式,以及資源的格式問題,最後簡單介紹一下天刀的一些資源管理取捨。
顧煜:天涯明月刀幕後(總目錄)
推薦閱讀:
※軟體危機の典型癥狀
※最好的代碼就是沒有代碼
※3.3 需求模型
※【軟體工程學習筆記】軟體體系結構
※3.2 獲得需求的方法
