— AI分享站

Archive
Tag "请求"

自开博以来,经常会有网友发信给我,询问一些关于AI方面的问题,一般我都会尽力一一回答,也希望我的这些经验,对网友有些帮助,我想,有些问题可能是大家都会有的,所以,这一次,我会把一些网友的提问,和我的回答列在这个地方,供更多的同学参考,也欢迎一起讨论,才疏学浅,不吝赐教。

Q:你好,我在你的博客上看了你的一些关于行为树的文章,写得很好,让我受益匪浅。我想实践一下怎么用,在网上找了一个相关的库libbehavior(https://code.google.com/p/libbehavior/),想用这个库做个小例子,它里面有一些演示程序,但牵涉到很多库,不好学习,我就想将一个简单的状态机示例程序源码改成用行为树实现,发现还是很茫然,不知道你是否有空给些提示,谢谢!

A:我下载了程序简单看了下,libbehavior好像已经实现了一个可用的behavior tree了,虽然可能还不够完善,但基本的行为树的样子已经有了,你可以考虑基于他这个,然后根据自己的需求添加。 不知道你有没有看到它里面的sample,我从Scenario3.h里摘录了一段建行为树的例子来说明下,我还画了一张示意图作为参考

 1: //opponent->brain是一个ParallelNode,并行的节点
 2: //在并行节点上添加第一个子节点,行为是转向目标TurnTowardsTarget
 3: opponent->brain->addChild(new TurnTowardsTarget(1));
 4: //建立一个随机节点作为第二个子节点,下面三个序列节点应该会随机选一个执行
 5: ProbabilityNode* pNode = new ProbabilityNode();
 6: //在这个随机节点上,建立一个序列节点
 7: pNode->addChild((new SequentialNode())
 8:         //在序列节点上建立第一个子节点,感觉这个应该是个判断条件
 9:          ->addChild(new BoolCondition<GameObject>(&GameObject::alignedWithPlayer,true))
 10:         //在序列节点上建立第二个子节点,行为是射击Fire
 11:          ->addChild(new Fire())
 12:         //在序列节点上建立第二个子节点,行为是等待CD,防止太频繁
 13:          ->addChild(new Cooldown(500)),5.0);
 14: //在这个随机节点上,再建立一个序列节点,下面的子节点就不说明了
 15: pNode->addChild((new SequentialNode())
 16:          ->addChild(new FloatCondition<GameObject>(&Ship::getXPosition,LESS_THAN_FP,200,1))
 17:          ->addChild(new GotoPoint(point(300,50),500))
 18:          ->addChild(new Cooldown(200)))
 19: //在这个随机节点上,再建立一个序列节点,下面的子节点就不说明了
 20: pNode->addChild((new SequentialNode())
 21:          ->addChild(new FloatCondition<GameObject>(&Ship::getXPosition,GREATER_OR_CLOSE,200,1))
 22:          ->addChild(new GotoPoint(point(100,50),500))
 23:          ->addChild(new Cooldown(200)));
 24: //在这个随机节点的上面做一个循环节点,把上面的行为无限循环
 25: opponent->brain
 26:     ->addChild((new RepeatNode(-1))
 27:         ->addChild(pNode));

Scenario3BevTree

 

所以如果你要把状态机改成行为树的话,可以考虑用我说过的Selector节点+Precondition的方式(libbehavior里好像是叫PriorityNode,但它好像没有Preconditon的概念),因为状态机里不是要跳转嘛,你原先每一个state都会有进入条件的,比如下面这个

 1: if (pMiner->PocketsFull())
 2: {
 3:     pMiner->ChangeState(VisitBankAndDepositGold::Instance());
 4: }

这样的话,你可以把pMiner->PocketsFull()作为VisitBankAndDepositGold的Precondition挂在一个Selector节点下面,然后让Selector节点帮助你选择应该进入那个状态。我建议,你可以先看看libbehavior每一个Scenario的行为树创建部分的代码,这样应该会有点感觉,另外,可以边参考我网上的文章,边来看libbehavior\BehaviorTree-src里的源码,会好理解很多 :) ,希望对你有所帮助!欢迎继续来信交流!(状态机和行为树之间转换可以参看这里这里

Q:看了你写的一系列文章,写的真不错。我也最进在游戏行业,做AI相关的工作。涉及一些游戏AI设计的问题,能否指教一下。目前我们游戏AI设计的部分考虑NPC的AI部分可配置成AI执行脚本文件方式。我考虑是可以设计AI框架,依据游戏对象的配置,框架决定加载相应的脚本引擎。我看 《浅谈层次化的AI架构》 不错,你能不能分享一下AI设计中如何分离AI框架和AI脚本内容的呢。分离的话,感觉状态转换是应该在AI脚本中完成的吧? 暴露AI引擎的API接口的通用设计一般是怎么样的呢?学习中...., 谢谢。

A:如果想在脚本中写很多AI逻辑的话,如何调试是个比较麻烦的事情,需要比较好的调试接口。我用过一种做法,你可以试试看,就是用脚本做AI的配置,逻辑还是在C++端。C++提供一个个AI逻辑模块,然后在脚本端把它配置起来,因为我用的是行为树(可以参看我博客上关于行为树的相关文章),所以比较容易配置。接口的话,首先最好是引擎能支持反射,然后就能方便的导出函数和变量直接给脚本用,至少我用lua做脚本语言,这样是可行的。

Q:您好。今天再搜索层次状态机相关资料的时候进入了您的博客。本人只在游戏中使用过简单的FSM,属于AI初学者,阅读了几篇文章后产生了几个问题:

  • HFSM和行为树到底是什么关系。我对行为树和状态机的区别还是把握不了……
  • 您有一文章说:大部分的AI,都可以分成“决策”和“行为”,二者通过一个双缓冲的“请求层”通信,但是这篇文章您没有举例子,我非常模糊。
  • 对“并行节点”我还是很模糊……您可以给我举个例子吗……麻烦了…。

A:谢谢,针对每一个问题,我的回答如下:

  • HFSM和行为树其实没什么关系,是两种不同的AI结构,区别的话,可以看我在这里的评论http://www.aisharing.com/archives/90#comment-111
  • 有人和你提出了一样的问题,可以参看我这里的回复,http://www.aisharing.com/archives/86#comment-115
  • 比如一个并行节点下有A和B两个节点,那在一次循环里,如果前提满足的话,既会执行A,又会执行B,举个实际的例子,比如要描述“又吃又喝”,就可以用并行节点:)

Q:最近在学习行为树,有幸拜读了您几篇关于行为树的文章,感觉收获颇多。刚开始接触行为树,所知有限,特来请教几个问题:

  • 关于以下代码,在文章评论中,您回复说 input 是传入行为树的参数,那么 request 是指输入的事件吗?比如移动事件、死亡事件等。 如果是事件,那您实例代码的行为树是事件驱动的,还是Tick驱动?
 1: action = root.FindNextAction(input);
 2: if action is not empty then
 3:     action.Execute(request,  input)  //request是输出的请求
 4: else
 5:     print “no action is available”
  • 在FSM中,状态进入和退出时可以做相对应的事情,比如进入某个状态身上挂个循环特效,退出该状态将这个特效移除,在行为树上,怎么实现同样的功能?
  • 按我现在对行为树的理解,感觉行为树就是将FSM中的状态变成了子树,挂在行为树的相应位置,通过行为树的节点完成状态的迁移。但是,在您文章的总结中提到:"在AI设计过程中,一般来说,我们并不是先有状态机,再去转化成行为树的,当我们选择用行为树的时候,我们就要充分的理解控制节点,前提,节点等概念,并试着用行为树的逻辑方式去思考和设计。",看后对自己将状态视为子树的理解产生了很大的怀疑,您能详细地解释下这句话吗?不知是否可以提供一张您平时设计的较复杂些的行为树供我学习之用?

A:谢谢,针对每一个问题,我的回答如下:

  • request是输出的请求,也就是行为树的输出,就像你说的,像移动,死亡等,可以看看我博客上关于分层次的AI架构的相关文章,整个行为树的决策还是在Tick中更新的。
  • 一般我在做行为树的Execute时会分成三个阶段,onEnter() ,onExecute(),onExit(),在第一次进入这个节点的时候,会调用onEnter,在从这个节点切换出去的时候,会调用onExit()。每个行为树的节点,我都会做成这种1P(前提Precondition) + 3E(Enter, Execute, Exit)的结构
  • 行为树的思考方式是指,对于每一个节点我都需要明确知道它的进入条件是什么,它要做的事情是什么。把跳转逻辑写到控制节点中,用控制节点来控制行为树的选择流程。和状态机不同的是,对于行为树的节点,一般不需要知道它是从哪个节点过来的,它只是需要关心,满足什么条件(前提)可以进入这个节点,这些条件的判断依据都是通过行为树的输入参数传入的,所以行为树和黑板的组合是很好用的,可以看看我博客上关于黑板和行为树结合的文章
  • 项目里的行为树图不太好给,呵呵,而且需要解释才能明白。复杂的话,一般就会用到序列节点,并行节点等等。行为树的好处就是只要定义了进入前提,就可以加入到现有的行为树中,这样就越来越复杂了~,不过,这也就是行为树的好处了 :)

Q:Hi,在我们现在的项目里面涉及到AI的一些东西,之前学习了关于你写得行为树,正好可以发挥一下,现在有一个问题,就是关于怪物行走,怪物行走我们这边的做法就是发目标A*路径给客户端通知其他玩家,然后,怪物每秒刷一步就走一步...这样的做法...我想问问...你们那边关于怪物行走是如何实现的呢?巡逻和追击 两个地方行为怎么做互斥?

A:针对你的问题,我在想哦,对于怪物移动的话,可以分两个部分,一般的移动,就是你说的巡逻,可以不需要用A*,因为毕竟A*的开销是比较大的,因为怪物一般的移动不会太复杂,而且一般是在一定的区域内移动(当然这是看你们怎么设计了),可以在区域内设置几个路点,然后每过一段时间选择临近的路点就可以了,也就是仅仅做一步的“寻路”。如果是要追击玩家的话,可以尝试用到A*。

网页游戏我没做过,其他游戏里,我们用到过同步位置信息的方式,也就是每隔一段时间同步一次位置,然后两次同步之间的移动,都是客户端本地模拟。如果模拟后的位置和服务器上的位置有偏差,简单的做法是瞬移到服务器上的位置,好一点的话就是平滑的从客户端现在的位置移动到服务器上的位置。

巡逻和追击我觉得比较大的不同是,巡逻中是没有目标敌人的,而追击是存在目标敌人的,所以这样就可以做到互斥了。

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

Read More

记得以前我在博客中,提到过一种层次化的AI架构,这种架构的核心就是定义了“请求层”的概念,用来分隔决策和行为,并通过行为请求来清晰的定义了决策和行为之间的输入输出关系,不过,当我们仔细审视这个结构的时候,发现其中貌似缺失了对于某种情况的处理,这就是我今天要谈到,如何处理“被动式的行为请求”

一般来说,我们通常所认为的决策是一种“主动式(Active)的行为请求”,比如,我按了个键,玩家所控制的角色就是做出某些行为,或者AI通过对于当前情况的判断,做出了下一步的行为决策,所以说,主动式的行为请求,就是表达了一种“我想要去做什么”的语义。在游戏中,大部分的行为都是输入主动式的行为,那FPS(第一人称射击)类游戏来说,它可能包含的主动式行为有,移动,跳跃,射击,与游戏场景中物体的交互(拾取物品,开门)等等,当玩家和NPC在行动的时候,就是在这些已经定义的主动式行为中,做出决策。在我们以前提到的那种层次化的AI架构中,可以很好的满足这些需求。

但在游戏中,还是一些行为并不在主动式的行为中,比如死亡,当角色生命值等于0的时候,游戏系统就会触发这个行为请求,它不是由玩家或者NPC所发起的,而是有游戏系统判断出某些条件满足的时候,自动触发并赋予角色的行为请求。这一类的请求,我们就可以称之为“被动式(Passive)的行为请求”,在谈如何处理前,我们可以先看看这种请求有什么样的特点。

首先它可能会覆盖原本的主动式的行为请求。比如玩家想要快跑向前走,那他就会发出一个“快速移动”的行为请求,但就在这个时候,角色正好被横在路中间的一根树干绊了一下,当游戏系统检测到这个事件的时候,就会触发一个“绊倒”的行为请求,期望做一个摔倒的行为。显然,“绊倒”这个行为不是玩家主动做出的,是属于被动式的请求,而且这个请求应该被立刻响应(要不就不真实了),所以,当比较这两个请求的时候,我们就会用“绊倒”覆盖掉原先的“快速移动”,将其作为当前请求传给行为层。那我这里为什么说是“可能会覆盖”呢?因为并不是所有的被动请求都需要被响应的,这取决于当前产生的主动和被动,这两个请求的优先级设定,这种优先级设定一般来说会来自与游戏设计的需要。假设,我们在游戏中有一个行为是“超级移动”,这种行为很强大,效果也很炫,是游戏的一个很大的卖点和可玩点,所以游戏设计者就希望尽量能让玩家触发出这种行为,以提到游戏的游戏性和画面效果,所以当玩家触发“超级移动”的时候,我们可能就会忽略掉同时触发的“绊倒”这个行为请求。

其次,它是触发式的,所以意味着同一时刻它可能会有多个。试想一下,对于主动式请求来说,因为角色不可能分身嘛(除非是超人游戏),所以一般来说,同一时刻主动请求只会有一个,也就说,你不可能既想做这个,又想做那个。而被动式请求,就完全不同,在同一时刻,可能会有多个条件被满足,然后同时触发了多个被动行为请求,比如在被绊倒的那一刻,你中了一枪,然后生命值正好减到0(真倒霉。。),所以同时就产生了“绊倒”,“死亡”两个被动请求。那我们就面对,多个请求需要权衡的情况,在上述的例子里,显然,“死亡”的优先级应该是最高的,所以它会成为最后的请求被送到行为层。

还有一个特点就是关于被动请求产生源,在角色与角色交互的时候会非常容易产生被动式的行为请求。举个FIFA的例子(对此我研究了很多 :) ),在足球游戏中,我们可以看到很多球员间交互的例子,比如碰撞后的摔倒,争头球的挤位,带球过程中的拉扯,躲避铲球的跳跃,等等,这些都是被动式的行为请求,是玩家不可操控的,而且这些请求,不像我前面举到的例子,是单个人身上的,而是属于一种“相互作用(Interaction)”的行为请求。它的触发和处理,都会牵涉到两个,甚至多个人的判断。

谈完了特点,接下去来思考下如何来处理这些请求。当然,处理的方案多种多样,我先抛砖引玉的谈谈我想到的两种结构。还是基于原有的层次化的AI架构模型,做一些小的修改。第一种方案采用的是比较直观的集中化的处理。先定义一个结构表示当前时刻所有产生的行为请求。

 1: struct CandidateRequests
 2: {
 3:     Request        m_ActiveRequest;      //主动请求,假设同一时刻只有一个 
 4:     Array<Request> m_PassiveRequests;    //被动请求,同一时刻可以有多个
 5: }

然后,我们定义一个模块来负责处理这个结构,并产生一个最终的行为请求,值得注意的是,我们把当前正在处理的请求也作为考虑的因素之一,传入这个函数中。

 1: class RequestFinalizedModule
 2: {
 3: public:
 4:     static Request GetFinalRequest(
 5:                 const Entity& entity,
 6:                 const Request& curReq,
 7:                 const CandidateRequests& nextReq)
 8:     {
 9:         //添加选择的规则,根据当前请求和下一时刻的候选请求,抉择出最终应该做的行为
 10:     }
 11: }

集中式的处理方式将所以处理的可能性放到一个函数中,这样的好处是便于排查可能导致的选择不正确的问题,而且对于原有的结构没有什么更改,仅仅是在决策和请求层之间加了一个处理模块。缺点就是这块的代码,随着优先级关系的复杂,会显的相对比较乱,不过将脏代码堆在一个地方,也是一种不错的设计 :) ,下面是修改后的架构图

layered-ai-architecture_modified

另一种方案是让游戏系统不直接触发被动式请求,而是触发一些标记,然后AI在决策的时候将这些标记作为决策参考信息的一部分,最终做出一个合理的行为决策,这种方案延伸了决策部分的定义,使其即能产生主动式请求,也能产生被动式请求。如果用行为树的话,可以非常好的表示出这种优先级的关系,如下图:

bt-flag-passive-action

这种方式的优点是可以用到行为树本身对于优先级处理的优势,而不需要额外的添加模块,只需修改原本行为树的设计即可,作为游戏信息的一部分,也可以沿用原有的收集游戏信息的相关模块,做适当的扩展就可以了。缺点是,由于我们将原有的触发式的模式,变成了轮询式的,所以,可能会降低行为树的些许效率,而且对于行为树的设计也提出了更高的要求。不过从架构上来看,是比较清晰,并且能很好的融入原有的设计。修改的架构图如下:

layered-ai-architecture_modified_2

好,就聊到这里了,不知道大家有什么想法,欢迎一起留言讨论。

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

Read More

自从开博以来,每天都会关心一下博客的访问情况,看到一些朋友的订阅或者访问,不胜欣喜,也促使我去写一些更好的博文,来和大家分享和交流,从访问统计来看,有相当一部分是来自于搜索引擎的流量,关键字以“行为树”,或者“Behavior Tree”居首位,我想大家对此可能有些兴趣,加上,这几年反反复复一直在AI中研究和运用行为树,所以这次就来谈谈关于行为树(Behavior Tree)的一些东西,以前也写过一些文章(123)来讨论行为树,不过已经是一两年前的事情了,较之以前,这次会更为系统,也会添加一些我新的思考和感悟。所谓行为树实践,其实在我脑海里就是Practice in Behavior Tree,没法子,受英文教材影响太多了 :)

我想通过一个例子来介绍一下行为树的基本概念,会比较容易理解,看下图:

bv-tree-1

这是我们为一个士兵定义的一颗行为树(可以先不管这些绿圈和红圈是干吗的),首先,可以看到这是一个树形结构的图,有根节点,有分支,而且子节点个数可以任意,然后有三个分支,分别是巡逻(Patrol),攻击(Attack),逃跑(Retreat),这个三个分支可以看成是我们为这个士兵定义的三个大的行为(Behavior),当然,如果有更多的行为,我们可以继续在根节点中添加新的分支。当我们要决策当前这个士兵要做什么样的行为的时候,我们就会自顶向下的,通过一些条件来搜索这颗树,最终确定需要做的行为(叶节点),并且执行它,这就是行为树的基本原理。

值得注意的是,我们标识的三大行为其实并不是真正的决策的结果,它只是一个类型,来帮助我们了解这个分支的一些行为是属于这类的,真正的行为树的行为都是在叶节点上,一般称之为行为节点(Action Node),如下图红圈表示的

bv-tree-action-node

这些叶节点才是我们真正通过行为树决策出来的结果,如果用我以前提到的那个层次化的AI结构来描述的话,这些行为结果,相当于就是一个个定义好的“请求”(Request),比如移动(Move),无所事事(Idle),射击(Shoot)等等。所以行为树是一种决策树,来帮助我们搜寻到我们想要的某个行为。

行为节点是游戏相关的,因不同的游戏,我们需要定义不同的行为节点,但对于某个游戏来说,在行为树上行为节点是可以复用的,比如移动,在巡逻的分支上,需要用到,在逃跑分支上,也会用到,这种情况下,我们就可以复用这个节点。行为节点一般分为两种运行状态:

  1. 运行中(Executing):该行为还在处理中
  2. 完成(Completed):该行为处理完成,成功或者失败

除了行为节点,其余一般称之为控制节点(Control Node),用树的“学名”的话,就是那些父节点,如下图绿圈表示

bv-tree-control-node

控制节点其实是行为树的精髓所在,我们要搜索一个行为,如何搜索?其实就是通过这些控制节点来定义的,从控制节点上,我们就可以看出整个行为树的逻辑走向,所以,行为树的特点之一就是其逻辑的可见性。

我们可以为行为树定义各种各样的控制节点(这也是行为树有意思的地方之一),一般来说,常用的控制节点有以下三种

  1. 选择(Selector):选择其子节点的某一个执行
  2. 序列(Sequence):将其所有子节点依次执行,也就是说当前一个返回“完成”状态后,再运行先一个子节点
  3. 并行(Parallel):将其所有子节点都运行一遍

用图来表示的话,就是这样,依次为选择,序列和并行

bv-tree-sel

bv-tree-seq

bv-tree-pal

可以看到,控制节点其实就是“控制”其子节点(子节点可以是叶节点,也可以是控制节点,所谓“执行控制节点”,就是执行其定义的控制逻辑)如何被执行,所以,我们可以扩展出很多其他的控制节点,比如循环(Loop)等,与行为节点不同的是,控制节点是与游戏无关的,因为他只负责行为树逻辑的控制,而不牵涉到任何的游戏代码。如果是作为一个行为树的库的话,其中就一定会包含定义好的控制节点库。

如果我们继续考察选择节点,会产生一个问题,如何从子节点中选择呢?选择的依据是什么呢?这里就要引入另一个概念,一般称之为前提(Precondition),每一个节点,不管是行为节点还是控制节点,都会包含一个前提的部分,如下图

bv-tree-precondition

前提就提供了“选择”的依据,它包含了进入,或者说选择这个节点的条件,当我们用到选择节点的时候,它就是去依次测试每一个子节点的前提,如果满足,则选择此节点。由于我们最终返回的是某个行为节点(叶节点),所以,当前行为的“总”前提就可以看成是:

当前行为节点的前提 And 父节点的前提 And 父节点的父节点的前提 And....And 根节点的前提(一般是不设,直接返回True)

行为树就是通过行为节点,控制节点,以及每个节点上的前提,把整个AI的决策逻辑描述了出来,对于每次的Tick,可以用如下的流程来描述:

action = root.FindNextAction(input);
if action is not empty then
action.Execute(request,  input)  //request是输出的请求
else
print "no action is available"

从概念上来说,行为树还是比较简单的,但对AI程序员来说,却是充满了吸引力,它的一些特性,比如可视化的决策逻辑,可复用的控制节点,逻辑和实现的低耦合等,较之传统的状态机,都是可以大大帮助我们迅速而便捷的组织我们的行为决策。希望这次简单的介绍,对大家有所帮助,能力有限,不一定能表述的很清楚,有问题,或者有指教的,都请和我多多交流,最后,我对这个士兵的巡逻分支画了一个示意图,供大家参考:

S -- 选择节点   Se -- 序列节点

bv-tree-patrol-example

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

Read More

记得在以前的一篇文章中谈到了一种类似于双缓冲的AI结构,最近在整理一些东西的时候,发现这样的AI结构具有一定的通用性,而且层与层之间耦合度相对较低,作为一种层次化的AI架构,非常值得一谈。

在我的脑海中,AI一般分为两个部分,一个是决策(Decision)部分,一个是行为(Behavior)部分,决策部分负责做什么,行为部分负责怎么做。在一些国外的公司里,AI程序员也大致分为这两种(不过,一些国内的企业可能就分的没有这么细,一般都是统称为AI程序员,或者有的分的更粗,将一些游戏中的其他游戏逻辑部分一起涵盖,统称为游戏性(Gameplay,GPP)程序员)。正因为这样,所以我们一般希望,在AI架构上,这两个部分的耦合度是相对较低的,这样也便于任务的分工。所谓层次化的AI架构(Layered AI Architeture)也就基于了这样的理念。看下面这个图:

layered-ai-architecture-1

在这样一个层次化的AI框图中,我们定义了“请求(Request)”这样一个概念,请求可以看作是AI决策的结果,或者称之为一个命令,比如,在射击游戏中,请求可能就定义为,射击,移动,逃跑等等,在动作游戏中,请求就会定义成攻击,格挡,跳跃等等。当行为层收到上层的请求后,就会设法去处理该请求的内容,还是以射击游戏为例,当行为层收到射击的指令,就会从射击的动画列表中选择某个射击动画,然后转向目标,播放动画等等工作来处理射击的请求。所以请求相当于就成了决策层和行为层之间的接口。这样,对于决策层和行为层的输入和输出就很明确了:

  • 决策层:输入(游戏世界信息),输出(请求)
  • 行为层:输入(请求),输出(修改游戏世界的相关信息)

layered-ai-architecture-2

由于有请求层作为中间接口层,所以决策和行为部分就很自然的分开了,而且有清晰的输入和输出,AI团队中的人员的工作职责也就很明确了,做为决策层的AI程序员,就只需要关心如何产生请求,而行为层的AI程序员,只需要关心如何处理请求,一旦定义好完备的请求内容,不管在代码还是在工作上都不会产生很大的粘连度了。

另外值得注意的是,这边的请求层用到了类似双缓冲的结构,分成后端和前端,换个词的话,可以说成当前在处理的请求(前端),和下一个要处理的请求(后端),具体的分析可以参考我以前的文章在AI结构中用双缓冲》,这里就不多做介绍了。

这样的层次化结构在AI中有很强的通用性,因为这是用最高的层面来总览AI的架构,而像其他诸如行为树(Behavior Tree),分层状态机(HFSM)等都可以看成是在决策或者行为层中的具体实现方式。所以不管AI代码是如何实现,大部分都可以归到这种层次化的结构中,因此,我想,我们在设计AI结构之初,就可以用这样的方式来思考和架构整个框架,分割决策和行为,定义请求,然后再针对每一层来选择具体的实现方法。

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

Read More

双缓冲(double buffer)是在渲染中用到的基本技术,目的是为了提高绘图速度,防止屏幕的闪动,原理可以简述如下,生成两块内存,一块作为后缓冲(background buffer),一块为前缓冲(foreground buffer),前缓冲内存的是实际显示在屏幕上的内容,后缓冲可以称为工作缓冲,所有的图形都先画在这块缓冲中,完毕后再与前缓冲做一个交换(Flip),也就是”前“变”后“,”后“变"前”。循环以上操作。

doublebuffer-1

双缓冲从原理上并没有限制一定要用在渲染中,其本质就是把工作区域和显示区域分开,如果把工作区域作为和上层代码的接口,把显示区域作为和下层代码的接口,这样双缓冲就相当于是一个连接两个层次代码的中间件。这样它就有了很大的灵活性。

AI架构中可以借鉴这样的技术,用双缓冲来解决一些问题。比如,高层AI要发一个请求,然后下层的行为要处理这个请求,如何做一个结构来符合这个要求呢?这个的解决方案很多,可能还要牵涉到很多其他问题,我这里提供一个我实践过一个solution,就用到了双缓冲技术,可以给大家做个借鉴。

我先把“请求”抽象出来,并做成两个,一个作为backgroud的请求(简称BGR),一个作为foreground的请求(FGR),有点双缓冲的样子了吧。

对高层AI来说,它发的所有请求都是修改BGR中的内容,等高层AI的Tick结束后,我们就得到了一个完整的BGR(相当于渲染中,我们画完了所有的图形)。下一步就是交换BGR和FGR,即Flip,然后下层的行为系统就把FGR中的内容作为输入,继续后续的工作。

可以看到,双缓冲结构的优点就是接口相当的清晰,可以很方便的看到每一层的输入输出是否正确(BGR为AI的输出,FGR为行为系统的输入)。很多人也许会觉得,这样的做法,可以用copy的方式来代替,比如copy一份请求给下层,我同意,但我觉得,双缓冲的方式在这里显得更为清楚,而且效率上也更高(只是index的变动而已),另外,在实际代码中,Flip的并不一定是每帧进行的,原因在于我们可能为根据一些实际情况(称之为Request Rule)来做一些对BGR做一些合法性检测,只对通过合法性检测的BGR作Flip,这里就不多说了。

好啦,就写到这里,希望双缓冲技术也可以作为大家解决问题的一种可选方案。

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

Read More