子系統的啓動和終止程序員
C++的靜態初始化次序不可用,由於C++在調用程序進入點(main())以前,全局及靜態對象已經被構建,而咱們對這些構造函數的調用次序不可預知。算法
遊戲引擎的子系統,常見的設計模式是爲每一個子系統定義單例類。經常使用的單例模式的實現方法,難以控制它的析構次序,並且一個獲取管理器的實例的方法,可能會有很高的開銷。所以,遊戲開發中,直接採用簡單粗暴的方法。數據庫
具體來講,就是將構造和析構函數留空,內部不作任何事情,直接在main函數中按須要的次序調用自定義的啓動和終止函數。編程
class RenderManager{ public: RenderManager(){} ~RenderManager(){} void startUp(){ //自定義的啓動函數 } void shutDown(){ //自定義的終止函數 } }; class PhysicsManager{/*同上面相似*/ }; class AnimationManager{/*同上面相似*/ }; class MemoryManager{/*同上面相似*/ }; RenderManager gRenderManager; PhysicsManager gPhysicsManager; AnimationManager gAnimationManager; MemoryManager gMemoryManager; int _tmain(int argc, _TCHAR* argv[]) { //啓動各個子系統 gMemoryManager.startUp(); ... gRenderManager.startUp(); gAnimationManager.startUp(); gPhysicsManager.startUp(); //運行遊戲 //終止各個子系統 gPhysicsManager.shutDown(); gAnimationManager.shutDown(); gRenderManager.shutDown(); ... gMemoryManager.shutDown(); return 0; }
內存管理設計模式
動態內存分配效率較低:數組
所以,遊戲開發中,維持最低限度的堆分配,而且永不在緊湊循環中使用堆分配。緩存
經常使用的定製分配器服務器
堆棧分配器數據結構
先預分配一塊連續的內存,堆棧分配器管理這塊內存;後面內存分配經過分配器的堆棧指針來實現,分配和釋放內存便是指針的移動。ide
注意:
堆棧分配器釋放內存時次序必須是分配時相反的順序,而不是任意的。實現方法,能夠不容許釋放個別的內存塊,而是由分配器提供一個函數,每次將堆棧指針回滾至標記的位置(它會釋放這之間的多個內存塊)。這個標記是位於分配的內存塊之間的邊界。
雙端堆棧分配器
與堆棧分配器相似,可是,雙端堆棧分配器在內存塊的兩端各有一個堆棧分配器,兩個堆棧指針從兩邊向中間靠攏。
池分配器
遊戲引擎可能會用到大量大小相同的小塊內存,此時,能夠用池分配器。
池分配器也會與分配一大塊內存,大小時分配的元素的整數倍。池中的每一個元素存放在一個自由鏈表中,池分配器收到分配請求時,將鏈表中的一個元素取出,分配出去;釋放元素時,把元素從新掛到鏈表中就能夠了。
自由元素的鏈表可實現爲單鏈表,單鏈表的next指針能夠直接存在每一個元素內;若是元素的尺寸小於指針,能夠用索引代替指針。
對齊功能的分配器
全部內存分配器都必須傳回對齊的內存塊,實現中,只需在分配內存時,分配多一點內存,而後將內存地址上調至對齊地址,最後傳回調整的地址。多分配的內存字節等於對齊的字節。如何調整內存地址?即如何計算對其須要的最小字節數,這個很簡單,這裏省略。
這樣釋放時如何釋放正確大小的內存呢?實現方式能夠再調整好的內存地址的前一個字節處記錄實際分配的內存大小,釋放時按照實際的大小來釋放。
單幀和雙緩衝內存分配器
單幀分配器和堆棧分配器相似,只是單幀分配器在每幀開始時,會將堆棧指針重置爲內存塊的底端地址。這樣就不須要手動釋放內存,由於每幀開始就會自動釋放前一幀的全部分配的內存。可是這也意味着,該分配器分配的內存只在當前幀上有效。
雙緩衝分配器與單幀分配器的區別在於,它有兩個大小相同的單幀分配器,它會交替使用兩個單幀分配器,這樣第i幀分配的內存在第i+1幀中仍然可用。
內存碎片
在支持虛擬內存的操做系統中,內存碎片不是大問題,可是不少遊戲引擎不會使用虛擬內存,由於它會致使不少額外的開銷。前面的介紹能夠看出堆棧和池分配器不會產生內存碎片。
內存碎片的整理須要移動已分配的內存塊,它會致使指針的失效;可使用重定位來解決這個問題。具體來講有兩種方法。
可是有些內存塊不能被重定位,那麼能夠將這些內存塊分配到不可重定位的內存區中,或者允許少許不可重定位的內存塊的存在。
內存整理是很耗時的過程,可是不須要一次完成,因此能夠把它分攤到多個幀中。
CPU會有多級高速緩存,緩存中存取數據的速度快與內存中的存取,爲了提升效率就須要提升高速緩存的命中率。對於多級緩存CPU最外層的緩存命中失敗的成本比內層的緩存命中失敗的成本高。緩存分爲指令緩存和數據緩存。
提升數據緩存命中率的方法
保證數據大小較小,且將他們儘量放到連續的內存塊中,順序訪問這些數據。(和堆棧分配器很契合)
提升指令緩存命中率的方法
單個函數的機器碼幾乎老是置於連續的內存;編譯器和連接器按函數在源代碼中出現次序排列內存佈局,所以一個源文件中的函數總在連續內存塊中。
容器
經常使用容器:數組(array)、動態數組(dynamic array)、堆棧(stack)、隊列(queue)、雙端隊列(deque)、優先隊列(priority queue)、樹(tree)、二叉查找樹(binary search tree)、二叉堆(binary heap)、字典(dictionary)、集合(set)、圖(graph)、有向非循環圖(directed acyclic graph)
迭代器的優勢
儘可能使用前置遞增,由於後置遞增有個拷貝的過程,若是是迭代器,這個拷貝可能很耗時。
如下的狀況下可能創建自定義容器
比較自定義數據結構和第三方庫,才能決定是否去自定義。爲此要先了解第三方庫。經常使用的包括:STL、STL的變種(STLport)、Boost。
STL
STL的優勢:
STL的缺點
STL的應用時機
Boost
Boost的優勢
Boost的缺點
模板元編程(template metaprogramming,TMP)是利用編譯器作一些一般在運行期纔會作的工做。Loki是一個強大的C++TMP庫。(http://loki-lib.sourceforge.net)
缺點
遊戲編程中常用固定大小的數組,由於它無需內存分配,且對緩存友好;可是編譯期間難以決定數組的大小,因此傾向於使用鏈表和動態數組。可是最後當數組的大小可以肯定時,把它改成固定大小的數組。
鏈表的建議
字典和散列表:注意散列(把任意類型的鍵轉換爲整數)函數的選擇是關鍵。若鍵爲32位整數,把其位模式詮釋爲32位整數;若鍵爲字符串,則把字符串中全部字符的ASCII或UTF碼合併爲單個32位整數,常見的字符串散列函數有LOOKUP三、CRC3二、MD5等。
字符串
字符串類雖然方便,但有隱性成本:傳遞字符串對象時,函數聲明或使用不當引發多個拷貝構造函數的開銷;複製字符串涉及動態內存分配。遊戲編程中通常避免字符串類,若必定要使用字符串類,應該查明其運行性能特性在可接受的範圍,並讓全部使用它的程序員知悉其開銷。在儲存和管理文件系統路徑時,使用特化的字符串類(如Path類)來處理多平臺的字符串差別,在遊戲引擎中是頗有價值的。
惟一標識符
惟一標識符(64位或128位的GUID字符串)用於識別遊戲對象或資產,因爲數量很是多,大量的比較在遊戲中可能極有影響。最好找到一種方法,既保留字符串的表達能力和彈性,又要有整數操做的速度。方法是能夠把字符串散列並存於表中(該過程稱爲字符串扣留),並經過散列碼(也稱爲字符串標識符,string id或SID)取回原來的字符串,但要選取恰當的散列函數保證不碰撞。
由於字符串扣留(散列,分配字符串內存,複製至查找表)很是緩慢,因此一般在運行時就進行,並且僅進行一次,把結果儲存備用。
#define U32 unsigned int #define StringId U32 static HashTable<StringId, const char *> gStringIdTable; StringId internString(const char *str){ StringId sid = hashCrc(str); if (gStringIdTable.find(sid) == gStringIdTable.end()){ //字符串未加入表中時,將其拷貝的副本加入表中 /* strdup函數原型: strdup()主要是拷貝字符串s的一個副本,由函數返回值返回,這個副本有本身的內存空間,和s不相干。 strdup函數複製一個字符串,使用完後要記得刪除在函數中動態申請的內存,strdup函數的參數不能爲NULL,一旦爲NULL,就會報段錯誤. 由於該函數包括了strlen函數,而該函數參數不能是NULL. **/ gStringIdTable[sid] = strdup(str); } return sid; } static StringId sid_foo = internString("foo");//確保只調用一次,而不要放到判斷條件時調用 static StringId sid_bar = internString("bar"); void fun(StringId id){ if (id == sid_foo){} else if (id == sid_bar){} }
本地化
對每一個向用戶顯示的字符串,都要事先翻譯爲須要支持的語言(程序內部使用的,永不顯示於用戶的字符串無須本地化)。除了經過使用合適的字體,爲全部支持語言準備字符字形,遊戲還須要處理不一樣的文本方向(針對一些閱讀順序很特殊的語言)。
推薦先閱讀這篇文章:《每一個軟件開發者都絕對必知的Unicode及字元集必備知識(沒有藉口!)》。遊戲引擎中最常採用的是UTF-8和UTF-16。
Windows下的Unicode
在Windows下,wchar_t用來表示單個「寬」UTF-16字符(WCS),char則用做ANSI字符及多字節UTF-16字符串(MBCS)。Windows允許程序員編寫字符集無關的代碼,即提供TCHAR數據類型,它會根據實際所用的字符集自動typedef爲特定的類型。
注意Windows中各類API和標準函數庫,無前綴表示普通ANSI字符,前綴爲「w」「wcs」表示寬字符,綴爲「mbs」表示多字節UTF-16,如strcmp()、wcscmp()和_mbscmp()。不一樣的引擎採用哪一種編碼並不重要,重要的是在項目中儘早決定,並始終貫徹使用。
其餘本地化要考慮的事
引擎配置
讀寫選項
可配置選項可簡單實現爲全局變量或單例中的成員變量,這些選項必須可供用戶配置,儲存到硬盤、記憶卡或其餘媒體,遊戲能隨時讀取。下面是一些讀寫選項的方法:
個別用戶選項
個別用戶選項保留了每一個玩家本身配置其喜歡的選項,與全局選項區分開來。須要當心控制每一個玩家只能「看見」本身的選項,而不會碰見其餘玩家在同一設備的選項。
在Windows上,應用程序一般在C:\Documents and Settings的隱藏文件夾Application Data文件夾中創建本身的文件夾,存放個別用戶數據。或者經過讀寫註冊表HKEY_CURRENT_USER下的註冊表項,來存取管理當前用戶的配置選項。
真實引擎中的配置管理
遊戲內置菜單選項:每一個可配置選項都實現爲全局變量,爲選項建立菜單項目時,會提供全局變量的地址,以後菜單項目就能直接控制該全局變量的值
命令行參數:可指定要載入的關卡名稱,以及其餘經常使用參數
Scheme(一種Lisp方言)數據定義:經過腳本定義數據結構,並用自建的數據編譯器轉換爲二進制文件,同時自動生成C/C++的頭文件以解釋二進制文件的數據。能夠在運行期間重編譯和重加載二進制文件,以便隨時修改數據結構並當即看到效果。這種系統給予程序員巨大的彈性,能夠定義複雜的數據結構,如細緻的動畫樹、物理參數、遊戲機制等。下面的代碼示例,用於爲動畫定義屬性,並導出2個動畫
;; Scheme代碼,定義一個新的數據類型,名爲simple-animation (deftype simple-animation () ( (name string) (speed float: default 1.0) (fade-in-seconds float: default 0.25) (fade-out-seconds float: default 0.25) )) ;; 定義此數據結構2個實例 (define-export anim-walk (new simple-animation :name "walk" :speed 1.0 ) ) (define-export anim-jump (new simple-animation :name "jump" :fade-in-seconds 0.1 :fade-out-seconds 0.1 ) )
Scheme代碼會產生如下C/C++頭文件:
// simple-animation.h // 警告:本文件是Scheme自動生成的,不要手工修改 struct SimpleAnimation { const char* m_name; float m_speed; float m_fadeInSeconds; float m_fadeOutSeconds; };
在遊戲編程中,可調用LookupSymbol()函數讀取數據,該函數以返回類型爲模板參數:
#include "simple-animation.h" void someFunction() { SimpleAnimation* pWalkAnim = LookupSymbol<SimpleAnimation*>("anim-walk"); SimpleAnimation* pJumpAnim = LookupSymbol<SimpleAnimation*>("anim-jump"); // 在此使用這些動畫...... }