《遊戲引擎架構》筆記五

子系統的啓動和終止程序員

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;
}
View Code

內存管理設計模式

動態內存分配效率較低數組

  • 堆分配器是通用的,它須要處理任何大小的內存分配,這就須要管理開銷;
  • 許多操做系統中,調用malloc/free會引發上下文切換,須要從用戶模式切換到內核模式,去處理請求。

所以,遊戲開發中,維持最低限度的堆分配,而且永不在緊湊循環中使用堆分配。緩存

經常使用的定製分配器服務器

堆棧分配器數據結構

先預分配一塊連續的內存,堆棧分配器管理這塊內存;後面內存分配經過分配器的堆棧指針來實現,分配和釋放內存便是指針的移動。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的優勢

    • 功能豐富
    • 許多平臺上都很健壯
    • 幾乎全部C++編譯器都帶有STL

STL的缺點

    • 相比爲某些問題定製的數據結構,STL較慢,且佔用更多內存
    • STL有較多的動態內存分配
    • STL的實如今各個編譯器上有微小差別

STL的應用時機

    • 使用STL前要認識它的效率和內存特性
    • 若代碼中重量級STL類會形成瓶頸,就要避免使用它們
    • 佔用少許內存的狀況下才使用STL
    • 若引擎支持多平臺可使用STLport(http://www.stlport.org)

Boost

Boost的優勢

    • boost由STL中沒有的一些功能
    • Boost提供了代替方案,能解決STL設計和實現上的一些問題
    • Boost能有效處理一些複雜問題
    • Boost文檔很易讀

Boost的缺點

    • Boost大部分核心類是模板,所以,須要包含一些頭文件,且有些Boost庫會生成較大的.lib文件,不適合小型遊戲項目
    • 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()。不一樣的引擎採用哪一種編碼並不重要,重要的是在項目中儘早決定,並始終貫徹使用。

其餘本地化要考慮的事

  • 本地化不只包括字符,還包括錄製語音、帶文字的紋理,還要注意一些符號在不一樣文化中意義的差異,注意不一樣市場的評級界限。
  • 本地化系統須要創建字符串數據庫,經過SID以及全局的「當前語言」設定來查找對應的語言字符串。其函數聲明可能爲:const wchar_t* getLocalizedString(const char* sid)
  • 數據庫的實現細節不是很重要,能夠用CSV,也能夠用專門的DBMS。
  • 程序員切記不要硬編碼原始字符串,而是採用上述查找函數取得所需字符串。注意字符串可能須要處理像"Player {0} Score: {1}"這樣的格式化串。

引擎配置

讀寫選項

可配置選項可簡單實現爲全局變量或單例中的成員變量,這些選項必須可供用戶配置,儲存到硬盤、記憶卡或其餘媒體,遊戲能隨時讀取。下面是一些讀寫選項的方法:

  • 文本配置文件:如INI、XML、JSON等;經過鍵值對,並將鍵值對以邏輯段分組。
  • 經壓縮的二進制文件:主要用於老式遊戲主機上儲存空間極其有限的記憶卡
  • Windows註冊表:以樹形式存儲,內部節點爲註冊表項(相似文件夾),葉節點以鍵值對儲存選項。任何應用程序均可預留一個註冊表項存儲任意內容
  • 命令行選項:經過掃描命令行取得選項設置
  • 環境變量
  • 線上用戶設定檔:存儲在中央服務器,必須經過聯網存取,通常用於存儲用戶成就、已購買或解鎖的遊戲內容、遊戲選項及其餘信息

個別用戶選項

個別用戶選項保留了每一個玩家本身配置其喜歡的選項,與全局選項區分開來。須要當心控制每一個玩家只能「看見」本身的選項,而不會碰見其餘玩家在同一設備的選項。
在Windows上,應用程序一般在C:\Documents and Settings的隱藏文件夾Application Data文件夾中創建本身的文件夾,存放個別用戶數據。或者經過讀寫註冊表HKEY_CURRENT_USER下的註冊表項,來存取管理當前用戶的配置選項。

真實引擎中的配置管理

  • 雷神之錘的主控臺變量(Console Variables,CVAR):一個儲存浮點數或字符串的全局變量,可在主控臺下查看及修改。多個CVAR存儲在全局鍵表中,每一個CVAR時動態配置的struct_cvar_t實例,鏈表的方式鏈接起來。部分值可儲存到硬盤上的config.cfg文件。
  • OGRE引擎:使用INI,像plugins.cfg記錄要啓用的插件及路徑,resources.cfg包含遊戲資產的路徑。經過Ogre::ConfigFile類可輕易讀寫全新的配置文件
  • 頑皮狗的神祕海域引擎:使用如下多種配置機制

遊戲內置菜單選項:每一個可配置選項都實現爲全局變量,爲選項建立菜單項目時,會提供全局變量的地址,以後菜單項目就能直接控制該全局變量的值

命令行參數:可指定要載入的關卡名稱,以及其餘經常使用參數

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");
    // 在此使用這些動畫......
}
相關文章
相關標籤/搜索