http://blog.friskit.me/2012/04/how-to-build-a-perfect-game-ai/
人工智能(Artificial Intelligence)在游戏中使用已经很多年了,并且到现在越来越完善。如果你不在你的游戏中加入完善的游戏智能,那么别人就认为你的游戏缺少可玩性。
在游戏中,AI并不一定要包括神经网络,学习系统和复杂的数学结构,游戏AI只是游戏中一个重要部分,它是活动的,并不是科学性质的。
我认为如何建立一个游戏AI,最主要的就是要明白你想在游戏中实现什么效果,就是你想让玩家看见什么;如果游戏中什么也没有发生,那你的游戏AI什么都没有做。
在本篇中,我们将讨论一个实时策略游戏(RTS)中游戏AI,不管怎样这些理论都可以很好地移植到其它的系统当中,所有的代码全部用c语言写成。
状态机
有限状态机
有限状态机(FSM)是一个只有有限的几个状态的系统。在一个实际的例子当中,状态的触发是通过一个拥有开或关状态的开关,或通过一个闹钟来调用时间决定的。通过有限状态机,我们可以在游戏中定义一些事件,然后由玩家在游戏中游动时,通过触发来实现某些事件。
有限状态机是游戏中最常用的游戏AI。下面,我们将讲解如何在游戏中使用有限状态机。
使用有限状态机
在游戏中,我们最常用的就是用有限状态机来模拟人的某些智能行为。比如,当一个NPC受到攻击时,它应该怎么办,当发现敌人时,它应该实行哪些行为,当敌多我寡时,应该实行哪些行为,等等。都可以用有限状态机来解决。
你可以在游戏中,将你的游戏AI设计的完美无疵,它可以万全模拟一个人的思维,能够自我思考,自我学习,但你要记住一点,在游戏中,我们应尽量将AI简单化,不要太科学化。
并不要以为,我们在这里反对神经网络,遗传算法等等,你要在游戏中使用哪种算法,完全由你自己根据你在游戏中要实现一个什么效果,完成一个什么目标来决定。相反,我们在这里最主要就是讲解如何在游戏中使用有限状态机。
游戏状态机
要在游戏中实现一些动作行为,你必须考虑很多方面,比如你就不能从你自身的角度来考虑问题,你就得从玩家的角度来考虑,玩家他到底要在游戏中干什么?要想达到玩家的想法,你就不得不在游戏中反复测试,以便于达到玩家想要的效果。
在一个实际的游戏当中,一般存在两种状态机:
第一种状态机主要就是完成游戏界面的转换,比如,玩家在游戏中暂停,应该显示什么界面,游戏中哪些UI应该让玩家可见,哪些又应该隐藏。
第二个状态机主要就是改变当前运行时的环境,比如当前玩家处于哪个游戏地图,关卡中NPC的出现,玩家任务完成的状态:成功或失败。还有就是我们可能在游戏中要引导玩家的一些参数或变量。
你可能需要一个描述NPC的系统,这个系统主要就是用于判断如果玩家点击NPC或者向NPC开火,NPC应该怎么办,我们完全可以用下面的结构来表示:
struct GameLevelState{ int alert; //NPC的当前状态 struct Positionshot; //玩家向NPC开火的位置 int shotTime; //子弹发射出去后的游戏循环 int hostage; //谁需要帮助 int explosives; //子弹是否爆炸 int tank; //自身是否受到破坏 int dialogue; //对话参数 int compete; //任务是否完成 }; |
扩展性
保证你的AI拥有良好的扩展性是重要的。在编程的时候,你要能够使你的AI最重要的部分能够很容易的扩展。因为你要明白游戏AI的设计是一个迭代的过程,你需要不断地去测试它,完善它。
群体动作
在游戏中,是由一个玩家来控制群体,什么是群体动作呢?比如,在中,玩家选中一群士兵,然后然它们朝一个目的地前进,如果没有良好的群体控制,那么这些士兵可能就会越走越散。这样就完全不符合玩家的思想。
解剖101
下面,我们来详细地看一下一个角色的数据结构:
struct Character{ struct Positionpos; //玩家所在的地图位置 int screenX,screenY; //玩家所在的屏幕位置 int animDir, animaction, animNum; //动画信息,角色动作和动画帧数 int rank; //军衔 int health; //生命值 int num; //在当前群体中有多少人 int group; //部队编号 int style; //部队的性质(步兵,骑兵等) struct AnimationObject animObj; //复杂动画的动画对象 }; |
现在,我们来详细讲解每一个参数:
pos参数用于决定部队在屏幕和游戏世界中的位置,这样是为了便于我们在屏幕的相应位置播放精灵动画,显示精灵信息等等。
AnimDir,animaction,animnum是用于决定我们当前应该在屏幕上显示哪段精灵动画。
Rank ,health 就不用说了。
Num 参数用于决定该角色在部队中的ID号。
Group参数用于决定该角色所在部队在所有部队中的编号。
Style,animobj参数用于决定部队的图像在屏幕上的显示。
建立过去基本
一旦你利用前面的方法建立了一个初始化部队的函数,那么现在就需要你对这些部队赋予生命。
这时候,你就需要思考你想让你的部队拥有什么样的动作,有什么样的反应。你需要思考,你的部队是否感情用事?是否像一个疯子?是否需要被冻结?
要做到这些,你就必须首先思考当人遇到某某事情时,他应该怎样做,然后再把你的思考变成代码,观看其效果,不对就再修改。
群组
群组或者非群组
如果你是制作第一人称射击游戏,你就不用考虑群组的问题了,因为就只有你一个。但当你想建立一个RTS类形的游戏,你想同时控制很多人的动作,那你就不得不考虑下面的问题:
我是否需要我的部队按照一种比较协调的方式行进?
如果回答是“是”,那么恭喜你,群组适合你。
群组的优点
1. 部队能够按照一个主要的移动信息列表前进。这样做的好处就是群组中的任何一个单元当受到其他信息,比如目标的改变等,其它成员还是可以按照先定的移动信息前进。
2. 多个成员自我调节行为。 比如,我们控制一个部队包围一个建筑物,部队中的成员能够相互协作完成对建筑物的包围。
3. 群组能够自动保持他们的结构,这样,当一个部队遇到一些不明情况时,他能够自适应的告诉部队中的每个成员该干什么。比如,行进当中要注意保持对形,先头部队首先遭遇敌人,应奋起阻击。
4. 依赖于群组的构成,你可以直接控制群组来控制群组中的其他成员,这样就简化了找路径等问题的麻烦程度。
大图片
如果你想在游戏AI中判断你所有部队的动作,你就不得不把这些部队的信息全部群组起来。一个好注意就是建立一个地方,用于共享所有部队的信息。
根据我的经练,这个部分应该分两步完成:第一部分就是将群组中的每个成员应该做什么事存储在每个成员自己的数据结构中,而其他需要组织和共享的信息,比如移动,行动等动作,应存储在群组数据结构中。
这意味着部队不拥有移动的信息,他们总是在群组的权利范围之下做出决策。如果硬要说每个部队都是独立的,那他们也只是在群组下独立。
许多中的一个
当我们为了适应系统的变化时,寻找一个群组去行动,那系统将建立一个单独的部分,并且最重要的是保持每个部队的行动信息。这样做的目的是便于我们打乱我们的结构然后完成一些特殊的任务。这样的数据结构如下:
struct GroupUnit{ int unitNum; //角色ID struct Unit *unit; //部队角色数据 struct Positionwaypoint[50]; //所有部队的路径 int action[50]; //部队在路径上应该的动作 int stepX,stepY; //各别部队的步骤 int run,walk,sneak,fire,hurt,sprint,crawl; //行动 int target; //所有部队的目标 struct Position targetPos; //目标位置 }; |
下面,我们来具体描述这些参数:
unitNum是部队在群组中的ID。如果群组容纳的最大部队数是10,那么最小的部队编号是0,最大的是9。
Unit 是一个指向部队角色数据的指针,它存储了包括角色当前的位置,生命值和其它相关信息。
Waypoint 包含了一个部队可以去的所有地方。所有行动和位置信息是包含在GroupUnit结构中,如果群组不是在一个结构中,那么部队中的所有成员将按照它们自己的方式移动。
Action数组包含了移动到目标的所有动作。这允许你建立一个详细的动作链,以便于玩家控制部队在森林里潜行,然后匍匐着靠近目标。
StepX ,stepY用于存储简单的速度信息;因为每个兵种,它们的移动速度都是不一样的,总不可能步兵的移动速度比火箭飞行兵快吧。
Target ,targetpos参数用于存储当我们选中了一个敌人部队时,敌人部队的编号和位置。关于敌人的其它信息,我们可以通过每次的游戏循环来找出。
群体心理
我们还需要一个中央集权的群组来管理我们所有的群组,下面,我们来看一下一个简单的结构。
struct Group{ int numUnits; //群组中的部队数 struct GroupUnit unit[4]; //部队信息 int formation; //部队和群组的结构信息 struct Position destPos; //目标 int destPX,destPY; //目标屏幕坐标 struct Postion wayX[50]; //群组的所有可去的目标点 float formstepX,formstepY; //群组移动的速度 int formed; //如果这个值为真,那么部队独立行动,否则按照群组的规则移动 int action,plan; //群组行动和计划 int run,walk ,sneak,sprint,crawl,sniper; //具体动作 struct Position spotPos; //攻击地点 int strategyMode; //群组的战越模式 int orders[5]; //群组的顺序 int goals[5]; //群组的目标 int leader; //群组的领导者 struct SentryInfo sentry; //哨兵列表 struct AIState aiState; //Ai状态 }; |
numUnits是指在群组中有多少部队,部队数组存储在GroupUnit结构中,在本例中,我们存储的最大数量是4.
Formation标志决定了群组是采用了什么队形,它们可以根据自身的状况分别采用柱,楔和三角等队形。
DestPos,destPX,destPY描述了群组在赶往目的地中途的什么地方。Waypoints,steps以相同的方式工作,它们都是描述了部队行进的速度。在这里,并不需要部队中每个成员来设置自己的速度,这个事完全由群组决定。
Formed 是最为重要的参数之一,它决定了部队是否按照对形行进或单独行进。如果那个群组被设置成按对形行进,那么群组中所有的部队就必须按照群组设置的对形行进,但是当遇到某些特殊的原因,比如遭受攻击或对形遭到了破坏,那么部队就按照他们自己的思路前进。
Actions参数就是表示部队在到达目的地中间的动作,比如跑,走,潜行等等,完全由玩家决定。
Strategymode 决定了当部队遇到敌人后应该怎么办,部队是反抗还是逃跑?
Orders,goals数组是为了便于群组之间的消息传输。
Sentry , aiState存储了哨兵信息和更为详细的AI信息,这些信息是为了更详细的模型匹配。
把它放在一起
到现在,关于我们的群组,我们已经有了一些结构。然后我们应该做什么呢?下一步就是找出我们在游戏中如何用编程来实现这些信息。
值得注意一点的就是,尽量将你的AI程序模块化,这样做的好处就是便于以后更好地扩充它们。详细一点就是说,你在编写函数时,尽量做到一个函数只做一件事情。
关键部分
在你准备写你的游戏之前,请不断的学习一些基本的东西,如果你的游戏需要一些功能函数,那么就为这些功能编写相应的函数。
不要学习某些高手用一个函数完成所有的功能,可能一个函数对他很管用,但对你就不那么行的通。
你要不断的创新,不断的挑战传统,不要看一些看似经典的程序你需要自己创造。如果你写的东西,它能正常工作,那么用它。在游戏编程中有一句经典的话是这样说的:“如果你觉得它是对的,它就是对的”。
不要被其它的思想绊住了脚,因为我们相信你是最强的。
其实,AI的意思就是如何让决策变得更聪明,在我们这篇文章中,就是要让玩家感觉游戏中的人物像真的一样。
在一个RTS(实时策略游戏)游戏中,我们所谓的动作包括移动,巡逻,避开障碍物,打击敌人和追赶它们。让我们来看一下每个动作的详细内容。
移动
移动,最简单的一种形式就是,在某一段时间之内从一个点到达另一个点。这个很容易实现,你能通过找一个距离向量,然后乘以这个部队移动的速度和所用的时间,就可以得到移动的位置。
因为我们的游戏是通过鼠标来驱动的,所以,我们并不能期望用户通过游戏杆之内的东西来驱动游戏人物前进时绕开障碍物,首先,你要明白一点,我们这里说的并不是第一人称射击类游戏。
我们应该怎样做呢?比如,玩家在一棵大树旁点击了鼠标,我们首先必须建立一个动作队列保存这个角色行动的路径,但是当角色到达大树时,他不可能穿墙而过吧。这个时候,我们就必须为角色添加一个动作,以便于角色绕过大树。
巡逻
巡逻是一个特别的移动,因为它包含了一系列预设的坐标。在移动中,一个部队它走到一个目标以后,它就不会走了,但巡逻不一样,当部队完成一个目标以后,他就会从自身的巡逻列表中抽出下一个坐标,然后又朝下一个坐标前进。
在我们这个实例当中,尽量不要让部队一直站立或等待,因为我们这个游戏是RTS游戏,如果一直站立或等待就不能突出游戏的战争气氛。
避开障碍物
避开障碍物的算法首先要看你的地图是如何工作的,还有就是你的部队在移动时应该让他们如何相互交互。在本例中,我们使用的是一个拥有很多小的建筑物和物体的室外环境。在这个游戏中,你不能进入建筑物,这些建筑物都是用一个凸出的拥有4个顶点的多边形表示。
在下面这个例子当中,部队和目标并不是成直线时。首先,我们先移动到和目标最近的一个坐标,这个坐标尽量和目标呈直角。然后再从这个点移动到目标。这样做的好处就是留下了很大的缓冲空间让我们绕过障碍物。
说白了,障碍物避开算法是由你将遇到的障碍物决定的。如果你想了解一些更好的路径寻找算法,我建议你看一下A*算法。A*算法是一个非常流行的最短路径寻找算法。
打击敌人
和其它部队作战通常依赖于你想你的玩家在游戏中能干什么。你可能想你的玩家的部队在见到敌人时能够自动开火,以便于玩家能够将注意力放在整个战场上。你可能还想你的部队能够监视周围的环境,如果出现敌人就马上开火。
比如,一个角色站在一个坐标上,可以将它的四周分为8个部分,这8个部分分别代表了角色的8个视域,当然你也可以设置这些视域的距离,也就是角色所能看到的距离。然后就循环监测这些视域中是否有敌人。当然,也有一种特殊情况,比如,敌人和障碍物同时出现在你的视域中,障碍物在前,敌人在后,你总不可能也看见敌人了吧!关于这个问题,我将在另一篇文章中讲解。
追赶
当一个敌人发现了一个猎物,也就是它自身的视域中出现了一个角色,这时候,它就要判断,这个角色是自己人吗?如果是,就不干什么。如果是猎物,就要检测它是否在视域内,如果是,就开枪射击。如果不是在视域内呢?那么就需要追踪它。
那将如何实现呢?首先,你需要保存猎物最后的一个坐标,然后将这个坐标设置成你的第一个移动目标,到达目标后,你就需要检测猎物是否还在视域范围内,如果在,就开枪射击。如果不在,就按照猎物的方向随机移动一个距离,然后继续检测。
在本篇的例子当中,我们假设角色能够在第一次移动过后找到猎物。但如果他移动后没有发现猎物,那么我们就将他的状态设置成巡逻状态,然后朝我们第一次得到的猎物的方向移动一个随机的距离。
总结
当你看懂了这篇文章,并且在程序中将它实现以后,你就会发现,其实游戏AI也没那么神秘,不是挺简单的吗?