遊戲設計模式——黑板模式

黑板(Blackboard)

「黑板」(Blackboard)在人工智能領域已是一個很古老的東西了。它基於一種很直觀的概念,就是一羣人爲了解決一個問題,在黑板前彙集,
每一個人均可以發表本身的意見,而後在黑板上寫下本身的見解,固然你也能夠基於別人記錄在黑板上的見解,
來發表和更新本身的見解,在這樣不斷的意見交換,見解更新的過程當中,愈來愈趨向於對於問題的最終解答。
一開始的黑板模式就是這樣一個由多個子系統來共同協做的人工智能解決方案。git

定義

基於上面的描述,咱們能夠看到黑板有幾個功能:github

  • 記錄:每一個人能夠寫下本身的見解。
  • 更新:調整已有的見解。
  • 刪除:刪除對於過期的,或者錯誤的見解。
  • 讀取:黑板上的內容誰都能自由閱讀。

因此從本質上來講,黑板就是這樣一個共享數據的結構,它對於多個系統間通訊是頗有幫助的。
它提供一種數據傳遞的方式,有助於系統的封裝和解耦合。redis

對於各個子系統而言,只須要把本身的運算的結果數據記錄在黑板上,至於這個數據誰會去用,並不須要關心。
反過來也是同樣,對於本身的運算時須要用到的數據,能夠從黑板上去獲取,至於這個數據是誰提供的,也不須要關心。
只要這個數據在黑板上,就夠能夠認爲是合法數據,這就提供的了一種靈活性,各個子系統的設計也會相對獨立。數據庫

好處

如今遊戲中,也大量的使用黑板(或者類黑板)模式,由於遊戲系統的模塊間通訊的需求也是不少的,AI,動畫,物理,實體與實體間,等等,他們都須要彼此交換數據,我想,你們常常碰到的一個頭疼的問題就是,這個數據應該存在哪裏?存在這裏也能夠,存在那裏也能夠,或者索性作個Data類來存,因此在Player類裏,變量會愈來愈多,變量列表愈來愈長。編程

針對這種狀況黑板能夠幫助解決一部分問題,特別是對於在多模塊之間須要通訊的數據,咱們再來看一下它幾個好處:設計模式

  • 解耦合:黑板作爲獨立的數據模塊,能夠」超然」於全部的模塊以外,提供一些額外的數據維護和管理的功能,這個讓我想到了那些內存數據庫,好比redis和memcached,從某種程度上,黑板就像程序內的數據庫。
  • 共享性:黑板的數據是共享的,好比咱們要去拿一個數據,咱們不須要先拿到它的實例(還須要考慮是否爲null),而後再經過get方法去取數據,咱們只須要存一個黑板的實例,而後經過黑板獲取數據的方法來獲取。這就相似設計模式中的Facade方法,黑板提供了這樣一個facade層,使得RWD的接口保持統一。
  • 數據的維護和管理:黑板提供數據的RWD,生命期,做用域等內容,讓咱們能夠從管理數據的漩渦中解脫出來,讓專業的人作專業的事。

缺點

  • RWD(讀寫刪)操做相對隨意,特別是WD操做,容易形成數據被破壞,或者產生子系統間的競爭:
    好比,系統A和系統B都會去修改data1,那到底以誰的值爲準呢?數據結構

  • 可能會產生非法數據:
    通常認爲,只要在黑板上的數據,就是合法的數據,在讀取的時候,不須要判斷它是否合法,
    但若是一個子系統沒有很好的維護它本身產生的數據(好比,該刪除的時候沒刪除,或者賦值錯誤),
    那別人讀取該數據的系統時候,就會產生錯誤的運算結果。編輯器

額外功能

博客(指AI分享站的博客)上有一篇較早的文章就討論過這樣的問題,像黑板這樣的共享數據結構,既是黃金屋,又是垃圾堆,用好不容易,因此在黑板原有的功能中,咱們能夠加一些額外的功能:memcached

  • 數據過時時間:對於寫入黑板的數據,能夠加一個過時時間的功能,好比3秒後,該數據過時,這很實用,能夠提升數據維護的便利程度。
  • 數據做用域:咱們能夠規定能夠讀寫該數據子系統,默認狀況下,黑板的數據都是全局可見的,就像程序中的全局變量同樣,但若是咱們但願某些數據只有對個別子系統開放,就能夠經過做用域字段來指定。

一個遊戲使用黑板模式的例子

需求:咱們在遊戲中有一個技能,能夠給角色提供一種狂暴狀態,持續10秒。

遊戲中不少別的系統在計算中,須要檢查該角色是否有這樣的一個狂暴的狀態,而後作一些後續的判斷。
在這樣一個例子中,常規的作法多是,在角色上存一個變量,技能觸發的時候,置成True,而後維護一個計時器,設爲10秒,
每幀檢查這個計時器,當時間到了,就把這個值再置成False,再提供一個get方法給外部系統調用。

這樣的邏輯正確,但相對繁瑣,不夠優雅。若是咱們換用黑板模式來維護這個數據應該怎麼寫呢?就一句話:

player.GetBB().SetValue(BBKEY_FURIOUS, true).SetExpiredTime(10);

咱們先獲取了黑板的實例(GetBB),而後設置了變量爲True(SetValue),而後再設置了過時時間爲10秒(SetExpiredTime),這樣在10秒內若是訪問這個變量,會返回True,但若是過了10秒,這個變量就會返回False,而全部對於數據的管理就被完整的封裝在了黑板的實現中。

固然,黑板能夠有不少塊,像我上面的例子,我就是在角色身上建了一塊黑板,用來存儲與角色相關的數據,還能夠建一塊全局的黑板,用來存儲整個遊戲層面上的數據通訊。無論建了幾塊這樣的黑板,它的原理都是同樣的,具體如何選擇,仍是取決於實際狀況。

有人可能會說,我把變量一個一個具體定義,和存在黑板中用key-value的結構好像區別也不大,確實,用黑板確實能帶來一些好處,但好處還不夠多。

但黑板有一個另外的優點,那就是支持可視化編程和數據驅動,結合如今的引擎來看,這樣的好處真是大大的。
如今主流的引擎,都會提供一個強大的可視化的編輯器,經過一些UI上的操做,就能完成一些複雜的遊戲邏輯,像行爲樹和狀態機在遊戲行業的經久不衰,一方面是由於它的概念比較簡單和直觀,另外一方面也是由於它在可視化編程和數據驅動方面的優點。黑板在這樣的潮流中,也是一點不落後。

首先它採用的存儲方式是key-value的字典結構,很通用,能夠經過配置文件簡單定義,經過範型和反射很容易去建立,修改和讀取。其次它做爲共享數據,能夠很好的和相似行爲樹和狀態機這樣的系統協同工做。

其餘使用黑板模式的例子

行爲樹通訊

行爲樹的節點間也是存在通訊的需求的,最多見的就是序列節點:
好比咱們有一個簡單的攻擊序列節點,第一個節點是選擇目標,第二個節點是攻擊,這裏就存在一個節點間通訊的需求。

在」選擇目標」的節點裏會選擇一個攻擊目標,而後在攻擊的節點裏會對這個目標實施攻擊。因此」攻擊目標」這個數據就會在兩個節點間進行通訊,第一個節點輸出,第二個節點輸入,那這個數據應該存在哪裏呢?

存在角色身上是一個選擇,還有一個選擇,就是存在與這個行爲樹綁定的黑板上面,
在Unity的Behaivor Design這個行爲樹插件裏,這樣的變量就叫共享變量。

它的概念其實就是和黑板相似的(它在兩個節點中分別建立了一個指向這個共享變量的引用,
主要是方便編輯器操做和代碼上的訪問),在編輯器中,咱們就能夠建立這樣一個變量,
而後把它拖到第一個和第二個節點的相應變量裏。

狀態機通訊

狀態機也是同樣的,當各個狀態跳轉的時候,勢必也會帶來一些數據的通訊。
這個時候,黑板就能很好的幫助這樣的系統進行共享數據的管理。

關於狀態機的例子,你們能夠看Unity上一個狀態機的插件PlayMaker。

(Unity裏Animator狀態機的黑板模式)

小結

黑板是一個很好的共享數據系統,我很推薦你們在本身的代碼庫中加一個黑板的庫,並應用到你核心遊戲部分的實現中,這個小小的東西,會帶來很大的思惟和代碼質量的提高。若是還不是很熟悉的同窗,能夠去用用看我剛剛說到Unity的那兩個插件,這樣你就會對數據通訊,共享數據,黑板等概念更爲清楚。

黑板模式的C++簡易實現

#pragma once
#include <map>
#include <any>
#include <list>

//黑板類
class BlackBoard
{
private:
    //黑板計時器
    struct BlackBoardTimer {
        float timer;
        std::string key;
        std::any value;
    };
protected:
    std::map<std::string, std::any> mDatas;
    std::list<BlackBoardTimer> mTimers;
public:
    BlackBoard();
    ~BlackBoard();
    //設置數據
    void setValue(std::string key, bool value);
    void setValue(std::string key, bool value, float expiredTime , bool expiredValue);
    void setValue(std::string key, int value);
    void setValue(std::string key, int value, float expiredTime, int expiredValue);
    void setValue(std::string key, float value);
    void setValue(std::string key, float value, float expiredTime, float expiredValue);
    void setValue(std::string key, std::string value);
    //訪問數據
    int getInt(std::string key);
    float getFloat(std::string key);
    bool getBool(std::string key);
    std::string getString(std::string key);
    //更新時間
    void update(float dt);
};
#include "BlackBoard.h"

BlackBoard::BlackBoard()
{
}

BlackBoard::~BlackBoard()
{
}

void BlackBoard::setValue(std::string key, int value)
{
    mDatas.emplace(key, value);
}

void BlackBoard::setValue(std::string key, int value, float expiredTime, int expiredValue)
{
    setValue(key, value);
    mTimers.emplace_back(BlackBoardTimer{ expiredTime,key,expiredValue });
}

void BlackBoard::setValue(std::string key, float value)
{
    mDatas.emplace(key, value);
}

void BlackBoard::setValue(std::string key, float value, float expiredTime, float expiredValue)
{
    setValue(key, value);
    mTimers.emplace_back(BlackBoardTimer{ expiredTime,key,expiredValue });
}

void BlackBoard::setValue(std::string key, bool value)
{
    mDatas.emplace(key, value);
}

void BlackBoard::setValue(std::string key, bool value, float expiredTime, bool expiredValue)
{
    setValue(key, value);
    mTimers.emplace_back(BlackBoardTimer{ expiredTime,key,expiredValue });
}

int BlackBoard::getInt(std::string key)
{
    auto & value = mDatas.at(key);
    return std::any_cast<int>(value);
}

void BlackBoard::setValue(std::string key, std::string value)
{
    mDatas.emplace(key, value);
}

float BlackBoard::getFloat(std::string key)
{
    auto& value = mDatas.at(key);
    return std::any_cast<float>(value);
}

bool BlackBoard::getBool(std::string key)
{
    auto& value = mDatas.at(key);
    return std::any_cast<bool>(value);
}

std::string BlackBoard::getString(std::string key)
{
    auto& value = mDatas.at(key);
    return std::any_cast<std::string>(value);
}

void BlackBoard::update(float dt)
{
    auto itr = mTimers.begin();
    while(itr != mTimers.end()) {
        itr->timer -= dt;
        if (itr->timer <= 0.0f) {
            mDatas[itr->key] = itr->value;
            itr = mTimers.erase(itr);
        }
        else {
            ++itr;
        }
    }
}

黑板模式的C#實現

可參考AI分享站的C#AI工具庫:https://github.com/FinneyTang/TsiU_AIToolkit_CSharp


參考

轉載並修改自原文—AI分享站的博文:http://www.aisharing.com/archives/801
原文對黑板模式的講解很是深入易懂,所以我僅作了部分的排版整理工做就直接搬運過來做爲筆記。

遊戲設計模式系列-其餘文章:https://www.cnblogs.com/KillerAery/category/1307176.html

相關文章
相關標籤/搜索