— 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的闲话(1)》,我谈到了一个AI参数的编辑/观察器,用来实时的修改和查看参数,重新审视那篇博文的时候,我发现我还有些需要补充的地方。

首先,我遗漏了一点,就是需要把调试后的参数信息保存下来,然后当游戏中的那些导出的变量初始化的时候,需要把那下存下来的值作为初始值赋给相应的变量,当然,这样一个文件的保存,载入包括赋值的过程,没有很大的技术难点,作为补充,记录在这个地方。另外,我说的这样一个调试器,是一个相当轻量化的解决方式,很独立,可以很容易作为额外的工具,嵌入已有的引擎,当然,正如我在前一篇文章里所说的,它存在这样或者那样的缺点,所以,如果复杂一点话,可以做的更好,比如用反射(Reflection)的解决方案,将AI中用到的物件(Object)或者类整个导出,现在很多引擎都或多或少用到了反射,并提供了强大的可视化编辑器来支持。这样的解决方案和引擎的契合度会很大,挺难独立的剥离出来(当然,也不是不可能)。

好,前面说的是对AI参数方面的调试,另外一种比较流行的方式,是实时的远程命令。通过向游戏引擎发送指令,来使其运行一段预定义的代码,不知道大家有没有玩过CS,CS里面有一个命令控制台,它预定义了一系列可以使用的命令,比如,addbot,votemap等等,这种以控制命令来调整的方式,其实可以非常好的用在AI调试中,而且也是一个非常独立,和轻量化的模块,同样,也是可以用同进程(如CS),或者不同进程的方式来实现,对于不同进程,用网络传输的方式传递命令字串,是一个很方便也很好的办法,因为网络的关系,甚至可以做到跨机器调试。技术上来说,这样的远程命令难点也不是很大,无非是对命令字串的解析,将函数导出成命令的封装。

AI的调试的麻烦之处,很多时候在于编译的速度(如果你有i7+8g内存,那你很幸运),所以上面说的两种方案,都是为了最大化的减少编译次数,但是,在改变AI逻辑的时候,还是不得不重新编译和link游戏,所以,我觉得,作为一个AI程序员,如果在项目起初,如果拿到引擎很“干净”(没有任何游戏逻辑代码),如果引擎没有提供脚本支持,一定要强烈建议做一套脚本化的AI引擎(不一定是全脚本化,但一定要考虑内嵌脚本),原则上来说核心引擎和AI是可以完全分开的,核心引擎部分,在某种程度上,对于AI程序员来说,可以是完全透明的,只要给我一个tick入口,我就能写AI。 :) 。随便说一句,暴雪在脚本化方面做的很好,玩过星际,魔兽的人应该都会有这样感觉,他里面的AI都可以通过外部的方式来改变,确实很强大。其实仔细想想,我们为什么不可以呢?也许我们有时缺少的就是这种对于标准,规范的坚持吧。所以经常我会想,如果有幸我能面试新人,我不会要求你了解多少,知道多少,或者会多少诡异的算法,我会很看重他/她写代码的规范程度,清晰程度,这是会对整个team有益的。

有点扯远了, :) ,总结下,对于影响AI调试的原因一(见前一篇),我想有以下几点能够考虑

  1. 使用脚本化的AI引擎,将AI逻辑与核心引擎分离
  2. 将需要调整或者观察的参数导出,做成一个运行时的参数编译/观察器
  3. 添加远程命令调用
  4. 升级你的电脑到i7+8g内存

好,先写到这里了,下次聊聊对于原因二的一些可以考虑的事情

相关:

--->关于调试AI的闲话(1)

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

Read More

最近在研究游戏引擎Core模块,其实这部份对AI来说并不是直接相关的,属于游戏最底层的接口,像数学库,内存管理,文件系统,以及对系统接口的封装等等。不过有个东西却引起了我极大的兴趣,并且在某种程度上和AI联系紧密,即Reflection系统,虽然这样的库很通用,一般都是找一个来直接用,没人会重头去做一个,但研究一下代码我觉得有两个好处,一是实现一个Reflection系统都会用到一些C++的高级特性和技巧,可以借以提高一下,二是我觉得在引擎里集成Reflection已经是一个趋势,熟悉一下总没有坏处。

Reflection是中文翻过来的说,叫反射系统,按照wikipedia上定义,“在计算机科学中,反射是指一种特定类型的计算机程序能够在运行时以一种依赖于它的代码的抽象特性和它的运行时行为的方式被更改的特性。用比喻来说,那种程式能够"观察"并且修改自己的行为。”,是不是看着超级晕,没事,一会儿我举个例子。Reflection并不被C++语言本身所支持,不过一些高级脚本语言,如Java,Python,Ruby,C#,Lua等都支持了Reflection的概念,就以Java来举个例子:

//常规方法,没有Reflection的类实例的创建,和类方法的调用
Foo foo = new Foo();
foo.hello();

 

//用Relection来创建类的实例和类方法的调用
Class cls = Class.forName("Foo"); //通过名字得到类的类型
Object foo = cls.newInstance();   //创建类实例
Method method = cls.getMethod("hello", null);
//通过名字得到类方法
method.invoke(foo, null); //调用类方法

其他语言的代码和调用方式基本一致,给人第一感觉是用Reflection反而需要写更多的代码,好像并不方便,但我们可以看到,用Reflection,我们可以在运行时来得到类类型,动态的创建不同的类对象,调用方法,并且可以用统一的调用接口。相当Cool~。在我们引擎里用C++来做了一套Reflection系统,作为和Lua和C#的接口,实现了C++内的函数和变量的输出。我所说的和AI的关系密切就是体现在此了。
今天还在看代码,看了一部份,还没有吃透,做个引子,以后再写。

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

Read More