关于调试AI的闲话【转载】

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

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

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

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

1 class AIExampleClass
2 {
3 EXPORT READONLY m_RDValue;
4 EXPORT WRITABLE m_WRValue;
5 }

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

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

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

在上面,我谈到了一个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调试来说,很多bug(或者说行为异常),很难找到一个切实的重现的方法,经常是看到后,加好断点,想刻意再玩一下却很难再玩出来了。所以,如何抓住现场,是在AI调试中的一个很值得去考虑问题。我们很自然的会想到一个解决方案,那就是“回放”。当看到问题时,马上“录像”,然后把这个场景再回放一遍,甚至是可以回放任意遍。这确实是一个自然而完美的方法。一般来说,游戏中的回放分两种:

  • 逻辑回放(logical playback)
  • 结果回放(result playback)/ 画面回放(screen playback)

逻辑回放是指,能回到过去的任意时刻,运行游戏的整个逻辑,保证相同的输出结果。结果回放(画面回放)是指,能回到过去的任意时刻,将逻辑运行的结果重新显示出来。简单来说,逻辑回放中所记录的游戏的数据是输入,而结果回放(画面回放)记录的数据是输出,见下图:

ai_debug_1

这两种的回放的实现都依赖于引擎的结构,和引擎实现的耦合度很高,很难做到独立和通用的模块,对于AI调试来说,显然逻辑回放是最好的,因为它能让我们再运行一次游戏逻辑,这样就可以通过设置断点来调试了。但是,现在的引擎一般都是多线程引擎,既然是多线程,就存在一定的时序问题,要做到同样的输入和上下文,两次结果完全一致会相当的困难,除非引擎从设计之初就考虑逻辑回放的问题,如果是改造这样的多线程引擎,工作量会比较大。如果是单线程的引擎,实现起来会容易很多,一般而言,逻辑回放主要记录几个内容:

  • 时间信息
  • 设备输入信息(比如,手柄,键盘,鼠标等)
  • 随机数信息
  • 当时的游戏世界上下文

对于第4点是针对从任意点回放而言的,如果当前的上下文信息不是很容易获取,可以考虑不支持任意点回放,而是每次都从头开始回放,这样实现起来更容易一点。

就如我前面所说,对于多线程引擎来说,逻辑回放的实现比较困难,那退而求其次,我们可以选择结果回放,结果回放记录的信息就比较“单纯”,就是每次画面需要画的那些Object信息,比如位置等等,回放时,我们只需把记录的结果再重新给渲染器,让它画出来即可。也许大家会问,我们没法重新调试AI的逻辑,这样的回放有什么意义呢?当然,如果只是单纯的把画面重画一遍,是没什么意义,结果回放需要结合另一个AI调试的方法一起来使用。那就是“调试信息”(Debug Draw)。一般引擎都提供一套可以在屏幕上画点圈,画点叉,写点字的接口,AI的调试,就是可以使用这样的接口在屏幕上打印出关键的信息,来帮助我们查看逻辑的“走向”。举个例子来说,我以前写过一篇博文,介绍用分数系统来做AI,如果要调试这样的AI,我们就可以利用调试信息,在屏幕上,将分数的情况都打印出来。由于结果回放是将所有输出到渲染器的信息都保存了下来,所以我们就可以通过回放来观察这些分数的变化,以此来调试AI的行为。结果回放的另一个优势是,它可以轻松的实现任意点的回放,包括后退,前进,暂停等等,因为结果数据和上下文是无关。

不管有没有回放的机制,很多时候AI的调试,都需要调试信息的帮助,可以让我们不用设置断点就知道逻辑的计算,我们甚至需要制作一些工具来获取当前AI的相关状态,比如前几篇说到的那个观察器。这也就是AI调试比较难的地方,一个字,“猜”,当然是有依据的猜,更准确的说,应该是推测吧。

总结下,要有好的AI调试体验,我们需要有好的AI架构,这是一切的基础,比如脚本,比如回放,其次要有好的配套工具,来辅助那个“猜”,并能形成独立可复用的模块,然后就是对于标准的遵守,比如不要用“魔数”的问题,当然,还有很多值得我们去想的,这一系列也希望能抛砖引玉,引起大家的思考吧。

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

posted @ 2018-01-17 23:07  前程路88号  阅读(234)  评论(0编辑  收藏  举报