游戏中的人工智能
本文概述了现代电脑游戏中的关键技术和算法。
本文由Janusz Grzyb撰写,最初发表于2005年6月号的《软件开发者杂志》。您可以在SDJ网站找到更多文章。
引言
电脑游戏中运用的人工智能元素经历了漫长的发展。最初,所开发的系统基于直接写入游戏代码中的规则集,或基于代码解释的行为脚本,整个系统通常基于在选择适当行为过程中,对随机因素重要性的恰当选择。那个时代诞生了诸如不朽的《River-Raid》、《大金刚》、《Boulder-Dash》等令人难忘的游戏,以及20世纪70年代八位机用户的许多其他痴迷对象。
发展过程的另一个步骤是引入简单的计算机科学方法,例如仍然流行且频繁使用的有限状态机方法,用于描述计算机控制的敌人的行为。然而,随着玩家需求的日益增长,游戏变得越来越复杂,这得益于使用越来越先进的计算算法。RTS(即时战略)类型游戏的时代到来,使得人们对确定地图上两个指定点之间最佳路径的算法产生了显著的兴趣转移(就使用频率而言)。
快速的技术进步和家用电脑处理能力的迅速提升,也推动了电脑游戏中人工智能应用的发展。最初的游戏和人工智能算法不得不受限于当时机器的有限能力,处理器频率不超过2兆赫兹。第一批PC带来了新的可能性和新的应用。在配备386/486处理器的PC成为家用电脑的标准后,程序员们获得了新的可能性;这导致了游戏开发公司之间的竞争。长期以来,电脑游戏质量的最重要指标是其三维图形的质量;然而,很快人们就意识到,漂亮的图形、音效和角色动画并非全部。最近,人工智能已被确定为电脑游戏最重要的元素之一,它是当今视频游戏所谓“可玩性”背后的主要因素。
电脑游戏的制作过程也发生了显著变化。尽管过去游戏的AI编程曾受到略微不公平的待遇,其实现往往被推迟到游戏引擎制作的尾声,但目前,规划人工智能模块及其与游戏其他组件的协作已成为规划过程中最重要的要素之一。
现在,一个编程团队中,通常会有一名成员从项目开始就全职负责人工智能模块的设计和编程。
目前,大多数家庭都能找到配备主频在3到4GHz的奔腾四处理器PC级电脑,这使得电脑游戏能够利用最先进、最复杂的人工智能方法:神经网络、遗传算法和模糊逻辑。在互联网和网络游戏时代,游戏中的人工智能系统被赋予了新的任务:电脑玩家在行为和游戏策略上,应与互联网另一端的真实玩家难以区分。
游戏人工智能发展中的里程碑
在讨论电脑游戏中人工智能的演变时,我们绝对应该提及那些在游戏智能行为发展中成为里程碑的游戏。
20世纪90年代最受欢迎的游戏之一是暴雪工作室开发的《魔兽争霸》。这是第一款如此大规模地运用寻路算法的游戏,游戏中有数百个单位参与大规模战斗。Maxis公司创作的《模拟城市》是第一款证明A-Life技术在电脑游戏领域可行性的游戏。另一个里程碑是2001年Lionhead Studios创作的《黑与白》,该游戏首次使用了电脑控制角色学习的技术。
FPS类游戏中的AI
FPS类游戏通常采用人工智能系统的分层结构。最底层的处理最基本的任务,例如确定到达目标(由层次结构中更高层确定)的最佳路径或播放适当的角色动画序列。更高的层次负责战术推理和根据AI代理当前策略选择其应采取的行为。
寻路系统通常基于描述世界的图。图的每个顶点代表一个逻辑位置(例如建筑物中的房间或战场的一部分)。当被命令前往给定点时,AI代理会使用图获取它应该依次前往的后续导航点,以到达指定的目标位置。在导航点之间移动时,AI系统还可以使用局部路径,这使得它能够确定两个导航点之间的确切路径,并避免动态出现的障碍。
动画系统以选定的速度播放适当的动画序列。它还应该能够为不同的身体部位播放不同的动画序列:例如,士兵可以奔跑并瞄准敌人,在奔跑时射击并重新装填武器。这类游戏经常采用逆运动学系统。IK动画系统可以适当地计算手臂定位动画的参数,使手能够抓住位于例如桌子或架子上的物体。高层模块的任务是选择适合情况的行为——例如,代理应该巡逻区域、进入战斗,还是在地图上奔跑寻找对手。
一旦AI系统决定了哪种行为最适合当前情况,低级模块就必须为完成该任务选择最佳战术。例如,在收到代理应该战斗的信息后,它会尝试确定当前最佳的方法——例如,我们应该潜近对手,躲在角落等待对手露出破绽,还是直接冲向他,盲目射击。
RTS类游戏中的AI
在RTS类游戏中,可以区分出人工智能系统的几个模块及其分层结构。其中一个基本模块是高效的寻路系统——有时,它需要在几分之一秒内为地图上的数百个单位找到移动解决方案——这不仅仅是找到从A点到B点的路径,同样重要的是检测碰撞并处理战场上的单位相互避让。此类算法通常基于将游戏地图表示为矩形网格,其网格表示固定大小的区域元素。在AI系统层次结构的更高层,有负责经济、发展或非常重要的地图分析模块。正是该模块分析地形属性,并根据评估建立定居点,例如,定居点是否位于岛屿上,从而需要更高程度地集中精力建造海军。地形分析器决定何时建造城市以及如何放置防御工事。
图1. RTS类游戏中的世界表示
图2. FPS类游戏中的世界表示
体育游戏中的AI
基本上,在大多数体育游戏中,我们都面临着大规模的作弊行为。以赛车游戏为例。对于AI的需求,从游戏地图的几何形状中,只区分属于电脑控制对手行驶轨道的那些多边形。然后在这条轨道上标记两条曲线:第一条代表最佳行驶轨道,第二条代表超车时使用的轨道。整个轨道被分成适当的小区域,并考虑到路面参数,计算出分割轨道每个元素的长度。然后,这些片段用于构建描述轨道的图,并获取车辆最近区域的道路特性。结果,电脑知道它应该减速,因为它正在接近弯道,或者知道它正在接近交叉路口,例如,可以走捷径。此类游戏中人工智能系统的两个重要属性是能够分析地形以检测路上的障碍物,以及与物理模块的严格协作。物理模块可以提供汽车打滑的信息,收到该信息后,人工智能系统应做出适当反应,并尝试重新控制车辆的牵引力。
图3. 赛车游戏中现实的表现方法(赛道的分割与优化)
图4. 赛车游戏中现实的表现方法
类似的作弊行为也存在于其他体育游戏中。在大多数情况下,电脑控制的玩家在回合开始前就已确定其所有行为——也就是说,他/她会,例如,着陆时摔倒(杂技、跳台滑雪等),速度不对,假启动等。此外,在模拟由裁判打分的体育游戏中,分数是根据相关体育机构定义的规则生成的。
电脑控制玩家的预设场景随后由角色动画系统执行。
电脑游戏中流行的人工智能算法
在文章的以下部分,我将讨论电脑游戏编程中使用的两种最流行的算法。掌握了这些知识,就能够成功设计出满足简单FPS或RTS游戏需求的简单人工智能系统。这两种算法中的第一种是A*算法,用于在游戏地图(图)上快速搜索连接两点的最佳路径。另一种是有限状态机,例如在为电脑控制的对手准备行为场景时非常有用,它通常将低级任务委托给寻路模块。
A*算法
在地图上找到从A点到B点路径的问题几乎是所有电脑游戏中的关键问题(可能不包括某些体育游戏和少数其他类型的游戏)。同时,这组算法属于游戏AI的较低层,作为构建更复杂、更智能行为类型的基础,例如战略规划、编队或群组移动等等。这个问题在电脑游戏世界中已经得到了彻底的评估,其中一种算法——A*——已经成为当今的标准。
游戏世界的表现
几乎任何电脑游戏的世界都可以用图来表示,其形式取决于游戏的种类。在RTS类游戏中,世界通常由二维数组表示,其每个元素对应于游戏世界矩形地图的片段。每个元素(除了边界元素)有八个邻居。使用这种RTS世界的表示,我们可以构建一个图,其中2D数组的每个元素都对应图的一个顶点。图的边(通常只存在于最近的邻居之间)说明了从地图的一个元素移动到相邻元素的可能性(或缺乏这种可能性)。在实时策略游戏中,我们通常将图的一个顶点分配给游戏中最小单位可以容纳的区域。
在FPS类游戏中,图的顶点通常是位置/房间,图的顶点表示两个房间之间存在直接连接。
选择算法
存在许多用于在图中寻找最佳路径的算法。其中最简单的算法,通常称为“草原上的火”,通过围绕起始点构建连续的圆来工作,算法的每一步都构建一个更宽的圆。连续的圆和属于它们的元素被分配越来越大的索引。正如在图5中看到的那样,索引为4的圆经过我们的目标点。
图5. 简单的寻路算法
现在,反方向前进,并遵循以下规则:每一步我们都移动到位于具有较小索引的圆上的最近地图点,直到到达起始点;我们返回时经过的地图元素构成了起始点和目的地之间的最短路径。
考察该算法的工作方式,可以看出,除了其巨大优点——简单性——它还具有一个严重的缺点。算法在我们的示例中找到的路径只包含游戏世界的五个元素,尽管在最坏情况下需要检查地图的81个字段。如果地图由256x256个字段组成,那可能意味着需要检查65536个地图元素!
A*算法及其主要优点是——通过有意识地将搜索方向导向目标,最小化被检查的区域。简而言之,当计算到达地图上某一点的成本时,A*算法会向其中添加一些启发式信息,指示到达目的地的估计成本;这个函数通常是从当前检查点到目的地的距离。
算法要求
最优寻路系统面临许多要求。“最优”不一定意味着最短;算法可以考虑其他因素,例如地形类型(例如,RTS游戏中的坦克绕过沼泽会比穿过沼泽更快)、转弯角度限制、区域内的敌人数量,以及许多其他取决于特定游戏的元素。算法应避免地图上不可穿越的区域,或者,例如,与友军单位保持距离。最重要的要求是,只要两点之间存在路径,算法就应该始终能够找到最优路径。列表1展示了描述A*算法的伪代码。
列表1. A*算法
PriorityQueue OpenList List ClosedList startNode.g = 0; startNode.h = EstimateCostToEndNode(startNode) startNode.f = startNode.g + startNode.h startNode.parent = null Open.Insert(startNode) while(OpenList is not empty) //obtain the topmost element from the priority queue Node node = Open.GetNode() if(node == endNode) return TRUE for(each neighbour _succ of the node node) newG = node.g + CalcCostFromNodeToNode(_succ, node); if(examined _succ is on OpenList or ClosedList and the new cost is >= than the previous) analyse another neighbour else _succ.parent = node _succ.g = newG _succ.h = EstimateCostToEndNode(_succ) _succ.f = _succ.g + _succ.h if(_succ is already on ClosedList) remove _succ from ClosedList if(_succ isn't on OpenList yet) add _succ to OpenList ClosedList.Insert(node) return FALSE
优化
由于在优先级队列(`OpenList`)和`ClosedList`中的操作耗时较长,直接应用该算法可能效率低下。存在多种编程方法可以解决这些缺陷。优化问题可以从两个方面着手:
- 优化搜索算法本身,
- 优化数据结构。
在第一种情况下,通常采用将整个世界(地图)划分为区域的方法,并将算法分为两部分:首先,通过检查我们应该经过哪些区域来搜索路径;然后,对于每个区域,我们从入口点移动到出口点。在每个区域内,我们使用A*在本地找到当前区域的最佳路径。这样,我们显著限制了搜索区域,从而减少了计算所需的资源量。
事实上,这种方法很大程度上基于人类寻找目标路径的方式——当旅行到一个大城市的另一端时,步行者不会以相同的精度规划整个路线;相反,他/她会在已知的方向点之间旅行,精确规划每两个点之间的路径,直到他/她要走的街道。
另一个优化因素是启发式函数和参数的适当选择,因为这决定了搜索区域在游戏地图上的扩展范围。
有限状态机
有限状态机是编程人工智能最不复杂,同时也是最有效和最常用的方法之一。对于电脑游戏中的每个对象,都可以辨别出其生命周期中的多种状态。例如:骑士可以武装自己、巡逻、攻击或战后休息;农民可以砍柴、建造房屋或抵御攻击。根据其状态,游戏内对象以不同的方式响应(有限的)外部刺激,或者如果没有刺激,则执行不同的活动。有限状态机方法使我们能够轻松地将每个游戏对象行为的实现划分为更小的片段,这些片段更易于调试和扩展。每个状态都包含负责该状态下对象的初始化和去初始化(也常称为状态转换代码)的代码,在游戏每一帧执行的代码(例如,为了满足人工智能功能的需求,或设置适当的动画帧),以及用于处理和解释来自环境消息的代码。
有限状态机通常使用以下两种方法之一实现:
- 有限状态机语言——在C语言中实现为一组预处理器宏,
- 状态设计模式——一种特殊的面向对象项目模式。
在面向对象设计和编程的时代,第一种方法正被第二种方法淘汰,即基于项目模式状态实现的机器。这里有一个这种面向对象机器的例子,描述了一个骑士的部分可能行为;对象的每个状态都由一个抽象基类表示:
class CGameObjectState{ public: virtual void OnEnter() { return; } virtual void OnLeave() { return; } virtual void OnUpdate() { return; } virtual void OnMessage(CMsg* pMessage) { return; } };
所有从该类派生的类都定义了每个状态的行为(列表2、3和4)。游戏中的对象拥有指向其当前状态基类对象的指针以及协助状态转换的方法(列表5)。一个骑士类从主对象派生,并在构造函数中初始化其默认状态。
class CKnightObject : public CGameObject{ CKnightObject(){ SetState(new CKnightPatrolState()); } };
列表2. 状态:受攻击的骑士
class CKnightAttackState : public CGameObjectState{ void OnUpdate(){ // If no enemy seen, change to “patrol” state if(CanSeeEnemy()==FALSE) SetState(new CKnightPatrolState()); // Here is the code handling the attack, e.g. get as close as needed // to the target if it is too far away, use the sword if it is close if(IsEnemyCloseEnough()==FALSE) MoveToEnemy(enemyID); else HitTheEnemy(enemyID); } void OnEnter(){ // Alert allied troops that you're under attack CallForReinforcement(); } void OnLeave(){ // Notify command of received wounds CallForMedic(); } void OnMessage(CMsg* pMsg){ // Check the type of the order, take appropriate action if it is // a “fall back” order CMsgGetBack* pGetBackMsg = dynamic_cast<CMsgGetBack*>pMsg; if(pGetBackMsg != NULL) SetState(new CKnightRunAwayState()); } };
列表3. 状态:巡逻中的骑士
class CKnightPatrolState : public CGameObjectState { void OnUpdate(){ // Here is the code to check if we can see the enemy if(CanSeeEnemy() == TRUE){ SetState(new CKnightAttackState()); } else{ // Continue the march through subsequent way points of the patrol GoToNextPatrolPoint(); } } void OnLeave(){ // Notify we are not patrolling any more, a substitution may be needed NotifyOneLessInPatrol(); } };
列表4. 状态:逃离敌人的骑士
class CKnightRunAwayState : public CGameObjectState{ void OnUpdate(){ // Here is the code to check if we can see the enemy if(CanSeeEnemy() == FALSE){ SetState(CKnightPatrolState()); } // Run as far away as possible from the nearest enemy RunFarAwayFromNearestEnemy(); } };
列表5. 对象的当前状态和状态转换
class CGameObject{ CGameObjectState* m_pMyState; public: SetState(CGameObjectState* pNewState){ // Call the deinitialisation method and destroy the object m_pMyState->OnLeave(); delete m_pMyState; // Assign the new state to the object // and call the method initialising it m_pMyState = pNewState; m_pMyState->OnEnter(); } OnUpdate(){ // perform tasks as defined for the present state m_pMyState->OnUpdate(); // now perform other, per-frame tasks of the object } void OnMessage(CMsg* pMsg){ // perform tasks as defined for the present state m_pMyState->OnMessage(pMsg); // now process the messages, regardless of the object's state } };
电脑游戏中的人工神经网络和高级算法
人工神经网络及其在视频游戏中的应用问题已成为近年来电脑游戏领域最热门的话题之一。多年来,许多杂志和门户网站都对它们在电脑游戏中的潜在应用进行了大量讨论。“电脑游戏中的神经网络”问题也曾在GDC(游戏开发者大会——每年在伦敦和圣何塞举行)上多次讨论。与此同时,我们不得不等待很长时间,才看到一款游戏的引擎至少在最低限度上基于人工神经网络理论的潜力,才能进入市场。
《科林麦克雷拉力赛2》是神经网络在电脑游戏中最早且取得巨大成功的应用之一。经过训练的人工神经网络负责让电脑玩家的赛车保持在赛道上,同时使其尽可能快地通过赛道。在这款游戏中,正如我在《体育游戏中的AI》一节中所述,每条赛道都由一组构成图的折线表示。简单来说,神经网络的输入参数包括:道路弯道的曲率、与弯道的距离、路面类型、速度或车辆属性等信息。神经网络的任务是生成输出数据,这些数据将被进一步传递给物理层模块,其选择方式是使赛车以给定条件下最佳的速度行驶并克服障碍或弯道。因此,与同类其他游戏不同,电脑玩家的驾驶风格显得非常自然。电脑可以避开小障碍物,抄近道,在湿滑路面上适时开始转弯等。该游戏使用多层感知器模型,其简化形式可见图6。
图6. 多层感知器模型
理论上,人工神经网络可用于解决电脑游戏中AI执行的大部分任务。不幸的是,在实践中存在一些限制神经网络在游戏中应用的障碍。这些障碍包括:
- 选择神经网络适当输入的问题,
- 神经网络对游戏动作逻辑变化的敏感性,以及每当这种情况发生时都需要重新训练网络,
- 理论相当复杂,且出现问题时难以调试,
- 训练网络的过程耗时且复杂。
为了在简单的电脑游戏中利用人工神经网络,我们需要采取哪些步骤?让我们简要地看一下:
首先,我们必须回答自己的问题:为了帮助我们解决给定的问题,神经网络应该向我们提供什么样的信息?例如,让我们考虑一个游戏中,神经网络控制我们对手的战斗机飞行。那么,我们应该从神经网络获得的信息将是,例如,最优的速度和加速度向量,这些向量在提供给物理模块后,将引导敌方战斗机飞向我们的飞机。另一个例子可以是用于在RTS类游戏中选择最佳策略的神经网络。基于情况分析,网络决定在多大程度上专注于发展、武器生产、战后修复等等。游戏所需的所有参数都将由神经网络在其输出端提供。
虽然定义神经网络动作的效果相当容易(因为我们确切知道我们想要实现什么),但选择网络的输入参数是一个更严重的问题。参数应该以这样的方式选择,即其不同的组合将使神经网络学习解决在示例信号集中未出现过的复杂情况。一般规则指出,输入数据(变量)应尽可能多地代表游戏世界的信息;例如,它可以是最近障碍物和最近对手的相对位置向量、敌人的强度,或者武器装备和损坏的当前状态。
另一个步骤是获取一组将用于训练网络的输入数据。直接方法可能意味着,例如,记住数百个成功的攻击和人类玩家的行动样本,并将记录的数据提供给神经网络。然而,通常使用的过程是自动化的,即样本本身是计算机生成的——这需要程序员付出额外的,通常是相当大的努力。
最后一步是训练神经网络。这里可以使用任何训练算法。训练过程应与同步测试交织进行,以确保游戏不会变得过于困难,或者相反,如果它仍然过于容易,则需要进一步训练和优化。
实际应用神经网络并非易事。它需要大量时间、经验和耐心。此外,神经网络通常与模糊逻辑一起使用,这使得计算机传统的零一推理能够更强烈地类似于人类的思维方式。逻辑使我们能够决定给定陈述是否以及在多大程度上是真实的。尽管同时使用这两种技术是一项艰巨的任务,但一旦成功,结果简直令人惊叹,与我们通过将规则硬编码到算法和传统逻辑中可以实现的效果无法比拟。神经网络、遗传算法和模糊逻辑等技术是电脑游戏的未来——而且这个未来已经不再遥远了。
人工智能库
开发一个先进的人工智能引擎需要时间和经验丰富的程序员团队。如果开发工作室无法分配足够的人力资源来构建人工智能系统,则可以购买市场上现有的人工智能系统,其中许多都可供选择。在这里,我将详细介绍市场上最流行的库之一——Renderware AI,以及其中一个较新的库,它可能是Renderware AI更便宜的替代品——AI.Implant。
渲染器AI
Renderware是一款商业化的多平台电脑游戏引擎。Renderware引擎由多个模块组成;其中,我们感兴趣的是Renderware AI人工智能模块。
Renderware模块既可用于完全基于Renderware引擎的游戏,也可用于使用自身或其他引擎,但希望利用Renderware AI作为高级人工智能系统基础的游戏。
Renderware AI库遵循构建人工智能系统的分层理念。Renderware AI区分三个层:
- 感知层,负责情境分析——主要分析静态(例如,地形)和动态(敌人、电梯等)环境。
- 决策层,负责根据感知模块提供的信息做出战略决策。低级动作,如攻击、寻路、规避等,由动作模块执行。
- 行动层。
整个库中最重要的元素是世界感知的表示,因为这是游戏AI更深层的基础。在Renderware AI中,这个模块称为PathData(一个略有误导性的名称,考虑到路径分析只是感知模块的功能之一),并使用名为PathData Generator的工具。PathData模块能够成功分析游戏世界在拓扑特性方面的表现,其流媒体方法使得即使对于非常大的游戏地图也能生成AI模块所需的信息。PathData既进行地形拓扑的全局分析,也进行单位最近环境的分析。如果需要,分析结果可以进行进一步的手动处理。
全局分析提供诸如从拓扑特性角度来看有趣的位置等信息。这些信息可能包括:地图上隐藏良好的位置,从这些位置可以很好地看到大片地图的区域,摄像机可以放置在哪里,使其视图不会被场景中的次要元素遮挡等。局部分析可以让我们检测墙壁、必须绕过或跳过的障碍物,以及许多其他局部重要的元素。
Renderware AI的另一个重要特性是负责广泛理解的单位移动规划和执行功能的模块。利用世界分析模块提供的数据,构建一个适当的图,然后由A*算法用于初步规划从A点到B点的最佳路径。其他特性包括:依赖于单位类型的路径、路径平滑、避免动态物体进入单位路径、与动画系统协调等等,这些在实践中都极其重要。
该引擎适用于多种平台,从索尼Playstation到任天堂或XBox,再到索尼PlayStation 2和PC。这些库针对每个平台进行了优化,并可以创建令人难以置信的先进AI系统。作为耗时开发自己人工智能解决方案的替代方案,它值得考虑。
AI.Implant
该引擎于2002年在游戏开发者大会上首次亮相,立即引起了电脑游戏开发商的广泛兴趣。该系统最重要的特点包括先进的分层路径规划算法、基于二叉决策树的决策模块以及便于编辑的友好用户界面。此外,其一大优点是与3DStudio Max和Maya等程序紧密集成,这使得在图形软件包开发阶段就可以直观地操作控制对象行为的数据。在AI.Implant软件包的众多特性中,值得一提的是其先进的群组行为模块,它能够非常真实地模拟人群。AI.Implant是一个多平台软件包,适用于PC、GameCube、XBox和索尼PlayStation架构。
接下来是什么?
人工智能是计算机科学中一个非常广泛且引人入胜的领域。在本文中,我向读者介绍了在电脑游戏编程中使用的一些人工智能算法和方法;然而,这只是任何真正的电脑游戏程序员必须掌握的知识的一小部分。此处未讨论的最重要问题包括:遗传编程、模糊逻辑、影响地图方法、群集算法以及许多其他内容;我衷心推荐所有读者熟悉这些内容。最后,我提供了一份书籍和网页参考列表,对于希望独立增加自己在电脑游戏人工智能领域知识的任何人都会有所帮助。
在网上
- BioGraphic Technologies – AI.Implant 的创造者
- Renderware,Renderware AI 的开发者
- Gamasutra - 游戏制作的艺术与商业
- GameDev.net - 满足您所有游戏开发需求
- 专门讨论人工智能问题的网站
参考文献
- Steve Rabin, AI Game Programming Wisdom, ISBN: 1584500778
- Steve Rabin, AI Game Programming Wisdom 2, ISBN: 1584502894
- 游戏编程精粹 1, 2, 3, 4
- 游戏开发者杂志
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 设计模式CD – 可复用面向对象软件的元素