游戏设计模式——黑板模式

黑板(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

相关文章
相关标签/搜索