说一下翻译这篇文章的目的。gamasutra上已经很久没有这么好的文章了,说好有两个原因:

1.目前游戏行业的单个项目投入和风险都与几年前大不相同。可能是出于与公司的合同细则之类的因素,越来越少有业内人士无私的分享实际项目的开发心得,除了收费的GDC。交流的匮乏其实阻碍了行业的发展,虽然在商业社会这无可厚非。与动辄数千万的费用投入和数年的耗时相比,对于大公司来说,游戏引擎之类可以“用钱解决的问题“都不是问题,真正制约品质的壁垒来自公司与项目团队目标的一致性,开发的理念方法和开发个体的经验能力。有ND和作者这样无私的存在,难能可贵。

2. AI是任何游戏必须考虑的问题,文章讲述了《神海2》的AI系统的技术细节,使用方式以及遇到的一些问题,非常有代表性。但让我们先忘掉细节,也忘掉作者对伤害设置,数值平衡,音效处理,视觉系统,AI细节表现(这几个部分确实亮了)的精巧处理。本文带给我的最大感触是:欧美的策划设计师乃至整个开发团队对游戏模块化对象化,逻辑分离和数据驱动的理念保持着高度统一的认识和追求。

首先,整个引擎,工具,以及游戏中的各种特性是如何划分为各种模块组件,这些模块组件之间的关系,使用配置的方式,他们非常了解。其次,策划根据自己在玩法设计,关卡设计创意面的需求,不断以”通用性“,”模块化“,”实时生效“等标准推动技术,美术的开发。他们了解构思一个好的玩法和实现一个好的玩法的不同,前者需要创意,后者则在把前者落实到实际游戏体验的过程中,更需要一整套高效的,便于原型开发和不断调整的实现方法。他们懂得这两个部分对于策划工作来讲都同样重要,也知道如何去规划和推动技术美术团队产出策划想要的那些开发,配置工具。

如果之前没有体验过《神海2》的话,这是一款不该错过的好作品。另外,对引擎脚本完全没有认识的话可能会影响阅读理解,可以通过一些好的资料做基础了解。推荐 UDN(中文的,你们懂的)。

==========================================================================================================================================

by Benson Russell 原文链接

概述

这篇文章是我关于《神秘海域2》中AI系统的一些技术细节的分享,讲讲我们是如何进行大家都需要面对的AI处理的。无论游戏类型如何,绝大多数的游戏都有这样或那样的AI,AI已经成为游戏产品必然包含的一坨需求了。大家或许都有自己熟悉的AI系统,也许还有一些这方面的新思路或者点子。

在顽皮狗,我们的AI系统由许多高度模块化的子系统构成,以满足咱们五花八门的需求。宏观的来看,AI系统可以划分为:

- 策划专用的组件,用以设置游戏玩法相关的实体行为。

- 美术专用的组件,用以修改实体的外观和动画。

- 决策组件,用以规划实体起始路径,以及在路径运动过程中的事件。

在AI系统的开发过程中,我们秉持着将模块化进行到底的理念,以便后续开发中一旦有了新的创意,或者游戏机制需要修改,可以非常快速的进行原型开发和实验。虽然这样做在开发阶段会带来团队更多的跨部门沟通成本,但与之带来的好处相比,绝对的物超所值。

这篇文章中我将专注于以一个策划的视角来解析《神秘海域2》的AI系统。

神经中枢… 核心系统…

《神海2》AI系统的核心是AI模板体系。AI模板让我们可以用一些默认值,通过AI系统的各子模块协同生成一个新的AI实体。这是一个非常开放的系统,整个《神海2》中策划需要的任何实体行为,都不是我们亲爱的程序员们写死在游戏引擎中的(当然如果确实有必要,这样的处理也是可行的)。策划主要通过脚本语言进行参数设置和AI开发,而且这样的开发在游戏运行时可以实时的进行。

下面我将以创建一个AI实体为例,详细的介绍整个《神海2》的AI系统。

AI原型模板

当一个AI实体在游戏中出现时,他实质上是由一堆数据来生成的。这些数据中层次最高的那些,构成了所谓的AI原型模板。原型模板与游戏关卡内的物件绑定后,就形成了一个AI实体。这样说还是太抽象,举个例子,《神海2》有一个“轻型手枪杂兵”的模板(你一定知道那个穿着浅灰色军装挥舞着Defender手枪的家伙):

【脚本不做翻译,只解释存疑部分,不然影响后续阅读】


(define-ai-archetype light-pistol   脚本起始,定义了一个叫“轻型手枪”的原型模板

:skills (ai-archetype-skill-mask    这个原型拥有技能列表为:

script                                    脚本化【后面有解释】

hunting                               追击

cover                                    利用掩体

open-combat                    主动攻击

alert                                      警戒

patrol                                   巡逻

idle                                        发愣

turret                                   利用炮台

cap                                        Cinematic Action Pack 影视化动作集

grenade-avoidance        躲避手雷

dodge                                   闪避近身攻击

startle                                  莫名惊诧

player-melee-new           乱斗

)

:health 100                                                          生命值100

:character-class ‘light                                     角色类型 轻型

:damage-receiver-class ‘light-class            受伤类型 轻型

:weapon-skill-id (id ‘pistol-basic-light)   武器技能(普通轻型手枪)

:targeting-params-id (id ‘default)              目标参数 (默认)

)

以上是一个原型模板最基本的组成部分,虽然简单,但却已经包括了构成一个AI的关键要素。

关键字:SKILL

也许你注意到了脚本的第一部分(也是最重要的一个部分)是原型的技能列表。它是AI的核心,定义了由此模板生成的实体的能力项。能力项同样是高度模块化的,可以很容易的添加和删除新技能。下面对例子中的技能做一些解释:

- Script: 脚本化关键字代表了此AI将会被脚本系统托管。这主要是应用在当我们需要游戏实体响应我们设定的命令时,比如播放一个特定的动作,或者移动到一个特定的位置。

- Cover: 让AI实体有识别掩体和利用掩体的能力(后叙其详)。如果要怪物直面血腥的人生,移除此关键字即可。

- Open-Combat: 冲出掩体主动进攻。

- Turret: 使用炮台(这不只是玩法上的也是技术上的设定,涉及到实体的动作动画和行为的匹配)。

- Idle: 未遇敌状态下各种发呆。

- Patrol: 添加了此技能的实体会沿我们在编辑器中设定的路线巡逻。

- CAP: CAP是影视化动作集( Cinematic Action Pack )的缩写。动作集( Action Packs (AP) ) 简单的说就是使得AI实体根据具体情况执行特定动作(比如飞檐走壁之类)。CAP 保证了实体在执行特殊动作时(比如抽烟)也能对突发事件做出及时响应,CAP包含了所有实体会用到的动作动画信息,包括一些用作状态切换的非常短促的动作。

当然,除上述的这些能力项,我们还针对游戏玩法的具体需求开发了大量的模块化的AI技能。我们的AI工程师只需开发一个新的技能,加入到技能列表中,策划们就可以轻松愉快的应用,在游戏中实时加载和移除各种技能让我们很容易调整游戏的具体玩法。

关键字:Health

生命值这个关键字无需赘述。特别的地方是,我们将生命值的数据类型由《神海1》的浮点型替换成了整型。原因是在《神海1》中我们常常会遇到浮点型数据带来的不可预知的问题(比如用1.0做判断,传参为 0.999999这样的悲剧,这就让玩家常常要再多打一枪才可以放到敌人)。在《神海2》中我们将所有的实体(无论玩家还是怪物)的基本生命都默认设定为100。有点奇怪是吧?如果需要一个强劲的敌人怎么办呢?大多数情况下,这样做的结果清晰,简单,易于平衡。具体问题后续会有详解。

关键字:character-class

角色类型决定了AI实体的基本行为和表现。它由程序和美术共同来设定。《神海2》中有轻型,中型,重型,装甲型,护盾型,村民和SLA这几个角色类型。(SLA是香格里拉军ShangriLa Army的缩写,玩过都懂的)

关键字:damage-receiver-class

受伤类型提供了一个便利的方式用以平衡AI实体在游戏中受到伤害,也就是玩家使用各种武器对怪物造成伤害的数值。这个模块同样被实现的非常开放,策划无需倚靠任何程序的帮助,就可以自如调整游戏中任何伤害相关的数值。

实现这个模块的初衷来自于我们在开发《神海2》的过程中意识到需要不断微调不同武器和不同类型的怪物之间的对应伤害数值。在《神海1》中(其实是在我参与开发过的大多数项目中),武器对不同的目标输出固定的伤害,当需要调整时,被调整是目标的生命值。显而易见,这种方法做平衡的时候会无比蛋疼。比如当你调整了一个武器的攻击力,显然你就得连带着调整所有此武器会攻击到的怪物的生命值,那又势必会调整到能攻击这些怪物的所有武器的攻击力,你懂的。。。。。。这样同时还会造成游戏内的怪物的生命值过于缭乱(怪物A 90,怪物B 400?!)

为了化解上述的问题,我们设计了受伤类型系统。策划面只需要维护一张简单的表格,表格内的数据可以通过Python脚本导入到我们的脚本系统中。表格如下图所示:


(点击放大)

表格中的列代表《神海2》中的所有武器(这里用的名字是我们开发中给每个武器命名的代号), 而每排则是一个受伤类型。 单元格里的数据就是每种武器对每种受伤类型造成的伤害。

还记得我们的例子使用的Defender手枪吗,表中的 “pistol-semi-a” 就代表了它。你可以看到,Defender会对轻型单位(light-class)造成34点伤害,而中型单位(medium-class)则是17点。策划面可以根据我们的需求设定受伤类型,在游戏内不只是AI实体,所有的单位都根据受伤类型来计算和平衡伤害,非常的简单明了。

关键字:Weapon Skill ID

通过武器技能id,AI实体就知道自己拿的是什么武器,应该如何使用了。我们使用一个非常简单的结构体配置武器的精确度,伤害和火力特性。下面我们来看个例子:

【看不懂的地方下面有详细解释】

(new ai-weapon-skill

:weapon ‘assault-rifle-b

:type (ai-weapon-anim-type machine-gun)

;; damage parms

:character-shot-damage 5

: object-shot-damage 120

;; rate of fire parms

:initial-sequence-delay (rangeval 0.0 0.0)

:num-bursts-per-sequence (rangeval-int 10000 10000)

:auto-burst-delay (rangeval 0.4 0.8)

:auto-burst-shot-count (rangeval-int 3 5)

:single-burst-chance 0.33

:single-burst-delay (rangeval 0.4 0.8)

:single-burst-shot-count (rangeval-int 1 3)

:single-burst-fire-rate (rangeval 0.16 0.20)

;; accuracy parms

:accuracy-curve *accuracy-assault-rifle-upgrade*

:time-to-accurate-cover 3.5

:accuracy-cover-best 1.0

:accuracy-cover-worst 0.0

:accuracy-running 0.9

:accuracy-rolling 0.5

:accuracy-standing 1.0

:master-accuracy-multiplier 1.0

:master-accuracy-additive 0.1

)

开始部分的参数是美术相关的。“Weapon”参数通过引用内部命名让游戏引擎知道应该使用什么模型材质,特效音效来表现这个武器。“Type则设定了动画 (手枪, 机枪,或者是散弹枪)。 伤害部分很简单不解释。

Rate Of Fire”参数定义了AI如何使用该武器,也是在这个部分设定武器的火力特性(主要是射速相关【射速是指武器每秒出膛的子弹数,越高表示子弹发射的频率越快 】)。设置此部分时有一些非常关键的参数。首先是武器发射子弹的数量,这影响到武器命中玩家后造成的伤害。其次是武器发射时的特性(比如是一把自动步枪还是一把老式手枪)。

另外一个非常关键的问题是音效,这可是个让人头痛的问题。我们非常希望AI在使用武器的时候能够如现实生活中的射击那样真实,同时又不可能让每个AI在战斗中拿着各种全自动武器没有任何间隔的一阵乱轰,这样的话不但效果适得其反,而且会严重破坏玩家的体验。如果几十把枪在同一时间一起射击,那只会制造一阵恼人的噪音,并且让音效模块超载。何况这样玩家就根本分辨不出AI是使用何种武器在跟自己对抗了。

Rate Of Fire“ 参数设定了一个序列,序列有一个初始化的间隔(在本例中没有使用这个间隔)。攻击序列由几个元素构成:爆炸参数,自动武器连射参数,点射攻击参数。看上面的例子,这把武器会以33%的几率进行点射,射击1到3颗子弹,射速为0.16~0.2秒,每次发射后间隔0.4~0.8秒。或者它可以自动发射,每次3到5颗子弹,间隔同样是0.4~0.8秒。

accuracy精度参数引用了一个”accuracy-curve“【精度曲线】,下面是它的配置方式:

(define *accuracy-assault-rifle-upgrade*

(make-ai-accuracy-curve

([meters 60.0] [chance-to-hit 0.0])                            60米以上命中几率为0,以下以此类推

([meters 30.0] [chance-to-hit 0.3])

([meters 20.0] [chance-to-hit 0.4])

([meters 12.0] [chance-to-hit 0.4])

([meters 8.0] [chance-to-hit 0.5])

([meters 4.0] [chance-to-hit 0.9])

)

)

精度曲线定义了武器不同射击距离下的精度跃变点,在不同的点之间的距离按照线性插值的方式取得实际精度。把精度曲线单独定义的目的,我想你们都猜到了,便于重用。。。。。

精度曲线后面的部分都比较好懂,可能会有疑问的是在”Time To Accurate Cover“ 部分。这个参数的含义为,玩家从掩体中探头射击AI时,AI的命中精度会自动设置为”Accuracy Cover Worst“ (例子中位0), 然后经过设定在” Time To Accurate Cover“中的时间 (例子中为3.5秒)后,其精度调整为”Accuracy Cover Best“ (例子中位100%命中).这样设置的目的是给玩家从掩体杀出时一定的回旋时间,之后AI会越打越准。

利用武器技能的配置,我们能够实现各种各样容易抑或很难对付的AI。比如我们例子中的 这位“轻型手枪杂兵”,就因为“Time To Accurate Cover”设置的很长,射速又很慢,非常好对付。如果是个“中型”单位,我们会提高它们的射速,缩短“Time To Accurate Cover”的时间,给玩家更多挑战。另外不同射速的武器因为听起来就会不一样,玩家就非常容易分辨(比如一把AK和M4火力特性就明显不同)。

关键字:Targeting

模板示例的最后一个参数是目标。AI的目标选择是通过仇恨系统进行计算的。每一个潜在目标都有一个自己的仇恨值,这个仇恨值配置在脚本中。AI会自动选择仇恨值最高的目标来攻击。下面来看看例子中定义的“default”默认配置是什么样子:

(new ai-targeting-params                    定义目标参数

:min-distance 0.0 ;; 最小检测目标距离

:max-distance 200.0 ;; 最大检测目标距离

:distance-weight 50.0 ;; weight to apply, scaled according to distance away

:target-eval-rate 1.0 ;; 检测目标频率

:visibility-weight 10.0 ;; 发现目标时的仇恨度

:sticky-factor 10.0 ;; 已瞄准的目标的仇恨度

:player-weight 10.0 ;; 目标是玩家的仇恨度

:last-shot-me-weight 20.0 ;; 被目标攻击的仇恨度

:targeting-me-weight 0.0 ;; 被目标瞄准的仇恨度

:close-range-distance 11.0 ;; 进入特定区域后的仇恨度

:close-range-bonus 0.0           上述参数的加成

:relative-distance-weight 10.0 ;;  后续其详

:relative-search-radius 100.0

:relative-dog-pile-weight -1.0 ;;

【特殊仇恨】

;; weights for specific vulnerability states,

:vuln-weight-cover 0.0

:vuln-weight-peek 0.0

:vuln-weight-blind-fire-cover 0.0

:vuln-weight-roll 0.0

:vuln-weight-run 10.0                          跑动仇恨加成

:vuln-weight-aim-move 20.0             瞄准移动仇恨加成

:vuln-weight-standing 20.0                站立仇恨加成

:in-melee-bonus 0.0 ;;

:preferred-target-weight 100.0 ;;

)

上面的配置含义如下:

该AI会在200单位距离内搜索目标,如果目标距离更近,或者目标可见(visibility-weight),或者被目标攻击 (last-shot-me-weight),或者目标是玩家(player-weight),或者是已经瞄准的目标(sticky-factor),都会增加仇恨。每秒钟AI会重新确认一次目标(target-eval-rate)。

还有一些参数是用来设定AI群攻玩家时的表现的。比如,如果是一场较长的战斗,我们肯定不想AI都同时攻击同一个目标。

relative-dog-pile-weight 表示有同类AI攻击相同目标时的仇恨值(正因为如此,这个值是负数)。实际运算时会用瞄准同一目标的同类AI的数量乘以该值来计算,所以能有效控制上述情况不会发生。

relative-distance-weight使得AI会按照relative-search-radius中设置的范围查找是否有比自己更接近目标的同类AI,一旦发现则仇恨衰减。这样的目的是当AI对目标群起而攻之的时候,离目标最近的AI会有更高的优先级。

特殊仇恨定义了特定情况下的仇恨加成。比如示例中,玩家在跑动,瞄准下移动和站立的情况下分别会有10点,20点和20点的仇恨加成。

preferred-target-weight 是便于策划对特定AI设置特定目标用的。这意味着一旦该AI的“preferred-target”(优先目标)出现,直接会加上100的仇恨。

如你所见,我们有许多参数来配置目标的选择。在《神海2》中我们开发了非常多这样的脚本,甚至会为某一个关卡设计专门的目标选择方案。上述的仇恨值参数都可以为负数,这也给我们在剧情开发时带来了许多的便利。

这就是AI原型模板!

上面的这些组件就构成了我们的AI原型模板!当然有一些组件本文不会提到(比如一些用于测试或者原型开发的),但基本上常用的都列出来了。灵活的运用这些参数,脚本,配置让我们一旦有了什么点子都可以很快的开发并在游戏中验证。整个开发的自主权都牢牢掌握在策划手中,只有当我们需要新的技能或者添加一些特殊的内容,才需要麻烦我们的程序员。

美术表现

是时候定义AI们的美术表现了(包括造型和动画)。造型方面,我们有一个非常灵活的“造型系统”供策划自由的设定AI的形象和动作。例如“轻型”单位和“中型”单位各有一个基本躯干(不包括头部和手),很多各式各样的“头手模块“(手和头单独出来是为了贴图的吻合),以及数量众多的,可以随意组合的各色武器,装备。通过脚本配置的方式,我们可以编写一个”造型模板“并使之与AI原型的模板绑定起来。这种”造型模板“是可以嵌套使用的。比如说,我们有一群同系列的怪物,他们戴着不同的帽子和面具,其他部分相同。则我们可以编写一个”基本造型模板“,在其中嵌套数个”头部造型模板“,这样在怪物出生时,他们会运行时随机的生成各自的头部。

《神海2》的动画系统有非常强大的映射插值能力,这意味着我们可以在任何时候平滑的进行任何动画表现。这就使得AI系统按照我们的配置进行各种动作时都可以有正确的表现,而不同动作也可以通过重新映射来完成实际播放中的调整,而不需要根据实体的具体表现不同把同一动作完全重做。比如”轻型单位“和”中型单位“在掩体后进行射击的动作是不同的,”轻型单位“射击动作幅度更大,把自己更多的暴露给玩家。在实现上这只需要在AI系统中配置不同的参数,动画系统会自行为”轻型单位“和”重型单位“运算不同的表现。在小巷护送受伤的Jeff,以及Drake拿着燃烧的火把在沼泽古庙中行走时也使用了这样的技术。

决策组件

最后来看看决策组件是如何让AI按照设计师的设定来行走,与游戏世界交互的。

寻路问题

AI要在游戏中自如的行走,干一些挺酷的事情,它们就必须要完完全全的”认知“自己所在的这个”世界“。为了达到这个目的,我们在地图中添加了导航网格navigation mesh(简称navmesh 不了解网格含义的话可以简单理解为3d模型),用以帮助AI界定游戏世界的边界。在Charter编辑器中我们有专门的工具快速直观的创建navmesh。

[下面的内容,如果读过作者前面一篇介绍《神海2》战斗设计的文章,更加容易理解 前文]


(点击放大)

如上图所示,我们在编辑器中初始化了一块navmesh。每个顶点上的黄碟(= =!)是我们编辑navmesh的特殊方式。


(点击放大)

通过拖放黄碟中心的绿色标尺可以编辑navmesh的高度


(点击放大)

点击连接两个顶点的红线,可以添加新顶点。点击黄碟中心则可以删除顶点


(点击放大)

点击navmesh的中心并拖放就可以在上面挖洞,洞本身的形状可以通过添加,删除顶点和调整顶点位置来完成。

多个navmesh可以通过顶点吸附结合为一体,通过设置navmesh,我们就为AI提供了导航的基础:游戏世界的边界。接下来的事情由导航地图navigation map(简称navmap完成。如下图所示,navmap是地图中的navmesh和其他运动物件在水平面的投影信息。我来说明一下


(点击放大)

图中中心位置的AI是被选中的AI实体(脚下有个红圈的那位)。游戏世界被投影到一个2d平面网格上,有的格子被渲染为黄色,这代表了这些区域是不可通行的。这些黄色格子是根据navmesh定义的边界以及一些运动物件的体积信息(比如AI)生成的。运动物件的体积信息一般直接使用碰撞盒,当然也可以在编辑器中动态设置,还可以在代码中进行特殊设定。navmesh和navmap给设计师进行玩法设计带来了非常高的自由度,同时也让AI在寻路的时候不需要不断实时计算各种障碍物。

在解决了基本的寻路问题后,我们面临的难题是,当需要AI跟随玩家通过一些复杂地形时,该如何处理。应对这种情况的解决方案我们称之为寻路动作集Traversal Action Packs(简称TAPS)。我们专门为游戏中的物体创建了地形相关的动作动画(比如扶墙入,跳阴沟,爬梯子,等等),并将之与Charter编辑器进行整合。结合我们的动画系统,AI就能活灵活现的在各种地形中自如移动了。

战斗行为系统

现在AI们了解游戏世界的环境了,接下来他们需要有办法对环境进行”评估“,该站在什么地方才适合下一步的动作?哪里是自己要移动到的特殊位置?为此我们设计开发了战斗行为系统Combat Behavior System。这是一张坐标权重图,AI以每秒一次的频率,根据下列条件计算不同位置的权重:

  • navmesh等地图信息

  • 掩体坐标

  • 区域标识Zone markers(后续其详)

这些位置的权重根据脚本来配置,下面是《神海2》中用到的一个例子:

(new ai-combat-behavior           创建战斗行为

:dist-target-attract 15.0 ;; 距离攻击目标最大距离

:dist-enemy-repel 5.0 ;; 距离攻击目标最小距离

:dist-friend-repel 2.0 ;; 距离己方单位最小距离

:cover-weight 10.0 ;; 掩体位置加成

:cover-move-range 15.0 ;; 移动到掩体的距离

:cover-target-exclude-radius 8.0 ;; 目标此距离范围内的掩体无视

:cover-sticky-factor 1.0 ;; 已经进入的掩体优先加成

:flank-target-front 0.0 ;; 目标正面区域加成

:flank-target-side 0.0 ;; 目标侧面区域加成

:flank-target-rear 0.0 ;; 目标背面区域加成

:target-visibility-weight 5.0 ;; 可以看见目标区域加成

)

虽然不是全部参数,但这个例子中包括了大多数重要的指令。这是一个简化了的《神海2》中距离AI的战斗行为配置脚本。这个AI的大体行为模式是:与攻击目标保持5~15米的距离,同时与己方单位保持2米以上的距离。他们喜欢呆在掩体内,为了进入掩体可以移动15米,但是他们不会在目标8米以内的区域寻找自己要待的掩体。同时他们比较喜欢已经进入的,或者可以看见目标的掩体。

战斗行为系统的高度模块化可以让策划通过建立行为库的方式支撑任何我们想要的剧情和玩法。另外在大型室外战斗中,因为玩家的行动路线是非线性不可预知的,通过战斗行为脚本的配置,AI可以自己选择最佳的位置执行设定的命令。当然这个系统也有一些我们无法彻底解决的缺陷。比如,按照配置脚本的运算,AI有可能会出现在两点间循环移动的情况。我们最后是通过设定一些特殊参数的方式来避免出现这类问题的。另外一个问题是,(我们的必须时刻牢记)战斗行为系统和寻路系统是完全独立的,战斗行为系统只是计算位置和距离,寻路系统则负责将AI移动到对应的位置。举个例子,战斗行为系统计算出5米内一个优先级非常高的位置,寻路系统也许需要一个非常诡异的路径才能让AI移动到那里,比如走十万八千里绕过一道无法攀爬的高墙)。这也是模块化系统一个非常核心的,需要小心处理的”弱点“,就是各个独立模块之间的数据共享我们也想了很多办法让我们的系统运转的更好。


区域

前面提到了区域”zone“这个概念,这是战斗行为系统的另外一个常用的配置。区域指关卡中一些特定的点,我们称之为区域标识Zone Marker组成的部分, 包含了范围和高度信息,以确定区域的边界。被设定了区域的AI在地图中只会在此区域内进行战斗行为的运算。区域可以在任何时候动态设置,甚至可以设置在游戏内的物件上(比如玩家身上)。这使得我们可以让某些AI在地图的特定区域或者与特定物体死磕。

知觉系统

最后,AI需要一些知觉来决定自己采取的行为(主要是听觉和视觉)。为了改变《神海1》中AI总是知道玩家在哪里的情况,我们设计新的游戏机制和AI表现,让他们不再像开了全图挂一样。

视觉系统主要由视锥体和计时器组成。视锥体分为内外锥体,主要由4个参数来配置,水平视角,垂直视角,视距和成像时间。角度参数决定AI们看的范围和远近,成像时间决定AI花多少时间”看到“有东西进入了它们的视野。成像时间这个参数是按照视距线性增加的,也就是说离得越近的物体,AI们越快看到。

当目标进入外锥体,计时器开始计时(请记得时间与距离的线性关系)。如果成像时间到,AI会觉得自己仿佛看到了什么,他们会转向发现目标的方向并移动到目标位置。如果此时目标进入内锥体,计时器又开始计时,同样的,成像时间一到,AI就会发现目标。双重锥体的好处显而易见,AI可以”丢失目标“。这给玩家的战斗体验加入了更多的策略性,也让AI表现的更加有生命力。

这样的视觉系统每个AI有三套,会根据玩法需要来选用,分别为:环境视觉,专注视觉和战斗视觉。环境视觉在AI没有发现任何目标的时候使用,通常是较小的视锥体和较长的成像时间,给玩家足够的机会进行潜入和攻击。专注视觉则适用于,如果AI真正聚精会神的干什么特别的事情,那他们对周遭的反应理应表现的不那么灵敏,当然,这意味着更小的视锥体和更长的成像时间。战斗视觉用在AI发现目标后,成像时间大幅降低,视锥体大幅扩展,AI看起来就反应敏捷生龙活虎了。

【听觉仿佛作者忘记了 = =!】

好了,这次就先到此为止。有关《神海2》开发中的一些技术细节和心得体会,未完待续。。。。。。

AI With Class

The Character Class sets the look of the character in terms of their animation sets. The classes are defined under the hood between the programmers and animators. For Uncharted 2 we had Light, Medium, Heavy, Armored, Shield, Villager, and SLA classes (as an interesting side-note, SLA stands for ShangriLa Army, which were the people mutated by the sap from the Tree Of Life that Drake fought at the end of the game).

Damage

The Damage Receiver Class is the main way we balance the game in terms of how much damage the enemies can take from the different player weapons. This is an open-ended system in that the designers have the power to set up everything however they choose without having to rely on the programmers.


We implemented this system in Uncharted 2 because we kept running into issues where we wanted to tune the different weapons against different types of enemies.

Originally for Uncharted 1 (as with most games I’ve worked on) weapons did a set amount of damage against everything, which you tuned by setting the health of everything. This tends to cause many headaches when it comes to balancing.

For example, if you decide to change the damage of a weapon, it would have a spiraling effect on having to re-balance almost everything because you would have to re-adjust the health of all the enemies, which in turn means changing the damage of other weapons, etc…

It also causes for a lot of confusion in that enemies would have seemingly random amounts of health upon inspection (i.e. enemy A has 90 health yet enemy B has 400?!)

So to solve this, we created the Damage Receiver Class system. It starts with a simple spreadsheet that designers can set up, and then we run a Python script to convert it into our game script. It looks like this:


(click for full size)

The columns represent the weapon names (these are under-the-hood names that designers have set up elsewhere), and the rows represent the different Damage Receiver Classes. The intersection of each row and column represents how much damage a particular weapon will do against a particular Damage Receiver Class.

So looking at the spreadsheet for example, “pistol-semi-a” (which is the Defender pistol) will do 34 damage against anything that has a Damage Receiver Class of “light-class,” and 17 damage against anything with “medium-class.” We can create as many Damage Receiver Classes as we need and assign them accordingly (and not just to AI). This makes balancing the game very straightforward and simple.

Weapon Skill ID

This is how the AI understand the weapon they’ve been given and how it’s supposed to operate. It’s a simple structure that defines how the weapon will behave in terms of accuracy, damage, and firing pattern. We purposefully separate the player’s version of these parameters so we can customize how effective the enemies will be with any particular weapon. Here’s what one would look like:

(new ai-weapon-skill
:weapon ‘assault-rifle-b
:type (ai-weapon-anim-type machine-gun)

;; damage parms
:character-shot-damage 5
:o bject-shot-damage 120

;; rate of fire parms
:initial-sequence-delay (rangeval 0.0 0.0)
:num-bursts-per-sequence (rangeval-int 10000 10000)

:auto-burst-delay (rangeval 0.4 0.8)
:auto-burst-shot-count (rangeval-int 3 5)

:single-burst-chance 0.33
:single-burst-delay (rangeval 0.4 0.8)
:single-burst-shot-count (rangeval-int 1 3)
:single-burst-fire-rate (rangeval 0.16 0.20)

;; accuracy parms
:accuracy-curve *accuracy-assault-rifle-upgrade*

:time-to-accurate-cover 3.5
:accuracy-cover-best 1.0
:accuracy-cover-worst 0.0

:accuracy-running 0.9
:accuracy-rolling 0.5
:accuracy-standing 1.0

:master-accuracy-multiplier 1.0
:master-accuracy-additive 0.1
)

At the top there’s some key information regarding the visuals. The Weapon parameter references the internal game name of the weapon so it knows what art, FX, and sounds to bring in. The Type parameter sets the animations to be used (either pistol, machine-gun, or shotgun). The Damage section I think is self explanatory, and again we use a base health of 100 for the player.

The Rate Of Fire parameters defines how the AI will use the weapon, and it is here that we set up the firing patterns. There are several key issues we have to keep in mind while setting these up. The first is the actual volume of bullets as that effects how much the player can be damaged. The second is the actual behavior of the weapon (i.e. making sure a fully automatic weapon behaves as such).

The third is how the audio will be perceived in combat. This is an issue that we’re constantly at odds with trying to balance. We want the AI to fire the weapon in a realistic manner as to what a normal person would do, but we also don’t want every AI in a combat space firing fully automatic weapons without any pauses.

It not only doesn’t feel right and breaks the immersion, it just sounds plain horrible! The cacophony of that many weapons going off at once overloads the system and instead of an intense firefight you get distorted noise. It also makes it more difficult for the player to be able to identify what weapon an enemy is using.

The way the Rate Of Fire parameters work is that there’s a firing sequence with an optional delay between each sequence (in this case it’s not being used). A sequence consists of a number of bursts (in this case set to 10000), and each burst can either be fully automatic (in which case it uses the fire-rate set by the weapon) or a set of single shots with a manually entered fire-rate.

So in the above case each burst has a 33 percent chance of being a set of single shots, fired at a rate of .16 – .2 seconds between each shot, and a delay of .4 – .8 seconds before the next burst fires. Else it will be a fully automatic burst consisting of 3 – 5 bullets, and a delay of .4 – .8 seconds before the next burst.

The accuracy curve points to a pre-defined structure that looks like this:

(define *accuracy-assault-rifle-upgrade*
(make-ai-accuracy-curve
([meters 60.0] [chance-to-hit 0.0])
([meters 30.0] [chance-to-hit 0.3])
([meters 20.0] [chance-to-hit 0.4])
([meters 12.0] [chance-to-hit 0.4])
([meters 8.0] [chance-to-hit 0.5])
([meters 4.0] [chance-to-hit 0.9])
)
)

The way this reads is it’s a set of points on a graph that define the accuracy of the weapon at a specified desired distance, with a linear ramp between each point. So in this case, from 0 – 4 meters there is a 90 percent chance of hitting, which then falls off to 8 meters where there is a 50 percent chance of hitting, which then falls off to 12 meters where this a 40 percent chance of hitting, etc… We purposefully set these accuracy curves up separately so we can easily re-use them in any number of weapon skills that we choose.

The rest of the parameters are modifiers that effect accuracy and should be mostly self explanatory. The one I’m sure you’re wondering about is the Time To Accurate Cover section. What this does is allow us to give the player a small window of opportunity when they pop out of cover to take a shot. It will lower the current accuracy of the AI by the percentage listed in Accuracy Cover Worst (in this case to 0 percent), and then over the duration set in Time To Accurate Cover (in this case 3.5 seconds) it will ramp the accuracy up to Accuracy Cover Best (in this case 100 percent).

So the net effect for the player in this case is when the player pops out of cover to take a shot they have 3.5 seconds where the AI’s accuracy will go from 0 percent back up to whatever it should be based on conditions.

By using these weapon skill definitions, we are able to make the different types and classes of AI more or less challenging. As an example, for Light class soldiers overall we gave them slightly slower fire rates and a longer Time To Accurate Cover to make them easier.

With the Medium class of soldiers we would increase their fire rates, boost their accuracy by 10 percent with the Master Accuracy Additive, and reduce their Time To Accurate Cover to make them tougher. We would also try to tailor the fire rates of different weapons to make them sound a bit more unique so as to be more easily identified by the player (i.e. a FAL would have a different pattern than the AK and the M4).

«