游戏 AI - 3

算法与数据结构

Posted by Kang Cai on July 3, 2020

提前说明一下,这篇文章没有具体的介绍 AI 的实现,只是对后面其它文章的一个纲领性的概述。

实现本书描述的技术有三个关键要素:算法本身,算法所依赖的数据结构(表征形式),以及向算法表示游戏世界的方式

一、算法

算法是为人工智能问题生成解决方案的一步一步的过程。我们将着眼于在游戏关卡中生成路径以达到目标的算法,确定向哪个方向移动以拦截逃跑的敌人的算法,了解玩家下一步应该做什么的算法,以及其他许多算法。

数据结构是算法的另一面。它们以一种算法可以快速操作的方式保存数据,从而得到解决方案。通常,数据结构需要针对一个特定的算法进行调整,它们的执行速度是内在联系在一起的。

你将需要知道一组元素来实现和调优算法,这些元素依次如下:

  1. 算法要解决的问题;
  2. 解决方案如何工作的一般描述,包括需要它们的图表;
  3. 算法的伪代码表示;
  4. 支持算法所需的数据结构的指示,必要时包括伪代码;
  5. 必要时寻求实现建议;
  6. 算法性能分析,包括执行速度、内存占用和可伸缩性;
  7. 方法的弱点。

通常,我将逐渐介绍一系列越来越复杂和功能强大的算法。介绍较简单的算法是用来帮助你了解复杂算法的结构,所以对这种简单算法的描述比整个系统略为粗略。

1.1 表现性能

游戏 AI 中的一些关键算法有上百种变体,这本书不能指望把它们全部编目并加以描述。在描述关键算法时,我通常会用较简短的术语介绍关于它的主要变种。

我会尽可能地对每个算法都列出算法的执行属性,即执行速度和内存消耗。执行速度和内存消耗通常取决于所考虑问题的大小,我使用了标准的符号 “O()” 来表示某个问题大小下的主要复杂度

一个算法可以被描述为 O(n log n) 的执行和 O(n)的内存,其中 n 通常是问题的某种组成部分,例如该区域中其他角色的数量,或该等级对应的属性增强个数。

关于计算机科学的一般算法的好文章将给出一个完整的数学处理 O() 值是如何得到的,以及它们对实际算法性能的影响。在这本书中,我省略了推导和证明。在过于复杂的情况下,我指出文本中的近似运行时间或内存,而不是试图推导准确的 O() 值。

有些算法具有误导性的性能特征;有可能故意设置极不可能的情况,使他们表现得很差;在常规使用中,它们会有更好的性能。在这种情况下,我试图同时指出预期和最坏情况下的结果。除非另有说明,否则您可以安全地忽略最坏情况的值。

1.2 伪代码

为了简洁和简单,这本书中的算法以伪代码的形式呈现。伪代码是一种虚构的编程语言,它去掉了任何实际编程语言特有的实现细节。它应该充分详细地描述算法,以便您可以用您选择的语言实现它。与你在算法方面的理论书籍中找到的伪代码相比,本书中的伪代码更像一种过程式编程语言。此外,我使用伪代码来描述数据结构和接口以及算法,因为本书中的算法通常与周围的软件紧密联系在一起,用代码可以更自然地捕获这些算法。

许多 AI 算法需要处理相对复杂的数据结构:列表、表、优先级队列、关联数组等等。一些语言提供这些内置的数据结构,另一些则将它们作为库或通过函数访问。为了使情况更清楚,伪代码将这些数据结构视为语言的一部分,以起到简化代码的作用。

下面的例子是一个从未排序数组中选择最大值的简单算法的伪代码:

编程职业者可能会注意到,伪代码与 Python 编程语言的相似之处不止于此,有时还会出现类似 ruby 的结构和丰富的 Lua。Python是一种易于阅读的语言,这是有意为之的。尽管如此,这种相似性只是表面的,上表仍然是伪代码,而不是Python实现,这种相似性并不意味着语言或实现上的偏差。

二、表征形式

游戏中的信息通常需要转换成适合 AI 使用的格式。通常,这意味着将其转换为不同的表示或数据结构。游戏可以将关卡存储为 3D 几何网格,角色位置存储为 (x;y;z) 的世界位置。在这些表示之间转换是一个关键的过程,因为它经常丢失信息(这就是关键:为了简化不相关的细节),所以总是冒着丢失错误数据的风险。

选择正确的表示是实现 AI 的关键元素,某些表示在游戏 AI 中尤为重要。书中的几个算法要求游戏以特定的格式呈现给他们。

虽然非常类似于数据结构,但我们通常不会直接担心表示是如何实现的,而是关注它呈现给 AI 代码的接口。通过创建正确的粘合代码将游戏数据转换为算法所需的表示,这使得您可以更容易地将 AI 技术集成到游戏中。

例如,假设我们想要计算出一个角色是否感觉健康,作为决定其行为的算法的一部分。我们可能只需要带有一个方法的角色类,表示如下:

然后,您可以通过检查角色的健康值、为每个角色保留一个布尔值“健康”,甚至通过运行一个完整的算法来确定角色的心理状态及其对自身健康状况的感知来实现这一点。就决策程序而言,价值是如何产生的并不重要。伪代码定义了一个接口,可以以您选择的任何方式实现该接口。

当一个数据表示形式特别重要或棘手时,我将更深入地描述可能的实现。

三、代码实现

甚至在十年前,大多数开发人员都在他们的AI代码中使用 c++,在那之前的10年,有相当一部分人依赖于 c。现在的游戏开发更加多样化:Swift 和 Java 用于移动平台,c# 用于unity游戏引擎,JavaScript 用于网络;还有许多其他语言比如 Lisp、Lua 或 Python,特别是作为脚本语言;ActionScript 为少数剩下的 Flash 开发人员。我个人曾与所有这些语言一起工作过,所以我尽可能地独立于语言,同时仍然给出一些关于实现的建议

其中,C 和 c++ 仍然用于那些必须尽可能快地运行的代码。在某些地方,将重点讨论数据结构和优化,因为优化是 c++ 特有的。