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

“黑板”(Blackboard)在人工智能领域已经是一个很古老的东西了。它基于一种很直观的概念,就是一群人为了解决一个问题,在黑板前聚集,

每个人都可以发表自己的意见,然后在黑板上写下自己的看法,当然你也可以基于别人记录在黑板上的看法,

来发表和更新自己的看法,在这样不断的意见交换,看法更新的过程中,越来越趋向于对于问题的最终解答。

一开始的黑板模式就是这样一个由多个子系统来共同协作的人工智能解决方案。

定义

基于上面的描述,我们可以看到黑板有几个功能:

  • 记录:每个人可以写下自己的看法。
  • 更新:调整已有的看法。
  • 删除:删除对于过时的,或者错误的看法。
  • 读取:黑板上的内容谁都能自由阅读。

所以从本质上来说,黑板就是这样一个共享数据的结构,它对于多个系统间通信是很有帮助的。

它提供一种数据传递的方式,有助于系统的封装和解耦合。

对于各个子系统而言,只需要把自己的运算的结果数据记录在黑板上,至于这个数据谁会去用,并不需要关心。

反过来也是一样,对于自己的运算时需要用到的数据,可以从黑板上去获取,至于这个数据是谁提供的,也不需要关心。

只要这个数据在黑板上,就够可以认为是合法数据,这就提供的了一种灵活性,各个子系统的设计也会相对独立。

好处

现在游戏中,也大量的使用黑板(或者类黑板)模式,因为游戏系统的模块间通信的需求也是很多的,AI,动画,物理,实体与实体间,等等,他们都需要彼此交换数据,我想,大家经常碰到的一个头疼的问题就是,这个数据应该存在哪里?存在这里也可以,存在那里也可以,或者索性做个Data类来存,所以在Player类里,变量会越来越多,变量列表越来越长。

针对这种情况黑板可以帮助解决一部分问题,特别是对于在多模块之间需要通信的数据,我们再来看一下它几个好处:

  • 解耦合:黑板做为独立的数据模块,可以”超然”于所有的模块之外,提供一些额外的数据维护和管理的功能,这个让我想到了那些内存数据库,比如redis和memcached,从某种程度上,黑板就像程序内的数据库。
  • 共享性:黑板的数据是共享的,比如我们要去拿一个数据,我们不需要先拿到它的实例(还需要考虑是否为null),然后再通过get方法去取数据,我们只需要存一个黑板的实例,然后通过黑板获取数据的方法来获取。这就类似设计模式中的Facade方法,黑板提供了这样一个facade层,使得RWD的接口保持统一。
  • 数据的维护和管理:黑板提供数据的RWD,生命期,作用域等内容,让我们可以从管理数据的漩涡中解脱出来,让专业的人做专业的事。

缺点

  • RWD(读写删)操作相对随意,特别是WD操作,容易造成数据被破坏,或者产生子系统间的竞争:

    比如,系统A和系统B都会去修改data1,那到底以谁的值为准呢?

  • 可能会产生非法数据:

    一般认为,只要在黑板上的数据,就是合法的数据,在读取的时候,不需要判断它是否合法,

    但如果一个子系统没有很好的维护它自己产生的数据(比如,该删除的时候没删除,或者赋值错误),

    那别人读取该数据的系统时候,就会产生错误的运算结果。

额外功能

博客上有一篇较早的文章就讨论过这样的问题,像黑板这样的共享数据结构,既是黄金屋,又是垃圾堆,用好不容易,所以在黑板原有的功能中,我们可以加一些额外的功能:

  • 数据过期时间:对于写入黑板的数据,可以加一个过期时间的功能,比如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++简易实现

为了存储任意类型数据,使用了C++17的std::any

#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/p/10054558.html

时间: 2024-10-10 14:49:15

游戏设计模式——黑板模式的相关文章

游戏设计模式——状态机模式

前言:状态机模式是一个游戏常用的经典设计模式,常被用作处理一个生物的各种状态(例如行走,站立,跳跃等). 假如我们正在开发一款动作游戏,当前的任务是实现根据输入来控制主角的行为--当按下B键时,他应该跳跃. 直观的代码: if (input == PRESS_B) { if (!m_isJumping) { m_isJumping = true; Jump();//跳跃的代码 } } 后来我们需要添加更多行为了,所有行为如下: 站立时按下 ↓ 键 => 蹲下. 蹲下时按下 ↓ 键 => 站立.

【游戏设计模式】之四 《游戏编程模式》读书笔记:全书内容梗概总结

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/53240330 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文的Github版本:QianMo/Reading-Notes/<游戏编程模式>读书笔记 这是一篇超过万字读书笔记,总结了<游戏编程模式>一书中所有章节与内容的知识梗概. 我们知道,游戏行业其实一直很缺一本系

23种设计模式----------代理模式(二)

(上一篇)23种设计模式----------代理模式(一) 之前说了基本的代理模式和普通代理模式.接下来开始看下强制代理模式和虚拟代理模式 三,强制代理模式: 一般的代理模式都是通过代理类找到被代理的对象,从而调用被代理类中的方法(即完成被代理类中的任务). 而,强制代理模式则是先找到被代理类自己去完成事情,然后被代理类又将该做的事情转交到代理类中,让代理类来完成. 假如:你有事求助于某位名人. 你告诉名人说有事想请他帮忙,然后他说最近一段时间比较忙,要不你找我的经纪人来办吧. (本来找名人办事

23种设计模式----------代理模式(三) 之 动态代理模式

(上一篇)种设计模式----------代理模式(二) 当然代理模式中,用的最广泛的,用的最多的是  动态代理模式. 动态代理:就是实现阶段不用关系代理是哪个,而在运行阶段指定具体哪个代理. 抽象接口的类图如下: --图来自设计模式之禅 所以动态代理模式要有一个InvocationHandler接口 和 GamePlayerIH实现类.其中 InvocationHandler是JD提供的动态代理接口,对被代理类的方法进行代理. 代码实现如下 抽象主题类或者接口: 1 package com.ye

23种设计模式----------代理模式(一)

代理模式也叫委托模式. 代理模式定义:对其他对象提供一种代理从而控制对这个对象的访问.就是,代理类 代理 被代理类,来执行被代理类里的方法. 一般情况下,代理模式化有三个角色. 1,抽象的主题类(或者接口) IGamePlayer 2,代理类. 3,被代理类. 下面以游戏玩家代理为例. 一,先来看下最基本的代理模式. 代码如下: 主题接口: 1 package com.yemaozi.proxy.base; 2 3 //游戏玩家主题接口 4 public interface IGamePlaye

16. 星际争霸之php设计模式--组合模式

题记==============================================================================本php设计模式专辑来源于博客(jymoz.com),现在已经访问不了了,这一系列文章是我找了很久才找到完整的,感谢作者jymoz的辛苦付出哦! 本文地址:http://www.cnblogs.com/davidhhuan/p/4248201.html============================================

设计模式-Decorator模式

目录 一个例子(贪玩蓝月) 传统继承实现 装饰器模式实现 对比 总结 Decorator(装饰器)模式属于结构型模式. 比如当其需要三种不同的附加特性,可以为其创建三个派生类.但是若它还需要同时具有其中两种特性或者是各种特性的任意组合的时候,类继承的方法就不再适合了. 它允许向一个现有的对象不通过继承来添加新的功能,同时又不改变其结构. 一个例子(贪玩蓝月) 前一阵子张家辉代言的<贪玩蓝月>广告火了,"我系喳喳辉,是兄弟就来砍我-"被洗脑到现在,正好用这个游戏来解释一下装饰

游戏设计模式——面向数据编程(新)

目录 面向数据编程是什么? 单指令流多数据流(SIMD) 什么是SIMD 为什么需要SIMD 支持SIMD技术的指令集 使用SIMD编程 使用汇编内联 使用指令集库 使用ISPC语言 并行循环 避免Gather行为 CPU缓存(CPU cache) 什么是CPU缓存 为什么需要CPU缓存 CPU缓存预先存的是什么 CPU缓存命中/未命中 提高CPU缓存命中率 使用连续数组存储要批处理的对象 避免无效数据夹杂在连续内存区域 冷数据/热数据分割 频繁调用的函数尽可能不要做成虚函数 重新认识C++ S

一文鸡游戏系统 理财模式

一文鸡游戏系统软件开发,139-2621-6127(微/电)一文鸡游戏系统理财模式,一文鸡游戏软件开发,一文鸡理财模式 一.一文鸡游戏简介:一文鸡注册需要330元,注册后游戏里会有300只小鸡,玩家每天手动收获鸡蛋,收获的鸡蛋可以卖出也可以孵化成小鸡,小鸡越多,生产数量也就越多.每天生产率的0.5%-5%之间.每天生产率的高低取决会员增长比例以及公司的其它游戏的盈利.一文科技专注于创新各种手游,其它手游将对接一文鸡游戏,会员拥有一文鸡也就成为小股东之一.小鸡和鸡蛋可以变现,也可以在各大游戏和商城