游戏 AI - 6

AI 与硬件的关系

Posted by Kang Cai on July 12, 2020

AI 开发人员工作的最大限制是机器的物理限制。游戏 AI 没有数日的处理时间和 TB 级的内存,我们甚至无法使用运行游戏的计算机的所有处理器和内存。其他任务需要空间和时间,比如图形、声音、网络和输入。在团队中,不同团队的开发人员必须同时从事他们的专业工作,CPU和内存预算通常会有严格的限制。

学术界或商业研究中的 AI 技术没有得到广泛应用的原因之一是它们的处理时间或内存需求,在一个简单的演示中引人注目的算法可能会使成品游戏直接卡死。

本节讨论与AI代码的设计和构造相关的底层硬件问题,这里所包含的大部分内容是对所有游戏代码的一般建议,如果你只是刚刚接触游戏编程,只是想了解 AI 相关内容,你可以直接跳过这篇文章,问题不大

一、处理器问题

对游戏效率最明显的限制是它所运行的处理器的速度。最初,所有的游戏机都有一个主处理器,它也负责图形处理;而现在大多数游戏硬件都有几个 CPU,以及专门用于处理图形的 GPU。

一般来说,CPU 更快更灵活,而 GPU 更并行。当一个任务可以被分割成许多简单的子任务,所有的子任务都同时运行时,GPU 上的数十到数千个处理核可以比相同的任务在 CPU 上顺序运行快数量级。

显卡驱动程序过去有固定函数的管道,其中的图形代码被内置到驱动程序中,并且只能在狭窄的参数内进行调整,因此除了显卡上的图形之外,不可能做很多其他的事情。而现在的驱动支持 Vulkan、DirectX 11、CUDA 和 OpenCL 等技术,这些技术允许通用代码在 GPU 上执行,所以更多的功能被转移到GPU,从而释放了更多的 CPU 处理能力。

在过去的 20 年里,用于 AI 的处理时间持续地增长。随着处理器速度的提高,这对于希望应用更复杂算法的 AI 开发人员来说显然是个好消息,尤其是在决策和战略制定方面。但是,尽管处理器时间的不断改进有助于开发新技术,但它们并不能解决根本问题,许多 AI 算法需要很长时间运行。一个功能全面的寻路系统会花费几十毫秒运行每个角色,很显然,在一个有 1000 个角色的 RTS 游戏中,没办法每一帧都运行。

在游戏中发挥作用的复杂 AI 需要被分解成小的组件以便进行分帧,将类似的这些技术应用于高开销的 AI 算法可以将它们带入到实际的应用中。

在过去的10年里,这个行业的一个大变化是 C++ 不再称霸游戏编程。现在,角色行为、游戏逻辑和 AI 通常是用高级语言编写的,比如 C#、Swift、Java,甚至是脚本语言比如 Python、Lua、Javascript等。这一点很重要,因为这些语言为程序员提供了更少的微操能力,比如内存管理。有 AI 程序员仍然写 C++,这需要对处理器的性能特征有着比较好的基础知识,但这样的程序员往往是致力于 AI 引擎底层逻辑的专家:设计的函数会在多个游戏中复用。

二、内存问题

大多数 AI 算法不需要大量的内存,通常只有几M,这种小的存储需求,在一般的移动设备上很容易实现,对于像地形分析和寻路这样的重量级算法来说是足够的。大型多人在线游戏(MMO游戏)通常需要更大的存储空间,但运行在可以安装足够内存的服务器群上(即使这样,我们说的也只有GB 级别的内存,甚至更少)。巨大的世界通常被分割成单独的部分,或者角色被限制在特定的区域,进一步减少了 AI 算法对内存的需求。

因此,限制因素通常不是内存的大小,而是使用内存的方式。内存分配和缓存一致性都是影响性能的内存问题,它们都可以影响人工智能算法的实现。

2.1 内存分配与垃圾回收

分配是请求存放数据的内存的过程;当不再需要内存时,它被称为释放。只要内存足够用,分配和释放相对较快。

当为特定对象分配内存时,像 C 这样的低级语言需要程序员手动释放内存,,而像 C++ 和 Swift 这样的语言会提供引用计数,它存储该对象存在的位置的数量,当对象不再被引用时,计数器下降到 0,内存被释放。不幸的是,这两种方法都可能意味着应该释放的内存永远不会释放,要么是程序员忘记手动释放,要么是存在循环引用,这样它们的计数器就不会降为 0。

许多高级语言实现了复杂的算法来收集这些垃圾,释放不再有用的内存,但不幸的是,垃圾收集的成本可能很高。在 C# 这样的语言中,特别是在运行 Unity 游戏引擎的mono运行时,垃圾收集的速度会慢到足以延迟渲染帧,从而导致视觉上的停顿,这是大多数开发人员无法接受的。

因此,为高级语言实现 AI 算法通常会在一个关卡运行时尽量不分配和释放对象:整个关卡所需的数据在级别开始时保留,只有在级别结束时才释放本书中的几个算法假设可以随时创建新的对象,当不再需要时,将会释放,但在一个垃圾回收很耗时的平台上,优化这些实现可能很重要。例如:一些寻路算法在初始化时,会创建并存储地图中每个位置的数据,当路径完成时,不需要中间位置数据。对垃圾收集友好的实现可以创建单个寻路对象,其中包含地图中每个位置的数据;而当需要寻路时,同样的对象被调用,它使用它需要的预先分配的位置数据并忽略重复运算。上面的优化实现起来稍微复杂一些,而如果需要寻路的多个角色必须依次使用一个寻路对象则可能会复杂得多。为了避免使底层算法复杂化,本书后面的文章将以最简单的形式呈现了它们:不考虑分配

2.2 缓存

内存大小本身并不是内存使用的唯一限制。从 RAM(存储器,内存)访问内存并将其准备好供处理器使用所花费的时间远远长于处理器执行其操作所花费的时间。所以,如果处理器必须依赖主 RAM(主存),它们就会不断地等待数据。

所有现代处理器都至少使用一级缓存:处理器中 RAM 的副本,可以非常快速地进行操作。缓存通常在 pages 中获取,使得主 RAM 中整个模块流到处理器,然后它就可以被随意操纵。当处理器完成它的工作时,缓存的内存被送回主 RAM。处理器通常不能在主存上工作:它需要的所有内存必须在缓存上。操作系统可能会为此增加额外的复杂性,因为内存请求可能必须通过操作系统例程,该例程将请求转换为对实际或虚拟内存的请求。这可能会引入进一步的约束,因为具有相似映射地址的两个物理内存位可能无法同时使用,这被称为混淆故障(aliasing failure)。

多级缓存的工作方式与单一缓存原理一样:大量内存被提取到最低级缓存中,其中的一个子集被提取到每个更高级别的缓存中,处理器只在最高级别上工作。

如果一个算法要使用在内存中的数据,那么所需要的内存不太可能刚好都在缓存中,这些缓存缺失会造成时间上的损失。处理器为了某几个指令需要抓取一个全新的内存块到缓存中,然后它还将其流回并请求另一个内存块。一个好的 profiling 系统会在缓存丢失发生时告知开发者。以我的经验来看,即使是对于那些不会给开发者控制内存的语言,也可以通过确保将一种算法所需的所有数据保存在相同的位置、相同的几个对象中来实现显著的速度提升

在本书中,为了便于理解,我使用了面向对象的风格来布局数据。特定游戏对象的所有数据都保存在一起,这可能不是缓存效率最高的解决方案。在一个有 1000 个角色的游戏中,最好将它们的位置放在一个数组中,这样基于位置进行计算的算法就不需要在内存中不断跳跃。与所有优化一样,profiling 就是一切,但是在编程时考虑到数据的连贯性就可以获得还不错的效率。

三、平台

随着游戏产业集中在几个游戏引擎上,平台差异对 AI 设计的影响比以前更小了。例如,图形程序员仍然需要担心主机和移动设备之间的差异,但 AI 编程往往更通用。在这一节中,我将考虑游戏的每一个主要平台,并强调任何特定于AI代码的问题。

3.1 PC

个人电脑可能是最强大的游戏机,铁杆玩家会购买高端昂贵的硬件,但是由于缺乏一致性,这可能会让开发人员感到麻烦。拥有一对顶级显卡、SSD 驱动器和快速内存的计算机,与集成图形的廉价 PC 之间存在巨大差异。

底层开发者依赖于应用程序编程接口 api,比如 Vulkan 和 DirectX, 这样可以做到隔离大多数硬件细节,但开发者还是需要注意特性支持和接口效率。在 Unity 和 Unreal 这样的引擎上工作的开发者就更容易了,但可能仍然需要使用内置的特性检测来确保他们的游戏在所有系统上运行良好。

在 PC 上工作涉及到构建一个具备强兼容性的软件,往低了看,需要满足一个休闲游戏玩家的有限系统,往高了看,需要到核心玩家的最新硬件的软件。对于图形的缩放,可以采用合理地模块化,例如,对于低规格机器,我们可以关闭高级渲染功能,使用更简单的阴影算法,或者用简单的纹理映射代替基于物理的着色器。图形复杂性的改变通常不会改变游戏玩法。

但 AI 不一样。如果 AI 分配工作时间变少了,它应该如何应对?它可以尝试做更少的工作。这种做法和开发更多 “蠢” 的 AI 是一样的,都会影响游戏的难度等级。在较低规格的机器上让游戏变得更容易可能是不可接受的。类似地,如果我们尝试让 AI 还是执行同样负荷的工作,它就需要更长的时间,这可能意味着更低的帧速率,也可能意味着角色决策分帧之间的帧距就更多,而反应缓慢的角色通常也更容易对付。

大多数开发人员使用的解决方案是将 AI 的目标放在最小公分母上:技术设计文档中列出的最小规格机器。人工智能的时间与机器的能力完全不匹配。速度更快的机器在 AI 上使用的处理预算比例更低。然而,在许多游戏中,可扩展的人工智能是可行的。许多游戏使用 AI 来控制周围的人物:行走在人行道上的行人,为比赛欢呼的人群,或者天空中成群结队的鸟儿。这种 AI 可以自由伸缩:当处理器时间可用时,可以展现更多的 AI 角色

3.2 主机

主机游戏比 PC 游戏更容易适配,因为你确切地知道你的目标机器环境,也不怎么需要担心推陈出新的硬件或不断变化的 api 版本。使用次世代技术的开发人员通常没有最终机器的确切规格或可靠的硬件平台,但大多数主机开发都有一个相当固定的目标。

主机制造商通常会进行技术要求检查表(The technical requirements checklist,TRC)过程,用于解决诸如帧率之类的问题,然后对游戏的运行设定最低标准。这意味着 AI 的预算可以锁定在一个固定的毫秒数,反过来,这使得我们能容易发现哪些算法可以使用,也更容易找到一个固定的优化目标。

游戏引擎同时支持 PC 和目标主机,会使得跨平台开发比过去更容易。幸运的是,现在很少有AI开发人员在开发游戏时会跟特定主机的底层细节打交道,几乎所有底层代码都由引擎或其它中间件处理。

3.3 移动端

苹果在 2007 年推出了 iPhone,开启了一场自 80 年代家用游戏机潮以来最大规模的游戏革命。在 2006 年左右的时候,手机游戏主要包括了专用的掌上游戏机,比如PSP 和任天堂的 GameBoy advance。而现在几乎 100% 的移动端市场都是手机和平板电脑

这个领域有两种平台:苹果的iOS设备(iPhone、iPad、iPod Touch)和 Android。在以前,这些设备上的开发差异很大,通常会要求游戏为每种平台单独编码。尽管两者都可以使用底层语言,如 C 和 C++,对于高级语言,苹果鼓励使用 Swift(以前是Objective-C), Android 鼓励使用 Java(或者编译成 Java 字节码的语言,比如 Kotlin)。

主要的市场份额游戏引擎,比如Unreal和Unity,以及许多较小的竞争对手,比如 Godot,都使用相同的游戏代码支持移动平台,这使得特定平台的实现变得没有必要,移动开发人员使用这些工具很大的转变了跨平台的工作方式。

能够运行游戏的智能手机是功能强大的机器,可与游戏机媲美,这使得在 PC 或主机运行的 AI 与可在移动设备上运行的 AI 之间不再有任何实际区别。手机可能需要更简单的图形或更小的人群规模,但在算法方面,都可以用相同的技术。

3.4 虚拟现实和增强现实

从 2016 年开始,虚拟现实和增强现实都被大肆宣传,技术和市场瞬息万变,现在说的结论在两年内就可能被验证是错的。

虚拟现实(VR)试图通过提供一种立体的 3D 视角让玩家沉浸在游戏世界中。玩家的动作可以被硬件检测到,并作为游戏中的动作纳入其中。VR 需要为每只眼睛渲染不同的场景,为了减少眩晕感,通常需要更高的帧率,比如 90 fps。

到目前为止,大多数虚拟现实设备都是与一台已有的游戏机相连的,比如 PC (Oculus Rift 和 Vive)、主机机 (PlayStation VR) 或 手机 (Gear VR)。目前,一些公司开始发布基于移动处理器的独立 VR 产品,其性能与高端手机差不多。

增强现实 (AR) 使用半透明的显示器将计算机生成的元素添加到现实世界中。尽管微软在 2016 年初发布了一个开发工具包,但消费者版还没有发布。Magic Leap 于 2018 年发布了他们的产品,但需求有限。增强现实也可以指使用手机摄像头并在捕捉到的图像中添加计算机生成元素的游戏,例如,《Pokémon Go》被认为是一款增强现实游戏,但并不需要专业的硬件。

虽然 VR 游戏的视觉呈现可能是非常规的,但游戏逻辑很少是非常规的。大多数商业游戏引擎都能通过手机的摄像头支持 VR、AR,并在产品发布时提供硬件支持。VR 和 AR 游戏在设计上非常相似,不需要非常规的 AI 算法。这些平台是否会带来新的设计可能性以及能否成为该行业的重要组成部分还有待观察