说明:
游戏的AI是一个听上去很高深但又是每一个玩家都有着直接明了的体会的东西。在目睹了游戏里面的AI人物做出种种神奇的举动之后,你会禁不住想要了解到底这些看上去很有智慧的行为是怎么实现的。要以普通玩家的身份分析游戏的AI结构实在是太难了,好在网络上流传着一些这方面的相对浅显易懂的资料。游戏开发者会议是游戏理论方面权威的机构,在最近的GDC会议上,有两个游戏先后有机会在大会上作了关于它们的AI系统的演讲,一个是Halo系列,另一个就是Killzone。
你可能会以为这样的演讲比较难以理解,但我仔细看过之后发现其实他们的表达方式还是很面对大众的,尤其是在Halo的演讲中,他们公开表示这样的内容所期望的听众对象里就包括了游戏的热心玩家。所以我把这些内容翻译过来,和喜欢了解这个游戏和这方面内容的大家一起分享。
这是两篇分别来自2002年和2005年游戏开发者会议上的关于Halo系列人工智能结构的讲座。两篇都有ppt的幻灯片和注解文章两部分。2002年的幻灯片比较详细,容易直接看懂,2005年的幻灯片则基本都是演示图表,不配合大量文字说明根本无法理解。所以2005年这一篇我翻译了文本全文,2002年的则是在保留其ppt结构的基础上添加了大量的解说文字。
Halo2 AI的复杂性
Damian Isla
AI Programmer / Bungie Studios
[email protected]
游戏AI的设计者总是希望在他们所建造的虚拟大脑里塞入更多的复杂性。然而复杂性是需要付出代价的,甚至是许多代价:运行起来不够快,很差的可扩展性,缺乏可操纵性,以及最可怕的,在玩家的眼里这样的AI可能是一片朦胧,AI的行为是随机的而不是有计划的。我们将讨论一下复杂性的来源,一些能让复杂的AI变得清晰的方法,以及我们再Halo2的工作中减轻这些负面效果所采用的构架手段。讨论将集中在可扩展的决策方法上,同时也会涉及到存储,知识模型,脚本的控制表现,以及关卡设计对AI的影响。
为了达成常识级别的AI所使用的暴力手段
当谈论游戏AI的时候,多通常来说就是更好,AI的行为库组织的越好,AI就能识别更多的独特的触发事件,AI所能做出的反应的独特方式也就更多,我们也就越倾向于把这个AI当成常识里的“生物”。一个符合“常识”理解的AI是人工智能研究团体长期努力的目标。对很多人来说,常识的问题都和如何获取和表达信息紧密相连。毕竟,这些常识通常都被认为是大量的理所应当的,日常的知识,它们非常的明显,比如走路,观看,思考这些人类从来都不需要去特别学习就能表现出来的行为。这样一来,这些常识就很容易被人们忽略。
对游戏而言,至少是对Halo2而言,我们并不感兴趣去编码那些简单的事实(比如鸟有翅膀,水会把东西打湿),我们要编码的是行为,这可能是另一种不同意义上的知识。这样的知识意味着:当你在车辆里面坐着,如果你要走出车门,就必须先从座位上起来,或者为了避免别人射击你,你就要通过移动把一个足够大的障碍物放置在你和敌人之间。当然在这些常识中,解决方案都是一样的:数量,AI知道的越多,越好。
数量,当然也就是复杂性,尤其是当你考虑到游戏本身带来的限制的时候。AI仅仅是能做很多事情是不够的,同样重要的还有,AI要把这些事情做对,在正确的时间去做,AI的行为方式不能打破玩家对日常生活的印象,不能破坏玩家对于AI行为目的和动机的揣测。在Halo 2中,如果玩家能感觉他们是在何一个活着的生物作战,并且可以相应的回应和预测这些生物的行动的话,这样的AI就是最好的。作为行为的主导者,我们的一个主要目标就是要去协助玩家所经历的游戏叙事过程:“噢,这个grunt刚刚大叫着逃跑了,是因为我拔出了能量剑把它吓到了,但当我把它逼到绝路的时候,它又转过身来开始战斗了。”
为了同时达成这两个目标:数量以及行为的完整性,带来了一个巨大的构架上的挑战。因为无论我们要去编码的知识内容是什么,我们都需要合适的容器来接纳它们,希望这样的容器可以关照到长久以来大型系统设计关于可扩展性,模块化,以及透明度等等各个方面的考虑。
为了达成AI的复杂性,我们在这些方面付出了代价:
•一致性:如果把行为理解成动作长时间的累积,我们就必须要保证我们的AI总是在合适的时机开始,终止,或者改变动作。我们必须不惜一切代价防止“行为抖动”(AI在数个动作之间快速的来回交替重复)。
•透明度:AI的行为必须可以被一个未经训练的观察者(通常水平的玩家)理解,可以合理的揣测AI的内在状态,可以解释和预测AI的动作。
•运行:所有限制中最明显的一个,AI必须与每秒30次或者更高的速度运行。
•智力限制:当我们无法理解系统的行为的时候,我们也就失去了对这个系统的控制。
常识的数量也不是复杂性的唯一来源,考虑下面这些方面:
•可用性:AI必须可以支持游戏的设定,这是对于游戏的关卡设计者而言。
•多样性:根据角色的不同,不同的AI有不同的行为方式。我们必须设计出这样的系统,在提供坚实的常识行为的同时也表现出角色和角色之间的不同特点。
•可变性:AI应该可以根据情况的变化作出不同的行为,尤其是在那些游戏设计者特别导演的和剧情相关的部分(比如AI在一段剧情里是你的盟友,下一段剧情里变成了敌人)。
•运行:这个因素可以对复杂性有正面或者负面的影响。Halo2中很多的结构复杂性的来源是因为我们试图避免许多不需要的工作。
这篇文章将会讨论Bungie在Halo2 AI设计中为了解决复杂性难题所采用的技术。文章的第一部分是关于内部构架,尤其是关于内存和决策方式。第二部分将会展示一些有用的关卡设计工具,我们用这些工具来控制AI,并为关卡编写脚本。
核心战斗循环
开头的时候很简单,最初的工作看上去就和下图所示一样。这张图就是游戏设计者用来描述玩家和AI接触方式的示意图。显然,这里的每一个状态都描述了AI和玩家不同的互动方式,最好每种状态都有它们自己的动作模式。我们如何才能把这张图表应用起来呢?
首先应该注意到的时,这张图包含了很多的隐藏复杂性。比如,对于每一个箭头,我们都可以提出这样的问题:“什么时候发生这样的转变才是合理的?”有些转变是自动的,其它的一些则是被外界信息驱动的,比如从战斗状态,当我们的目标移动到障碍物之后的时候,AI就转变成闲置或者搜寻的状态。换句话说,这个图表是一个有用的概念工具,尤其是对游戏设计者而言,但还不能被直接应用。
行为
真正的控制结构是什么样子的?何很多其它的系统一样,Halo2使用了分层有限状态自动机(HFSM),或者叫做行为树,更特定的来说,是一个行为有向无环图(DAG),因为单一的行为或者行为子树可以占据图中的几个不同位置。下图给出了一个例子,这是Halo2真正的核心行为DAG的一个高度简化版本,它包括了50个不同的行为。
HFSM是决策领域一个广为人知的经过了时间考验的技术。我们下面的讨论将会限制在我们在Halo2游戏中发现的有用的特性上。
决策途径
在典型的HFSM系统里,非枝叶行为的作用是用来做决定,枝叶行为是用来具体的完成任务。如果是在前者身上发生的决策过程,那么通常都有两种方式:(a)让父行为利用专门的代码做出决定,或者(b)让子行为去竞争,让父行为根据子行为的渴求程度和关联度判断最终的取舍。两种方式我们都用了,所以我们预留了编写定制的决策路线的可能。
设计原则一:让所有的内容都可以定制
当然只要可能,我们还是跟多的使用第二种机制,尤其是对战斗循环的核心部件,每一个都带有很多的子行为(10-20个)。对于这样的父行为是用这种方式是一个很好的注意,因为要给这样20选1的情况写硬代码(代码不考虑可扩展性、可移植性,把赋值等等都写死)是很麻烦的,而且也不容易扩充。
假设我们在使用子行为竞争决策模式,我们应该怎么选择胜出者呢?每一个子行为提供一个浮点数来表达自己的相关性,相关性最高的胜出,前次的胜者可以在这个数值上得到奖励,从而避免前面提到的“行为抖动”现象的出现。但这样还是会不可避免地遇到扩展性的问题,当这些竞争的子行为数目超过一定程度之后,尤其是当一系列特定的优先权被指定的时候。比如“和目标交战,除非目标是在车辆里面的,在这样的情况下,先进入该车辆。”当选择只有两种三种的时候,去调解那些浮点数来给出一定的规则和优先权是可行的,但当子行为多达20种的时候这就基本不可能了。
我们可以通过对关联性作二元判断来大大简化这个问题。通过这种手段,我们可以定义数量相对较少的标准决策计划,
•优先次序列表:根据子行为的优先次序依次检查,有最高优先次序而且可以被运行的那个子行为将被选中。其它有着更高优先次序的子行为在合适的情况下可以打断当前被选中的子行为取得优先权。
•顺序列表:依次运行每一个子行为,跳过那些当前不相关的,而且也永不重新访问。当遇到列表结尾的时候,父行为就结束了。
•顺序循环:和上面的方式基本一样,但当我们走到列表结尾的时候,跳转到表头重新开始。
•随机性的:在相关的子行为中随机选择。
•唯一性的:选择随机或者优先次序的方式,但永不重复同样的选择。
在这些方法中,优先次序列表是最经常使用的。它有一些优点,其中相当重要的一点是,这种方和我们日常生活中解决问题的思维方式是一致的:我们先考虑最好的方式,如果失败了再去考虑次之的,再次之的……无论我们选择了哪一个,当有另一个更好的方案出现的时候,我们都会立刻转移到更好的方案上去。
行为刺激
然而,这也带来了一个新的问题:如果优先权顺序不是确定怎么办?换句话说,在某些情况下行为A的优先权高于B(战斗,而不是进入临近的车辆),但在其它的情况下B的优先权又要高于A(除非玩家就在车辆里面,这时候优先进入车辆)。为了解决这个问题,我们使用了“行为刺激”。刺激是一个自由触发事件,它像完整的行为一样提供二元相关性,但它自身仅仅是一个完整行为的参照。当刺激在子行为竞争中获胜的时候,要么目前正在运行的行为堆栈被重新定向到这个刺激所指向的行为,要么这么被指向的行为就在刺激所处的位置开始运行。在上面给出的例子中,我们对后一种方式更加感兴趣。我的优先级列表就变成了:
玩家在车辆中_刺激
战斗_行为
进入车辆_行为
这里,我们就清晰的把原本会导致更加倾向于进入车辆这个行为的条件,拆分出来,成了一个单独的刺激,这个刺激指向同样的行为。
设计原则二:清晰比什么都重要
就像上面提到的一样,刺激也可以被用来把目前的行为堆栈重定向到(行为树)的另一个部分。比如,可能存在自卫的刺激(由于被伤害,遇到了可怕的敌人,等等),它是遇敌行为的一个子行为,这样的话,这个刺激就只在AI遇敌的时候才会被考虑。当这样的刺激被选择的时候,我们不会去运行遇敌下面所属的那个自卫,而是在行为树中创建一个新的层,在这个自然的地方去运行自卫刺激。也就是说,刺激可以在一定的情况下被当作指针使用,指向行为树的其他分支,引起分支之间的跳跃。
刺激还在另一种方式上为我们服务。考虑一个永远不会返回正关联的刺激:这个刺激永远不能给我们提供一个可供运行的行为。另一方面,这是一个随意的,不重要代码段,可以在行为树的任何地点自主运行。这样的代码可以用来做什么呢?任何事情。也许我们可以做一个数据巡检,来记录我们曾经到达过优先级列表的这个位置的事实。或者我们可以用它来在主机做一些调试功能。或者可以用来让游戏人物在特定的情况下发出某种声音。事实上,代码并不需要一定是行为中明确的一部分也一样可以有用。也许这可以被认为是某种程度的Hack行为,因为我们特意的绕过了行为执行这一步,但这是刺激的设计目的之一:为了让我们方便的在行为树的特定位置放置任意的代码段。
设计原则三:一定要提供可以日后修改的可能
行为标签
当行为树的规模越来越大,我们很容易想象,要决定行为的相关程度会成为占用运行时间的主要因素。毕竟,我们通常是在检查大量并不在实际运行的行为和刺激。有趣的时,我们发现很多基本的相关条件在很多候选者中都是相同的。比如,在Halo2中,驾驶状态(AI人物是车辆的驾驶员,还是乘员,还是徒步)和机警状态(AI人物是看到了目标,仅仅意识到了目标的存在,还是根本没有感觉的任何目标)在决定相关性的时候基本上都会被检测。
行为标签的想法,就是把这些共同的条件从相关性方程中移除,编码成行为的一个标签,在决策的时候直接检查。在Halo2中,这些条件被编码为一个比特向量,被用来和代表AI目前真实状态的比特向量直接比较。那些满足了特定条件的行为和刺激再进行完整的相关性检测。其它的责备完全忽略。
这种方法可以被看成是简单得为了提高相关性检测的速度,不过还有另一种有趣的解读方式。我们可以把这些条件看成对于行为树的大范围的上锁和解锁,从而可以从根本上修改行为树的结构。比如,对于一个车辆的乘员而言,被解锁的行为树范围实际很小,控制逃脱,自卫和搜寻的部分都不可以使用。而一个车辆的驾驶员就可以部分的接触到这些行为,但依然不如一个步兵AI所能做的多。如果我们仔细的考察遇敌这个行为,我们还会发现其他的东西,驾驶员的战斗行为和一个步兵的战斗行为是不同的,步兵单位使用战斗_行为(倾向于选择一个地点据守),而驾驶员则使用车辆_战斗_行为(这个行为保持AI单位不停的移动)。类似的,对于步兵和驾驶员而言,搜寻的过程也是很不相同的,主要是对于后者而言,有很多的团队行为可以去实现团队搜寻。
这是我们要展示的几个可以通过直接修改行为树本身来影响决策过程的技术之一。
图三: 步兵,驾驶员和乘员所可以利用的不同的行为,红色的部分是他们不能使用的行为。
激发行为
这里又是一个冗余性的考虑:想象一个“当首领死亡的时候就逃跑”这样的刺激。这个刺激会等到“首领死亡” 才会发生,然后转化为行为,检测AI人物是不是首领本身,战场附近还有没有其它的首领,等等。如果这些条件都得到满足,就会触发逃跑行为。问题是,在我们描述的结构下,每一秒钟都要去检测这些刺激。我们希望在我们知道根本没有任何AI单位死亡的情况下(当然也就不会有首领死亡了)不要去不停的监测这个刺激。从某种意义上,我们希望这样的刺激变成“事件驱动”的。
其中一种我们考虑的方案,是通过“激发行为”。这样的行为/刺激并不在静态的行为树结构中出现,而是通过“事件处理者”动态的添加到行为树里面去。在这个例子中,AI单位将会在它的主更新循环中异步的接受到“有AI单位死亡”这样的事件。假设之后这个死亡的单位被确认是一个首领级的AI单位,这就将触发“逃跑_因为_首领_死亡”这样的驱动刺激,在接受者AI单位的行为树上增加这样的内容。这意味着,在一定的时间内(在Halo2中是1-2秒钟),这个刺激将会被和其它静态行为和刺激一起被考虑执行。
图四: 同伴死亡的事件转化成“首领死亡,撤退”这样的刺激,这个刺激被动态的添加到行为树根的子行为列表里,这个刺激会触发撤退行为,打断交战和战后处理以及闲置这三种行为,但不会影响到自卫和撤退本身(假设AI在接受到这个刺激之前已经开始作出这些行为了)。
为什么说把刺激添加到真正的行为树里面很重要呢?其实,我们也可以简单的要求AI单位根据事件处理者代码的一些本地决策来发动一个逃跑的行为。我们没有这样做,因为的决定没有和整个行为树的环境整合起来,就不是一个“通盘考虑”过的决定。在上面这个例子里,如果我们已经运行了进入_玩家_车辆这个行为,那么就不可能逃跑,而如果我们只是在运行遇敌这个行为那么就可以逃跑。只有把激发行为放到行为树里面去,我们才能保证高等级和高优先级的行为在激发行为发生作用之前(之上)起作用。
这一点很重要,因为它说明了这样的事实:行为树的放置不但对相关性方程起作用,对于行为和刺激的决策过程也一样是重要的组成部分。激发行为也完全可以成为一个非枝叶行为,从而可以允许在它下面添加一整个分支行为树。从而,我们又发现了一个可以修改行为树结构从而得到精确的行为库的方法。
定制行为
当我们做不同的AI单位的时候,也可以采取相似的策略。在创建一个新的种类的单位的时候,我们希望高等级行为树结构保持相同——就像图一中所示——但要求转变触发机制的细节有所变化。在某些情况下我们就是简单的调节行为的参数来达成需要的结果。在其它的情况下,更加特别的触发机制是必需的,我们将使用定制行为。和激发行为一样,定制行为也被插入到行为树当中,在这里是一个预处理步骤,这样产生的子行为优先次序列表就不需要每一次都拿出来重新计算。使用这样的方法,我们可以添加任何数量的具体单位特有的刺激,行为,或者分支行为树,很多AI单位的个性就是通过这样的添加表现出来的。比如grunt,Halo2中胆小懦弱的物种,被添加了大量的撤退刺激,而那些marine就拥有大量的相互合作相关的行为,这让他们能够更好的表现出团体行为。
设计原则四:从已有的成功的地方出发,作相应的修改
当然,如果需要一个真正彻底不同的大脑,这样的从共同基础开始的做法就不实际了。在Halo2中的flood swarm,它们和一般的AI单位之间的区别非常的大,这就必须要为它们制作完全特制的行为树。
记忆和内存
对于大型的行为树,我们面临着临一个挑战:存储空间。在一个理想的世界,每一个AI都应该有一个属于自己的行为树,每一个行为都应该有一个属于自己的永久储存空间,从而每一个对于它们的功能来说必要的状态都会随时可用。然而,假设有100个AI单位,每一个都有大约60种行为的行为树,每一个行为占据32bytes内存,这就需要192K的永久行为储存空间。很明显,当行为树越来越大,内存的消耗也越来越夸张,尤其是对于Xbox这样的平台而言。
在大多数情况下,我们都只对一小部分行为感兴趣——那些真正在运行的行为(当前的枝叶行为,它的父行为,以及由此上溯的所有父行为)。如果我们注意到这一点,那么就有办法显著的降低对内存的消耗。一个显然的优化方法,是给每一个AI单位建立一个小的状态内存池,根据等级划分成小块。行为树变成了独立的静止结构,那些行为变成了代码片断,可以批量执行。这样一来,我们对内存的使用立刻就变得更加有效率了,100个AI单位乘以64bytes(存储行为所需要的容量上限)乘以4层(Halo2为例),总共是25K。很重要的是,这个数字只和行为树的最大深度相关,和具体的行为数量无关。
但这又给我们带了另一个问题,关于永存行为状态的问题。在Halo2中,有很多这样的例子,某些行为在上次成功地实行之后一段时间里不能再被实行(比如投掷手雷)。在理想的状态下,一个叫做“上次执行时间”的信息应该被存贮在永久存在的扔手雷这个行为里。然而,根据上面的说法,存储只能是暂时的,我们需要另外一个地方来储存这些永存性的行为数据。
还有一个更糟糕的例子:那些目标相关的永存行为状态怎么办?比如搜寻行为。当AI单位对一个特定目标执行这个动作失败了的时候,搜寻行为就会通知这个AI单位把注意力转移到其他方面去,但这并不排除转而去搜寻另一个不同的目标的可能性。所以这样的行为在失败了之后不应该被简单的关闭。
记忆的需求把行为树结构的一个内在的问题表现了出来。解决方案是,建立一个内存空间,或者几个内存空间,在行为树之外当作记忆树的存储代理来使用。
如果我们一般性的来考虑内存的需求,那么可以把它们分成四个不同的种类:
•行为相关的,永存的:扔手雷,最近的车辆行动
•行为相关的,短时的:那些当行为结束时状态就结束的
•物体相关的:感官信息,最后被看到的位置,最后被看到的方向
•物体行为都相关的:上一次格斗的时间,搜寻失败,寻径失败
第一种很简单,我们只需要在AI单位对象里定义一些名城确定的变量,让这些行为知道如何读取和操作这些变量就可以了。而且我们总是保证需要维持永存状态的行为数量和它们实际保存的状态数量都是最低,否则我们还是会遇到同样的内存容量问题。第二种我们也已经讨论过了,随着特定行为的开始和结束,行为状态会被在内存中分配和取消相应的空间。
对于第三和第四种种情况就要复杂一些了。这意味着对于AI单位所能考虑到的每一个目标,都应该有一个内在的相关的参数表现。事实上,有这样的表现会带来很多的好处。
图五: 内存结构
在Halo2中这样的表现被称为一个“命题”,这些东西的主要目的是存储游戏世界的特定物体的感觉信息。这些包括位置,方向,寻径定位之类的信息和游戏世界里的区分开来,被该AI单位的感觉所过滤(比如它们看不见墙后面的东西)。这样一来AI单位所掌握的信息就和实际游戏世界中的信息有了区别,AI所相信的东西不一定是真的,从而允许AI可以被玩弄,惊吓,使失望,迷惑等等。也正是在这样的“信以为真”的状态下,AI单位以此为基础作出决定。
有新意的是,这样做在行为方面也有益处,因为这些“命题”可以作为那些物体行为相关的内容的记忆存储场所。把行为状态和感觉历史放在一个地方允许我们方便的把这两类信息结合起来,从而可以有效的回答这类问题:“这个我当前听到的敌人是否是我以前已经搜寻过的目标?”和前面一样,需要存储物体相关的永存信息的行为越少越好,而且这样的存储空间需要被保持的尽可能小。
“命题”的概念是Halo2 AI的基石之一,实际上成为了整个AI知识结构的基础,AI对于游戏世界的观察和理解都围绕着它进行。不幸的是,这个模型还是非常的原始。说到底,它只是一系列对象的列表,没有空间联系(相对位置),没有结构信息(是否包含彼此),很有限的时间分析(比如连续的位置信息可以被判读成弹道轨迹)。而且,在实际使用中的一个巨大限制是,只有那些潜在的目标被允许出现在这个列表中,比如车辆,这排除了其他潜在的和行为相关的物体,比如障碍,机器,电梯,武器等等。
无论如何,给AI一个内在的对这个世界的印象产生了有趣的结果,更真实的行为,并且允许我们克服了一个和行为树相关的重要的存贮上的问题。
游戏设计者的控制权
让我们从另一个角度来考虑复杂性,来考虑(AI的)易用性和可操作性。这次,我们不关心AI是否会表现的可信,而是那些关卡设计者是否能够轻松的使用这些AI,利用这样的系统来完成一个对玩家来说有趣而精彩的游戏体验。
这也是复杂性导致的问题。比如,想想参数蔓延的问题。有很多不同种类的角色,每一种都有很多可以执行的不同的行为。每一种行为都被一些参数所控制。结合这些因素我们所要面对的数据量几乎是不可思议的大。到底是其中哪一个数字会让一个特定的敌人感觉到“不适”呢?这确实很难说。
游戏设计者必须告诉AI要去做什么--但是要详细到什么程度?显然我们并不感兴趣那种要求设计者把AI每一个行为每一件事情都实现计划好的系统,这样太复杂了。我们也确实需要AI能够掌控高级的行为,比如指导它们作风更加有侵略性,或者一般而言更加的怯懦。同样的,在位置控制上,我们也希望指令比较模糊一些:“去控制这一片区域”(而不是也用不着详细到具体的点)。
在下面的章节中,我们会看到解决这些问题的一些办法。
指令和火力点
在Halo1中,AI的定位是通过游戏设计者放置的火力点来实现的。火力点是一些AI在采取空间行动的时候会当作目的地来考虑的一些分离的点。比如,如果目标在障碍物后隐藏自己,AI就会试图去到一个可以直接看到目标的当前“假想”位置的火力点去--说是假想位置是因为此时目标可能已经移动了,但这种移动在AI的感官信息数据里没有更新。当执行射击行为的时候,除了要选取那些能看到目标的火力点,还要考虑到在这个火力点的时候,AI所使用的武器的射程能够把目标包含在内。
当我们开始给AI设定在给定时间的可用的射击位置的时候,火力点成为了一个极端有用的控制机制。在Halo1中,AI们被集合成遭遇群,遭遇群也包含了一套火力点。根据它们在和玩家遭遇时候的具体状态不同(伤亡大不大之类的),火力点中的特定的一部分会成为AI可用的。在Halo2中,基本的想法还是一样的,但表达有所不同。我们不再使用单一的遭遇群结构,取而代之的是小队的概念,这是一群AI单位,区域,以及火力点的集合。在AI、区域和火力点之间的映射是一种新的结构,被称作指令。
本质上,指令是对于一群火力点的描述。当一个指令被下达给一个小队的时候,这个指令所描述的火力点就对这个小队里的AI单位开放。这是一个简单的机制,不过指令也可以加入一些基本的脚本功能,从而允许在多个指令之间自动的转换。设计者可以使用一系列的可能性触发条件,比如“是否有一定数量的小队被消灭了?”“这个小队是否发现了玩家?”等。当某一个触发条件得到满足,这个小队就被赋予一个新的指令。这样一来,设计者就可以用非常简单的高级表述方式来描述一般的战斗流程了。
指令的概念和现实生活中军队的司令官给他的士兵下命令是一样的。“我们要去占领那个山头!”“我们要守卫这个地方直到骑兵到来。”大多数这样的指令都是“去某地做某事”的变体,或者更精确一些:“去某地,执行这样的行为。”
到这里为止我们只说了我们的指令是如何做到前面一半的。事实上指令对于表达其后一半也是非常的有用。在Halo2中,游戏设计者可以通过在指令里添加特定目的的旗标来决定:是否允许使用车辆,是否悄悄的接触,限制接战的规则(随意射击还是等敌人开火我们再开火)等。
指令还可以通过引用“风格”来影响行为。风格也许是我们最后也是最直接的控制AI行为树结构的方法。风格其实是一些列被允许和被禁止的行为的列表。就像如果一个行为的标签和AI单位的目前状态不相符就不能被使用一样,一个行为要被使用,也必须是被指令的风格明确允许的。
图六: 指令和风格。一个接受到“攻击开始”这样的指令的小队可以在后续的游戏中转变成“继续推进”或者“放弃后撤”的指令,这取决于战斗的走势。每一个指令都引用属于它自己的火力点和风格,从而指导游戏行为是需要“有侵略性”“保守一点”还是“鲁莽的”。
由于风格机制的直接性,这种手段是非常有力也非常危险的。有可能给AI这样风格,让它实际上什么都做不了,或者让它极度的衰弱只能随机行动。由于这些原因,风格通常不会在每一个遭遇中都被使用。实际情况是,当设计者建立一个指令的时候,他们有一个风格库可以选择。当然风格可以允许产生一些有趣的变化。防守风格不允许冲锋或者搜寻行为被执行。侵略风格则不去考虑自我保护。非战斗风格完全禁止任何的战斗行为,只允许采取闲置或者撤退的行为。风格也允许设计者从某种程度上去改变那些控制行为的参数,比如在怯懦风格下,AI单位会更容易的逃跑。
指令和风格是我们让战斗的游戏性变化多样的主要手段。使用这两个工具,游戏设计者可以让同一个AI在不同的情况下表现出不同的行为,当然这种不用需要和故事的进展和关卡推进相结合。
参数蔓延
现在我们来讨论最后一个我们游戏设计者面对的复杂性相关的问题,这个问题我们上面已经提到过,就是参数蔓延。一般来说为了保证行为的可定制性,一个行为要附带3-5个可供游戏设计者编辑的参数。然而,三个参数,乘以115个左右的行为,再乘以大约30个AI单位类型,我们就有10350个不同数字需要维护。
这显然不是一个有条理的状况,我们可以极大的减轻游戏设计者的负担。记住上面我们说的第四条:从已经证明有效的东西开始,然后再逐步改变。
导致AI单位种类数量巨大的一个原因是因为存在种类变化。一个白色的精英战斗的时候比红色的更加有侵略性,其它物种内部也存在这样的问题。然而在其他方面,白色和红色的精英都是一样的。这么一来,为这两种精英战士分别设计完整的行为参数组就是一种浪费。我们实际上是用的,使一个可以允许我们只定义那些能区别这两个种类的参数的系统,剩下的内容都交给父类种(精英,相应的白色和红色的精英就成为这里的子AI单位)完成。
所有的AI单位和行为参数都储存在一个.character文件中,这个文件包含了单位的名称,以及该单位的几何模型数据。当游戏设计者放置AI的时候,他首先选择一个这样的.character文件。
这个文件并不是一个纯参数列表,而是一个参数块列表,每一个块都以一定的逻辑方式组织起来,控制行为的一个特定方面:比如自卫参数块,攻击参数块,武器使用参数块等等。当然并不是所有的文件中都包含了所有的参数块。当一个AI试图运行一个在自身的.character文件中缺失的参数块相应的行为的时候,它就会回溯到上一级父类种的.character文件中去找寻,如果还不存在就继续回溯直到找到为止。由此就形成了AI单位的等级系统。整个等级系统树的根位置上是一个一般性单位,它负责给所有的参数块定义合理的参数值。
图七: AI单位的等级
===========================================================
.