用游戏内置记录器系统调试AI | Game AI Pro

Debugging AI with Instant In-Game Scrubbing
David Young
6.1 Introduction
实时捕获AI的错误信息,对理解发生了什么逻辑错误,以及错误产生的根本原因至关重要。
不幸的是,当你看到AI发生错误的时候,往往意味着已经丢失了大量的关键信息。理想情况下,如果当错误发生的时候,整个游戏能重新恢复到错误发生的时间点,那解决BUG和理解BUG发生的原因将变得十分轻松。
游戏引擎通常使用确定性回放方法(Dickinson 2001)来处理这些问题,该方法使整个游戏状态回溯到发生问题的时间点,然后一遍又一遍重现这些问题。不幸的是,将目前还不是“决定论”的引擎改造成“决定论”不是一件容易的事,需要仔细的维护以防止无意中破坏“决定论”(determinism?不清楚什么意思)。
幸运的是,调试后的AI没有再出现这些问题。游戏录制系统提供了记录所有相关数据所需的功能,并允许游戏模拟以可视化的方式在时间线上移动,以了解错误发生的时间、地点和原因。本章将阐述Activision Treyarch开发的3A游戏创作中使用的游戏录制系统的架构。
6.2 In-Game Recorder
在较高的层次上,游戏内置记录器是一个相对简单的概念:在每个模拟帧完成后,遍历每个游戏对象,并记录再现游戏对象的精确视觉状态所需的最少数据量。要重放录制的数据,请暂停游戏模拟,并从单个模拟帧还原每个录制的游戏对象的所有录制数据。为了将记录系统有效的应用在游戏开发中,还必须考虑其他现实因素。游戏对象的数据记录不能对游戏运行速度产生影响,系统的内存要求必须是固定的,游戏对象的数据记录必须不会改变任何原本的结果。
考虑到这些要求,本章的其余部分将详细介绍用于创建游戏内置录制系统的每个主要组件,以及使用录制的调试信息和导航播放(也称为拖动播放)来调试人工智能的实用操作。
6.2.1 Recorder Architecture
记录数据的最基本的构建块是存储在记录的游戏对象上的单个数据包。在游戏的每个模拟帧,每个对象构造一个记录数据包,存储位置、方向、联合矩阵和可见性等信息。每个模拟帧记录的所有数据包的整体将波动,并集中存储在记录数据的帧内。每个记录帧都用模拟的绝对时间以及最后一个记录帧之间的相对时间作为时间戳,以便于快速检索和准确回放。
所有被记录的模拟帧都由一个记录器管理器存储和管理,该管理器提供与记录器系统交互以及更新每个子系统以记录对象数据之间的接口。管理器在内部存储一个固定大小的内存缓冲区,该缓冲区分配和释放每个数据包和每帧记录器数据消耗的内存。当存储器缓冲区内没有额外的可用存储器可供分配时,为了分配足够的存储器来存储新帧的分组数据,将释放最旧的记录数据帧。
序列化记录器数据的责任留给单个记录处理程序对象去实现,这些实现仅根据为其分类的对象类型来序列化记录数据。在实践中,只需要几个种类的记录处理程序来覆盖录制动画模型、第一人称视图模型、玩家角色和玩家摄像机。为了处理记录数据的回放,回放处理程序对象根据其分类对象的类型实现反序列化功能。通常,记录处理程序和回放处理程序一对一映射,这些处理程序已注册供记录器使用。
系统的最后一个组件处理用户在记录回放期间的交互。当启用记录器回放时,专用控制器实现将覆盖系统默认控制器实现。控制器方案提供了各种功能按键,如倒退、快进和单步执行记录数据的各个帧。此外,控制器方案允许选择特定的感兴趣对象,来减少不需要的多余调试信息。
图6.1显示了记录器架构的每个子系统之间的高层关系以及系统使用的内部数据的所有权。

6.3 Recording Data Primitives
由于记录器只是过去信息的可视化,因此在构造原始包数据时,只需要将重建游戏对象状态所需的最小视觉信息序列化。通常,至少,这段数据包括游戏对象id、对象类型、对象的位置,以及在录像机回放期间对象是否可定位。图6.2显示了基于记录的原语类型的不同类型的记录器数据包信息的示例。

对图6.2中特定的包类型 RecorderModelPacket,让我们分析每个游戏对象都存储了哪些信息。首先是ID,表示游戏对象在整个游戏引擎中的唯一标识符。尽管包中没有存储有关特定模型的信息,但是使用唯一id可以检索该信息。
存储的下一个字段定义id对应的对象类型,并通过引擎对特定游戏对象进行分类。
指向下一个分组的指针被存储在每个记录器分组中,以形成帧内所有分组数据的单链表。数据包的单链表被用作记录帧的内部数据结构,因为单个数据包可以有不同的大小,这取决于它们序列化的游戏对象。例如,记录模型数据取决于特定模型具有的骨骼数量,尽管游戏对象类型相同,但不同模型的骨骼数量也不同。
下一个字段表示是否应该选择模型,以便在录制器播放期间切换其他录制的调试信息。
位置字段是不言而喻的,表示对象在世界中的当前位置。
“角度”字段表示模型的当前方向。
最后一个字段表示模型中所有骨骼的当前方向和位置。如果模型没有骨骼,则此字段保留为空,否则将存储骨骼矩阵数组。尽管存储游戏对象的整个骨骼的详细表示可能有违直觉,但存储动画骨骼矩阵比直接存储动画数据更可取,以便考虑物理驱动或反向运动学骨骼约束。
除了存储游戏对象的数据外,记录器还存储额外的调试原语(primitives),它提供了许多使用记录器系统的调试功能。公开与特定游戏对象相关联的多个调试原语,如线条、球体、长方体、多边形和文本原语,可以方便地可视化人工智能系统的底层决策结构。在正常的游戏模拟过程中,只要调用记录器中相同的调试原语,就能在运行时和刷洗记录器回放(scrubbing through recorder playback)时实现相同调试功能。
虽然以这种方式记录每个游戏对象在开发过程中有好处,但有选择地仅记录感兴趣的游戏对象,可以延长由固定内存预算分配的录制时间。录制通道设备的使用,允许用户在系统内任意时间点选择特定的对象类型来录制。 每个游戏对象类型都可以归类为属于一个或多个频道分类,如AI、动画、脚本和fx。调试原语还可以指定它们应该属于哪个通道,以便在调试特定系统(如动画或脚本)时适当显示它们。
6.3.1 Record Handlers
由于每种游戏对象类型对序列化可视表示的要求通常都有显著的不同,记录器公开了一个简单的接口来实现不同的记录处理程序。记录处理程序主要在记录器中提供两个不同的功能:计算任何特定游戏对象实例所需的内存量,以及在指定的分配内存中序列化游戏对象类型。每个记录处理程序实例依次向记录器注册,该记录器指定处理程序能够序列化的游戏对象的类型。
当记录器正在处理游戏对象以进行序列化时,首先将相应的记录处理程序排队,以确定构造有关游戏对象的信息的重播包所需的内存量。如果内存缓冲区中存在足够的可用内存,记录器将立即调用传入请求内存的记录;否则,记录器必须首先释放内存缓冲区中最旧的记录数据,直到可以分配请求的大小。
将序列化功能从记录器的主要功能中分离出来,允许游戏引擎扩展记录器对新游戏对象类型的支持,而无需修改记录器系统本身。特别是,通过创建新的记录处理程序并在记录器注册期间分配记录处理程序的实例,可以轻松实现新的游戏对象类型。
但要注意的一个方面是记录处理程序内存系统的不透明性。传递到记录函数的重放数据包内存只是从记录器的内存缓冲区传递的未初始化内存。记录器本身只知道如何解释初始记录器数据包字段:id、type、next、targetable、position和所有记录数据包之间共享的角度,但不知道如何解释不同数据包类型中剩余的附加内存。为了正确地解释额外的内存,记录处理程序必须与回放处理程序协同工作。
6.4 Recording Simulation Frames
来自单个模拟帧的所有记录器数据包存储在单个记录器帧结构中。图6.3显示了典型记录器帧实例的数据结构。第一个字段id表示记录器帧的唯一标识符。接下来的两个字段存储指向上一个录制帧以及下一个录制帧(如果存在)的指针。
由于帧时间可以变化,且能正确地将记录帧与特定的时间戳关联在在一起,这在记录器系统中是至关重要的,同时存储模拟的绝对时间以及自上次记录的帧以来的增量时间,以便播放原汁原味的录制帧。最后一个字段 packets 指向与帧相关联的记录器分组数据的单链接列表的头部。由于包数据的遍历始终是线性的,因此链表足以满足快速访问的时间需求。

管理记录器数据的每个模拟帧主要集中在当在时间上向前或向后遍历帧时快速访问相邻帧数据。每个新记录的数据帧存储一个指向第一个记录器数据包的头部指针以及指向附加模拟帧记录的上一个和下一个地址的指针。使用双链接列表数据结构可以在添加或销毁帧时实现最小的维护,因为新帧总是添加到列表的开头,而删除帧则是从列表的结尾剪除的。
为了能够正确地回放记录的帧,通过存储游戏世界的绝对时间以及自上次记录帧以来的增量时间,能确定任意记录帧表示的游戏世界中的任意特定时刻,并在时间步中计算差异。即使大多数游戏模拟运行在固定的时间步,一个模拟帧的实际时间量可能超过分配的时间预算。在每帧中存储相对增量时间允许以与原始捕获率相同的速率重放数据。
即使存储了绝对时间戳和相对时间戳,为了简单起见,其他系统与记录器的交互严格基于游戏时间。计算正确的游戏时间利用每帧数据中存储的绝对时间,而向后和向前擦除时间只利用帧中存储的增量时间。
6.4.1 Recorder Manager
管理记录器系统围绕着两个不同的职责,通过标准的轮询更新循环记录模拟帧后的数据,并在擦洗记录帧时重放记录的数据。图6.4显示了记录器管理员的成员数据和外部接口的高级类图。

记录器管理器中最关键的内部任务之一,就是正确分配和释放记录器数据包和帧。在内部,管理器将内存缓冲区用作循环缓冲区,其中丢弃最旧的记录帧,以允许记录新帧。管理器的初始化从分配一个连续的内存块开始,以成为缓冲区内部管理的内存。在管理器开始记录游戏对象数据之前,记录处理程序和回放处理程序就会开始注册。
游戏引擎的整体管理和记录系统的更新计时由记录管理器的单个实例处理。在游戏模拟循环结束后调用记录器管理器的更新函数,以确保在记录处理程序处理每个游戏对象之前,已完成对游戏对象的所有处理。当记录器管理器处理每个游戏对象时,首先根据对象类型对游戏对象进行分类,并使用相应的记录处理程序来确定从缓冲区请求的适当内存量。一旦分配了内存,记录器管理器将分配的内存和游戏对象直接传递给记录处理程序进行序列化。信息包序列化后,管理器将正确地修复每个数据包指针,并将帧的起始指针分配给记录的第一个信息包。
一旦记录器帧内的所有分组数据被序列化,管理器将更新当前记录器数据第一帧的对应下一帧和上一帧指针,以将新记录的数据帧附加到双链接列表。新的数据帧现在成为管理器在请求回放时使用的第一个数据帧。
附加信息存储在管理器中,以允许快速访问最近记录的数据帧和最旧记录的数据帧,以便在释放帧和分配新帧时清除和更新双链接列表。在录像机回放期间,正在回放的数据的当前帧被单独存储,以便快速恢复游戏中的擦除以及有选择地向前和向后移动。
6.4.2 Memory Management
由于记录器在固定内存预算内连续在后台运行,每当内存缓冲区无法分配请求的内存量时,记录器管理器将连续释放最旧的记录帧,直到有足够的可用内存来容纳新的分配请求。连续释放最旧的记录帧并用新的帧数据替换这些帧,在内存缓冲区内实现最小的内存碎片,从而使缓冲区充当循环缓冲区,即使只使用连续内存。
图6.5显示了当帧被连续添加到缓冲区中时的内存状态,直到剩余的可用内存量不足以容纳额外的一帧记录器数据。

在本例中,图6.6显示了内存缓冲区如何处理第一帧存储数据(第1帧)的释放,该帧立即被第3帧的数据替换。作为循环缓冲区,使用此设置时,唯一无法存储的部分将位于最后记录的帧和第一个记录的帧之间,以及位于内存缓冲区分配的内存尾部的不可用内存的某些部分。

6.5 Frame Playback
为了回放录制的帧,需要考虑一些因素,以防止影响游戏模拟本身。由于恢复记录的数据(如位置和方向)将直接作用于游戏对象的当前状态,因此记录器必须等到记录最新帧的分组数据结束后,才能暂停游戏模拟并覆盖游戏对象数据。相反,在录像机退出播放模式之前,必须将上次录制的帧数据重新应用到每个游戏对象,以使每个游戏对象在启用播放模式之前处于相同的状态。
当向记录器管理器请求回放模式时,除非外部源已请求记录器及时跳回特定的时间戳,否则模拟将暂停,而无需记录器执行任何其他工作。在这种情况下,当基于时间戳遍历帧数据时,记录器将从最新记录的帧数据开始,并在数据的链接列表中向后工作,以找到与所请求的确切时间戳(紧靠在所请求的时间之前的第一个时间戳)匹配的帧,或链接列表中最旧的剩余时间戳。
在时间上向前和向后擦洗的工作原理与游戏世界的时间完全不同。擦洗可以是向前或向后移动一帧数据,也可以以与原始记录完全相同的速率重放数据。实时擦除时,记录器的更新循环将保持自请求擦除以来的增量时间,并且仅基于每个帧数据的累积增量时间连续向后或向前移动重放帧数据。基于原始录制速率管理时间增量允许重播数据帧,而不考虑游戏的增量时间,这可能与每帧记录器数据之间的实际增量时间不同。
6.5.1 Controlling Recorder Playback
一旦进入回放模式,使用自定义控制器方案覆盖标准控制器方案将提供操作回放所需的灵活性。尤其是,对于与回放交互而言,至少两种快速转发和重绕的操作速度是至关重要的。能够以正常的游戏模拟速度播放数据,可以对动画工作进行微调,而每次只需将录制器移一帧,这通常是调试决策逻辑所必需的。由于行为选择通常是在特定的模拟帧上确定的,因此所有相关的记录调试原语通常只在单个记录帧期间绘制。此外,控制器方案用于有选择地确定在回放期间呈现哪个游戏对象的调试原语,这有助于缩小对任何特定游戏对象的调试范围。
6.6 Debugging AI
在以前的Treyarch中,AI广泛地使用了一个录制系统来进行动画、决策和指导调试。为了调试动画播放和选择,人工智能的整个动画树都会被记录下来,每个人工智能的动画贡献、速率、标准化时间和选择标准都可用。在调试动画弹出窗口、不正确的动画混合和不正确的动画选择时,能按时精确地来回移动是关键。
使用记录器调试决策允许在游戏中记录人工智能的整个行为树,该行为树显示树中当前正在执行的分支以及其他可能的决策分支中的哪些分支过早终止执行。每当一个人工智能表现出不正确的行为或者没有选择正确的行为时,立即进入录像机回放是很简单的;及时向后拖动,并立即理解行为树的哪个分支没有按预期执行。
使用记录器调试与转向相关的错误,通常涉及理解和解决不在预期中的速度振荡或错误的动画选择,这导致人工智能彼此冲突或其他障碍。
6.6.1 Synchronizing External AI Tools
除了在游戏中利用记录器调试之外,还可以直接在当前时间戳中引入外部工具。使用自定义消息总线系统,记录器将广播当前时间戳被清除的内容,这允许其他工具将外部存储信息的回放与记录器的可视回放同步。利用相同的消息总线,记录器也可以通过其他工具进行外部控制,以进入回放模式并跳转到特定的时间戳。此外,在引擎中添加了一种称为记录器断言的新型断言,它将暂停游戏模拟,并将摄像机立即设置为特定的游戏对象,通知所有其他外部工具通过记录器与目标游戏对象同步。
6.7 Conclusion
想要找出人工智能出问题的根本原因是个很大的挑战,更不用说还得在需求不断变化、功能不断迭代的开发过程中去完成这个任务。尽管记录器并不是万能的,也不能阻止BUG的产生,但它能直接在生产层面上进行调试,而无需在用户层面去重现问题,这使得它成为了一个十分重要的生产工具,可以节省非常多的工时。
任何调试信息或文本输出的记录,都无法在即时性上与记录器相比。Treyarch人工智能的过去和现在的发展,都使用一个记录器作为中心工具,围绕这个工具和过程来创建其他的工具和过程,这样一个系统的好处还在持续的被挖掘中。
References Dickinson
P. 2001. Instant replay: Building a game engine with reproducible behavior. Gamasutra. http://www.gamasutra.com/view/feature/131466/instant_replay_building_ a_game_.php
Llopis, N. 2008. Back to the future. Game Developer Magazine. http://gamesfromwithin.com/ back-to-the-future-part-1,http://gamesfromwithin.com/back-to-the-future-part-2 (accessed June 11, 2016).
文章来源:http://www.gameaipro.com/
如侵犯版权,请联系译者删除。
翻译不易,读者若需要转载,请注明出处。
若有错误,欢迎指正。