— AI分享站

Archive
Tag "编辑器"

(ecto真不好用,居然写得东西都没了,害我只能把这篇文章再写了一遍,郁闷啊,跪求一个mac下的支持wordpress的离线博客编辑软件)

提到参数,在我们写程序的时候,作为一个常用的提醒是,一般都会避免去使用“魔数”(Magic Number),因为它含义不清,比如if(i>5){...},如果没有任何注释的话,很难有人会理解这个“5”是什么意思。在AI中,这样静态的参数的使用更频繁,有时为了定义一些阈值,有时为了可以给游戏设计人员调整AI行为等等,当这些参数慢慢多起来的时候,就需要更好的来管理,修改和查看,所以,如何设计一个好的参数系统就很有必要了。这个想法也源自我上一个项目的一些教训,在上个项目中,我们最后一共定义了超过1600个参数,由于历史原因,由一个简单的参数系统来维护,但实际操作中,这样的简单系统存在很多问题,首先参数没有很好的文档化,导致游戏设计人员很难搞清楚这个参数到底是干什么的,最后需要程序员的不停参与,工作的粘合度就提高了,另外也没有做到逻辑的关联,无法了解这些参数之间的相互关系。总之,这样那样的问题促成了我对于设计一个新的参数系统的想法。

相较与原来那个参数系统的种种问题,对于一个新的参数系统,我希望它能有如下的特性:

  • 仅支持静态的参数:对于游戏来说是只读的,也就是游戏在运行时不允许修改这些参数,但可以通过外部工具进行调试
  • 支持文档化
  • 支持实时的修改:不需要重新启动游戏
  • 支持序列化到文件:可以将调整好的参数存成文件,以便下一次启动游戏时生效
  • 支持逻辑关联
  • 支持自动的合法性检查
  • 高效和便捷的定义和调用

以上列出的大部分特性还是比较容易理解的,对于第5点,我特别的说明一下,假设有一个参数A,当A的值发生变化的时候,逻辑上,参数(B1, B2, B3, … , Bn)也会相应发生变化,那我们就称A和(B1, B2, B3, … , Bn)有“逻辑关联”,也就是A的变化会影响到(B1, B2, B3, … , Bn)的值,反之不成立,也就是说,逻辑关联是单向的。举个例子,比如我们有一个可以调整游戏难度的参数,它关联到敌人的血量,攻击值,防御值,当把游戏难度调成“高”时,血量,攻击值,防御值会加倍,当把游戏的难度调成“低”时,血量,攻击值,防御值会减半。在我们原来的参数系统中,这种逻辑关联是隐含的,也就是说所有的参数并没有直接的相互关联,他们的结构是“平面化”的,彼此相互独立,所以当游戏设计人员想调高游戏难度的时候,他就需要记得相应的要去调整血量,攻击值,防御值。这样很容易造成错误和疏漏,而且这种隐含的逻辑关联无处可循,只能存与程序员活或者游戏设计人员的脑海中。

针对我们的需求,我定义了一个参数P所需要包含的各个部分

  • 可识别的名称
  • 当前值:该参数当前的值(一定是合法值),供游戏读取
  • 参数类型:表示这个参数是离散型的,还是连续型的
  • 上下限数组:给连续型参数检查合法性使用,如果值不在取值区间中,则为不合法的参数值,用数组的原因,主要是可能有多个取值区间,[Vmin1, Vmax1], [Vmin2, Vmax2], … , [Vminn, Vmaxn]
  • 候选值数组:给离散型参数检查合法性使用,如果值不在候选值数组中,则为不合法的参数值。
  • 参数描述:对于该参数的简要描述
  • 趋势描述:对于该参数的变化所带来的影响的简要描述
  • 逻辑关联项数组:该参数所影响的参数项列表(P1, P2, … , Pn)
  • 被逻辑关联项数组:能影响该参数的参数项列表(P1, P2, … , Pn)

作为补充说明,对于上下限的定义,还需要有一个标志来表示是开区间还是闭区间。在逻辑关联项的数组中,和每个参数项还需要绑定一个非常重要的“关联方式”的实现。关联方式是非常灵活的,可能非常简单,就像我们前面举的那个例子,仅仅是加倍和减半,但有时可能非常复杂,有更多的逻辑计算,所以,我想可以调用一段代码来计算关联方式,可以是虚函数,也可以是一段脚本。更新关联项的方式是递归的,直到所有的关联项都被更新为止。文档化有几个部分组成,一个是名称,要求要可以识别,一个是两段描述,这两个部分很难做到要写成什么格式(文档的东西一般都是自觉自愿),但作为规范是要求定义参数的人能详细填写的,另一个部分就是关联项和合法性提示,这个可以用代码来生成文档信息(试想一下开发环境中的代码提示功能)。

我想,有了这样一个参数结构的定义,要实现一个参数系统来满足我上面的要求,并不是一件很难的事情,当然现在的这些只是我一个粗略的想法,还没有实现,有兴趣的同学可以实现一个,我有时间的话,也会做一个出来,因为它很独立,可以作为第三方库用在任何的引擎中,实现技巧的话,我想用“反射”(Reflection)做的话,对于序列化,对于做一个可视化的编辑器,对于文档化,都是挺有帮助的,值得一试。

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

Read More

我们在学习程序的时候,都会提到一些逻辑计算方面的事情,像与(and),或(or),非(not)等,在编程语言层面也提供了相关的符号来表述逻辑的计算。我们都知道,对于AI来说,逻辑是非常重要的,一些简单的AI系统,就是由if-else搭起来的一个庞大的逻辑网,里面包含了各种预设好的可能性,最后得到一个决策结果,这样的系统还有一个名字,叫基于规则的AI系统(Rule based AI System),当然随着游戏逻辑越来越复杂,这样简单的靠分支的系统已经不能满足要求了,所以出现了各种AI架构,像有限状态机(FSM),行为树(Behavior Tree),计划器(Planner)等等。但不管架构如何变化,我们在AI中还是充斥着各种逻辑的计算。

那为什么我们要用类来表示逻辑计算呢?这就要牵扯到我们一直在聊的行为树的问题了,我们知道行为树在每个节点上都需要一个称之“前提”(Precondition)的部分,用来帮助我们来决定此节点是否能被执行,所以这个“前提”就会包含一个逻辑表达式,要么返回False,要么返回True,对于不同的节点来说,它的“前提”基本上都不会相同,那自然的,我们就会从程序设计的角度来思考,如何去实现它。当然,方法有很多,不过如果从以下两个方面来考量的话,可以考虑用类的方式来做。

  1. 模块化(目的复用逻辑项)
  2. 为行为树的编辑器做准备(目的可视化的逻辑表达)

说到类的实现方式之前,我们可以先来看一下,一般代码中的逻辑表示,以c/c++为例:

 1: bool a = IsAOk();
 2: bool b = IsBOk();
 3: bool c = IsCOk();
 4: if(a && (b || c))
 5:     //do sth
 6: else
 7:     //do other things

这样的方式很直接,很简单,作为“前提”的实现也是很好的,比如我们可以提供这样一个类,然后让用户继承这个类,自己在isTrue的方法里写需要的逻辑计算。

 1: class Precondition
 2: {
 3: public:
 4:     virutal bool isTrue() const = 0;
 5: };

但考虑到我上面的两点需求,这样的实现就显得不够模块化,因为我们并不能复用上面的任何一个逻辑项,而且更重要的是,我们也没有办法在编辑器中“看到”它的逻辑表达。基于这样的原因,我们再来看看用如何用类来表示逻辑计算,还是用上面这个纯虚类,我们定义几个称之为类逻辑操作符的新类

  • PreconditionAND
  • PreconditionOR
  • PreconditionNOT
  • PreconditionXOR

这些类继承自上面的Precondition,并重写了isTrue方法,我们用PreconditionAND举例:

 1: class PreconditionAND : public Precondition
 2: {
 3: public:
 4:     PreconditionAND(Precondition* lhs, Precondition* rhs)
 5:         : m_Lhs(lhs),m_Rhs(rhs)
 6:     {}
 7:     virtual bool isTrue() const
 8:     {
 9:         return m_Lhs->isTrue() && m_Rhs->isTrue();
 10:     }
 11: protected:
 12:     Precondition* m_Lhs;
 13:     Precondition* m_Rhs;
 14: };

可以看到,这个类很简单,它接受两个Precondition的实例,然后在isTrue里再调用这两个实例的isTrue方法,并返回它们的And结果。对于其余的逻辑符号类,我们也用同样的方式来定义,值得注意的是,对于PreconditionNOT来说,它只接受一个Precondition的实例,因为Not是一元的操作符。

有了逻辑操作符,我们就可以复用Precondition的逻辑项了,比如

 1: Precondition* precon = new PreconditionNOT(
 2:                             new PreconditionAND(
 3:                                 new PreconditionA(), new PreconditionB()
 4:                             )
 5:                         );
 6: //ret means !(A && B)
 7: bool ret = precon->isTrue();

我们用类逻辑操作符链接了逻辑项,使之达到了复用和模块化的效果。对于这种方式,我们需要不断的整理和提炼,并预先定义一些我经常用到的一些单个的逻辑项,就像上面所示的A,B一样,然后通过类逻辑操作符,组合成我们需要的逻辑计算结果。对于这些单个的逻辑项,因为需要被复用的关系,所以要尽量保持内部的逻辑是简单的。

就我们想要的行为树编辑器来说,我们可以将这些预先定义好的单个逻辑项导出到编辑器,然后在编辑器里,我们通过类逻辑操作符,来组成出我们需要“前提”,这样,我们就可以在编辑器里直观的“看到”逻辑的表达式,也方便我们检查逻辑的正确性。

当然,这种方式比起第一种直接在代码中书写逻辑的方式,相对复杂,在开发过程中,team对于这种理念的坚持程度也有待验证(因为需要不断的优化逻辑表达式),但对于一种用类实现逻辑的方式,我觉得还是有必要在这里介绍一下。我在实践中,两种方式都用到过,各有利弊,对于极度追求模块化的设计理念,并且想做行为树的可视化编辑器的朋友来说,用类的方式还是非常值得一试的。

好,今天就聊到这里,希望对大家有所帮助。

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

Read More

在上次博文《关于调试AI的闲话(1)》中,提到一个利用共享内存实现的参数调试模块,我整理了一下代码,现在放出来,给大家参考,

下载地址:

GoogleCode下载点

也可用svn通过以下地址来得:

http://tsiu.googlecode.com/svn/branches/blogver/

编译方法:

用VS2005以上打开,选择Debug NoDx或者Release NoDx, 在AITest工程,ATInit.cpp中,有一个宏

#define APP_ONE

打开这个宏,编译一次生成一个可执行文件AITest.exe(在exe\aitest\中),重命名后(比如,AITestA.exe),再关闭这个宏,重新编译一次。

测试方式:

打开编译好的两个exe文件,在其中一个窗口中(有文字提示),按住鼠标左键移动鼠标,就可以看到在另一个窗口中有一个红色的圈在同时移动,另外绿色的圈也是通过这个模块来通信的。

参数调试模块:这次提到的模块代码主要是这两个文件,

TAI_RefValue.h
TAI_RefValue.cpp

说明:

  1. 使用方法可以参考AITest中的例子,注意,需要每帧调用Flush来刷新。
  2. 删除后内存回收的功能还没做,这个需要改变内存的组织方式来做,现在是用数组的方式,可能需要改成链表的方式来组织
  3. 不建议将RefValue用在临时变量里,因为观察一个临时变量的某个值没有什么意义

顺便说一下,整个分享出来的代码是基于一个叫“TsiU”的框架来做的,它是我一直在维护的一个自己用的小型的框架,我平时做的一些AI的sample,或者一些工具,都会基于这个框架,TsiU有一些基本的UI控件库,网络模块库,GDI绘图模块,D3D绘图模块等等,可以快速的做成一个小型的示例程序,很方便(具体可参考AITest),并且整个架构是用Object的方式来组织,非常容易理解和扩展。整个框架很轻量化,基本就是做了一些底层的基本的功能,这样我在平时做东西的时候,就不需要重新写底层了,把精力都放在高层的实现了。以后分享代码都会基于这个框架,大家也可以通过svn来随时update到我最新的改动。下图就是TsiU里的几个工程介绍,代码不多,大家想看的也可以自己看一下 :)

tsiu-project

————————————————————————
作者: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

项目进行到后期,越来越多是在修复一些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