資源及文件系統程序員
載入及管理多種媒體,是遊戲引擎必須具有的能力。多數引擎會採用某種類型的資源(或資產)管理器,載入並管理遊戲所需的資源,並確保在同一時間每一個媒體文件只可載入一份。每一個資源管理器都會大量使用文件系統。本文將介紹現代三維遊戲引擎中的各類文件系統API,再分析典型資源管理器的運做方式。數據庫
文件系統網絡
文件名和路徑數據結構
關於文件和文件夾路徑的概念,絕對路徑和相對路徑的概念,它們在各類操做系統之間的區別,屬於常識範疇,此處不贅述。異步
關於搜尋路徑,是指含若干個路徑(以特殊字符分隔)的字符串,尋找文件時會從這些路徑逐個尋找,PATH環境變量就是一種搜尋路徑。在運行期搜尋資產是費時的作法,而一般資產路徑會在運行期以前就得知,因此應該徹底避免搜尋資產。數據庫設計
關於路徑API,通常用於對路徑進行多種操做,如分離「目錄/文件名/擴展名」、使路徑規範化、絕對和相對路徑互轉等等。遊戲引擎一般會實現或封裝輕量化的路徑處理API,以便實現跨平臺,從各類特殊的儲存媒體(如記憶棒、DVD盤、網絡文件系統等等)中存取數據,以及提供操做系統API未能提供的功能,如串流(即在遊戲運行中同時載入數據)。ide
許多遊戲引擎都會把文件I/O API封裝成自定義的API,這樣至少有三個好處:函數
保證I/O API在全部目標平臺上均有相同行爲;工具
API能夠簡化到只剩下實際須要的函數,使維護開支維持最小限度;佈局
可提供延伸功能,如處理各類特殊的儲存媒體(同自定義路徑處理API)。
每次調用輸入/輸出,都須要稱爲緩衝區的數據區塊,以供程序和磁盤之間傳送字節。當API負責管理數據緩衝,就稱之爲有緩衝功能的API,不然爲無緩衝。C標準程序庫中,以f開頭的文件API是帶緩衝的,如fread()
,沒有f開頭是無緩衝的,如read()
。有時自行管理緩衝區是有必要的。例如往日誌寫數據可能會顯著下降性能,能夠先把數據累積在內存緩衝,滿溢後才寫進盤內,甚至把緩衝輸出函數置於另外一線程裏,以免令主遊戲循環發生流水線停頓。
C標準庫的兩種文件I/O庫都是同步的,即程序發出I/O請求之後,必須等待讀/寫數據完畢,程序才能繼續運行。
串流是指在背景載入數據,而主程序同時繼續運行。爲了支持串流,必須使用異步文件I/O庫。多數異步I/O庫允許主程序在請求發出後一段時間,等待I/O操做完成才繼續運行。有些異步I/O庫允許程序員取得某異步操做所需時間的估算,一些API也能夠爲請求設置時限,並設置請求超時的安排(例如取消請求、通知程序、繼續嘗試等)。
異步I/O操做常有不一樣的優先權,例如從硬盤中串流音頻,而且在串流其餘資源時播放音頻,顯然前者優先權高於後者。異步I/O系統必須能暫停較低優先權的請求,纔可讓較高優先權的I/O請求有機會在時限前完成。
異步文件I/O的實現原理,通常是利用另外一線程進行同步操做來實現。主線程調用異步函數時,會把請求放入一個隊列,並當即傳回。同時,I/O線程從隊列中取出請求,並以阻塞I/O函數處理這些請求。請求的工做完成後,就會調用主線程以前提供的回調函數告之該操做己完成。若主線程選擇等待完成I/O請求,就會使用信號量處理(每一個請求對應一個信號量,主線程把自身處於休眠狀態,等待I/O線程在完成請求工做後通知信號量)。
資源管理器
資源管理器由兩部分組成:一部分負責管理離線工具鏈,用來建立資產並把它們轉換成引擎可用的形式;另外一部分在執行期管理資源,確保資源在使用前已載入內存,不須要時從內存卸下。
離線資源管理與工具鏈
資產的版本控制
小型的遊戲項目中,遊戲資產的管理方式能夠是把組織不嚴謹的文件以項目特設的目錄結構置於公用網盤中;有些遊戲團隊使用源碼版本控制工具來管理資源。
可是,藝術資產一般有極大的數據量,直接從中央版本庫複製到本地每每是低效的。如下是一些參考解決方案:
遊戲引擎不會使用多數資產本來的格式,而是須要經過一些資產調節管道(ACP)將資產轉換爲引擎所需的格式,其中每一個資源須要有元數據描述如何對資源進行處理。例如描述壓縮紋理時,使用哪一種壓縮方法;描述導出動畫片斷時,導出哪一個範圍的幀。
爲了管理這類元數據便須要某種數據庫。不一樣的引擎差異巨大,有的是嵌入到資產源文件自己,有的是每一個資產源文件伴隨一個小文本文件,有的將元數據寫進XML文件中,有的使用真正的關係數據庫。它通常提供如下功能:
用Perforce以提供版本控制,元數據改成XML。Builder管理演員(包含行爲的動態對象)和關卡(含靜態背景網格和關卡信息等)兩種類型的資源,動畫能夠組成名爲動畫包(buddle)的僞文件夾;引擎含一組基於命令行的工具,用於查詢數據庫,處理資源原生DCC文件,生成某演員或關卡。
資產調節管道用於將DCC原生格式文件轉換成引擎可用的形式,通常通過3個處理階段:
如同程序的源文件,各資產之間也有依賴關係。這些依賴關係一般會影響資產在管道內的處理次序,也可告訴咱們,當某個源資產作出改動後,要從新生成哪些資產。生成依賴不單圍繞資產自己的改動,也關係到數據格式的改動。每一個資產調節管道都須要一組規則來描述資產間的依賴關係,並本身搭建系統或使用像make這樣的工具來以正確順序生成資產。必定要管理好資產間的依賴。
資源通常儲存爲磁盤上的文件,並位於使創做者方便而組織的樹狀目錄中。但引擎一般不會理會資源被放置於資源樹中的哪一個位置,引擎會把多個資源包裹爲單一文件。文件載入時間和尋道時間、開啓每一個文件的時間、從文件讀至內存的時間相關。這種方法能減小文件載入時間。
OGRE使用ZIP存檔資源,ZIP格式的好處:
虛幻3採起相似的手法,可是其全部資源都必須置於大型的pak自定義格式文件中,並不允許資源以盤上獨立文件出現。
每類資源均可能有不一樣的文件格式。單一文件格式也可儲存多種不一樣類型的資產。許多引擎會自定義文件格式,由於引擎所需部分信息可能沒有標準格式能夠支持,以及對資源脫機處理,以讓其聽從某種內存佈局加速運行時載入。
資源全局統一標識符
全部資源都須要資源全局統一標識符(GUID)來識別,最多見就是使用資源的文件系統路徑。也有使用128位散列碼。虛幻3的GUID格式是包名和包內資源路徑串接而成,如《戰爭機器》的一個資源GUID爲Locust_Boomer.PhysicalMaterials.LocustBommerLeather
。
資源管理器都含某種形式的資源註冊表,以保證在任什麼時候間,載入內存的每一個資源只會有一份副本。最簡單的實現方法是使用字典,鍵爲資源的GUID,而值是指向內存中資源的指針。資源載入內存時,加進資源註冊表字典。卸下資源時,就刪除其註冊表記錄。
若不能從表中找到請求的資源,最直覺的處理手法就是自動載入該資源。但這樣作可能會由於臨時從硬盤或光驅等緩慢設備讀取數據而嚴重拖慢遊戲幀率。
所以引擎可採起這兩種替代手法:
資源管理器的職責之一是自動管理資源生命期,或對遊戲提供所需API供手動管理。每一個資源對生命期有不一樣需求:
某資源的載入時期一般在玩家第一次看見該資源便能決定,但什麼時候卸下資源歸還內存,就難以回答,由於可能存在多個關卡共享的資源。解決方案之一就是對資源引用計數,即載入新關卡時,遍歷所需資源並引用加1,再遍歷即將結束的關卡的資源,全部引用減1。當有引用計數減爲0是卸載,當有新的資源的引用計數由0變爲1時載入。
資源加載的內存位置可能不一樣,像紋理、頂點緩衝、着色器駐留在顯存,大部分資源駐留在主內存,但不一樣的資源可能須置於不一樣的地址範圍。設計遊戲引擎時,內存分配器和資源系統要相互配合。有時用已有的內存分配器來設計資源系統,有時則要讓內存分配器配合資源管理所需。
每一個資源文件可包含一個或多個數據對象,這些對象可能以不一樣的方式引用或依賴其餘對象,資源數據庫能夠表達爲相互依賴的數據對象所組成的有向圖。交叉引用能夠分爲內部(單個文件裏對象間的引用)和外部(引用另外一個文件的對象)。
處理資源內部引用
在C++中, 因爲指針的內存地址總會變,並且離開運行中的程序就失去意義,因此不能用指針來表示對象間的依賴。
將資源引用存爲包含全局惟一標識符(GUID)的字符串或散列碼,資源管理器要維護一個全局資源查找表,其中鍵爲GUID,值爲資源在內存中的地址。這樣每次經過全劇資源查找表就能夠將資源對象的GUID轉換爲指針。
儲存對象到二進制文件的另外一經常使用方法是,把指針轉換爲文件偏移值,並創建指針修正表。
下圖給出了儲存二進制文件以及將文件載入內存的指針修正示意圖,具體過程爲:
從文件載入C++對象,建立對象時必須調用構造函數。這個問題有兩個常看法決方案:
void* pObject = ConvertOffsetToPointer(objectOffset);
::new(pObject) ClassName; // placement new語法,ClassName爲對象所屬的類名
處理資源外部引用
要正確表示外部引用,除了指明偏移值或GUID,還要加上資源對象所屬文件的路徑。通常作法是:載入每一個資源文件時,掃描文件中的交叉引用表,並載入全部被外部引用但未載入的資源文件,當載入全部互相依賴的資源時,就用主查找表把全部指針轉換成真實的內存地址(經過GUID或文件偏移值)。
有一些資源載入後須要一些處理才能供引擎使用,這種載入後的全部處理被稱爲載入後初始化。
資源的載入後初始化和拆除,都有獨特的需求。在C中,可使用查找表,把每一個資源類型映射到一對函數指針,一個負責載入後初始化,一個負責拆除。在C++中,可使用構造函數和析構函數來處理載入後初始化和拆除。可是爲了方便多態,通常爲每一個類設置如Init()
和Destroy()
的虛函數用於獨立初始化和銷燬工做。
載入後初始化和資源內存分配策略息息相關,有時初始化會在文件的數據上新增數據(如額外計算類中的成員數據),有時初始化的數據用來取代己載入的數據(如引擎載入過期格式的網格數據,自動轉換爲最新格式,以保證向後兼容)。能夠採用先載入到臨時內存區域,初始化完成後再把相關數據複製到內存最終位置(例如《迅雷賽艇》的引擎)。