— AI分享站

Archive
Tag "共享内存"

这篇文章针对于已了解行为树和黑板的读者,如果不是很了解,请参考此处(123)。

黑板(Blackboard)是一种数据集中式的设计模式,一般用于多模块间的数据共享,我在做行为树的过程中,发现黑板非常适合作为行为树的辅助模块来使用,这次就来谈谈如何在行为树中使用黑板。

行为树的决策一般要依赖于外部的输入,如下图所示。

输入内容的来源取决于行为树用在整个AI架构的哪一层,可以是游戏世界的信息,或者是上层模块的输出。输入的形式,可以是分散的(Decentralized),也可以是集中的(Centralized)。举个例子来说,如果我们做一个战士是移动,还是攻击的决策,这是决策层的行为,所以输入内容就是游戏世界的信息,它可能包括战士自身状态(在模块A中),敌人状态(在模块B中),装备物品情况(在模块C),地图场景情况(在模块D中)等等,所以,当我们搜索和执行行为树时,我们需要从4个模块中获取信息来帮助决策,这样的方式就是我上面说的分散的方式,它的好处是调用非常直接(可能是用多个Singleton提供的接口),没有数据冗余,缺点是使得行为树对于数据的依赖度太分散。

集中的方式的话,就是我们可以定义一个数据结构专门用于行为树的输入,将上面提到的需要用到的数据,在进行行为树决策前,先从各个模块中收集到这个数据结构里,然后再递交给行为树使用。集中式的输入减少了输入和行为树之间的接口数量(只和预定义的数据结构通信),但缺点是,存在数据冗余。不过,我们可以看到集中式的数据输入使得行为树的表现更像一个黑盒了(可以伪造数据来测试行为树),这也是我们一直以来想要的。可以参看下面对于两种方式的示意图:

BehaviorTreeInputModule_1

基于上面的原因,黑板(Blackboard)这样一个概念正好符合我们的需要,所以我们就可以用黑板从各个模块中来收集行为树决策和执行的过程中需要用到的数据,然后提交给行为树使用。值得注意的是,这块黑板对于行为树来说是只读(Readonly)的,行为树不允许修改和添加任何信息到这块黑板上面。因为很难从程序上去限制(就算用const,有时为了方便还能强转成非const),所以限制只能是一种规则,或者说约定。

说完了外部世界的黑板,我们再说说另一块可能会被用到的黑板。这也可以看成是对上面这块只读黑板的补偿吧, :)

在行为树的使用过程中,发现有时候节点和节点间,行为树和行为树之间确实需要有数据共享,比如对于序列(Sequence)节点来说,它的执行行为是依次执行每一个子节点,直白一点说的话,就是执行完一个再执行下一个。一般用到序列的行为,其子节点间总会有一些联系,这里就可能存在节点间通信的问题。再比如,在一些团队AI的决策过程中,当前AI的行为树决策可能需要参考其他AI的决策结果,所以这样就存在了行为树之间需要通信的情况。

所以,在实践过程中,我们还会定义另一块黑板来负责行为树间和节点间的通信需求,示意图如下

BehaviorTreeInputModule_2

可以看到这块黑板是又可以读又可以写的,为了防止黑板混乱的问题(可以参看我以前对于共享数据的文章),我们必须在使用时规定一些限制,可以称之为黑板数据的“作用域”,我们知道很多编程语言里,变量都是存在作用域的概念的,有全局,有局部等等,借鉴于此,我也在这块黑板上规定了作用域,由上面的分析我们可以将黑板上的数据分成如下几种作用域

  • 全局域(G):此数据可以给其他行为树访问
  • 行为树域(T):此数据可以给行为树内的任意节点访问
  • 指定节点域(N):此数据可以给指定的行为树内的某节点(可以是多个)访问

这样的话,黑板的混乱程度就会好很多了,我们可以提供相关的接口来帮助操作黑板上的这些变量。

好了,基本上黑板在行为树中的应用就聊这么多了,希望对大家在使用行为树的过程中有所帮助。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

项目进行到后期,越来越多是在修复一些AI的错误和调整AI的行为,但不得不承认,AI有时候很难调试,我想主要是有两个原因,一是现在的引擎越来越复杂,改一点点地方,编译都会等很久,时间上效率不高(所以对于AI程序员来说,真的是非常欢迎那种引擎核心和游戏逻辑分开的架构);二是如果出现问题的话,情况不可重现,或者很难重现。由于以上原因的存在,我一直在考虑如何来改善AI的调试环境,并且使之模块化,以便在不同的引擎上可以重用。

就我上面的第一个原因,如果要提高调试AI的时间上的效率,减少编译次数是一个关键,最近听到有同事抱怨,说他们用的那个引擎,一编译就超过20分钟,不能忍啊!而AI代码里,除了逻辑部分,很多地方是一些可调整的参数,这些参数如果写死在代码里,每次调整,都要重新编译链接,那一天基本上干不了什么活了。所以有一个可以在运行时,动态调整AI参数的编辑器就很重要了。这种参数编辑器的实现方法有很多种,可以实现在同一进程中(随游戏弹一个编辑器出来),也可以通过进程间通信的各种方式,用另一个独立的编辑器来实现。

更进一步,这样参数编辑器的另一个作用,就是查看AI中的各种值的变化,也就是说这个编辑器变成了一个动态的观察器,可以实时的观察那些我们关心的数据变化,举个例子,比如在一般的RPG游戏中,有些技能会提高人物的属性,当这些属性是隐藏的,不会在UI中显示的,或者说UI显示部分还没完成的时候,就需要用到这样的观察器来观测这些属性是否正常的变化。

是编辑器,还是观察器,其实就是对AI中导出变量,哪方是只读,哪方是只写的问题(对于单个变量,不存在两方面都是只写的情况,要不就乱了 :) ),如果用代码来表示的话,我希望是这样的:

class AIExampleClass
{
EXPORT READONLY m_RDValue;
EXPORT WRITABLE m_WRValue;
}

我实现过一个用共享内存作为通信方式的模块,可以方便的在类中导出变量,并且在一个对应的编辑器里修改和观察。基本原理是,在游戏程序启动的时候开了一块共享内存,然后将内存分为头区域和数据区域,头区域存有所有导出变量的信息,数据区存真正的变量数据,当变量发生变化的时候(不管是从游戏端修改,还是从编辑器端修改),会将数据同步更新到共享内存中,保证两边的结果一致。

这样的做的好处是,由于是内存拷贝,所以同步的速度很快,变化很迅速,而且我封装了对于导出变量的声明,几乎可以做到和不导出时,对变量的使用方式是一致的,以实现对代码的改变的最小化,当然,这样的实现有一个缺点,由于对于编辑器而言,它看到的只是内存,所以需要对内存解析有个类似“协议”的声明,以保证正确地解析出变量的值,这个“协议”就需要和游戏的导出变量同步改变了。另外,对于“小型变量”,比如一个bool,这样的导出成本有点高,因为都需要有额外的头部信息。

有了这样的参数编辑/观察器,可以大大的提高调试AI的效率,当然,前提是,要积极使用!,以前我写过一篇叫《AI程序员的痛苦》,因为我们经常可以看到在AI代码中充斥着不知所谓的“魔数”(magical number),这样写起来是简单了,但对以后的调试,别人甚至是自己以后的维护,都是有百害而无一利的。现在,我很推崇这种对于AI参数的管理,希望对大家有所帮助!

先这些,以后再聊~

PS:关于上面说的那个共享内存的模块,我整理一下再放出,写的不好,仅供参考,如果有同学能帮着一起完善,就更是感激不尽了~~

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More