遊戲編程模式 Game Programming Patterns (Robert Nystrom 著)

第1篇 概述html

第1章 架構,性能和遊戲 (已看)前端

第2篇 再探設計模式html5

第2章 命令模式 (已看)java

第3章 享元模式 (已看)node

第4章 觀察者模式 (已看)git

第5章 原型模式 (已看)程序員

第6章 單例模式 (已看)github

第7章 狀態模式 (已看)算法

第3篇 序列型模式數據庫

第8章 雙緩衝 (已看)

第9章 遊戲循環 (已看)

第10章 更新方法 (已看)

第4篇 行爲型模式

第11章 字節碼 (已看)

第12章 子類沙盒 (已看)

第13章 類型對象 (已看)

第5篇 解耦型模式

第14章 組件模式 (已看)

第15章 事件隊列 (已看)

第16章 服務定位器 (已看)

第6篇 優化型模式

第17章 數據局部性 (已看)

第18章 髒標記模式 (已看)

第19章 對象池 (已看)

第20章 空間分區 (已看)

第1章 架構,性能和遊戲

  1.1 什麼是軟件架構

這本書關於代碼的組織方式

    1.1.1 什麼是好的軟件架構

第一個關鍵部分是,架構意味着變化.衡量一個設計好壞的方法就是看它應對變化的靈活性

    1.1.2 你如何作出改變

一旦你理解了問題和它涉及的代碼,則實際的編碼有時是微不足道的

    1.1.3 咱們如何從解耦中受益

你能夠用一堆方式來定義"解耦",但我認爲若是兩塊代碼耦合,意味着你必須同時瞭解這兩塊代碼.若是你讓它們解耦,那麼你只需瞭解其一.

固然,對解耦的另外一個定義就是當改變了一塊代碼時沒必要更改另一塊代碼.很明顯,咱們須要更改一些東西,可是耦合得越低,更改所波及得範圍就越小

  1.2 有什麼代價

良好的架構須要很大的努力及一系列準則.每當你作出一個改變或者實現一個功能時,你必須很優雅地將它們融入到程序的其他部分.你必須很是謹慎地組織代碼並保證其在開發週期中通過數以千計的小變化以後仍然具備良好的組織性

  1.3 性能和速度

沒有人能夠在紙上設計出一個平衡的遊戲.這須要迭代和實驗.

  1.4 壞代碼中的好代碼

原型(把那些僅僅在功能上知足一個設計問題的代碼融合在一塊兒)是一個徹底正確的編程實踐

  1.5 尋求平衡

開發中咱們有幾個因素須要考慮

  1. 咱們想得到一個良好的架構,這樣在項目的生命週期中便會更容易理解代碼

  2. 咱們但願得到快速的運行時性能

  3. 咱們但願快速完成今天的功能

這些目標至少部分是相沖突的.好的架構從長遠來看,改進了生產力,但維護一個良好的架構就意味着每個變化都須要更多的努力來保持代碼的乾淨

最快編寫的代碼實現卻不多是運行最快的.相反,優化須要消耗工程時間.一旦完成,也會使代碼庫僵化:高度優化過的代碼缺少靈活性,很難改變

完成今日的工做並擔憂明天的一切總伴隨着壓力.可是,若是咱們儘量快的完成功能,咱們的代碼庫就會充滿了補丁,bug和不一致的混亂,會一點點地消磨掉咱們將來的生產力

這裏沒有簡單的答案,只有權衡

  1.6 簡單性

  1.7 準備出發

抽象和解耦可以使得你的程序開發變得更快和更簡單.但不要浪費時間來作這件事,除非你確信存在問題的代碼須要這種靈活性

在你的開發週期中要對性能進行思考和設計,可是要推遲那些下降靈活性的,底層的,詳盡的優化,能晚則晚

儘快地探索你的遊戲的設計空間,可是不要走得太快留下一個爛攤子給本身.畢竟你將不得不面對它

若是你將要刪除代碼,那麼不要浪費時間將它整理得很整潔.搖滾明星把酒店房間弄得很亂是由於他們知道次日就要結帳走人.

可是,最重要得是,若要作一些有趣得玩意,那就樂在其中地作吧

第2章 命令模式

"將一個請求(request)封裝成一個對象,從而容許你使用不一樣的請求,隊列或日誌將客戶端參數化,同時支持請求操做的撤銷和恢復"

我想你也和我同樣以爲這句話晦澀難懂.

首先,它的比喻不夠形象.在軟件界以外,一詞每每多義."客戶(client)"指代同你有着某種業務往來的一類人.據我查證,人類(human beings)是不可"參數化"的

其次,句子的剩餘部分只是列舉了這個模式可能的使用場景.而萬一你遇到的用例不在其中,那麼上面的闡述就不太明朗了.

 

我對命令模式的精煉歸納以下: 命令就是一個對象化(實例化)的方法調用(A command is a reified(具象化) method call)

 

這個術語意味着,將某個概念(concept)轉化爲一塊數據(data),一個對象,或者你能夠認爲是傳入函數的變量等.

GOF後面這樣補充到: 命令就是面向對象化的回調(Commands are an object-oriented replacement for callbacks)

 

一些語言的反射系統(Reflection system)可讓你在運行時命令式地處理系統中的類型.你能夠獲取到一個對象,它表明着某些其餘對象的類,你能夠經過它試試看這個類型能作些什麼.話句話說,反射是一個對象化的類型系統

  2.1 配置輸入

簡單實現

void InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) jump();
    else if (isPressed(BUTTON_Y)) fireGun();
    else if (isPressed(BUTTON_A)) swapWeapon():
    else if (isPressed(BUTTON_B)) lurchIneffectively();
}
View Code
用對象來表明一個遊戲動做
class Command {
public:
    virtual void execute() = 0;
    virtual ~Command() {}
};

class JumpCommand: public Command {
public:
    virtual void execute() { jump(); }
};

class FireCommand: public Command {
public:
    virtual void execute() { fireGun(); }
};


class InputHandler {
public:
    void handleInput();
    // Methods to bind commands...
private:
    Command * buttonX_;
    Command * buttonY_;
    Command * buttonA_;
    Command * buttonB_;
};

void InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) buttonX_->execute();
    else if (isPressed(BUTTON_Y)) buttonY_->execute();
    else if (isPressed(BUTTON_A)) buttonA_->execute();
    else if (isPressed(BUTTON_B)) buttonB_->execute();
}
View Code

  2.2 關於角色的說明

控制任意遊戲角色

class Command {
public:
    virtual void execute(GameActor & actor) = 0;
    virtual void ~Command() {}
};

class JumpCommand: public Command {
public:
    virtual void execute(GameActor & actor) {
        actor.jump();
    }
};

Command * InputHandler::handleInput() {
    if (isPressed(BUTTON_X)) return buttonX_;
    if (isPressed(BUTTON_Y)) return buttonY_;
    if (isPressed(BUTTON_A)) return buttonA_;
    if (isPressed(BUTTON_B)) return buttonB_;
    
    return NULL;
}

Command * command = inputHandler.handleInput();
if (command) {
    command->execute(actor);
}
View Code

  2.3 撤銷和重作

在上個例子中,咱們想要從被操控的角色中抽象出命令,以便將角色和命令解耦.在這個例子中,咱們特別但願將命令綁定到被移動的單位上.這個命令的實例不是通常性質的"移動某些物體"這樣適用於不少情境下的操做,在遊戲的回合次序中,它是一個特定具體的移動

這凸顯了命令模式在實現時的一個變化.在某些狀況下,像咱們第一對的例子,一個命令表明了一個可重用的對象,表示一件可完成的事情(a thing that can be done).

這裏,這些命令更加具體.他們表示一些可在特定時間點完成的事情.

class MoveUnitCommand: public Command {
public:
    MoveUnitCommand(Unit * unit, int x, int y) : unit_(unit),x_(x),y_(y) {}
    virtual void execute() {
        unit_->moveTo(x_, y_);
    }
private:
    Unit * unit_;
    int x_;
    int y_;
};

Command * handleInput() {
    Unit * unit = getSelectedUnit();
    
    if (isPressed(BUTTON_UP)) {
        int destY = unit->y() - 1;
        return new MoveUnitCommand(unit, unit->x(), destY);
    }

    if (isPressed(BUTTON_DOWN)) {
        int destY = unit->y() + 1;
        return new MoveUnitComand(unit, unit->x(), destY);
    }

    return NULL;
}
View Code

可撤銷的命令

class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() {}
};

class MoveUnitCommand: public Command {
public:
    MoveUnitCommand(Unit * unit, int x, int y) : unit_(unit), x_(x), y_(y), xBefore(0), yBefore(0) {}

    virtual void execute() {
        xBefore_ = unit_->x();
        yBefore_ = unit_->y();
        unit_->moveTo(x_, y_);
    }

    virtual void undo() {
        unit_>moveTo(xBefore_, yBefore_);
    }

private:
    Unit * unit_;
    int x_, y_;
    int xBefore_, yBefore_;
};
View Code

  2.4 類風格化仍是函數風格化

function makeMoveUnitCommand(unit, x, y) {
    // This function here is the command object;
    return function() {
        unit.move(x, y);
    }
}

function makeMoveUnitCommand(unit, x, y) {
    var xBefore, yBefore;
    return {
        execute: function() {
            xBefore = unit.x();
            yBefore = unit.y();
            unit.moveTo(x, y);
        },
        undo: function() {
            unit.moveTo(xBefore, yBefore);
        }
    };
}
View Code

  2.5 參考

1. 你可能最終會有不少不一樣的命令類.爲了更容易地實現這些類,能夠定義一個具體的基類,裏面有着一些實用的高層次的方法,這樣即可以經過對派生出來的命令組合來定義其行爲,這麼作一般是有幫助的.它會將命令的主要方法execute()變成子類沙盒

2. 在咱們的例子中,咱們明確地選擇了那些會執行命令的角色.在某些狀況下,尤爲是在對象模型分層的狀況下,它可能沒有這麼直觀.一個對象能夠響應一個命令,而它也能夠決定將命令下放給其從屬對象.若是你這樣作,你須要瞭解下責任鏈

3. 一些命令如第一個例子中的JumpCommand是無狀態的純行爲的代碼塊.在相似這樣的狀況下,擁有不止一個這樣命令類的實例會浪費內存,由於全部的實例是等價的.享元模式就是解決這個問題的.

第3章 享元模式

使用共享以高效地支持大量的細粒度對象

  3.1 森林之樹

用代碼來表示一顆樹

class Tree {
private:
    Mesh mesh_;
    Texture bark_;
    Texutre leaves_;
    Vector position_; 
    double height_;
    double thickness_;
    Color barkTinit_;
    Color leafTinit_;
};
View Code

要讓GPU在每幀都顯示成千上萬的樹數據量會很大,尤爲是網格和紋理.

咱們能夠將對象分割成兩個獨立的類,遊戲中每一顆樹的實例都有一個指向共享的TreeModel的引用

class TreeModel {
private:
    Mesh mesh_;
    Texture bark_;
    Texture leaves_;
};

class Tree {
private:
    TreeModel * model_;
    
    Vector position_;
    double height_;
    double thickness_;
    Color barkTint_;
    Color leafTint_;
};
View Code

  3.2 一千個實例

Geometry_instancing(實例繪製)

  3.3 享元模式

享元(Flyweight),顧名思義,通常來講當你有太多對象並考慮對其進行輕量化時它便能派上用場

享元模式經過將對象數據切分紅兩種類型來解決問題.

第一種類型數據是那些不屬於單一實例對象而且可以被全部對象共享的數據.GoF將其稱爲內部狀態(the intrinsic state),但我更喜歡將它認爲是"上下文無關"的狀態.在本例中,這指的即是數木的幾何形狀和紋理數據等.

其餘數據即是外部狀態(the extrinsic state), 對於每個實例它們都是惟一的.在本例中,指的是每顆樹的位置,縮放比例和顏色.

  3.4 紮根之地

簡陋的實現

enum Terrain {
    TERRAIN_GRASS,
    TERRAIN_HILL,
    TERRAIN_RIVER
    // Other terrains...
};

class World {
private:
    Terrain tiles_[WIDTH][HEIGHT];
};

int World::getMovementCost(int x, int y) {
    switch (tiles_[x][y]) {
        case TERRAIN_GRASS: return 1;
        case TERRAIN_HILL: return 3;
        case TERRAIN_RIVER: return 2;
        // Other terrains...
    }
}

bool World::isWater(int x, int y) {
    switch (tiles_[x][y]) {
        case TERRAIN_GRASS: return false;
        case TERRAIN_HILL: return false;
        case TERRAIN_RIVER: return true;
        // Other terrains...
    }
}
View Code

使用享元

class Terrain {
public:
    Terrain(int movementCost, bool isWater, Texture texture): moveCost_(moveCost), isWater_(isWater), texture_(texture) {}
    int getMoveCost() const { return moveCost_; }
    bool isWater() const { return isWater_; }
    const Texture & getTexture() const {
        return texture_;
    }

private:
    int moveCost_;
    bool isWater_;
    Texture texture_;
};

class World {
public:
    World() : grassTerrain_(1, false,GRASS_TEXTURE), hillTerrain_(3, false, HILL_TEXTURE), riverTerrain_(2, true, RIVER_TEXTURE) {}
private:
    Terrain grassTerrain_;
    Terrain hillTerrain_;
    Terrain riverTerrain_;
    // Other stuff...
};

void World::generateTerrain() {
    for (int x = 0; x < WIDTH; x++) {
        for (int y = 0; y < HEIGHT; y++) {
            if (random(10) == 0) {
                tiles_[x][y] = &hillTerrain_;
            } else {
                tiles_[x][y] = &grassTerrain_;
            }
        }
    }
    
    int x = random(WIDTH);
    for (int y = 0; y < HEIGHT; y++) {
        tiles_[x][y] = &riverTerrain_;
    }
}
View Code

  3.5 性能表現如何

  3.6 參考

1. 在上面草地瓦片的例子中,咱們只是匆忙地爲每一個地形類型建立一個實例而後將之存儲到World中.這使得查找和重用共享實例變得很簡單.然而在許多狀況下,你並不會在一開始便建立全部的享元.若是你不能預測哪些是你真正須要的,則最好按需建立它們.爲了得到共享優點,當你須要一個對象時,你要先看看你是否已經建立了一個相同的對象.若是是,則只需返回這個實例.這一般意味着在一些用來查找現有對象的接口背後,你必須作些結構上的封裝.像這樣隱藏構造函數,其中一個例子就是工廠方法模式

2. 爲了找到之前建立的享元,你必須追蹤哪些你已經實例化過的對象的池(pool).正如其名,這意味着,對象池模式對於存儲它們會頗有用

3. 在使用狀態模式時,你常常會擁有一些"狀態"對象,對於狀態所處的狀態機而言它們沒有特定的字段.狀態的標識和方法也足夠有用.在這種狀況下,你能夠同時在多個狀態機中始使用這種模式,而且重用這個相同的狀態實例並不會帶來任何問題

第4章 觀察者模式

在對象間定義一種一對多的依賴關係,以便當某對象的狀態改變時,與它存在依賴關係的全部對象都能收到通知並自動進行更新

在計算機上隨便打開一個應用,它就頗有可能就是採用Model-View-Controller架構開發,而其底層就是觀察者模式.觀察者模式應用十分普遍,Java甚至直接把它集成到了系統庫裏面(java.util.Observer),C#更是直接將它集成在了語言層面(event關鍵字)

  4.1 解鎖成就

簡陋實現

void Physics::updateEntity(Entity & entity) {
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if (wasOnSurface && !entity.isOnSurface()) {
        notify(entity, EVENT_START_FAILE);
    }
}
View Code

  4.2 這一切是怎麼工做的

    4.2.1 觀察者

class Observer {
public:
    virtual void onNotify(const Entity & entity, Event event) = 0;
    virtual void ~Observer() {}
};

class Achievements: public Observer {
public:
    virtual void onNotify(const Entity & entity, Event event) {
        switch( event) {
            case EVENT_ENTITY_FELL:
                if (entity.isHero() && heroIsOnBridge_) {
                    unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
                }
            break;
            // Handle other events...
            // Update heroIsOnBridge...
        }
     }

private:
    void unlock(Achievement achievenment) {
        // Unlock if not already unlocked...
    }

    bool heroIsOnBridge_;
};
View Code

    4.2.2 被觀察者

通知方法會被正在被觀察的對象調用.在GoF的術語裏,這個對象被稱爲"被觀察對象(Subject)".它有兩個職責.首先,它擁有觀察者的一個列表,這些觀察者在隨時候命接收各類各樣的通知,其次就是發送通知

class Subject {
private:
    Observer * observers_[MAX_OBSERVERS];
    int numObservers_;
};

class Subject {
public:
    void addObserver(Observer * observer) {
        // Add to array...
    }
    void removeObserver(Observer * observer) {
        // Remove from array...
    }

protected:
    void notify(const Entity & entity, Event event) {
        for (int i = 0; i < numObservers_; i++) {
            observers_[i]->onNotify(entity, event);
        }
    }
    // Other stuff...
};
View Code

容許外部的代碼來控制誰能夠接收通知.這個被觀察者對象負責和觀察者對象進行溝通,可是,它並不與它們耦合
同時,被觀察者對象擁有一個觀察者對象的集合,而不是單個觀察者,這也是很重要的.它保證了觀察者們並不會隱式地耦合在一塊兒.例如,聲音引擎也註冊了落水事件,這樣在該成就達成的時候,就能夠播放一個合適的聲音.若是被觀察者對象不支持多個觀察者的話,當聲音引擎註冊這個事件的時候,成就係統就沒法註冊該事件了
這意味着,兩個系統會相互干擾對方----並且是以一種很不恰當的方式,由於第二個觀察者使第一個觀察者失效了.觀察者集合的存在,可讓每個觀察者都互相不干擾.在它們各自的眼裏,都認爲被觀察者對象眼裏只有它本身

    4.2.3 可被觀察的物理模塊

class Physics: public Subject {
public:
    void updateEntity(Entity & entity);
};
View Code

在實際代碼中,我會盡可能避免使用繼承.取而代之的是,咱們讓Physics系統有一個Subject實例.與觀察物理引擎相反,咱們的被觀察者對象會是一個單獨的"下落事件"對象.觀察者會使用下面的代碼 physics.entityFell().addObserver(this);

對我而言,這就是"觀察者"系統和"事件"系統的區別.前者,你觀察一個事情,它作了一些你感興趣的事.後者,你觀察一個對象,這個對象表明了已經發生的有趣的事情.

  4.3 它太慢了

發送一個通知,只不過須要遍歷一個列表,而後調用一些虛函數.老實講,它比普通的函數調用會慢一些,可是虛函數帶來的開銷幾乎能夠忽略不計,除了對性能要求極其高的程序

  4.4 太多的動態內存分配

    4.4.1 鏈式觀察者

class Subject {
    Subject(): head_(NULL) {}
   
    // Methods... 
private:
    Observer * head_;
};

void Subject::addObserver(Observer * observer) {
    observer->next_ = head_;
    head_ = observer;
}

void Subject::removeObserver(Observer * observer) {
    if (head_ == observer) {
        head_ = observer->next_;
        observer->next_ = NULL;
        return;
    }
    
    Observer * current = head_;

    while (current != NULL) {
        if (current->next_ == observer) {
            current->next_ = observer->next_;
            observer->next_ = NULL;
            return;
        }

        current = current->next_;
    }
}

void Subject::notify(const Entity & entity, Event event) {
    Observer * observer = head_;
    while (observer != NULL) {
        observer->onNotify(entity, event);
        observer = observer->next_;
    }
}

class Observer {
    friend class Subject;

public:
    Observer(): next_(NULL) {}
    
    // Other stuff...
private:
    Observer * next_;
};
View Code

    4.4.2 鏈表節點池

  4.5 餘下的問題

設計模式會遭人詬病,大部分是因爲人們用一個好的設計模式去處理錯誤的問題,因此事情變得更加糟糕了

    4.5.1 銷燬被觀察者和觀察者

當一個被觀察者對象被刪除時,觀察者自己應該負責把它本身從被觀察者對象中移除.一般狀況下,觀察者都知道它在觀察着哪些被觀察者,因此須要作的只是在析構器中添加一個removeObserver()方法

當一個被觀察者對象被刪除時,若是不咱們不想讓觀察者來處理問題,則能夠修改如下作法.咱們只須要在被觀察者對象被刪除以前,給全部的觀察者發送一個"死亡通知"就能夠了.這樣,全部已註冊的觀察者均可以收到通知並進行相應的處理

    4.5.2 不用擔憂,咱們有GC

    4.5.3 接下來呢

觀察者模式很是適合於一些不相關的模塊之間的通訊問題.它不適合於單個緊湊的模塊內部的通訊.

這也是爲何它適合咱們的例子: 成就係統和物理系統是徹底不相關的領域,並且頗有多是由不一樣的人實現的.咱們想讓它們的通訊儘量地減小,這樣任何一個模塊都不用依賴另外一個模塊就能夠工做

  4.6 觀察者模式的現狀

  4.7 觀察者模式的將來

第5章 原型模式

使用特定原型實例來建立特定種類的對象,而且經過拷貝原型來建立新的對象.

  5.1 原型設計模式

初始實現

class Monster {
    // Stuff...
};

class Ghost: public Monster {};
class Demon: public Monster {};
class Sorcerer: public Monster {};

class Spawner {
public:
    virtual Monster * spawnMonster() = 0;
    virtual ~Spawner() {}
};

class GhostSpawner: public Spawner {
public:
    virtual Monster * spawnMonster() {
        return new Ghost();
    }
};

class DemonSpawner: public Spawner {
public:
    virtual Monster * spawnMonster() {
        return new Demo();
    }
};

// Other
View Code

原型模式提供了一種解決方案.其核心思想是一個對象能夠生成與自身類似的其餘對象.若是你有一個幽靈,則你能夠經過這個幽靈製做出更多的幽靈,若是你有一個魔鬼,那你就能製做出其餘魔鬼.任何怪物都能被看做是一個原型,用這個原型就能夠複製出更多不一樣版本的怪物

class Monster {
public:
    virtual Monster * clone() = 0;
    virtual ~Monster() {}
    // Other stuff...
};

class Ghost: public Monster {
public:
    Ghost(int health, int speed): health_(health), speed_(speed) {}

    virtual Monster * clone() {
        return new Ghost(health_, speed_);
    }

private:
    int health_;
    int speed_;
};

class Spawner {
public:
    Spawner(Monster * prototype): prototype_(prototype) {}
    
    Monster * spawnMonster() {
        return prototype_->clone();
    }
private:
    Monster * prototype_;
};

Monster * ghostPrototype = new Ghost(15, 3);
Spawner * ghostSpawner = new Spawner(ghostPrototype);
View Code

關於這個模式,有點比較優雅的是,它不只克隆原型類,並且它也克隆了對象的狀態.

    5.1.1 原型模式效果如何

    5.1.2 生成器函數

Monster * spawnGhost() {
    return new Ghost();
}

typedef Monster * (*SpawnCallback)();

class Spawner {
public:
    Spawner(SpawnCallback spawn): spawn_(spawn) {}

    Monster * spawnMonster() { return spawn_(); }
private:
    SpawnCallback spawn_;
};

Spawner * ghostSpawner = new Spawner(spawnGhost);
View Code

    5.1.3 模板

class Spawner {
public:
    virtual Monster * spawnMonster() = 0;
    virtual ~Spawner() {}
};

template <class T>
class SpawnerFor: public Spawner {
public:
    virtual Monster * spawnMonster() { return new T(); }
};

Spawner * ghostSpawner = new SpawnerFor<Ghost>();
View Code

    5.1.4 頭等公民類型(First-class types)

  5.2 原型語言範式

    5.2.1 Self語言

    5.2.2 結果如何

    5.2.3 JavaScript如何

function Weapon(range, damage) {
    this.range = range;
    this.damage = damage;
}

var sword = new Weapon(10, 16);

Weapon.prototype.attack = function(target) {
    if (distanceTo(target) > this.range) {
        console.log("Out of range!");
    } else {
        target.health -= this.damage;
    }
}
View Code

  5.3 原型數據建模

第6章 單例模式

確保一個類只有一個實例,併爲其提供一個全局訪問入口 http://wiki.c2.com/?SingletonPattern 

  6.1 單例模式

    6.1.1 確保一個類只有一個實例

在有些狀況下,一個類若是有多個實例就不能正常運做.最多見的就是,這個類與一個維持着自身全局狀態的外部系統進行交互的狀況.

    6.1.2 提供一個全局指針以訪問惟一實例

class FileSystem {
public:
    static FileSystem & instance() {
        // Lazy initialize
        if (instance_ == NULL) {
            instance_ = new FileSystem();
        }
        return *instance_;
    }
private:
    FileSystem() {}
    static FileSystem * instance_;
};

// 更現代的版本
class FileSystem {
public:
    static FileSystem & instance() {
        static FileSystem * instance = new FileSystem();
        return *instance;
    }

private:
    FileSystem() {}
};
View Code

  6.2 使用情境

優勢

  1. 若是咱們不使用它,就不會建立實例

  2. 它在運行時初始化

  3. 你能夠繼承單例,這是一個強大可是常常被忽視的特性

class FileSystem {
public:
    static FileSystem & instance();

    virtual char * read(char * path) = 0;
    virtual void write(char * path, char * text) = 0;
    virtual ~FileSystem() {}
protected:
    FileSystem() {}
};

class PS3FileSystem: public FileSystem {
public:
    virtual char * read(char * path) {
        // Use Sony file IO API...  
    }
    virtual void write(char * path, char * text) {
        // Use sony file IO API...
    }
};

class WiiFileSystem: public FileSystem {
public:
    virtual char * read(char * path) {
        // Use Nintendo file IO API...
    }
    virtual void write(char * path, char * text) {
        // Use Nintendo file IO API...
    }
};

FileSystem & FileSystem::instance() {
#if PLATFORM == PLAYSTATION3
    static FileSystem * instance = new PS3FileSystem();
#elif PLATFORM == WII
    static FileSystem * instance = new WiiFileSystem();
#endif

    return *instance;
}
View Code

  6.3 後悔使用單例的緣由

    6.3.1 它是一個全局變量

咱們學到的一個教訓就是,全局變量是有害的,理由以下

  1. 它們令代碼晦澀難懂

  2. 全局變量促進了耦合

  3. 它對併發不友好

    6.3.2 它是個多此一舉的解決方案

    6.3.3 延遲初始化剝離了你的控制

  6.4 那麼咱們該怎麼作

    6.4.1 看你到底是否須要類

我見過的遊戲中的許多單例類都是"managers"----這些保姆類只是爲了管理其餘對象.我見識過一個代碼庫,裏面好像每一個類都有一個管理者: Monster, MonsterManager, Particle, ParticleManager, Sound, SoundManager, ManagerManager.有時爲了區別,它們叫作"System"或「Engine",不過只是改了名字而已

儘管保姆類有時是有用的,不過這一般反映出它們對OOP不熟悉.好比下面這兩個虛構的類

class Bullet {
public:
    int getX() const { return x_; }
    int getY() const { return y_; }
    void setX(int x) { x_ = x; }
    void setY(int y) { y_ = y; }
private:
    int x_;
    int y_;
};

class BulletManager {
public:
    Bullet * create(int x, int y) {
        Bullet * bullet = new Bullet();
        Bullet->setX(x);
        Bullet->setY(y);
        return bullet;
    }

    bool isOnScreen(Bullet & bullet) {
        return bullet.getX() >= 0 &&
                  bullet.getY() >= 0 && 
                  bullet.getX() < SCREEN_WIDTH &&
                  bullet.getY() < SCREEN_HEIGHT;
    }

    void move(Bullet & bullet) {
        bullet.setX(bullet.getX() + 5); 
    }
};
View Code

或許這個例子有點蠢,可是我見過不少代碼在剝離了外部細節以後,所暴露出來的設計就是這樣的.若是你查看這段代碼,那你天然會想,BulletManager應該是個單例.畢竟任何包含Bullet的對象都須要這個管理器,而你須要有多少個BulletManager實例呢?

事實上,這裏的答案是零.咱們是這樣解決管理類的"單例"問題的:

class Bullet {
public:
    Bullet(int x, int y): x_(x), y_(y) {}
    bool isOnScreen() {
        return x_ >= 0 && x_ < SCREEN_WIDTH &&
                  y_ >= 0 && y_ < SCREEN_HEIGHT;
    }

    void move() { x += 5; }
private:
    int x_, y_;
};
View Code

就這樣.沒有管理器也沒有問題.設計糟糕的單例一般會"幫助"你往其餘類中添加功能.若是能夠,你只須要將這些功能移動到它所幫助的類中去就能夠了.畢竟,面向對象就是讓對象本身管理本身

    6.4.2 將類限制爲單一實例

一個assert()意味着"我確保這個應該始終爲true,若是不是,這就是一個bug,而且我想馬上中止以便你能修復它".這可讓你在代碼域之間定義約定.若是一個函數斷言它的某個參數不爲NULL,那麼就是說"函數和調用者之間約定不可以傳遞NULL".

    6.4.3 爲實例提供便捷的訪問方式

通用的原則是,在保證功能的狀況下將變量限制在一個狹窄的範圍內.對象的做用越小,咱們須要記住它的地方就越少.在咱們盲目地採用具備全局做用域的單例對象以前,讓咱們考慮下代碼庫訪問一個對象的其餘途徑

  傳遞進去: 最簡的解決方式,一般也是最好的方式,就是將這個對象看成一個參數傳遞給須要它的函數

  在基類中獲取它: 許多遊戲架構有淺層次可是有寬度的繼承體系,一般只有一層繼承 面向切面編程

class GameObject {
protected:
    Log & Log() { return log_; }
private:
    static Log & log_;
};

class Enemy: public GameObject {
    void doSomething() {
        getLog().write("I can log!");
    }
};
View Code

  經過其餘全局對象訪問它: 咱們能夠經過將全局對象類包裝到現有類裏面來減小它們的數量.那麼除了依次建立Log, FileSystem和AudioPlayer單例外,咱們能夠:

class Game {
public:
    static Game & instance() { return instance_; }
    Log & log() { return *log_; }
    FileSystem & fileSystem() { return *file_; }
    AudioPlayer & audioPlayer() { return *audio_; }
    
    // Functions to set log_, et. al. ...
private:
    static Game instance_;
    Log *log_;
    FileSystem *files_;
    AudioPlayer *audio_;
};
View Code

  經過服務定位器來訪問: 到如今爲止, 咱們假設全局類就是像Game那樣的具體類.另一個選擇就是定義一個類專門用來給對象作全局訪問. 這個模式被稱爲服務定位器模式

  6.5 剩下的問題

還有一個問題,咱們應該在什麼狀況下使用真正的單例呢?老實說,我沒有在任何遊戲中使用GoF實現版本的單例.爲了確保只實例化一次,我一般只是簡單地使用一個靜態類.若是那不起做用,我就會用一個靜態的標識位在運行時檢查是否只有一個類實例被建立

第7章 狀態模式

容許一個對象在其內部狀態改變時改變自身的行爲.對象看起來好像是在修改自身類. https://en.wikipedia.org/wiki/State_pattern

  7.1 咱們曾經相遇過

void Heroine::handleInput(Input input) {
    if (input == PRESS_B) {
        if (!isJumping_ && !isDucking_) {
            // Jump...
        }
    } else if (input == PRESS_DOWN) {
        if (!isJumping_) {
            isDucking_ = true;
            setGraphics(IMAGE_DUCK);
        } else {
            isJumping_= false;
            setGraphics(IMAGE_DIVE);
        }
    } else if (input == RELEASE_DOWN) {
        if (isDucking_) {
            // Stand...
        }
    }
}
View Code

  7.2 救星: 有限狀態機

有限狀態機(FSM)能夠看做最簡單的圖靈機

整個狀態機能夠分爲: 狀態,輸入和轉換

  你擁有一組狀態,而且能夠在這組狀態之間進行切換

  狀態機同一時刻智能處於一種狀態

  狀態機會接收一組輸入或者事件

  每個狀態有一組轉換,每個轉換都關聯着一個輸入並指向另一個狀態

  7.3 枚舉和分支

enum State {
    STATE_STANDING,
    STATE_JUMPING,
    STATE_DUCKING,
    STATE_DIVING
};

void Heroine::handleInput(Input input) {
    switch(state_) {
        case STATE_STANDING:
            if (input == PRESS_B) {
                state_ = STATE_JUMPING;
                yVelocity_ = JUMP_VELOCITY;
                setGraphics(IMAGE_JUMP);
            } else if (input == PRESS_DOWN) {
                state_ = STATE_DUCKING;
                setGraphics(IMAGE_DUCK);
            }
        break;
        // Other states...
    }
}

void Heroine::update() {
    if (state_ == STATE_DUCKING) {
        chargeTime_++;
        if (chargeTime_ > MAX_CHARGE) {
            superBomb();
        }
    }
}
View Code

  7.4 狀態模式

    7.4.1 一個狀態接口

class HeroineState {
public:
    virtual void handleInput(Heroine & heroine, Input input) {}
    virtual void update(Heroine & heroine) {}
    virtual ~HeroineState() {}
};
View Code

    7.4.2 爲每個狀態定義一個類

class DuckingState: public HeroineState {
public:
    DuckingState(): chargeTime_(0) {}
    
    virtual void handleInput(Heroine & heroine, Input input) {
        if (input == RELEASE_DOWN) {
            // Change to standing state...
            heroine.setGraphics(IMAGE_STAND);
        }
    }

    virtual void update(Heroine & heroine) {
        chargeTime++;
        if (chargeTime_ > MAX_CHARGE) {
            heroine.superBomb();
        }
    }

private:
    int chargeTime_;
};
View Code

    7.4.3 狀態委託

class Heroine {
public:
    virtual void handleInput(Input input) {
        state_->handleInput(*this, input);
    }

    virtual void update() { state_->update(*this); }

    // Other methods...
private:
    HeroineState * state_;
};
View Code

  7.5 狀態對象應該放在哪裏呢

    7.5.1 靜態狀態

class HeroineState {
public:
    static StandingState standing;
    static DuckingState ducking;
    static JumpingState jumping;
    static DivingState diving;
    // Other code...
};

if (input == PRESS_B) {
    heroine.state_ = &HeroineState::jumping;
    heroine.setGraphics(IMAGE_JUMP);
}
View Code

    7.5.2 實例化狀態

void Heroine::handleInput(Input input) {
    HeroineState * state = state_->handleInput(*this, input);
    if (state != NULL) {
        delete state_;
        state_ = state;
    }
}

HeroineState * StandingState::handleInput(Heroine & heroine, Input input) {
    if (input == PRESS_DOWN) {
        // Other code...
        return new DuckingState();
    }
    // Stay in this state.
    return NULL;
}
View Code

  7.6 進入狀態和退出狀態的行爲

HeroineState * DuckingState::handleInput(Heroine & heroine, Input input) {
    if (input == RELEASE_DOWN) {
        heroine.setGraphics(IMAGE_STAND);
        return new StandingState();
    }

    // Other code...
}

class StandingState: public HeroineState {
public:
    virtual void enter(Heroine & heroine) {
        heroine.setGraphics(IMAGE_STAND);
    }
    // Other code...
};

void Heroine::handleInput(Input input) {
    HeroineState * state = state_->handleInput(*this, input);
    if (state != NULL) {
        delete state_;
        state_ = state;

        // Call the enter action on the new state 
        state_->enter(*this);
    }
}

HeroineState * DuckingState::handleInput(Heroine & heroine, Input input) {
    if (input == RELEASE_DOWN) {
        return new StandingState();
    }
    // Other code...
}
View Code

  7.7 有什麼收穫嗎

  7.8 併發狀態機

class Heroine {
    // Other code...
private:
    HeroineState * state_;
    HeroineState * equipment_;
};

void Heroine::handleInput(Input input) {
    state_->handleInput(*this, input);
    equipment_->handleInput(*this, input);
}
View Code

  7.9 層次狀態機

層次狀態機:一個狀態有一個父狀態.當有一個事件進來的時候,若是子狀態不處理它,那麼沿着繼承鏈傳給它的父狀態來處理.換句話說,它有點像覆蓋繼承的方法

class OnGroundState: public HeroineState {
public:
    virtual void handleInput(Heroine & heroine, Input input) {
        if (input == PRESS_B) {
            // Jump...
        } else if (input == PRESS_DOWN) { 
            // Duck...
        }
    }
};

class DuckingState: public OnGroundState {
public:
    virtual void handleInput(Heroine & heroine, Input input) {
        if (input == RELEASE_DOWN) {
            // Stand up...
        } else {
            // Didn't handle input, so walk up hierarchy
            OnGroundState::handleInput(heroine, input);
        }
    }
};
View Code

  7.10 下推自動機

下推自動機(pushdown automata)

  你能夠把新的狀態放入棧裏面.當前的狀態永遠存在棧頂,因此你總能轉換到當前狀態.可是當前狀態會將前一個狀態壓在棧中自身的下面而不是拋棄掉它

  你能夠彈出棧頂的狀態,改狀態將被拋棄.與此同時,上一個狀態就變成了新的棧頂狀態了

  7.11 如今知道它們有多有用了吧

即便有了這些通用的狀態機擴展,它們的使用範圍仍然是有限的.在遊戲的AI領域,最近的趨勢是愈來愈傾向於行爲樹和規劃系統.

可是這並不意味着有限狀態機,下推自動機和其餘簡單的狀態機沒有用.它們對於解決某些特定的問題是一個很好的建模工具.當你的問題知足如下幾點要求的時候,有限狀態機將會很是有用

  你有一個遊戲實體,它的行爲基於它的內部狀態而改變

  這些狀態被嚴格劃分爲相對數目較少的小集合

  遊戲實體隨着時間的變化會響應用戶輸入和一些遊戲事件

第8章 雙緩衝

  8.1 動機

    8.1.1 計算機圖形系統是如何工做的(概述)

諸如計算機顯示器的顯示設備在每一時刻僅繪製一個像素.顯示設備從左至右地掃描屏幕屏幕每行中的像素,並如此從上至下地掃描屏幕上的每一行.當它掃描至屏幕的右下角時,它將重定位至屏幕的左上角並如前述那樣地重複掃描屏幕.這一掃描過程是如此地快速(大概每秒60次),以致於咱們的眼睛沒法察覺這一過程.對於咱們而言,掃描的結果就是屏幕一塊彩色像素組成的靜態區域,即一張圖片

咱們的程序一次只渲染一個像素,同時咱們要求顯示器一次性顯示全部的像素----可能這一幀看不到任何東西,但下一幀顯示的就是完整的笑臉.雙緩衝模式解決了這一問題

    8.1.2 第一幕,第一場

    8.1.3 回到圖形上

雙緩衝中的一個緩存用於展現當前幀,於此同時,渲染代碼正在另外一個緩衝區中寫入數據.當渲染代碼完成時,經過交換緩衝區,使得顯卡驅動開始從第一個緩衝區轉向第二個緩衝區以讀取其數據進行渲染.只要它掌握好時機在每次刷新顯示器結束時進行切換,咱們就不會看到任何銜接的裂隙,且整個場景能一次性的瞬間顯示出來

  8.2 模式

定義一個緩衝區類來封裝一個緩衝區:一塊能被修改的狀態區域.這塊緩衝區能被逐步地修改,但咱們但願任何外部的代碼將對該緩衝區的修改都視爲原子操做.爲實現這一點,此類中維護兩個緩衝區實例:後臺緩衝區和當前緩衝區

當要從緩衝區讀取信息時,老是從當前緩衝區讀取.當要往緩衝區中寫入數據時,則總在後臺緩衝區上進行.當改動完成後,則執行"交換"操做來將當前緩衝區與後臺緩衝區進行瞬時的交換,以便讓新的緩衝區爲咱們所見,同時剛被換下來的當前緩衝區則成爲如今的後臺緩衝區以供複用

  8.3 使用情境

當下面這些條件都成立時,使用雙緩衝模式:

  咱們須要維護一些被逐步改變着的狀態量

  同個狀態可能會在其被修改的同時被訪問到

  咱們但願避免訪問狀態的代碼能看到具體的工做過程

  咱們但願可以讀取狀態但不但願等待寫入操做的完成

  8.4 注意事項

不像那些較大的架構模式,雙緩衝模式處於一個實現層次相對底層的位置.所以,它對代碼庫的影響較小----甚至多數遊戲都不會察覺到這些差異

    8.4.1 交換自己須要時間

雙緩衝模式須要在狀態寫入完成後進行一次交換操做,操做必須是原子性的:也就是說任何代碼都沒法在這個交換期間對緩衝區內的任何狀態進行訪問.一般這個交換過程和分配一個指針的速度差很少,但若是交換用去了比修改初始狀態更多的時間,那這樣模式就毫無助益了

    8.4.2 咱們必須有兩份緩衝區

這個模式的另一個後果就是增長了內存使用.

  8.5 示例代碼

class Framebuffer {
public:
    // Constructor and methods...
private:
    static const int WIDTH = 160;
    static const int HEIGHT = 120;

    char pixels_[WIDTH * HEIGHT];
};

void Framebuffer::clear() {
    for (int i = 0; i < WIDTH * HEIHGT; i++) {
        pixels_[i] = WHITE;
    }
}

void Framebuffer::draw(int x, int y) {
    pixels_[(WIDTH * y) + x] = BLACK;
}

const char * Framebuffer::getPixels() {
    return pixels_;
}

class Scene {
public:
    void draw() {
        buffer_.clear();
        buffer_.draw(1, 1);
        buffer_.draw(4, 1);
        buffer_.draw(1, 3);
        buffer_.draw(2, 4);
        buffer_.draw(3, 4);
        buffer_.draw(4, 3);
    }

    Framebuffer & getBuffer() { return buffer_; }

private:
    Framebuffer buffer_;
};

buffer_.draw(1, 1); 
buffer_.draw(4, 1);
// <- Video driver reads pixels here!
buffer_.draw(1, 3);
buffer_.draw(2, 4);
buffer_.draw(3, 4);
buffer_.draw(4, 3);

class Scene {
public:
    Scene(): current_(&buffer_[0]), next_(&buffer_[1]) {}

    void draw() {
        next_->clear();
        next_->draw(1,1);
        // ...
        next_->draw(4, 3);
        swap();
    }

    Framebuffer & getBuffer() { return *current_; }

private:
    void swap() {
        // Just switch the pointers.
        Framebuffer * temp = current_;
        current_ = next_;
        next_ = current_;
    }

    Framebuffer buffers_[2];
    Framebuffer * current_;
    Framebuffer * next_;
};
View Code

    8.5.1 並不是只針對圖形

雙緩衝模式所解決的核心問題就是對狀態同時進行修改與訪問的衝突.形成此問題的緣由一般有兩個,咱們已經經過上述圖形示例描述了第一種狀況----狀態直接被另外一個線程或中斷的代碼所直接訪問

而另外一種狀況一樣很常見:進行狀態修改的代碼訪問到了其正在修改的那個狀態.這會在不少地方發生:尤爲是實體的AI和物理部分,在它與其餘實體進行交互時會發生這樣的狀況,雙緩衝模式每每能在此情形下奏效

    8.5.2 人工非智能

class Actor {
public:
    Actor(): slapped_(false) {}
    
    virtual void update() = 0;
    virtual ~Actor() {}

    void reset() { slapped_ = false; }
    void slap() { slapped_ = true; }
    bool wasSlapped() { return slapped_; }

private:
    bool slapped_;
};

class Stage {
public:
    vodi add(Actor * actor, int index) {
        actors_[index] = actor;
    }

    void update() {
        for (int i = 0; i < NUM_ACTORS; i++) {
            actors_[i]->update();
            actors_[i]->reset();
        }
    }

private:
    static const int NUM_ACTORS = 3;
    Actor * actors_[NUM_ACTORS];
};

// 對於用戶而言,角色開始同步地各自移動,但從內部看,一個時刻僅有一個角色被更新


class Comedian: public Actor {
public:
    void face(Actor * actor) { facing_ = actor; }
    
    virtual void update() {
        if (wasSlapped()) { facing_->slap() };
    }
    
private:
    Actor * facing_;
};

Stage stage;

Comedian * harry = new Comedian();
Comedian * baldy = new Comedian();
Comedian * chump = new Comedian();

harry->face(baldy);
harry->face(chump);
harry->face(harry);

stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);

harry->slap();
stage.update();
View Code

    8.5.3 緩存這些巴掌

class Actor {
public:
    Actor(): currentSlapped_(false) {}

    virtual void update() = 0;
    virtual ~Actor() {}

    void swap() {
        currentSlapped_ = nextSlapped_;
        nextSlapped_ = false;
    }

    void lap() { nextSlapped_ = true; }
    bool wasSlapped() { return currentSlapped_; }
private:
    bool currentSlapped_;
    bool nextSlapped_;
};

void Stage::update() {
    for (int i = 0; i < NUM_ACTORS; i++) {
        actors_[i]->update();
    }
    for (int i = 0; i < NUM_ACTROS; i++) {
        actors_[i]->swap();
    }
}
View Code

  8.6 設計決策

    8.6.1 緩衝區如何交換

交換緩衝區指針或者引用

在兩個緩衝區之間進行數據的拷貝

    8.6.2 緩衝區的粒度如何

  8.7 參考

你幾乎能在任何一個圖形API種找到雙緩衝模式的應用.例如,OpenGL種的swapBuffers()函數,Direct3D種的"swap chains",微軟XNA框架在endDraw()方法種也使用了幀緩衝區的交換

第9章 遊戲循環

實現用戶輸入和處理器速度在遊戲行進時間上的解耦

  9.1 動機

假若有哪一個模式是本書最沒法刪減的,那麼非遊戲循環模式莫屬.遊戲循環模式是遊戲編程模式種的精髓.幾乎全部的遊戲都包含着它,無一雷同,相比而言那些非遊戲程序中卻難見它的身影

    9.1.1 CPU探祕

while (true) {
    char * command = readCommand();
    handleCommand(command);
}
View Code

    9.1.2 事件循環

若是剝去現代的圖形應用程序UI的外衣,你將發現它們和舊得冒險遊戲是如此類似

while (true) {
    Event * event = waitForEvent();
    dispatchEvent(event);
}
View Code

不一樣於其餘大多數軟件,遊戲即使在用戶不提供任何輸入時也一直在運行.加入你坐下來盯着屏幕,遊戲也不會卡住.動畫依舊在播放,各類效果也在閃動跳躍

這是真實的遊戲循環的第一個關鍵點:它處理用戶輸入,但並不等待輸入.遊戲循環始終在運轉:

while (true) {
    processInput();
    update();
    render();
}
View Code

    9.1.3 時間以外的世界

假如用現實時間來衡量遊戲循環的速度,咱們就獲得了遊戲的"幀率(FPS,frames per second)".

兩個因素決定了幀率.

  第一個是循環每一幀要處理的信息量.複雜的物理運算,一堆對象的數據更新,許多圖形細節等都將讓你的CPU和GPU忙個不停,這都會讓一幀消耗更多的時間

  第二個是底層平臺的速度.速度越快的芯片相同時間內可以處理更多的代碼.多核,多GPU,專用聲卡以及操做系統的調度器都會影響着你的一幀中所能處理的代碼量

    9.1.4 秒的長短

遊戲循環模式的另外一個要點:這一模式讓遊戲在一個與硬件無關的速度常量下運行.

  9.2 模式

一個遊戲循環會在遊戲過程當中持續地運轉.每循環一次,它非阻塞地處理用戶輸入,更新遊戲狀態,並渲染遊戲.它跟蹤流逝的時間並控制遊戲的速率

  9.3 使用情境

  9.4 使用須知

  9.5 示例代碼

    9.5.1 跑,能跑多快就跑多快

while (true) {
    processInput();
    update();
    render();
}
View Code

它的問題在於你沒法控制遊戲運轉的快慢.在較快的機器上游戲循環可能會快得令玩家看不清楚遊戲在作什麼,在慢的機器上游戲則會變慢變卡

    9.5.2 小睡一下子

你但願遊戲運行在60幀,假如你肯定每16ms甚至更短的時間就能處理全部的信息

while (true) {
    double start = getCurrentTime();
    processInput();
    update();
    render();

    sleep(start + MS_PER_FRAME - getCurrentTime());
}
View Code

對於超過16ms時間的處理時間,則無能爲力

    9.5.3 小改動,大進步

double lastTime = getCurrentTime();
while (true) {
    double current = getCurrentTime();
    double elapsed = current - lastTime;
    processInput();
    update(elapsed);
    render();
    lastTime = current;
}
View Code

    9.5.4 把時間追回來

double previous = getCurrentTime();
double lag = 0.0;
while (true) {
    double current = getCurrentTime();
    double elapsed = current - previous;
    previous = current;
    lag += elapsed;
    processInput();
    
    while (lag >= MS_PER_UPDATE) {
        update();
        lag -= MS_PER_UPDATE;
    }
    render();
}
View Code

    9.5.5 留在兩幀之間

  9.6 設計決策

    9.6.1 誰來控制遊戲循環,你仍是平臺

使用平臺的事件循環

使用遊戲引擎的遊戲循環

本身編寫遊戲循環

    9.6.2 你如何解決能量耗損

限制幀率

    9.6.3 如何控制遊戲速度

非同步的固定時間步長

同步的固定時長

變時時長

定時更新迭代,變時渲染

  9.7 參考

講述遊戲循環模式的一篇經典文章是來自 Glenn Fiedler 的 "Fix Your Timestep". 沒有這篇文章,這一章就無法寫成如今這樣

Witters的文章 game loops 也值得一看

Unity 的框架具備一個複雜的遊戲循環,這裏有一個對其很詳盡的闡述

第10章 更新方法

經過對全部對象實例同時進行幀更新來模擬一系列相互獨立的遊戲對象

  10.1 動機

// Skeleton variables...
Entity skeleton;
bool patrollingLeft = false;


Entity leftStatue;
Entity rightStatue;
int leftStatueFrames = 0;
int right StatueFrames = 0;

// Main game loop
while (true) {
    if (patrollingLeft) {
        x--;
        if (x == 0) patrollingLeft = false;
    } else {
        x++;
        if (x == 100) patrollingLeft = true;
    }

    skeleton.setX(x);

    if (++leftStatueFrames == 90) {
        leftStatueFrames = 0;
        leftStatue.shootLightning();
    } 

    if (++rightStatueFrames == 80) {
        rightStatueFrames = 0;
        rightStatue.shootLightning();
    }
    
    // Handle user input and render game...
}
View Code

你會發現這代碼的可維護性不高.咱們維護着一堆其值不斷增加的變量,並不可避免地將全部代碼都塞進遊戲循環裏,每段代碼處理一個遊戲中特殊的實體.爲達到讓全部實體同時運行的目的,咱們把它們雜糅在一塊兒了.

你可能猜到咱們所要運用的設計模式該幹些什麼了;它要爲遊戲中的每一個實體封裝其自身的行爲.這將使得遊戲循環保持整潔並便於往循環中增長或移除實體

爲了作到這一點,咱們須要一個抽象層,爲此定義一個update()的抽象方法.遊戲循環維護對象集合,但它並不關心這些對象的具體類型.它只是更新它們.這將每一個對象的行爲從遊戲循環以及其餘對象那裏分離了出來

每一幀,遊戲循環遍歷遊戲對象集合並調用它們的update().這在每幀都給與每一個對象一次更新本身行爲的機會.經過逐幀調用update方法,使得這些對象的表現獲得同步

遊戲循環維護一個動態對象集合,這使得向關卡里添加或者移除對象十分便捷----只要往集合裏增長或移除就好

  10.2 模式

遊戲世界維護一個對象集合.每一個對象實現一個更新方法以在每幀模擬本身的行爲.而遊戲循環在每幀對集合中全部的對象調用其更新方法,以實現和遊戲世界同步更新

  10.3 使用情境

假如把遊戲循環比做有史以來最好的東西,那麼更新方法模式就會讓它錦上添花.

更新方法模式在以下情境最爲適用:

  你的遊戲中含有一系列對象或系統須要同步地運轉

  各個對象之間的行爲幾乎是相互獨立的

  對象的行爲與時間相關

  10.4 使用須知

    10.4.1 將代碼劃分至單幀之中使其變得更加複雜

    10.4.2 你須要在每幀結束前存儲遊戲狀態以便下一幀繼續

    10.4.3 全部對象都在每幀進行模擬,但並不是真正同步

在本設計模式中,遊戲循環在每幀遍歷對象集並逐個更新對象.在update()的調用中,多數對象可以訪問到遊戲世界的其餘部分,包括那些正在更新的其餘對象.這意味着,遊戲循環遍歷更新對象的順序意義重大

    10.4.4 在更新期間修改對象列表時必須謹慎

不在本幀處理新添加的對象

int numObjectsThisTurn = numObjects_;
for (int i = 0; i < numObjectsThisTurn; i++) {
    objects_[i]->update();
}
View Code

在迭代時移除對象

一種方法是當心地移除對象並在更新任何計數器時把被移除的對象也算在內.還有一個辦法是將移除操做推遲到本次循環遍歷結束以後.將要被移除的對象標記爲"死亡",但並不從列表中移除它.在更新期間,確保跳過那些被標記死亡的對象接着等到遍歷更新結束,再次遍歷列表來移除這些"屍體"

  10.5 示例代碼

class Entity {
public:
    Entity(): x_(0), y_(0) {}
    
    virtual void update() = 0;
    virtual ~Entity() {}

    double x() const { return x_; }
    double y() const { return y_; }

    void setX(double x) { x_ = x; }
    void setY(double y) { y_ = y; }

private:
    double x_, y_;
};

class World {
public:
    World(): numEntities_(0) {}

    void gameLoop();

private:
    Entity * entities_[MAX_ENTITIES];
    int numEntities_;
};

void World::gameLoop() {
    while (true) {
        // Handle user input...
        
        // Update each entity
        for (int i = 0; i < numEntities_; i++) {
            entities_[i]->update();
        }

        // Physics and rendering...
    }
}
View Code

    10.5.1 子類化實體

    10.5.2 定義實體

class Skeleton: public Entity {
public:
    Skeleton(): patrollingLeft_(false) {}
    
    virtual void update() {
        if (patrollingLeft_) {
            setX(x() - 1);
            if (x() == 0) patrollingLeft_ = false;
        } else {
            setX(x() + 1);
            if (x() == 100) patrollingLeft_ = true;
        }
    }

private:
    bool patrollingLeft_;
};

class Statue: public Entity {
public:
    Statue(int delay): frames_(0), delay_(delay) {}

    virtual void update() {
        if (++frames_ == delay_) {
            shootLightning();
            // Reset the timer.
            frames_ = 0;
        }
    }

private:
    int frames_;
    int delay_;
    
    void shootLightning() {
        // Shoot the lightning...
    }
};
View Code 

    10.5.3 逝去的時間

void Skeleton::update(double elapsed) {
    if (patrollingLeft_) {
        x -= elapsed;
        if (x <= 0) {
            patrollingLeft_ = false;
            x = -x;
        }
    } else {
        x += elapsed;
        if (x >= 100) {
            patrollingLeft_ = true;
            x = 100 - (x - 100);
        }
    }
}
View Code

  10.6 設計決策

    10.6.1 update方法依存於何類中

你顯然必須決定好該把update()方法放在哪個類中

  實體類中  假如你已經建立了實體類,那麼這是最簡單的選項.由於這不會往遊戲中增長額外的類.假如你不須要不少種類的實體,那麼這種方法可行,但實際項目中不多這麼作

  組件類中  更新方法模式與組件模式享有相同的功能----讓實體/組件獨立更新,它們都使得每一個實體/組件在遊戲世界中可以獨立於其餘實體/組件.渲染,物理,AI都僅需專一於本身

  代理類中  將一個類的行爲代理給另外一個類,設計了其餘幾種設計模式.狀態模式可讓你經過改變一個對象的代理來改變其行爲.對象類型模式可讓你在多個相同類型的實體之間共享行爲

        假如你適用上述射擊模式,那麼天然而然地須要將update()方法至於代理類中.這麼一來,你可能在主類中仍保留update()方法,但它會成爲非虛的方法並簡單地指向代理類對象的update()方法

void Entity::update() {
    state_->update();
}
View Code

    10.6.2 那些未被利用的對象該如何處理

你常須要在遊戲中維護這樣一些對象:不論處於何種緣由,它們暫時無需被更新.一種方法是單獨維護一個須要被更新的"存活"對象表.

  10.7 參考

這一模式與遊戲循環和組件模式共同構成了多數遊戲引擎的核心部分

當你開始考慮實體集合或循環中組件在更新時的緩存功能,並但願它們更快地運轉時,數據局部性模式將會有所幫助

Unity 的引擎框架在許多類模塊中使用了本模式,包括Monobehaviour類

微軟的XNA平臺在Game和GameComponent類中均使用了這一模式

Quintus 是基於JavaScript的遊戲引擎,在其主要的Sprite類中使用了這一模式

第11章 字節碼

經過將行爲編碼成虛擬機指令,而使其具有數據的靈活性

  11.1 動機

    11.1.1 魔法大戰

    11.1.2 先數據後編碼

    11.1.3 解釋器模式

class Expression {
public:
    virtual double evaluate() = 0;
    virtual ~Expression();
};

class NumberExpression: public Expression {
public:
    NumberExpression(double value): value_(value) {}

    virtual double evaluate() { return value_; }

private:
    double value_;
};

class AdditionExpression: public Expression {
public:
    AdditionExpression(Expression * left, Expression * right): left_(left), right_(right) {}
    virtual double evaluate() {
        // Evaluate the operands
        double left = left_->evaluate();
        double right = right_->evaluate();

        // Add them.
        return left + right;
    }

private:
    Expression * left_;
    Expression * right_;
};
View Code

    11.1.4 虛擬機器碼

  11.2 字節碼模式

指令集定義了一套能夠執行的底層操做.一系列指令被編碼爲字節序列.虛擬機逐條執行指令棧上這些指令.經過組合指令,既可完成不少高級行爲

  11.3 使用情境

這是本書中最複雜的模式,它可不是輕易就能放進你的遊戲裏的,僅當你的遊戲中須要定義大量行爲,並且實現遊戲的語言出現下列狀況才應該使用:

  編程語言太底層了,編寫起來繁瑣易錯

  因編譯時間太長或工具問題,致使迭代緩慢

  它的安全性太依賴編碼者.你想確保定義的行爲不會讓程序崩潰,就得把它們從代碼庫轉移至安全沙箱中

固然,這個列表複合大多數遊戲的狀況.誰不想提升迭代速度,讓程序更安全?但那是有代價的,字節碼比本地碼要慢,因此它並不適合用做對性能要求極高的核心部分

  11.4 使用須知

    11.4.1 你須要個前端界面

    11.4.2 你會想念調試器的

  11.5 示例

    11.5.1 法術API

void setHealth(int wizard, int amount);
void setWisdom(int wizard, int amount);
void setAgility(int wizard, int amount);

void playSound(int soundId);
void spawnParticles(int particleType);
View Code

    11.5.2 法術指令集

enum Instruction {
    INST_SET_HEALTH = 0x00,
    INST_SET_WISDOM = 0x01,
    INST_SET_AGILITY = 0x02,
    INST_PLAY_SOUND = 0x03,
    INST_SPAWN_PARTICLES = 0x04
};

swtich (instruction) {
    case INST_SET_HEALTH:
        setHealth(0, 100);
        break;
    case INST_SET_WISDOM:
        setWisdom(0, 100);
        break;
    case INST_SET_AGILITY:
        setAgility(0, 100);
        break;
    case INST_PLAY_SOUND:
        playSound(SOUND_BANG);
        break;
    case INST_SPAWN_PARTICLES:
        spawnParticles(PARTICLE_FLAME);
        break;
}

class VM {
public:
    void interpret(char bytecode[], int size) {
        for (int i = 0; i < size; i++) {
            char instruction = bytecode[i];
            switch (instruction) {
                // Case for each instruction...
            }
        }
    }
};
View Code

    11.5.3 棧機

class VM {
public:
    VM(): stackSize_(0) {}

    // Other stuff...
private:
    static const int MAX_STACK = 128;
    int stackSize_;
    int stack_[MAX_STACK];
};

class VM {
private:
    void push(int value) {
        // Check for stack overflow.
        assert(stackSize_ < MAX_STACK);
        stack_[stackSize_++] = value;
    }

    int pop() {
        // Make sure the stack isn't empty.
        assert(stackSize_ > 0);
        return stack_[--stackSize_];
    }
    
    // Other stuff...
};

switch (instruction) {
    case INST_SET_HEALTH:
        int amount = pop();
        int wizard = pop();
        setHealth(wizard, amount);
        break;
    // Similar for SET_WISDOM and SET_AGILITY...
    case INST_PLAY_SOUND:
        playSound(pop());
        break;
    
    case INST_SPAWN_PARTICLES:
        spawnParticles(pop());
        break;
}

switch (instruction) {
    // Other instruction cases...
    case INST_LITERAL:
        // Read the next byte from the bytecode.
        int value = bytecode[++i];
        push(value);
        break;
}
View Code

毫無疑問,這個架構就是所謂的棧機.例如Forth, PostScriptFactor這類編程語言將這個模型直接暴露給了用戶

    11.5.4 組合就能獲得行爲

case INST_GET_HEALTH:
    int wizard = pop();
    push(getHealth(wizard));
    break;

case INST_GET_WISDOM:
case INST_GET_AGILITY:
    // You get the iead...

case INST_ADD:
    int b = pop();
    int a = pop();
    push(a + b);
    break;

setHealth(0, getHealth(0) + (getAgilit(0) + getWisdom(0)) / 2);

LITERAL 0 [0] # Wizard index
LITERAL 0 [0, 0] # Wizard index
GET_HEALTH [0, 45] #getHealth()
LITERAL 0 [0, 45, 0] # Wizard index
GET_AGILITY [0, 45, 7] # getAgility()
LITERAL 0 [0, 45, 7, 0] # Wizard index
GET_WISDOM [0, 457, 7, 11] getWisdom()
ADD [0, 45, 18] # Add agility and wisdom
LITERAL 2 [0, 45, 18, 2] # Divisor
DIVIDE [0, 45, 9] # Average them
ADD [0, 54] # Add average to health
SET_HEALTH [] #Set health to result
View Code

    11.5.5 一個虛擬機

    11.5.6 語法轉換工具

《編譯器: 原則, 技術和工具》

  11.6 設計決策

    11.6.1 指令如何訪問堆棧

字節碼虛擬機有兩種大風格: 基於棧和基於寄存器.在基於棧的虛擬機中,指令老是操做棧頂,正如咱們的實例代碼同樣.例如, "INST_ADD"出棧兩個值,將它們相加,而後將結果入棧

基於寄存器的虛擬機也有一個堆棧.惟一的區別是指令能夠從棧的更深層次中讀取輸入.不像"INST_ADD" 那樣老是出棧操做數,它在字節碼中存儲兩個索引來表示應該從堆棧的哪一個位置讀取操做數

基於棧的虛擬機

  指令很小

  代碼生成更簡單

  指令數更多

基於寄存器的虛擬機

  指令更大

  指令更小

    11.6.2 應該有哪些指令

外部基本操做

內部基本操做

控制流

抽象化

    11.6.3 值應當如何表示

單一數據類型

  它很簡單

  你沒法使用不一樣的數據類型

標籤的一個變體

enum ValueType {
    TYPE_INT,
    TYPE_DOUBLE,
    TYPE_STRING
};

struct Value {
    ValueType type;
    union {
        int intValue;
        double doubleValue;
        char * stringValue;
    }
};
View Code

  值存儲了自身的類型信息

  佔用更多內存

不帶標籤的聯合體

  緊湊

  快速

  不安全

一個接口

class Value {
public:
    virtual ~Value() {}
    virtual ValueType type() = 0;
    virtual int asInt() {
        // Can only call this on ints
        assert(false));
        return 0;
    }

    // Other conversion methods...
};

class IntValue: public Value {
public:
    IntValue(int value): value_(value) {}

    virtual ValueType type() { return TYPE_INT; }
    virtual int asInt() { return value_; }
private:
    int value_;
};
View Code

  開放式

  面向對象

  累贅

  低效

    11.6.4 如何生成字節碼

若是你必定了一種基於文本的語言

  你得定義一種語法

  你要實現一個分析器

  你必須處理語法錯誤

  對非技術人員沒有親和力

若是你設計了一個圖形化編輯器

  你要實現一個用戶界面

  不易出錯

  可移植性差

  11.7 參考

這個模式是Gof解釋器模式的姊妹版.它們都會爲你提供一種用數據組合行爲的方法.事實上,你常常會將兩個模式一塊兒使用.你用來生成字節碼的工具一般會有一個內部對象樹來表達代碼.這正是解釋器模式能作的事情.爲了將它編譯成字節碼,你須要遞歸遍歷整棵樹,正如你在解釋器模式中解析它那樣.惟一的不一樣是你並非直接執行一段代碼而是將它們輸出成字節碼指令並在之後執行它們

Lua編程語言是遊戲中普遍使用的編程語言.它內部實現了一個緊湊的基於寄存器的字節碼虛擬機

Kismet是內置在UnrealEd(Unreal Engine的編輯器)中的圖形化腳本工具

我本身的小型腳本語言,Wren,是一個簡單的基於堆棧的字節碼解釋器

第12章 子類沙盒

使用基類提供的操做集合來定義子類中的行爲

  12.1 動機

這個設計模式會催生一種扁平的類層次架構.你的繼承鏈不會太深,可是會有大量的類與Superpower掛鉤.經過使一個類派生大量的直接子類,咱們限制了該代碼在代碼庫裏的影響範圍.遊戲中大量的類都會獲益於咱們精心射擊的Superpower類

  12.2 沙盒模式

一個基類定義了一個抽象的沙盒模式方法和一些預約義的操做集合.經過將它們設置爲受保護的狀態以確保它們僅供子類使用.每一個派生出的沙盒子類根據父類提供的操做來實現沙盒函數

  12.3 使用情境

沙盒模式適用於如下狀況

  你有一個帶有大量子類的函數

  基類可以提供全部子類可能須要執行的操做集合

  在子類之間有重疊的代碼,你但願在它們之間更簡便地共享代碼

  你但願使這些繼承類與程序其餘代碼之間的耦合最小化

  12.4 使用須知

脆弱的基類問題

  12.5 示例

class Superpower {
public:
    virtual ~Superpower() {}
protected:
    // 沙盒函數
    virtual void activate() = 0;
    
    // 與其餘系統耦合
    void move(double x, double y, double z) {
        // Code here...
    }

    // 與其餘系統耦合
    void playSound(SoundId sound) {
        // Code here...
    }

    // 與其餘系統耦合
    void spawnParticles(ParticleType type, int count) {
        // Code here...
    }

    double getHeroX() {}
    double getHeroY() {}
    double getHeroZ() {}
};

class SkyLaunch: public Superpower {
protected:
    virtual void activate() {
        if (getHeroZ() == 0) {
            move(0, 0, 20);
            playSound(SOUND_SPROING);
            spawnParticles(PARTICLE_DUST, 10);
        } else if (getHeorZ() < 10.0f) {
            playSound(SOUND_SWOOP);
            move(0, 0, getHeroZ() - 20);
        } else {
            playSound(SOUND_DIVE);
            spawnParticles(PARTICLE_SPARKLES, 1);
            move(0, 0, -getHeroZ());
        }
    }
};
View Code

起初,我建議對power類採用數據驅動的方式.此處就是一個你決定不採用它的緣由.若是你的行爲是複雜的,命令式的,那麼用數據定義它們會更加困難

  12.6 設計決策

    12.6.1 須要提供什麼操做

經驗法則

  若是所提供的操做僅僅被一個或者少數的子類所使用,那麼沒必要將它加入基類.這隻會給基類增長複雜度,同時將影響每一個子類,而僅有少數子類從中受益.將該操做與其餘提供的操做保持一致或許值得,但讓這些特殊子類直接調用外部系統或許更爲簡單和清晰

  當你在遊戲的其餘模塊進行某個方法調用時,若是它不修改任何狀態,那麼它就不具有侵入性.它仍然產生了耦合,但這是個"安全"的耦合,由於在遊戲中它不帶來任何破壞.而另外一方面,若是這些調用確實改變了狀態,則將與代碼庫產生更大的耦合,你須要對這些耦合更上心.所以此時這些方法更適合由更可視化的基類提供

  若是提供的操做,其實現僅僅是對一些外部系統調用的二次封裝,那麼它並無帶來多少價值.在這種狀況下,直接調用外部系統更爲簡單.然而,極其簡單的轉向調用也仍有用----這些函數一般訪問基類不像直接暴露給子類的狀態

    12.6.2 是直接提供函數,仍是由包含它們的對象提供

class SoundPlayer {
    void playSound(SoundId sound) { }
    void stopSound(SoundId sound) {}
    void setVolume(SoundId sound) {}
};

class Superpower {
protected:
    SoundPlayer & getSoundPlayer() {
        return soundPlayer_;
    }

    // Sandbox method and other operations...
    
private:
    Soundplayer soundPlayer_;
};
View Code

把提供的操做分流到一個像這樣的輔助類中能給你帶來些好處

  減小了基類的函數數量.

  在輔助類中的代碼一般更容易維護

  下降了基類和其餘系統之間的耦合

    12.6.3 基類如何獲取其所需的狀態

把它傳遞給基類構造函數

class Superpower {
public:
    Superpower(ParticleSystem * particles): particles_(particles) {}
    
    // Sandbox method and other operations...
private:
    ParticleSystem * particles_;
};

class SkyLaunch: public Superpower {
public:
    SkyLaunch(ParticleSystem * particles): Superpower(particles) {}
};
View Code

進行分段初始化

Superpower * power = new Superpower();
power->init(particles);

Superpower * createSkyLaunch((ParticleSystem * particles) {
    Superpower * power = new SkyLaunch();
    power->init(particles);
    return power;
}
View Code

將狀態靜態化

class Superpower {
public:
    static void init(ParticleSystem * particles) {
        particles_ = particles;
    }

    // Sandbox method and other operations...
private:
    static ParticleSystem* particles_;
};
View Code

使用服務定位器

class Superpower {
protected:
    void spawnParticles(ParticleType type, int count) {
        ParticleSystem & particles = Locator::getParticles();
        particles.spawn(type, count);
    }

    // Sandbox method and other operations...
};
View Code

  12.7 參考

當你採用更新方法模式的時候,你的更新函數一般也是一個沙盒函數

模板方法模式正好與本模式相反.在這兩個模式中,你都使用一系列操做原語來實現一個函數.使用子類沙盒模式時,函數在繼承類中,原語操做則在基類中.使用模板方法時,基類定義函數骨架,而原語操做被繼承類實現

你能夠將這個模式看做是在外觀模式上的一個變種.外觀模式將許多不一樣的系統隱藏在了一個簡化的API之下.在子類沙盒模式中,基類對於子類來講充當着隱藏遊戲引擎實現細節的角色

第13章 類型對象

經過建立一個類來支持新類型的靈活建立,其每一個實例都表明一個不一樣的對象類型

  13.1 動機

    13.1.1 經典的面向對象方案

class Monster {
public:
    virtual const char * getAttack() = 0;
    virtual ~Monster() {}
protected:
    Monster(int startingHealth): health_(startingHealth) {}
private:
    int health_;
};

class Dragon: public Monster {
public:
    Dragon(): Monster(250) {}

    virtual const char * getAttach() {
        return "The dragon breathes fire!" ;
    }
};

class Troll: public Monster {
public:
    Troll(): Monster(48) {}

    virtual const char * getAttack() {
        return "The troll clubs you!";
    }
};
View Code

    13.1.2 一種類型一個類

爲了將怪物與種族關聯起來,咱們讓每一個Monster實例化一個包含了其種族信息的Bread對象引用.Breed類本質上定義了怪物的"類型".每一個種族實例都是一個對象,表明着不一樣的概念類型,而這個模式的名字就是: 類型對象

  13.2 類型對象模式

定義一個類型對象類和一個持有類型對象類.每一個類型對象的實例表示一個不一樣的邏輯類型.每一個持有類型對象類的實例引用一個描述其類型的類型對象

實例數據被存儲在持有類型對象的實例中,而全部同概念類型所共享的數據和行爲被存儲在類型對象中.引用同一個類型的對象之間能表現出"同類"的性狀.這讓咱們能夠在類似對象集合中共享數據和行爲,這與類派生的做用有幾分類似,但卻無需硬編碼出一批派生類

  13.3 使用情境

當你須要定義一系列不一樣"種類"的東西,但又不想把那些種類硬編碼進你的類型系統時,本模式都適用.尤爲是當下面任何一項成立的時候:

  你不知道未來會有什麼類型

  你須要在不從新編譯或修改代碼的狀況下,修改或添加新的類型

  13.4 使用須知

    13.4.1 類型對象必須手動跟蹤

    13.4.2 爲每一個類型定義行爲更困難

經過類型對象去定義類型相關的數據很是容易,可是定義數據類型相關的行爲卻很難.

有幾種方法能夠跨越這個限制.

一個簡單的方法是建立一個固定的預約義行爲集合,讓類型對象中的數據從中任選其一

另外一個更強大,更完全的解決方案是支持在數據中定義行爲.若是咱們能讀取數據文件並提供給上述任意一種模式來實現,行爲定義就徹底從代碼中脫離出來,而被放進數據文件內容中.

  13.5 示例

class Breed {
public:
    Breed(int health, const char * attack): health_(health), attack_(attack) {}

    int getHealth() { return health_; }
    const char * getAttack() { return attack_; }
private:
    int health_;
    const char * attack_;
};

class Monster{
public:
    Monster(Breed & breed): health(breed.getHealth()), breed_(breed) {}

    const char * getAttack() {
        return breed_.getAttack();
    }

private:
    int health_;
    Breed & breed_;
};
View Code

    13.5.1 構造函數: 讓類型對象更加像類型

class Breed {
public:
    Monster * newMonster() {
        return new Monster(*this);
    }

    // Previous Breed code...
};

class Monster {
    friend class Bread;

public:
    const char * getAttack() {
        return breed_.getAttack();
    }

private:
    Monster(Breed & breed): health_(breed.getHealth()), breed_(breed) {}

    int health_;
    Breed & bread_;
};
View Code

    13.5.2 經過繼承共享數據

仿照多個怪物經過種族共享特性的方式,讓種族之間也可以共享特性.咱們不採用語言自己的派生機制,而是本身在類型對象裏實現它

class Breed {
public:
    Breed(Breed * parent, int health, const char * attack): parent_(parent), health_(health), attack_(attack) {}

    int getHealth();
    const char * getAttack();

private:
    Breed * parent_;
    int health_;
    const char * attack_;
};
View Code

在屬性每次被請求的時候執行代理調用

int Breed::getHealth() {
    // Override
    if (health_ != 0 || parent_ == NULL) {
        return health_;
    }
    // Inherit
    return parent->getHealth();
}

const char * Breed::getAttack() {
    // Override
    if (attack_ != NULL || parent_ == NULL) {
        return attack_;
    }
    // Inherit
    return parent->getAttack();
}
View Code

若是咱們能確保基種族的屬性不會改變.那麼一個更快的解決方案是在構造時採用繼承.這也被稱爲"複製"代理,由於咱們在建立一個類型時把繼承的特性複製到了這個類型內部

Breed(Breed * parent, int health, const char * attack): health_(health), attack_(attack) {
    // Inherit non-overridden attribute
    if (parent != NULL) {
        if (healt_ == 0) health_ = parent->getHealth();
        
        if (attack == NULL) {
            attack_ = parent->getAttack();
        }
    }
}

int getHealth() { return health_; }
const char * getAttack() { return attack_; }
View Code

假設遊戲引擎從JSON文件建立種族

{
    "Troll": {
        "health": 25,
        "attack": "The troll hits you!"
    },
    "Troll Archer": {
        "parent": "Troll",
        "health": 0,
        "attack": "The troll archer fires an arrow!"
    },
    "Troll Wizard": {
        "parent": "Troll",
        "health": 0,
        "attack": "The troll wizard casts a spell"
    }
}
View Code

  13.6 設計決策

    13.6.1 類型對象應該封裝仍是暴露

若是類型對象被封裝

  類型對象模式的複雜性對代碼庫的其餘部分不可見.它成爲了持有類型對象才需關心的實現細節

  持有類型對象的類能夠有選擇性地重寫類型對象的行爲

若是類型對象被公開

  外部代碼在沒有持有類型對象類實例的狀況下就能訪問類型對象

  類型對象如今是對象公共API的一部分 

    13.6.2 持有類型對象如何建立

經過這種模式,每一個"對象"如今都成了一對對象:主對象以及它所使用的類型對象.那麼咱們如何建立並將它們綁定起來呢?

  構造對象並傳入類型對象

  在類型對象上調用"構造"函數

    13.6.3 類型可否改變

類型不變

  不管編碼仍是理解起來都更簡單

  易於調試

類型可變

  減小對象建立

  作約束時要更加當心

    13.6.4 支持何種類型的派生

沒有派生

  簡單

  可能會致使重複勞動

單繼承

  仍然相對簡單

  屬性查找會更慢

多重派生

  能避免絕大多數的數據重複

  複雜

  13.7 參考

這個模式所圍繞的高級問題是如何在不一樣對象之間共享數據.從另外一個不一樣角度嘗試解決這個問題的是原型模式

類型對象與享元模式很接近.它們都讓你在實例間共享數據.享元模式傾向於節約內存,而且共享的數據可能不會以實際的"類型"呈現.類型對象模式的重點在於組織性和靈活性

這個模式與狀態模式也有諸多類似性.它們都把對象的部分定義工做交給另外一個代理對象實現.在類型對象中,咱們一般代理的對象是:寬泛地描述對象的靜態數據.在狀態模式中,咱們代理的是對象當前的狀態,即描述對象當前配置的臨時數據.當咱們討論到可改變類型對象的時候,你能夠認爲是類型對象在狀態模式的基礎上身兼二職

第14章 組件模式

容許一個單一的實體跨越多個不一樣域而不會致使耦合

  14.1 動機

    14.1.1 難題

    14.1.2 解決難題

將獨立的Bjorn類根據域邊界切分紅相互獨立的部分.舉個例子,咱們將全部用來處理用戶輸入的代碼放到一個單獨的類InputComponent中.而Bjorn將擁有整個類的一個實例.咱們將重複對Bjorn類包含的全部域作相同的工做

當咱們完成工做後,咱們幾乎將Bjorn類中的全部東西都清理了出去.剩下的即是一個將全部組件綁定在一塊兒的外殼.咱們經過簡單地將代碼分割成多個更小類的方式解決了整個超大類的問題,但完成這項工做所達到的效果遠遠不止這些

    14.1.3 寬鬆的末端

    14.1.4 捆綁在一塊兒

繼承有它的用戶,可是對某些代碼重用來講實現起來太麻煩了.相反,軟件設計的趨勢應該是儘量地使用組合而不是繼承.爲實現兩個類之間的代碼共享,咱們應該讓它們擁有同一個類的實例而不是繼承同一個類

  14.2 模式

單一實體跨越了多個域.爲了能保持域之間相互隔離,每一個域的代碼都獨立地放在本身的組件類中.實體自己則能夠簡化爲這些組件的容器

  14.3 使用情境

組件最多見於遊戲中定義實體的核心類,可是它們也可以用在別的地方.當以下條件成立時,組件模式就可以發揮它的做用

  你有一個涉及多個域的類,可是你但願讓這些域保持相互解耦

  一個類愈來愈龐大,愈來愈難以開發

  你但願定義許多共享不一樣能力的對象,但採用繼承的辦法卻沒法令你精確地重用代碼

  14.4 注意事項

組件模式相較直接在類中編碼的方式爲類自己引入了更多的複雜性.每一個概念上的"對象"成爲一系列必須被同時實例化,初始化,並正確關聯的對象的集羣.不一樣組件之間的通訊變得更具挑戰性,並且對它們所佔用內存的管理將更復雜

使用組件的另一個後果是你常常須要經過一系列間接引用來處理問題,考慮容器對象,首先你必須獲得你須要的組件,而後你才能夠作你須要作的事情,在一些性能要求較高的內部循環代碼中,這個組件指針可能會致使低劣的性能

  14.5 示例代碼

    14.5.1 一個龐大的類

class Bjorn {
public:
    Bjorn(): velocity_(0), x_(0), y_(0) {}
    
    void update(World & world, Graphics & graphics);
private:
    static const int WALK_ACCCELERATION = 1;

    int velocity_;
    int x_, y_;
    
    Volume volume_;
    
    Sprite spriteStand_;
    Sprite spriteWalkLeft_;
    Sprite spriteWalkRight_;
};

void Bjorn::update(World & world, Graphics & graphics) {
    switch (Controller::getJoystickDirection()) {
        case DIR_LEFT:
            velocity_ -= WALK_ACCELERATION;
            break;
        case DIR_RIGHT:
            velocity_ += WALK_ACCELERATION;
            break;
    }

    x_ += velocity_;
    world.resolveCollision(volume_, x_, y_, velocity_);

    Sprite * sprite = &spriteStand_;
    if (velocity_ < 0) sprite = &spriteWalkLeft_;
    else if (velocity_ > 0) sprite = &spriteWalkRight_;
    graphics.draw(*sprite, x_, y_);
}
View Code

    14.5.2 分割域

class InputComponent {
public:
    void update(Bjorn & bjorn) {
        switch(Controller::getJoystickDirection()) {
            case DIR_LEFT:
                bjorn.velocity -= WALK_ACCELERATION;
                break;
            case DIR_RIGHT:
                bjorn.velocity += WALK_ACCELERATION;
                break;
        }
    }
private:
    static const int WALK_ACCELERATION = 1;
};

class Bjorn {
public:
    int velocity;
    int x, y;
    
    void update(World & world, Graphics & graphics) {
        input_.update(*this);
        
        x += velocity;
        world.resolveCollision(volume_, x, y, velocity);
        
        Sprite * sprite = &spriteStand_;
        
        if (velocity < 0) {
            sprite = &spriteWalkLeft_;
        } else if (velocity > 0) {
            sprite = &spriteWalkRight_;
        }
        
        graphics.draw(*sprite, x, y);
    }

private:
    InputComponent input_;
    Volume volume_;
    
    Sprite spriteStand_;
    Sprite sptireWalkLeft_;
    Sprite spriteWalkRight_;
};
View Code

    14.5.3 分割其他部分

class PhysicsComponent {
public:
    void update(Bjorn & bjorn, World & world) {
        bjorn.x += bjorn.velocity;
        world.resolveCollision(volume_, bjorn.x, bjorn.y, bjorn.velocity);
    }

private:
    Volume volume_;
};

class GraphicsComponent {
public:
    void update(Bjorn & bjorn, Graphics & graphics) {
        Sprite * sprite = &spriteStand_;
        if (bjorn.velocity < 0) {
            sprite = &spriteWalkLeft_;
        } else if (bjorn.velocity > 0) {
            sprite = &spriteWalkRight_;
        }
        
        graphics.draw(*sprite, bjorn.x, bjorn.y);
    }

private:
    Sprite spriteStand_;
    Sprite spriteWalkLeft_;
    Sprite spriteWalkRight_;
};

class Bjorn {
public:
    int velocity;
    int x, y;

    void update(World & world, Graphics & graphics) {
        input_.update(*this);
        physics_.update(*this);
        graphics_.update(*this);
    }

private:
    InputComponent input_;
    PhysicsComponent inpiut_;
    GraphicsComponent input_;
};
View Code

如今Bjorn類基本只作兩件事:持有一些真正定義了Bjorn的組件,並持有這些域所共享的那些狀態量.位置和速度的信息之因此還保留在Bjorn類中主要有兩個緣由

首先他們是"泛域"(pan-domain)狀態,幾乎全部的組件都會使用它們,因此若是將它們放到組件中是不明智的

第二點也是最重要的一點就是,將位置和速度這兩個狀態信息保留在Bjorn類中使得咱們可以輕鬆地在組件之間傳遞信息而不須要耦合它們.

    14.5.4 重構Bjorn

到目前爲止,咱們已經將行爲封裝到單獨的組件類中,可是咱們沒有將這些行爲從核心類中抽象化.Bjorn仍然精確地知道行爲是在哪一個類中被定義的.

咱們將處理用戶輸入的組件隱藏到一個接口下,這樣就可以將輸入組件變成一個抽象的基類

class InputComponent {
public:
    virtual void update(Bjorn & bjorn) = 0;
    virtual ~InputComponent() {}
};

class PlayerInputComponent: public InputComponent {
public:
    virtual void update(Bjorn & bjorn) {
        switch (Controller::getJoystickDirection()) {
            case DIR_LEFT:
                bjorn.velocity -= WALK_ACCELERATION;
                break;
            case DIR_RIGHT:
                bjorn.velocity += WALK_ACCELERATION;
                break;
        }
    }

private:
    static const int WALK_ACCELERATION = 1;
};

class Bjorn {
public:
    int velocity;
    int x, y;
    
    Bjorn(InputComponent * input): input_(input) {}

    void update(World & world, Graphics & graphics) {
        input_->update(*this);
        physics_.update(*this, world);
        graphics_.update(*this, graphics);
    }

private:
    InputComponent * input_;
    PhysicsComponent physics_;
    GraphicsComponent graphics_;
};

Bjorn * bjorn = new Bjorn(new PlayerInputComponent());

class DemoInputComponent: public InputComponent {
public:
    virtual void update(Bjorn & bjorn) {
        // AI to automatically control Bjorn...
    }
};
View Code

如今,僅僅只是交換了一個組件,咱們就獲得了一個功能完備的徹底由電腦控制的演示模式.咱們可以重用Bjorn的全部其餘代碼,包括物理以及圖形,甚至不須要了解這二者之間有什麼區別

    14.5.5 刪掉Bjorn

如今讓咱們看看Bjorn類,你會發現基本上沒有Bjorn獨有的代碼,它更像是個組件包.事實上,它是一個可以用到遊戲中全部對象身上的遊戲基本類的最佳候選

class PhysicsComponent {
public:
    virtual void update(GameOjbect & object, World & world) = 0;
    virtual ~PhysicsComponent() {}
};

class GraphicsComponent {
public:
    virtual void update(GameObject & object, World & world) = 0;
    virtual ~GraphicsComponent() {}
};

class GameObject {
public:
    int velocity;
    int x, y;

    GameObject(InputComponent * input, PhysicsComponent * physics, GraphicsComponent * graphics): input_(input), physics_(physics), graphics_(graphics) {}

    void update(World & world, Graphics & graphics) {
        input_->update(*this);
        physics_->update(*this, world);
        graphics_->update(*this, graphics);
    }

private:
    InputComponent * input_;
    PhysicsComponent * physics_;
    GraphicsComponet * graphics_;
};



class BjornPhysicsComponent: public PhysicsComponent {
public:
    virtual void update(GameObject & obj, World &  world) {
        // Physics code...
    }
};

class BjornGraphicsComponent: public GraphicsComponent {
public:
    virtual void update(GameObject & object, Graphics & graphics) {
        // Graphics code...
    }
};

GameObject * createBjorn() {
    return new GameObject(new PlayerInputComponent(), new BjornPhysicsComponent(), new BjornGraphicsComponent());
}
View Code

  14.6 設計決策

關於這個設計模式的最重要的問題是:你須要的組件集合是什麼?答案取決於你的遊戲需求與風格.引擎越大越複雜,你就越想要將組件切分得更細

    14.6.1 對象如何得到組件

若是這個類建立了本身的組件

  它確保了這個類必定有它所須要的組件

  可是這麼作將致使從新配置這個類變得困難

若是由外部代碼提供組件

  對象將變得靈活.咱們徹底能夠經過添加不一樣的組件來改變類的行爲

  對象能夠從具體的組件類型中解耦出來

    14.6.2 組件之間如何傳遞信息

完美地將組件互相解耦而且保證功能隔離是個很好的想法,但這一般是不現實的.這些組件同屬於一個對象的事實暗示了它們都是總體的一部分所以須要相互協做----亦即通訊

因此組件之間又是如何傳遞信息的呢?有好幾個選擇

  經過修改容器對象的狀態

    它使得組件間保持解耦

    它要求組件間任何須要共享的數據都由容器對象進行共享

    這使得信息傳遞變得隱祕,同時對組件執行的順序產生依賴

  直接互相引用

class BjornGraphicsComponent {
public:
    BjornGraphicsComponent(BjornPhysicsComponent * physics): physics_(physics) {}

    void Update(GameObject & obj, Graphics & graphics) {
        Sprite * sprite;
        if (!physics_->isOnGround()) {
            sprite = &spriteJump_;
        } else {
            // Existing graphics code...
        }
        graphics.draw(*sprite, obj.x, obj.y);
    }
private:
    BjornPhysicsComponent * physics_;
    
    Sprite spriteStand_;
    Sprite spriteWalkLeft_;
    Sprite spriteWalkRight_;
    Sprite spriteJump_;
};
View Code

    這簡單且快捷

    組件之間緊密耦合.缺點就是會變得至關混亂.

  經過傳遞信息的方式

    這是選項中最複雜的一個.咱們能夠在容器類中創建一個小的消息傳遞系統,讓須要傳遞信息的組件經過廣播的方式去創建組件間的聯繫.GoF稱之爲中介模式,兩個或者兩個以上的對象經過將信息傳遞到一箇中介的方法來取得相互之間的聯繫.而本章節中,容器類則充當了中間的角色

class Component {
public:
    virtual void receive(int message) = 0;
    virtual ~Component() {}
};

class ContainerObject {
public:
    void send(int message) {
        for (int i =0; i < MAX_COMPONENTS; i++) {
            if (components_[i] != NULL) {
                components_[i]->receive(message);
            }
        }
    }

private:
    static const int MAX_COMPONENTS = 10;
    Component * components_[MAX_COMPONENTS];
};
View Code

    兄弟組件之間是解耦的

    容器對象十分簡單

意料以外的是,沒有哪一個選擇是最好的.你最終有可能將上述所說的三種方法都使用到

  14.7 參考

Unity框架的核心GameObject類徹底圍繞組件來設計

開源引擎Delta3D有一個GameActor基類,該基類使用一個名叫ActorComponent的基類實現了組件模式

微軟的XNA遊戲框架附帶了一個核心遊戲類.它擁有一系列遊戲組件對象.本文中的舉例是在單個遊戲層面上使用組件,而XNA則實現了主要遊戲對象的設計模式,可是本質是同樣的

這種設計模式與GoF中的策略模式很相似.都是將對象的行爲委託給一個獨立的從對象.不一樣的是策略模式的"策略"對象一般都是無狀態的,它封裝了一個算法,可是沒有數據.它定義了一個對象的行爲方式,而不是對象自己

組件自己具備必定的功能性.它們常常會持有描述對象以及定義對象實際標識的狀態.然而,這個界限可能有點模糊.你可能有一些不須要任何狀態的組件.在這種狀況下,你能夠在跨多個容器對象的狀況下使用相同的組件實例.在這一點上,它的確表現得像是一個策略對象

第15章 事件隊列

對消息或事件的發送與受理進行時間上的解耦

  15.1 動機

事件驅動式編程

    15.1.1 用戶圖形界面的事件循環

    15.1.2 中心事件總線

    15.1.3 說些什麼好呢

class Audio {
public:
    static void playSound(SoundId id, int volume);
};

void Audio::playSound(SoundId id, int volume) {
    ResourceId resource = loadSound(id);
    int channel = findOpenChannel();
    if (channel == -1) return;
    startSound(resource, channel, volume);
}

class Menu {
public:
    void onSelect(int index) {
        Audio::playSound(SOUND_BLOOP, VOL_MAX);
        // Other sutff...
    }
};
View Code

問題1: 在音效引擎徹底處理完播放請求前,API的調用一直阻塞着調用者

問題2: 不能批量地處理請求

問題3: 請求在錯誤的線程被處理

  15.2 事件隊列模式

事件隊列是一個按照先進先出順序存儲一系列通知或請求的隊列.發出通知時系統會將該請求置入隊列並隨即返回,請求處理器隨後從事件隊列中獲取並處理這些請求.請求可由處理器直接處理或轉交給對其感興趣的模塊.這一模式對消息的發送者與受理者進行了解耦,使消息的處理變得動態且非實時

  15.3 使用情境

若是你只想對一條消息的發送者和接收者進行解耦,那麼諸如觀察者模式和命令模式都能以更低的複雜度知足你.須要在某個問題上對時間進行解耦時,一個隊列每每足矣

按照推送和拉取的方式思考:代碼A但願另外一個代碼塊B作一些事情.A發起這一請求最天然的方式就是將它推送給B

同時,B在其自身的循環中適時地拉取該請求並進行處理也是十分天然的.當你具有推送端和拉取端以後,在二者之間須要一個緩衝.這正是緩衝隊列比簡單的解耦模式多出來的優點

隊列提供給拉取請求的代碼塊一些控制權:接收者能夠延遲處理,聚合請求或者徹底廢棄它們.但這是經過"剝奪"發送者對隊列的控制來實現的.全部的發送端能作的就是往隊列裏投遞消息.這使得隊列在發送端須要實時反饋時顯得很不適用

  15.4 使用須知

不像本書中其餘更簡單的模式,事件隊列會更復雜一些而且對你的遊戲框架產生普遍而深遠的影響.這意味着你在決定如何使用,是否適用本模式時須三思

    15.4.1 中心事件隊列是個全局變量

該模式的一種廣泛用法被稱爲"中央樞紐站",遊戲中全部模塊的消息均可以經過它來傳遞.它是遊戲中強大的基礎設施,然而強大並不總意味着好用

關於"全局變量是糟糕的"這點,大多數人在走過很多彎路後才恍然大悟.當你有一些系統的任何部分都能訪問的狀態時,各類細小部分不知不覺地產生了相互依賴.本模式將這些狀態封裝成爲一種不錯的小協議,但讓然是全局性的,故仍具備任何全局變量所包含的危險性da

    15.4.2 遊戲世界的狀態任你掌控

當你接收到一個事件,你要十分謹慎,不可認爲當前世界的狀態反映的是消息發出時世界的狀態.這就意味着隊列事件視圖比同步系統中的事件具備更重量級的數據結構.後者只需通知"某事發生了"而後接收者能夠檢查系統環境來深刻細節,而適用隊列時,這些細節必須在事件發生時被記錄以便稍後處理消息時適用

    15.4.3 你會在反饋系統循環中繞圈子

任何一個事件或消息系統都得留意循環

  1. A發送了一個事件

  2. B接收它,以後發送一個響應事件

  3. 這個響應事件恰巧是A關心的,因此接收它.做爲反饋A也會發送一個響應事件...

  4. 回到2

當你的消息系統是同步的,你很塊就能發現死循環----它們會致使棧溢出並形成遊戲崩潰.對於隊列來講,異步的放開棧處理會適這些僞事件在系統中來回徘徊,但遊戲可能會保持運行.一個經常使用的規避法則是避免在處理事件末端代碼中發送事件

  15.5 示例代碼

struct PlayMessage {
    SoundId id;
    int volume;
};

class Audio {
public:
    static void init() { numPending_ = 0; }
    // Other stuff...

private:
    static const int MAX_PENDING = 16;
    static PlayMessage pending_[MAX_PENDING];
    static int numPending_;
};

void Audio::playSound(SoundId id, int volume) {
    assert(numPending_ < MAX_PENDING);
    pending_[numPending_].id = id;
    pending_[numPending_].volume = volume;
    numPending_++;
}

class Audio {
public:
    static void update() {
        for (int i = 0; i < numPending_; i++) {
            ResourceId resource = loadSound(pending_[i].id);
            int channel = findOpenChannel();
            if (channel == -1) return;
            startSound(resource, channel, pending_[i].volume);
        }
        numPending_ = 0;
    }

    // Other stuff...
};
View Code

    15.5.1 環狀緩衝區

class Audio {
pubic:
    static void init() {
        head_ = 0;
        tail_ = 0;
    }

    // Methods...
private:
    static int head_;
    static int tail_;
    // Array...
};

void Audio::playSound(SoundId id, int volume) {
    assert((tail_ + 1) % MAX_PENDING != head_);
    
    // Add to the end of the list
    pending_[tail_].id = id;
    pending_[tail_].volume = volume;
    tail_ = (tail_ + 1) % MAX_PENDING;
}

void Audio::update() {
    // If there are no pending requests, do nothing
    if (head_ == tail_) return;

    ResourceId resource = loadSound(pending_[head_].id);
    int channel = findOpenChannel();
    if (channel == -1) return;
    startSound(resource, channel, pending_[head_].volume);
    head_ = (head_ + 1) % MAX_PENDING;
}
View Code

    15.5.2 彙總請求

    15.5.3 跨越線程

  15.6 設計決策

許多遊戲將事件隊列做爲通信架構的一個關鍵部分,你能夠花大量的時間來設計各類複雜的路由和消息過濾機制.但在你準備創建相似於洛杉磯電話交換機系統那樣的東西以前,我建議你開始要簡單點.下面是入門時要考慮的一些問題

    15.6.1 入隊的是什麼

迄今爲止,"事件"和"消息"老是被我替換着使用,由於這無傷大雅.不管你往隊列裏塞什麼,它都具有相同的解耦與聚合能力,但兩者仍然有一些概念上的不一樣

若是隊列中是事件

  一個"事件"或"通知"描述已經發生的事情,好比"怪物死亡".你將它入隊,因此其餘對象能夠響應事件,有幾分像一個異步的觀察者模式

    你可能會容許多個監聽器.因爲隊列包含的事件已經發生.所以發送者不關心誰會接收到它.從這個角度來看,這個事件已通過去而且已經被忘記了

    可訪問隊列的域每每更廣.事件隊列常常用於給任何和全部感興趣的部分廣播事件.爲了容許感興趣的部分有更大的靈活性,這些隊列每每有更多的全局可見性

若是隊列中是消息

  一個"消息"或"請求"描述一種"咱們指望"發生在"未來"的行爲,相似於"播放音樂".你能夠認爲這是一個異步API服務

    你更可能只有單一的監聽器.示例中,隊列中的消息專門向音頻API請求播放聲音.若是遊戲的其餘任何部分開始從隊列中偷竊消息,那並不會起到好的做用

    15.6.2 誰能從隊列中讀取

單播隊列  當一個隊列是一個類的API自己的一部分時,單播再合適不過了.相似咱們的聲音示例,站在調用者的角度,它們能調用的只是一個"playSound()"方法

  隊列成爲讀取者的實現細節.

  隊列被更多地封裝

  你沒必要擔憂多個監聽器競爭的狀況

廣播隊列  這是大多數"事件"系統所作的事情.當一個事件進來時,若是你有十個監聽器,則它們都能看見該事件

  事件能夠被刪除

  可能須要過濾事件

工做隊列  相似於一個廣播隊列,此時你也有多個監聽器.不一樣的是隊列中的每一項只會被投遞到一個監聽器中.這是一種對於併發線程支持很差的系統中常見的工做分配模式

  你必須作好規劃

    15.6.3 誰能夠寫入隊列

一個寫入者  這種風格尤爲相似於同步式觀察者模式.你擁有一個能夠生成事件的特權對象,以供其餘模塊接收

  你隱式地知道事件的來源

  一般容許多個讀取者.你能夠創造一對一接收者的隊列,可是,這樣不太像通訊系統,而更像是一個普通的隊列數據結構

多個寫入者  這是咱們的音頻引擎例子的工做原理.由於"playSound()"函數是一個公共方法,因此任何代碼庫部分均可覺得隊列添加一個請求,"全局"或"中央"事件總線工做原理相似

  你必須當心反饋循環

  你可能會想要一些發送方在事件自己的引用

    15.6.4 隊列中對象的生命週期是什麼

轉移全部權  這是手動管理內存時的一種傳統方法.當一個消息排隊時,隊列聲明它,發送者再也不擁有它.當消息處理時,接收者取走全部權並負責釋放它

共享全部權  當前,雖然C++程序員能更舒服地進行垃圾回收了,但分享全部權會容易接受.這樣一來,只要任何事情對它有一個引用,消息就依然存在.當被忘記時它就會自動釋放

隊列擁有它  另外一個觀點是消息老是存在於隊列中.不用本身釋放消息,發送者會從隊列中請求一個新的消息.隊列返回一個已經存在於隊列內存中的消息引用,接着發送者會填充隊列.消息處理時,接收者參考隊列中相同消息的操做.

  15.7 參考

我已經提到事件隊列許屢次了,但在不少方面,這個模式能夠當作是咱們所熟知的觀察者模式的異步版本

和不少模式同樣,事件隊列有過一些其餘別名.其中一個概念叫作"消息隊列",它一般是指一個更高層面的概念.當事件隊列應用於應用程序內部時,消息隊列一般用於消息之間的通訊

另外一個術語是"發佈/訂閱",有時縮寫爲"訂閱".相似於"消息隊列",它一般在大型分佈式系統中被說起,而不專用於像咱們例子這阿姨那個簡陋的編碼模式中

一個有限狀態機,相似於GoF的狀態模式,須要一個輸入流.若是你想要異步地響應它們,把它們入列就好.當你有一堆狀態機互相發送消息的時候,每一個狀態機都一個小的隊列等待輸入(稱爲郵箱),因而你就從新發明出了計算角色模型

Go編程語言內置的"通道"類型,本質上就是一個事件隊列或者消息隊列

第16章 服務定位器

爲某服務提供一個全局訪問入口來避免使用者與該服務具體實現類之間產生耦合

  16.1 動機

在遊戲編程中,某些對象或者系統幾乎出如今程序的每一個角落.在某些時刻,你很難找到一個不須要內存分配,日誌記錄或者隨機數生成的遊戲.咱們一般認爲相似這樣的系統是在整個遊戲中須要被隨時訪問的服務

// Use a static class?
AudioSystem::playSound(VERY_LOUD_BANG);

// Or maybe a singleton?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);
View Code

儘管咱們實現了想要的目的,但整個過程卻帶來了不少耦合.遊戲中每一處調用音頻系統的地方,都直接引用了具體的AudioSystem類和訪問AudioSystem類的機制----使用靜態類或者單例

這些調用音頻系統的地方,的確須要耦合到某些東西上以便播放聲音,但直接耦合到音頻具體實現類上就好像讓一百個陌生人知道你家的地址,而僅僅是由於須要它們投遞信件.這不只是隱私問題,並且當你搬家時必須告訴每一個人你的新地址,這實在是太痛苦了

這裏有個更好的解決辦法:電話簿.每個想要聯繫咱們的人可以經過查找名字來獲得咱們當前的地址.當咱們搬家時,咱們告訴電話公司,它們更新電話簿,這樣每一個人都能獲得新的地址了.實際上,咱們甚至沒必要給出咱們真正的地址.咱們可以列出一個郵政信箱,或者其餘可以"表明"咱們的東西.經過讓訪問者查詢電話簿來找到咱們,咱們便有了一個方便的能夠控制如何查找咱們的地方

這就是服務定位器模式的簡單介紹----它將一個服務的"是什麼(具體實現類型)"和"在什麼地方(咱們如何獲得它的實例)"與須要使用整個服務的代碼解耦了

  16.2 服務定位器模式

一個服務類爲一系列操做定義了一個抽象的接口.一個具體的服務提供器實現了這個接口.一個單獨的服務定位器經過查找一個合適的提供器來提供這個服務的訪問,它同時屏蔽了提供器的具體類型和定位這個服務的過程.

  16.3 使用情境

每當你將東西變得全局都能訪問的時候,你就是在自找麻煩.這就是單例模式存在的主要問題,而這個模式存在的問題也沒有什麼不一樣.對於什麼時候使用服務定位器,個人簡單建議就是: 謹慎使用

與其給須要使用的地方提供一個全局機制來訪問一個對象,不如首先考慮將這個對象傳遞進去.這極其簡單易用,並且將耦合變得直觀.這能夠知足絕大部分需求

可是,有時手動地將一個對象傳來傳去顯得毫無理由或者使得代碼難以閱讀.有些系統,好比日誌系統或內存管理系統,不該該是某個模塊公開API的一部分.渲染代碼的參數應該必須和渲染相關,而不是像日誌系統那樣的東西

一樣地,它也適用於一些相似功能的單一系統.你的遊戲可能只有一個音頻設備或者顯示系統讓玩家與之打交道.傳遞的參數是一項環境屬性,因此將它傳遞10層函數以便讓一個底層的函數可以訪問,爲代碼增長了毫無心義的複雜度

在這些狀況下,這個模式可以起到做用.它用起來像一個更靈活,更可配置的單例模式.當被合理地使用時,它可以讓你的代碼更有彈性,並且幾乎沒有運行時的損失.

  16.4 使用須知

服務定位器的關鍵困難在於,它要有所依賴(鏈接兩份代碼),而且在運行時才鏈接起來.這給與了你彈性,但付出的代價就是閱讀代碼時比較難以理解依賴的是什麼.

    16.4.1 服務必須被定位

當使用單例或者一個靜態類時,咱們須要的實例不可能變得不可用.可是,既然這個模式須要定位服務,那麼咱們可能須要處理定位失敗的狀況

    16.4.2 服務不知道被誰定位

既然定位器是全局可訪問的,那麼遊戲中的任何代碼都有可能請求一個服務而後操做它.這意味着這個服務在任何狀況下都必須能正確工做.

  16.5 示例代碼

    16.5.1 服務

class Audio {
public:
    virtual void playSound(int soundID) = 0;
    virtual void stopSound(int soundID) = 0;
    virtual void stopAllSounds() = 0;
    virtual ~Audio() {}
};
View Code

    16.5.2 服務提供器

class ConsoleAudio: public Audio {
public:
    virtual void playSound(int soundID) {
        // Play sound using console audio api...
    }

    virtual void stopSound(int soundID) {
        // Stop sound using console audio api...
    }

    virtual void stopAllSounds() {
        // Stop all sounds using console audio api...
    }
};
View Code

    16.5.3 簡單的定位器

class Locator {
public:
    static Audio * getAudio() { return service_; }
    
    static void provide(Audio * service) {
        service_ = service;
    }

private:
    static Audio * service_;
};

ConsoleAudio * audio = new ConsoleAudio();
Locator::provide(audio);

Audio * audio = Locator::getAudio();
audio->playSound(VERY_LOUD_BANG);
View Code

靜態函數getAudio()負責定位工做.咱們能在代碼的任何地方調用它,它能返回一個Audio服務的實例提供咱們使用

它"定位"的方法十分簡單----在使用這個服務以前它依賴一些外部代碼來註冊一個服務提供器.

這裏使用的技術叫作依賴注入,這個術語表示了一個基本的思想.假設你有一個類,依賴另一個類.在咱們的例子中,咱們的Locator類須要Audio服務的一個實例.一般,這個定位器應該負責爲本身構建這個實例.依賴注入卻說外部代碼應該負責爲這個對象注入它所須要的這個依賴實例

這裏關鍵須要注意的地方是調用playSound()的代碼對ConsoleAudio具體實現絕不知情.它只知道Audio的抽象接口,一樣重要的是,甚至是定位器自己和具體服務提供器也沒有耦合.代碼中惟一知道具體實現類的地方,是提供這個服務的初始化代碼

這裏還有更深一層的解耦----經過服務定位器,Audio接口在絕大多數地方並不知道本身正在被訪問.一旦它知道了,它就是一個普通的抽象基類了.這十分有用,由於這意味着咱們能夠將這個模式應用到一些已經存在的但並非圍繞這個來設計的類上.這和單例有個對比,後者影響了"服務"類自己的設計

    16.5.4 空服務

「時序解耦」----兩份單獨的代碼必須按正確的順序調用來保證程序正確工做.每一個狀態軟件都有不一樣程度的"時序耦合",可是就像其餘耦合那樣,消除時序耦合會使得代碼易於管理

class NullAudio: public Audio {
public:
    virtual void playSound(int soundID);
    virtual void stopSound(int soundID);
    virtual void stopAllSounds();
};

class Locator {
public:
    static void initialize() {
        service_ = &nullService_;
    }

    static Audio & getAudio() { return *service_; }

    static void provide(Audio * service) {
        // Revert to null service.
        if (service == NULL) service = &nullService_;
        
        service_ = service;
    }

private:
    static Audio * service_;
    static NullAudio nullService_;
};
View Code

    16.5.5 日誌裝飾器

裝飾器模式

class LoggedAudio: public Audio {
public:
    LoggedAudio(Audio & wrapped): wrapped_(wrapped) {}

    virtual void playSound(int soundID) {
        log("play sound");
        wrapped_.playSound(soundID);
    }

    virtual void stopSound(int soundID) {
        log("stop sound");
        wrapped_.stopSound(soundID);
    }

    virtual void stopAllSounds() {
        log("stop all sounds");
        wrapped_.stopAllSounds();
    }
    
private:
    void log(const char * message) {
        // Code to log message...
    }

    Audio & wrapped_;
};

void enableAudioLogging() {
    // Decorate the existing service.
    Audio * service = new LoggedAudio(Locator::getAudio());

    // Swap it in.
    Locator::provide(service);
}
View Code

  16.6 設計決策

    16.6.1 服務是如何被定位的

外部代碼註冊

  它簡單快捷

  咱們控制提供器如何被構建

  咱們能夠在遊戲運行的時候更換服務提供器

  定位器依賴外部代碼

在編譯時綁定

class Locator {
public:
    static Audio & getAudio() { return service_; }

private:
    #if DEBUG
        static DebugAudio service_;
    #else 
        static ReleaseAudio service_;
    #endif
};
View Code

  它十分快速

  你能保證服務可用

  你不能方便地更改服務提供器

在運行時配置

  咱們不需重編譯就能切換服務提供器

  非程序員可以更換服務提供器

  一份代碼庫可以同時支持多份配置

  不像前幾個解決方案,這方案比較複雜且十分重量級

  定位服務須要時間

    16.6.2 當服務不能被定位時發生了什麼

讓使用者處理

  它讓使用者決定如何處理查找失敗

  服務使用者必須處理查找失敗

終止遊戲

  使用者不須要處理一個丟失的服務

  若是服務沒有被找到,遊戲將會中斷

返回一個空服務

  使用者不須要處理丟失的服務

  當服務不可用時,遊戲還能繼續

    16.6.3 服務的做用域多大

若是是全局訪問

  它鼓勵整個代碼庫使用同一個服務

  咱們對什麼時候何地使用服務徹底失去了控制

若是訪問被限制到類中

  咱們控制了耦合.

  它可能致使重複的工做

 

個人通常原則是,若是服務被限制在遊戲的一個單獨域中,那麼就把服務的做用域限制到類中.好比,獲取網絡訪問的服務就可能被限制在聯網的類中.而更普遍使用的服務,好比日誌服務應該是全局的

  16.7 其餘參考

服務定位器模式在不少方面和單例模式很是相近,因此值得考慮二者來決定哪個更適合你的需求

Unity框架把這個模式和組件模式結合起來,並使用在了GetComponent()方法中

Microsoft的XNA遊戲開發框架將這個模式內嵌到它的核心Game類中.每一個實例有一個GameService對象,可以用來註冊和定位任何類型的服務

第17章 數據局部性

經過合理組織數據利用CPU的緩存機制來加快內存訪問速度

  17.1 動機

RAM的存取速度遠遠跟不上CPU的速度

    17.1.1 數據倉庫

對剛訪問數據的鄰近數據進行訪問的術語叫作訪問局部性(locality of reference)

    17.1.2 CPU的托盤

當代計算機有多級緩存,也就是你所聽到的那些"L1", "L2", "L3"等.它們的大小按照其等級遞增,但速度卻隨等級遞減

    17.1.3 等下,數據即性能

  17.2 數據局部性模式

當代CPU帶有多級緩存以提升內存訪問速度.這一機制加快了對最近訪問過的數據的鄰近內存的訪問速度.經過增長數據局部性並利用這一點能夠提升性能----保持數據位於連續的內存中以提供程序進行處理

  17.3 使用情境

若是多數優化措施,知道咱們使用數據局部性模式的第一條準則就是找到出現性能問題的地方.不要在那些代碼庫裏非頻繁執行的部分浪費時間,它們不須要本模式.對那些非必要的代碼進行優化將使你的人生變得艱難----由於結果老是更加複雜且笨拙.因爲此模式的特殊性,所以你可能還但願肯定你的性能問題是不是由緩存未命中引發的,若是不是,那麼這個模式也幫不上忙

Cachegrind

  17.4 使用須知

爲了作到緩存友好,你可能須要犧牲一些以前所作的抽象化.你越是在程序的數據局部性上下工夫,你就越要犧牲繼承,接口以及這些手段所帶來的好處.這裏並無高招,只有利弊權衡的挑戰.而樂趣便在這裏

  17.5 示例代碼

    17.5.1 連續的數組

class GameEntity {
public:
    GameEntity(AIComponent * ai, PhysicsComponent * physics, RenderComponent * render): ai_(ai), physics_(physics), render_(render) {}
    
    AIComponent * ai() { return ai_; }
    PhysicsComponent * physics() { return physics_; }
    RenderComponent* render() { return render_; }

private:
    AIComponent * ai_;
    PhysicsComponent * physics_;
    RenderComponent * render_;
};

class AIComponent {
public:
    void update() {
        // Work with and modify state
    }

private:
    // Goals, mood, etc, ...
};

class PhysicsComponent {
public:
    void update() {
        // Work with and modify state...
    }

private:
    // Rigid body, velocity, mass, etc, ...
};

class RenderComponent {
public:
    void render() {
        // Work with and modify state...
    }

private:
    // Mesh, textures, shaders, etc. ...
};


while (!gameOver) {
    for (int i = 9; i < numEntities; i++) {
        entities[i]->ai()->update();
    }

    for (int i = 0; i < numEntities; i++) {
        entities[i]->physics()->update();
    }

    for (int i = 0; i < numEntities; i++) {
        entities[i]->render()->update();
    }

    // Other game loop machinery for timing...
}



while (!gameOver) {
    // Process AI
    for (int i = 0; i < numEntities; i++) {
        aiComponents[i].update();
    }

    // Update Physics
    for (int i = 0; i < numEntites; i++) {
        physicsComponents[i].update();
    }

    // Draw to screen
    for (int i = 0; i < numEntities; i++) {
        renderComponents[i].render();
    }

    // Other game loop machinery for timing...
}
View Code

    17.5.2 包裝數據

class Particle {
public:
    void update() { /* Gravity, etc. ... */ }
    // Position, velocity, etc. ...
};

class ParticleSystem {
public:
    ParticleSystem(): numParticles_(0) {}

    void update();
private:
    static const int MAX_PARTICLES = 100000;

    int numParticles_;
    Particle particles_[MAX_PARTICLES];
};

void ParticlesSystem::update() {
    for (int i = 0; i < numParticles_; i++) {
        particles_[i].update();
    }
}

for (int i = 0; i < numParticles_; i++) {
    if (particles_[i].isActive()) {
        particles_[i].update();
    }
}

void ParticleSystem::activateParticle(int index) {
    // Shouldn't already be active!
    assert(index >= numActive_);

    // Swap it with the first inactive particle right
    // after the active ones.
    Particle temp = particels_[numActive_];
    particles_[numActive_] = particles_[index];
    particles_[index] = temp;
    numActive_++;
}

void ParticleSystem::deactivateParticle(int index) {
    // Shouldn't already be inactive!
    assert(index < numActive);
    numActive_--;

    // Swap it with the last active particle right
    // before the inactive ones.
    Particle temp = particles_[numActive_];
    particles_[numActive_] = particles_[index];
    particles_[index] = temp;
}
View Code

    17.5.3 熱/冷分解

class AIComponent {
public:
    void update() { /* ... */ }
private:
    Animation * animation_;
    double energy_;
    Vector goalPos_;
};

class AIComponent {
public:
    void update() { /* ... */ }
private:
    // Previous fields ...
    LootType drop_;
    int minDrops_;
    int maxDrops_;
    double chanceOfDrop_;
};

class AIComponent {
public:
    // Methods...
private:
    Animation * animation_;
    double energy_;
    Vector goalPos_;
    
    LootDrop * loot_;
};

class LootDrop {
    friend class AIComponent;
    LootType drop_;
    int minDrops_;
    int maxDrops_;
    double chanceOfDrop_;
};
View Code

  17.6 設計決策

這種設計模式更適合叫作一種思惟模式.它提醒着你,數據的組織方式是遊戲性能的一個關鍵部分.這一塊的實際拓展空間很大,你可讓你的數據局部性影響到遊戲的整個架構,又或者它只是應用在一些核心模塊的數據結構上.對這一模式的應用,你最須要關心的就是該什麼時候何地使用它.而隨着這個問題咱們也會看到一些新的顧慮

    17.6.1 你如何處理多態

避開繼承

  安全而容易

  速度更快

  靈活性差

爲不一樣的對象類型使用相互獨立的數組

  這樣的一系列集合讓對象緊密地封包

  你能夠進行靜態地調用分發

  你必須時刻追蹤這些集合

  你必須注意每個類型

使用指針集合

  這樣作靈活性高

  這樣作並不緩存友好

    17.6.2 遊戲實體是如何定義的

假如遊戲實體經過類中的指針來索引其組件

  你能夠將組件存於相鄰的數組中

  對於給定實體,你能夠很容易地獲取它的組件

  在內存中移動組件很困難

假如遊戲實體經過一系列ID來索引其組件

  這更加複雜

  這樣作更慢

  你須要訪問組件管理器

假如遊戲實體自己就只是個ID

  你的遊戲實體類徹底消失了,取而代之的是一個優雅的數值包裝

  實體類自己是空的

  你無須管理其生命週期

  檢索一個實體的全部組件會很慢

  17.7 參考

本章節的許多內容涉及到組件模式,而組件模式中的數據結構是在優化緩存使用時幾乎最經常使用的.事實上,使用組件模式使得這一優化變得更加簡單.由於實體一次只是更新它們的一個域(AI模塊和物理模塊等),因此將這些模塊劃分爲組件使得你能夠將一系列實體合理地劃爲緩存友好的幾部分.但這並不意味着你只能選擇組件模式實現本模式!不論什麼時候你遇到涉及大量數據的性能問題,考慮數據的局部性都是很重要的

Tony Albrecht寫做的<<Pitfalls of Object-Oriented Programmi9ng>>一書被普遍閱讀,這本書介紹瞭如何經過遊戲的數據結構設計來實現緩存友好性.它使得許多人(包括我!)意識到數據結構的設計對性能有多麼地重要

與此同時,Noel Lopis就同一個話題撰寫了一篇廣爲流傳的博客

本設計模式幾乎徹底地利用了同類型對象的連續數組的優勢.隨着時間推移,你將會往這個數組中添加和移除對象.對象池模式偏偏闡釋了這一內容

Artemis遊戲引擎是首個也是最爲知名的對遊戲實體使用簡單ID的框架

第18章 髒標記模式

  18.1 動機

許多遊戲都有一個稱之爲場景圖的東西.這是一個龐大的數據結構,包含了遊戲世界中全部的物體.渲染引擎使用它來決定將物體繪製到屏幕的什麼地方

就最簡單的來講,一個場景圖只是包含多個物體的列表.每一個物體都含有一個模型(或其餘圖元)和一個"變換".變換描述了物體在世界中的位置,旋轉角度和縮放大小.想要移動或者旋轉物體,咱們能夠簡單地修改它的變換

當渲染器繪製一個物體時,它將這個物體的變換做用到這個物體的模型上,而後將它渲染出來.若是咱們有的是一個場景"袋"而不是場景"圖"的話,事情會變得簡單不少

然而,許多場景圖是分層的.場景中的一個物體會綁定在一個父物體上.在這種狀況下,它的變換就依賴於其父物體的位置,而不是遊戲世界中的一個絕對位置.

舉個例子,想象咱們的遊戲中有一艘海盜船在海上.桅杆的頂部是一個瞭望塔,一個海盜靠在這個瞭望塔上,抓在海盜肩膀上的是隻鸚鵡.這艘船的局部變換標記了它在海中的位置,瞭望塔的變換標記了它在船上的位置,等等

鸚鵡->海盜->瞭望塔->海盜船

這樣,當一個父物體移動時,它的子物體也會自動地跟着移動.若是咱們修改船的局部變換,瞭望塔,海盜,鸚鵡也會隨之變更.若是在船移動時咱們必須手動調整船上全部物體的變換來防止相對滑動,那會是一件很頭疼的事情

    18.1.1 局部變換和世界變換

計算一個物體的世界變換是至關直觀的----只要從根節點沿着它的父鏈將變換組合起來就行.也就是說鸚鵡的世界變換就是

鸚鵡世界變換 = 船的局部變換 x 瞭望塔的局部變換 x 海盜的局部變換 x 鸚鵡的局部變換

咱們每幀都須要世界中每一個物體的世界變換.因此即便每一個模型中只有少數的幾個矩陣相乘,卻也是代碼中影響性能的關鍵所在.保持它們及時更新是棘手的,由於當一個父物體移動,這會影響它本身和它全部的子物體,以及子物體的子物體等的世界變換

最簡單的途徑是在渲染的過程當中計算變換.每一幀中,咱們從頂層開始遞歸地遍歷場景圖.對每一個物體,咱們計算它們的世界變換並馬上繪製它

可是這對咱們寶貴的CPU資源是一種可怕的浪費.許多物體並非每一幀都移動.想一想關卡中那些靜止的幾何體,它們沒有移動,但每一幀都要重計算它們的世界變換是一種多麼大的浪費

    18.1.2 緩存世界變換

一個明顯的解決方法是將它"緩存"起來.在每一個物體中,咱們保存它的局部變換和它派生物體的世界變換.當咱們渲染時,咱們只使用預先計算好的世界變換.若是物體從不移動,那麼緩存的變換始終是最新的,一切都很美好

當一個物體缺失移動了,簡單的方法就是當即刷新它的世界變換.可是不要忘了繼承連!當一個父物體移動時,咱們須要重計算它的世界變換並遞歸地計算它全部子物體的世界變換

想象某些比較繁重的遊戲場景.在一個單獨幀中,船被扔進海里,瞭望塔在風中晃動,海盜斜靠在邊上,鸚鵡跳到他的頭上.咱們修改了4個局部變換.若是咱們在每一個局部變換變更時都匆忙地從新計算世界變換,結果會發生什麼

咱們只移動了4個物體,可是咱們作了10次世界變換計算.這6次無心義的計算在渲染器使用以前就被扔掉了.咱們計算了4次鸚鵡的世界變換,可是隻渲染了一次

問題的關鍵是一個世界變換可能依賴於好幾個局部變換.因爲咱們在每一個這些變換變化時都馬上重計算,因此最後當一幀內有好幾個關聯的局部變換改變時,咱們就將這個變換從新計算了好多遍

->Move Ship

  * Recalc Ship

    * Recalc Nest

      * Recalc Pirate

        * Recalc Parrot

->Move Nest

  * Recalc Nest

    * Recalc Pirate

      * Recalc Parrot

->Move Pirate

  * Recalc Pirate

    * Recalc Parrot

->Move Parrot

  * Recalc Parrot

    18.1.3 延時重算

咱們經過將修改局部變換和更新世界變換解耦來解決這個問題.這讓咱們在單次渲染中修改多個局部變換,而後在全部變更完成以後,在實際渲染器使用以前僅須要計算一次世界變換

要作到這點,咱們爲圖中每一個物體添加一個"flag"."flag"和"bit"在編程中是同義詞----它們都表示單個小單元數據,可以存儲兩種狀態中的一個.咱們稱之爲"true"和"false",有時也叫"set"和"cleared".

咱們在局部變換改動時設置它.當咱們須要這個物體的世界變換時,咱們檢查這個flag.若是它被標記爲"set"了,咱們計算這個世界變換,而後將這個flag置爲"clear".這個flag表明,"這個世界變換是否是過時了?"因爲某些緣由,傳統上這個"過時的"被稱做"髒的".也就是"髒標記","Dirty bit"也是這個模式常見的名字.可是我想我會堅持使用那種看起來每那麼"污穢"的名字

若是咱們運用這個模式,而後將咱們上個例子中的全部物體都移動,那麼遊戲看起來以下:

->Move Ship

->Move Nest

->Move Pirate

->Move Parrot

  Render

    * Recalc Ship

      * Recalc Nest

        * Recalc Pirate

          * Recalc Parrot

這是你能指望的最好的辦法.每一個被影響的物體的世界變換隻須要計算一次.只須要一個簡單的位數據,這個模式位咱們作了很多事:

  它將父鏈上物體的多個局部變換的改動分解爲每一個物體的一次重計算

  它避免了沒有移動的物體的重計算

  一個額外的好處: 若是一個物體在渲染以前移除了,那就根本不用計算它的世界變換

  18.2 髒標記模式

一組原始數據隨時間變化.一組衍生數據通過一些代價昂貴的操做由這些數據肯定.一個髒標記跟蹤這個衍生數據是否和原始數據同步.它在原始數據改變時被設置.若是它被設置了,那麼當須要衍生數據時,它們就會被從新計算而且標記被清除.不然就使用緩存的數據

  18.3 使用情境

髒位標記設計兩個關鍵詞:"計算"和"同步".在這兩種狀況下,處理原始數據到衍生數據的過程在時間或其餘方面會有很大的開銷

這裏也有些其餘的要求:

  原始數據的修改次數比衍生數據的使用次數多

  遞增地更新數據十分困難

  18.4 使用須知

    18.4.1 延時太長會有代價

這個模式把某些耗時的工做推遲到真正須要時才進行,而到有須要時,每每刻不容緩.

    18.4.2 必須保證每次狀態改動時都設置髒標記

既然衍生數據是經過原始數據計算而來,那它本質上就是一份緩存.當你獲取緩存數據時,棘手的問題是緩存失效----當緩存和原始數據不一樣步時,什麼都不正確了.在這個模式中,它意味着當任何原始數據變更時,都要設置髒標記

    18.4.3 必須在內存中保存上次的衍生數據

  18.5 示例代碼

class Transform {
public:
    static Transform origin();
    Transform combine(Transform & other);
};

class GraphNode {
public:
    GraphNode(Mesh * mesh): mesh_(mesh), local_(Transform::origin() ) {}

private:
    Transform local_;
    Mesh * mesh_;

    GraphNode * children_[MAX_CHILDREN];
    Int numChildren_;
};



GraphNode * graph_ = new GraphNode(NULL);
// Add children to root graph node...


void renderMesh(Mesh * mesh, Transform transform);
View Code

    18.5.1 未優化的遍歷

void GraphNode::render(Transform parentWorld) {
    Transform world = local_.combine(parentWorld);
    if (mesh_) renderMesh(mesh_, world);

    for (int i = 0; i < numChildren_; i++) {
        children_[i]->render(world);
    }
}

graph->render(Transform::origin());
View Code

    18.5.2 讓咱們"髒"起來

class GraphNode {
public:
    GraphNode(Mesh * mesh): mesh_(mesh), local_(Transform::origin()), dirty_(true) {}

    // Other methods...

private:
    Transform world_;
    bool dirty_;
    // Other fields...
};

void GraphNode::render(Transform parentWorld, bool dirty) {
    dirty != dirty_;
    if (dirty) {
        world_ = local_.combine(parentWorld);
        dirty_ = false;
    }

    if (mesh_) renderMesh(mesh_, world_);

    for (int i = 0; i < numChildren_; i++) {
        children_[i]->render(world_, dirty);
    }
}
View Code

  18.6 設計抉擇

    18.6.1 什麼時候清除髒標記

當須要計算結果時

  當計算結果從不使用時,它徹底避免了計算

  若是計算十分耗時,會形成明顯的卡頓

在精心設定的檢查點

  這些工做並不影響用戶體驗.

  當工做執行時,你失去了控制權

在後臺

  你能夠調整工做執行的頻率

  你能夠作更多冗餘的工做

  須要支持異步操做

    18.6.2 髒標記追蹤的粒度多大

更精細的粒度

  你只須要處理真正變更了的數據,你將船的真正變更的木塊數據發送給服務器

更粗糙的粒度

  你最終須要處理未變更的數據

  存儲髒標記消耗更少的內存

  固定開銷花費的時間要更少

  18.7 參考

這種模式在遊戲外的領域也是常見的,好比在Angular這種BS(browser-side)框架中,它利用髒標記來跟蹤瀏覽器中有變更並須要提交到服務端的數據

物理引擎跟蹤着物體的運動和空閒狀態.一個空閒的物體直到受到力的做用纔會移動,它在受力以前不須要處理.這個"是否在移動"就是一個髒標記,用來標記哪些物體收到了力的做用並須要計算它們的物理狀態

第19章 對象池

使用固定的對象池重用對象,取代單獨地分配和釋放對象,以此來達到提高性能和優化內存使用的目的

  19.1 動機

    19.1.1 碎片化的害處

    19.1.2 兩者兼顧

  19.2 對象池模式

定義一個保持着可重用對象集合的對象池類.其中的每一個對象支持對其"使用(in use)"狀態的訪問,以肯定這一對象目前是否"存活(alive)".在對象池初始化時,它預先建立整個對象的集合(一般爲一塊連續堆區域),並將它們都置爲"未使用(not in use)"狀態

當你想要建立一個新對象時就向對象池請求.它將搜索到一個可用的對象,將其初始化未"使用中(in use)"狀態並返回給你.當該對象再也不被使用時,它將被置回"未使用(not in use)"狀態.使用該方法,對象即可以在無需進行內存或其餘資源分配的狀況下進行任意的建立和銷燬

  19.3 使用情境

這一設計模式被普遍地應用於遊戲中的可見物體,如遊戲實體對象,各類視覺特效,但同時也被使用於非可見的數據結構中,如當前播放的聲音.咱們在如下狀況使用對象池:

  當你須要頻繁地建立和銷燬對象時

  對象的大小一致時

  在堆上進行對象內存分配較慢或者會產生內存碎片時

  每一個對象封裝着獲取代價昂貴且可重用的資源,如數據庫,網絡的鏈接

  19.4 使用須知

    19.4.1 對象池可能在閒置的對象上浪費內存

    19.4.2 任意時刻處於存活狀態的對象數目恆定

    19.4.3 每一個對象的內存大小是固定的

    19.4.4 重用對象不會被自動清理

    19.4.5 未使用的對象將佔用內存

  19.5 示例代碼

class Particle {
public:
    Particle(): framesLeft_(0) {}

    void init(double x, double y, double xVel, double yVel, int lifetime);
    void animate();
    bool inUse() const { return framesLeft_ > 0; }
private:
    int framesLeft_;
    double x_, y_;
    double xVel_, yVel_;
};

void Particle::init(double x, double y, double xVel, double yVel, int lifetime) {
    x_ = x;
    y_ = y;
    xVel_ = xVel;
    yVel_ = yVel;
    frameLeft_ = lifetime;
}

void Particle::animate() {
    if (!inUse()) return;
    framesLeft_;
    x_ += xVel_;
    y_ += yVel_;
}

class ParticlePool {
public:
    void create(double x, double y, double xVel, double yVel, int lifetime);
    void animate();
private:
    static const int POOL_SIZE = 100;
    Particle particles_[POOL_SIZE];
};

void ParticlePool::animate() {
    for (int i = 0; i < POOL_SIZE; i++) {
        particles_[i].animate();
    }
}

void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!particles_[i].inUse()) {
            particles_[i].init(x, y, xVel, yVel, lifetime);
            return;
        }
    }
}
View Code
class Particle {
public:
    // Previous stuff...
    Particle * getNext() const { return state_.next; }
    void setNext(Particle * next) {
        state_.next = next;
    }
private:
    int framesLeft_;
    
    union {
        // State when it's in use
        struct {
            double x, y, xVel, yVel;
        } live;
        
        // State when it's available.
        Particle * next;
    } state_;
};

class ParticlePool {
    // Previous stuff...
private:
    Particle * firstAvailable_;
};

ParticlePool::ParticlePool() {
    // The first one is available.
    firstAvailable_ = &particles_[0];
    // Each particle points to the next.
    for (int i = 0; i < POOL_SIZE - 1; i++) {
        particles_[i].setNext(&particles_[i + 1]);
    }

    // The last one terminates the list
    particles_[POOL_SIZE - 1].setNext(NULL);
}

void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime) {
    // Make sure the pool isn't full.
    assert(firstAvailable_ != NULL);

    // Remove it from the available list.
    Particle * newParticle = firstAvailable_;
    firstAvailable_ = new Particle->getNext();

    newParticle->init(x, y, xVel, yVel, lifetime);
}

bool Particle::animate() {
    if (!inUse()) return false;
    
    framesLeft_;
    x_ += xVel_;
    y_ += yVel_;

    return framesLeft_ ==0;
}

void ParticlePool::animate() {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (particles_[i].animate()) {
            // Add this particle to the front of the list.
            particles_[i].setNext(firstAvailable_);
            firstAvailable_ = &particles_[i];
        }
    }
}
View Code

  19.6 設計決策

    19.6.1 對象是否被加入對象池

假如對象與對象池耦合

  實現很簡單,你能夠簡單地爲那些池中的對象增長一個"使用中"的標誌位或者函數,這就能解決問題了

  你能夠保證對象只能經過對象池建立.在C++中,只需簡單地將對象池類做爲對象類的友元類,並將對象的構造函數私有化便可

class Particle {
    friend class ParticlePool;
private:
    Particle(): inUse_(false) {}

    bool inUse_;
};

class ParticlePool {
    Particle pool_[100];
};
View Code

  你能夠避免存儲一個"使用中"的標誌位,許多對象已經維護了能夠表示自身是否仍然存活的狀態

假如對象獨立於對象池

  任意類型的對象能夠被置入池中.這是個巨大的優勢.經過對象與對象池的解綁,你將可以實現一個通用,可重用的對象池類

  "使用中"狀態必須可以在對象外部被追蹤.最簡單的作法是在對象池中額外建立一塊獨立的空間:

template <class TObject>
class GenericPool {
private:
    static const int POOL_SIZE = 100;

    TObject pool_[POOL_SIZE];
    bool inUse_[POOL_SIZE];
};
View Code

    19.6.2 誰來初始化那些被重用的對象

假如在對象池內部初始化重用對象

  對象池能夠徹底封裝它管理的對象

  對象池與對象如何被初始化密切相關

class Particle {
public:
    // Multiple ways to initialize
    void Init(double x, double y);
    void Init(double x, double y, double angle);
    void Init(double x, double y, double xVel, double yVel);
};

class ParticlePool {
public:
    void create(double x, double y);
    void create(double x, double y, double angle);
    void create(double x, double y, double xVel, double yVel);
};
View Code

假如對象在外部被初始化

  此時對象池的接口會簡單一些

class Particle {
public:
    void init(double x, double y);
    void init(double x, double y, double angle);
    void init(double x, double y, double xVel, double yVel);
};

class ParticlePool {
public:
    Particle * create() {
        
    }

private:
    Particle pool_[100];
};

ParticlePool pool;
pool.create()->init(1, 2);
pool.create()->init(1, 2, 0.3);
pool.create()->init(1, 2, 3.3, 4.4);
View Code

  外部編碼可能須要處理新對象建立失敗的狀況

Particle * particle = pool.create();
if (particle != NULL) particle->init(1, 2);
View Code

  19.7 參考

對象池模式與享元模式看起來很類似.它們都管理着一系列可重用對象.其差別在於"重用"的含義.享元模式中的對象經過在多個持有者中併發地共享相同的實例以實現重用.它避免了因在不一樣上下文中使用相同對象而致使的重複內存使用.對象池的對象也被重用,但此"重用"意味着在原對象持有者使用完對象以後,將其內存回收.對象池裏的對象在其生命週期中不存在着由於被共享而引致的異常

將那些類型相同的對象在內存上整合,可以幫助你在遍歷這些對象時利用好CPU的緩存區.數據局部性設計模式闡釋了這一點

第20章 空間分區

  20.1 動機

將對象存儲在根據位置組織的數據結構中來高效地定位它們

    20.1.1 戰場上的部隊

假設咱們在製做一款即時策略遊戲.對立陣營的上百個單位將在戰場上相互廝殺.勇士們須要知道該攻擊他們附近的哪一個敵人,簡單的方式處理就是查看每一對單位看看他們彼此距離的遠近

void handleMelle(Unit * units[], int numUnits) {
    for (int a = 0; a < numUnits - 1; a++) {
        for (int b = a + 1; b < numUnits; b++) {
            if (units[a]->position() == units[b]->position()) {
                handleAttack(units[a], units[b]);
            }
        }
    }
}
View Code

這裏咱們用一個雙重循環,每層循環都遍歷了戰場上的全部單位.這意味着咱們每一幀對檢驗的次數隨着單位個數的平方增長.每增長一個額外的單位,都要與前面的全部單位進行比較.當單位數目很是大時,局面便會失控

    20.1.2 繪製戰線

  20.2 空間分區模式

對於一組對象而言,每個對象在空間都有一個位置.將對象存儲在一個根據對象的位置來組織的數據結構中,該數據結構可讓你高效地查詢位於或靠近某處的對象.當對象的位置變化時,應更新該空間數據結構以即可以繼續這樣查找對象

  20.3 使用情境

這是一個用來存儲活躍的,移動的對象以及靜態圖像和遊戲世界的幾何形狀等對象的常見模式.複雜的遊戲經常有多個空間分區來應對不一樣類型的存儲內容

該模式的基本要求是你有一組對象,每一個對象都具有某種位置信息,而你由於要根據位置作大量的查詢來查找對象從而遇到了性能問題

  20.4 使用須知

空間分區將O(n)或者O(n2)複雜度的操做拆解爲更易於管理的結構.對象越多,模式的價值就越大.相反,若是你的n值很小,則可能不值得使用該模式.因爲該模式要根據對象的位置來組織對象,故對象位置的改變就變得難以處理了.你必須從新組織數據結構來跟蹤物體的新位置,這會增長代碼的複雜性併產生額外的CPU週期開銷.你必須確保這麼作是值得的

空間分區會使用額外的內存來保存數據結構.就像許多的優化同樣,它是以空間換取速度的.若是你的內存比時鐘週期更吃緊的話,這多是個虧本生意

  20.5 示例代碼

    20.5.1 一張方格紙

設想一下戰場的整個區域.如今,往上鋪一張方格大小固定的網,就像蓋張方格紙那樣.咱們用這些網格中的單元格來取代一維數組以存儲單位.每一個單元格存儲那些處於其邊界以內的單位列表.咱們在處理戰鬥時,只考慮在同一個單元格內的單位.咱們不會將每一個單位與遊戲中的其餘單位一一比較,取而代之的是,咱們已經將戰場劃分爲一堆更小的小型戰鬥,每個小戰場裏的單位要少不少

    20.5.2 相連單位的網格

class Unit {
    friend class Grid;
public:
    Unit(Grid * grid, double x, double y): grid_(grid), x_(x), y_(y) {}
    void move(double x, double y);

private:
    double x_, y_;
    Grid * grid_;
};

class Grid {
public:
    Grid() {
        // Clear the grid
        for (int x = 0; x < NUM_CELLS; x++) {
            for (int y = 0; y < NUM_CELLS; y++) {
                cells_[x][y] = NULL;
            }
        }
    }

    static const int NUM_CELLS = 10;
    static const int CELL_SIZE = 20;

private:
    Unti * cells_[NUM_CELLS][NUM_CELLS];
};

class Unit {
    // Previous code...

private:
    Unit * prev_;
    Unit * next_;
};
View Code

    20.5.3 進入戰場

Unit::Unit(Grid * grid, double x, double y): grid_(grid), x_(x), y_(y), prev_(NULL), next_(NULL) {
    grid_->add(this);
}

void Grid::add(Unit * unit) {
    // Determin which grid cell it's in
    int cellX = (int)(unit->x_ / Grid::CELL_SIZE);
    int cellY = (int)(unit->y_ / Grid::CELL_SIZE);

    // Add to the front of list for the cell it's in
    unit->prev_ = NULL;
    unit->next_ = cells_[cellX][cellY];
    cells_[cellX][cellY] = unit;

    if (unit->next_ != NULL) {
        unit->next_->prev_ = unit;
    }
}
View Code

    20.5.4 刀光劍影的戰鬥

void Grid::handeMelee() {
    for (int x = 0; x < NUM_CELLS; x++) {
        for (int y = 0; y < NUM_CELLS; y++) {
            handelCell(cells_[x][y]);
        }
    }
}

void Grid::handleCell(Unit * unit) {
    while (unit != NULL) {
        Unit * other = unit->next_;
        
        while (other != NULL) {
            if (unit->x_ == other->x_ && unit->y_ == other->y) {
                handleAttack(unit, other);
            }
            other = other->next_;
        }
        
        unit = unit->next_;
    }
}
View Code

    20.5.5 衝鋒陷陣

void Unit::move(double x, double y) {
    grid_->move(this, x, y);
}

void Grid::move(Unit * unit, double x, double y) {
    // See which cell it was in.
    int oldCellX = (int)(unit->x_ / Grid::CELL_SIZE);
    int oldCellY = (int)(unit->y_ / Grid::CELL_SIZE);

    // See which cell it's moving to
    int cellX = (int)(x / Grid::CELL_SIZE);
    int cellY = (int)(y / Grid::CELL_SIZE);

    unit->x_ = x;
    unit->y_ = y;

    // If it didn't change cells, we're done.
    if (oldCellX == cellX && oldCellY == cellY) return;
    
    // Unlink it from the list of its old cell.
    if (unit->prev_ != NULL) {
        unit->prev_->next_ = unit->next_;
    }

    // If it's the head of a list, remove it
    if (cells_[oldCellX][oldCellY] == unit) {
        cells_[oldCellX][oldCellY] = unit->next_;
    }

    // Add it back to the grid at its new cell.
    add(unit);
}
View Code

    20.5.6 近在咫尺,短兵相接

if (distance(unit, other) < ATTACK_DISTANCE) {
    handleAttack(unit, other);
}

void Grid::handleUnit(Unit * unit, Unit * other) {
    while (other != NULL) {
        if (distance(unit, other) < ATTACK_DISTANCE) {
            handleAttack(unit, other);
        }

        other = other->next_;
    }
}

void Grid::handleCell(int x, int y) {
    Unit * unit = cells_[x][y];
    while (unit != NULL) {
        // Handle other units in this cell
        handelUnit(unit, unit->next_);
        unit = unit->next_;
    }
}

void Grid::handleCell(int x, int y) {
    Unit * unit = cells_[x][y];
    while (unit != NULL) {
        // Handle other units in this cell
        handleUnit(unit, unit->next_);
        
        // Also try the neighboring cells
        if (x > 0) handleUnit(unit, cells_[x - 1][y]);
        if (y > 0) handleUnit(unit, cells_[x][y - 1]);
        if (x > 0 && y > 0)
            handelUnit(unit, cells_[x - 1][y - 1]);
        if (x > 0 && y < NUM_CELLS - 1)
            handleUnit(unit, cells_[x - 1][y + 1]);

        unit = unit->next_;
    }
}
View Code

  20.6 設計決策

    20.6.1 分區是層級的仍是扁平的

在網格例子中,咱們將網格劃分紅了一個單一扁平的單元格集合.與此相反,層級空間分區則是將空間劃分紅幾個區域.而後,若是這些區域中仍然包含着許多的對象,就會繼續劃分.整個遞歸過程持續到每一個區域的對象數目都少於某個約定的最大對象數量爲止

若是它是一個扁平的分區

  相對簡單

  內存使用量恆定

  當對象改變位置時能夠更爲快速地更新

若是它是一個層級的分區

  它能夠更有效地處理空白的空間

  它在處理對象稠密區域時更爲有效

    20.6.2 分區依賴於對象集合嗎

若是分區依賴於對象

  對象能夠被逐步地添加

  對象能夠快速地移動

  分區能夠不平衡

若是分區自適應於對象集合

  你能夠確保分區間的平衡

  對整個對象集合進行一次性的分區時更爲高效

若是分區不依賴於對象,而層級卻依賴於對象

  能夠逐步地增長對象

  對象能夠快速地移動

  分區是平衡的

    20.6.3 對象只存儲在分區中嗎

若是它是對象惟一存儲的地方

  這避免了兩個集合的內存開銷和複雜性

若是存在存儲對象的另一個集合

  遍歷全部的對象會更爲快速

  20.7 參考

在這章中我避開對具體空間分區結構的詳細討論,以保持章節的高層次歸納性(而且也不會太長),可是下一步你應該要去了解一些常見的結構.儘管它們的名字嚇人,但卻出奇的簡單明瞭.常見的有

  網格[Grid (spatial_index) ]

  四叉樹

  二叉空間分割

  k-dimensioanl樹

  層次包圍盒

每個空間數據結構基本都是從一個現有已知的一維數據結構擴展到多維,瞭解它們的線性結構會幫助你判斷它們是否適合於解決你的問題:

  網格是一個連續的桶排序

  二叉空間分割,k-d樹,以及層次包圍盒都是二叉查找樹

  四叉樹和八叉樹都是Trie樹

相關文章
相關標籤/搜索