Unreal Engine 4 系列教程 Part 9:AI教程

原文:Unreal Engine 4 Tutorial: Artificial Intelligence
作者:Tommy Tran
译者:Shuchang Liu

在本篇教程中,你将学习如何使用行为树和AI感知来创建一个能四处走动,攻击敌人的简单AI。

在视频游戏中,人工智能(AI)通常指的是拥有自主决策行为的非玩家角色。AI可以是看到玩家然后进行攻击的简单角色,也可以是即时策略(RTS)游戏里的强大对手。

在Unreal引擎里,我们可以通过行为树创建AI。行为树是一个决定AI做哪种行为的实时决策系统。比如,如果AI有战斗和逃跑两种行为。你可以创建行为树,让AI在高于50%血量时进行战斗,低于50%血量时逃跑。

在本篇教程中,你将学习到:

  • 创建AI实体用于控制角色单位

  • 创建并使用行为树和黑板

  • 使用AI感知让角色单位获得视野

  • 创建行为让角色单位四处走动并攻击敌人

    注意:本篇教程只是Unreal Engine 4系列教程的其中一篇:

起步入门

下载示例项目并解压。进入项目文件夹,双击MuffinWar.uproject打开项目。

按下Play运行游戏,在围栏内点击左键生成蘑菇小人。

在本例中,我们将创建一个能四处走动的AI,当其他蘑菇小人进入AI的视野时,AI会追逐对方并进行攻击。

要创建一个AI角色,我们需要三个元素:

  1. 身体:这个是角色的物理表现,在本例中,蘑菇小人就是身体
  2. 灵魂:这个是控制角色行为的实体,既能是玩家本身,也可以是AI
  3. 大脑: AI进行决策行为的逻辑,我们可以用C++代码,蓝图或者是行为树来实现逻辑。

现在我们已经有了身体,接着要搞来灵魂和大脑。首先,我们要创建控制器作为灵魂。

什么是控制器?

控制器是一个能控制角色单位的非物理Actor。这里所说的“控制”,具体指的是什么意思呢?

对于玩家而言,控制指的是能通过按键操控角色单位。控制器获取玩家输入,并将输入直接传给角色。当然,控制器也可以获取输入进行处理,然后再告诉角色单位做哪个行为。

对于AI来说,角色单位就是由控制器或“大脑”(取决于实现方式)来通知其做什么行为的。

为了用AI控制蘑菇小人,我们需要创建一类特殊的控制器——AI控制器

创建AI控制器

打开Characters\Muffin\AI目录并创建Blueprint Class,选中AIController作为父类并命名为AIC_Muffin

接着,我们需要让蘑菇小人使用这个AI控制器,打开Characters\Muffin\Blueprints并双击打开BP_Muffin

默认情况下,Details面板会显示蓝图的默认设置,如果没有显示,就点击Toolbar的Class Defaults

在Details面板找到Pawn设置,将AI Controller Class设为AIC_Muffin,这样当蘑菇小人生成时,就会对应生成一个AI控制器实例。

由于我们要动态生成蘑菇小人,Auto Possess AI要设成Spawned。这样当蘑菇小人生成时,AIC_Muffin就会自动控制BP_Muffin

点击Compile并关闭BP_Muffin

现在,我们要来创建决策蘑菇小人行为的逻辑,就要用上行为树

创建行为树

打开Characters\Muffin\AI目录,并选择Add New\Artificial Intelligence\Behavior Tree,将其命名为BT_Muffin并打开。

行为树编辑器

行为树编辑器包含3个新面板:

  1. Behavior Tree:这个图表面板用于创建行为树节点
  2. Details:展示选中节点的参数
  3. Blackboard:展示黑板的所有键值(后续讲解)和其对应数值。只有在游戏运行时才会有显示

像蓝图一样,行为树也是由节点构成的。行为树有4类节点,前两种分别是任务(tasks)组合(composites)节点。

什么是任务和组合节点?

顾名思义,任务节点负责完成具体任务,可以是表现一套连招这样的复杂任务,也可以是原地等待这样的简单任务。

要完成多个任务,我们就要用上组合节点。一个行为树由许多分支(行为)组成。每个分支的根节点,都是一个组合节点。不同类型的组合节点,执行其子节点的方式也各不相同。

比如,我们有一组如下序列的行为:

要按顺序执行每个行为,我们就要用上Sequence组合节点,因为Sequence节点能够从左至右的执行子节点,图表看起来是这样的:

注意:从组合节点衍生出来的节点可以称为子树(subtree)。通常来说,这些节点就统称为一个行为。比如,SequenceMove To EnemyRotate Towards EnemyAttack就统称为“攻击敌人”行为。

如果Sequence的任意节点执行失败,整个Sequence节点就会停止执行。

比如,如果角色无法移动到敌人身边,Move To Enemy节点就执行失败了,这样Rotate Towards EnemyAttack节点也就无法继续执行了。反之,如果角色成功移动到敌人边上,就能执行随后两个节点。

后续我们还会学习Selector组合节点,不过现在先让我们用Sequence节点实现角色随机移动到某个位置并原地停留。

随机移动位置

首先,创建Sequence节点并与Root节点相连。

接着,我们需要让角色移动起来,创建MoveTo节点与Sequence节点相连,这个节点可以驱动角色移动到特定位置或Actor。

随后,创建Wait节点与Sequence节点相连,确保将其放置在MoveTo节点右边,放置顺序非常重要,因为子节点是按照从左到右的顺序执行的。

注意:你可以通过每个节点右上角的数字确认其执行顺序。数字越小执行顺序越高。

恭喜你,你刚刚创建了你的第一个行为!它将会驱动角色移动到指定位置并原地停留数秒。

为了让角色移动,我们还需要指定要移动的位置。由于MoveTo节点只接受由黑板提供的数值,我们要先创建一个黑板。

创建黑板

黑板是一个单纯用来存放变量(键值)的资源。我们可以将其理解为AI的内存。

虽然黑板不是必须使用的,但它确实为我们读取,存取数据提供了极大便利,这么说的原因是很多行为树节点只接受黑板键值作为参数输入。

要创建一个黑板,我们在Content Browser选择新建Add New\Artificial Intelligence\Blackboard,将其命名为BB_Muffin并打开。

黑板编辑器

黑板编辑器由2个面板组成:

  1. Blackboard:展示所有键值列表
  2. Blackboard Details:展示所选键值的参数

现在,我们要创建一个键值用于存放目标位置。

创建目标位置键值

由于是3D空间里的一个位置点,我们需要用Vector来进行存储。点击New Key并选择Vector,将其命名为TargetLocation

接着,我们需要随机生成一个位置并将其存在黑板里,我们就需要用到第三种类型的行为树节点:服务(service)节点。

什么是服务节点?

服务节点类似于任务节点,用于完成一些事情。然而,不同于操控角色做特定行为,服务节点用于执行检查或更新黑板操作。

服务并不是独立节点,而是依附于任务节点或者组合节点。这样使得行为树更加简洁易于组织,不会横生太多节点。如果我们用任务节点来实现,效果如下图所示:

如果用服务节点来实现,则如下图所示:

现在,让我们来创建一个生成随机位置的服务吧。

创建服务

回到BT_Muffin并点击New Service

这样就会新建一个服务并自动打开,我们回到Content Browser将其重命名为BTService_SetRandomLocation

服务应当且仅当在角色准备移动时才执行,因此我们要将它附着在MoveTo节点上。

打开BT_Muffin右键点击MoveTo节点,从弹出菜单选择Add Service\BTService Set Random Location

现在,当MoveTo激活执行时,BTService_SetRandomLocation也会跟着激活执行。

接着,我们需要随机生成目标点位置。

生成随机位置

打开BTService_SetRandomLocation

为了监听获知服务何时触发执行,我们创建Event Receive Activation AI节点,这个节点会在服务父类(所附着的节点)激活时触发执行。

注意:另一个事件Event Receive Activation也有着相同的触发时机,两者区别在于Event Receive Activation AI事件额外提供了Controlled Pawn参数。

为了生成随机位置,添加如下高亮节点,确保将Radius设置为500

这样就能返回得到该角色500单位半径内的一个随机可达目标点。

注意:GetRandomPointInNavigableRadius节点使用了导航数据(称之为NavMesh)来判断一个点是否可达。在本例中,我已提前创建好了NavMesh。你可以通过在Viewport选中Show\Navigation观察NavMesh。


如果你想创建自己的NavMesh,请创建Nav Mesh Bounds Volume,缩放其大小为理想可达区域。

接下来,我们需要将位置数据存储到黑板里。有两种方式指定要存放的键值:

  1. 我们可以使用Make Literal Name节点指定键值名字
  2. 我们可以将变量暴露给行为树,这样就能在行为树里通过下拉列表选中变量

这里我们使用第二种方法。创建类型为Blackboard Key Selector的变量。将其命名为BlackboardKey并启用Instance Editable,这样行为树里的服务就会出现对应变量。

随后,创建如下高亮节点:

小结:

  1. Event Receive Activation AI节点会在其父类(本例中的MoveTo节点)激活时执行
  2. GetRandomPointInNavigableRadius节点返回角色500单位半径内的一个随机可达目标点
  3. Set Blackboard Value as Vector节点将一个黑板键值(BlackboardKey)数值设为随机位置点

点击Compile并关闭BTService_SetRandomLocation

接着,我们需要让行为树来使用这个黑板值。

使用黑板

打开BT_Muffin并确保没有选中任何东西。在Details面板的Behavior Tree设置处,将Blackboard Asset设为BB_Muffin

然后MoveToBTService_SetRandomLocation就会自动使用黑板的第一个键值,在本例中,就是TargetLocation

最后,我们需要让AI控制器来运行行为树。

运行行为树

打开AIC_Muffin并连接Run Behavior Tree节点与Event BeginPlay节点,将BTAsset设为BT_Muffin

这样当AIC_Controller生成时就会执行BT_Muffin

点击Compile并返回主编辑器,按下Play运行游戏,生成一些蘑菇小人,观察它们四处走动吧。

虽然设置很繁琐,我们还是搞定了!接着,我们要进一步设置AI控制器,让它可以在一定范围内感知敌人所在。要实现这点,就要使用AI感知(AI Perception)

设置AI感知

AI感知是一个可以添加给Actor的组件,通过它,我们可以给AI添加感官能力(如视觉和听觉)

打开AIC_Muffin并添加AIPerception

接着,我们要添加一个感官,由于我们想要蘑菇小人能够感知到其他小人靠近,我们给它加上视觉感官。

选中AIPerception并在Details面板的AI Perception设置处,给Senses Config添加新元素。

将元素0设置为AI Sight config并展开它。

对于视觉有3个主要设置:

  1. Sight Radius:蘑菇小人的最远视觉范围,将其设置为3000
  2. Lose Sight Radius:如果蘑菇小人已经看到了敌人,那敌人要逃离小人视野的距离,将其设置为3500
  3. Peripheral Vision Half Angle Degrees:决定蘑菇小人视野的角度,将其设置为45,蘑菇小人就会有90度的范围视角。

默认情况下,AI感知只检测敌人(被指定为不同队伍(team)的Actor)。然而,Actor默认是没有设置队伍的,如果Actor没有队伍,AI感知就会将其认为中立(neutral)角色。

截至目前,还没有方法能通过蓝图设置Actor的队伍,退而求其次,我们展开Detection by Affiliation设置,启用Detect Neutrals

点击Compile并回到主编辑器。按下Play运行游戏来生成蘑菇。按下 ‘ 键可以显示AI调试信息,按下小键盘的数字键4可以可视化AI感知组件。当蘑菇小人进入视野时,就会显示绿色球体。

接着,我们要让蘑菇小人往敌人的方向走去。要实现这点,行为树就要了解敌人的信息,我们通过在黑板存储敌人的引用来完成这件事。

创建敌人键值

打开BB_Muffin并添加类型为Object的键值,将其命名为Enemy

现在,我们还不能在MoveTo节点使用Enemy,因为其键值类型为Object,但MoveTo只接受VectorActor类型的键值。

为了解决这点,我们选中Enemy并展开Key Type,将Base Class设置为Actor。这样行为树就能将Enemy识别为Actor了。

关闭BB_Muffin,现在,我们要创建一个行为让AI向敌人走去。

朝敌人移动

打开BT_Muffin并断开SequenceRoot连接。我们可以通过按住Alt键点击连线来做到,并将移动子树移到一边。

接着,创建如下高亮节点,并将Blackboard Key设置为Enemy

这样角色就会朝Enemy走去。有时候,角色不会刚好面对着它的目标,所以我们还需要用上Rotate to face BB entry节点。

现在,我们需要在AI感知检测到其他蘑菇时,将其设置为Enemy的值。

设置敌人键值

打开AIC_Muffin并选中AIPerception组件,添加Perception Updated事件。

只要感官发生更新,这个事件就会触发执行。在本例中,当AI获得或丢失了某物体的视野,这个事件就会执行,并提供了其当前所能感知到的Actor列表。

添加如下高亮节点,并确保将Make Literal Name节点设置为Enemy

这样就可以判断AI目前有没有敌人对象,如果没有,我们就要给它设置一个敌人,因此添加如下节点:

小结:

  1. IsValid节点负责判断Enemy键值是否有值
  2. 如果还没设置,遍历当前所有检测到的Actor
  3. Cast To BP_Muffin节点负责检查Actor是否为蘑菇
  4. 如果是蘑菇,进一步判断是否已死亡
  5. 如果IsDead返回false,将蘑菇设置为新敌人,并退出循环

点击Compile并关闭AIC_Muffin,按下Play运行游戏并生成两个蘑菇小人,其中一个生成暴露在另一个面前,后者就会自动向前者走过去。

接着,你要创建一个自定义任务,让蘑菇小人可以表演攻击行为。

创建攻击任务

我们可以直接在Content Browser创建任务,而无须通过行为树编辑器。创建新的Blueprint Class类,并将BTTask_BlueprintBase作为其父类。

将新建类命名为BTTask_Attack并打开,添加Event Receive Execute AI节点,这个节点会在行为树激活BTTask_Attack时触发执行。

首先,你需要让蘑菇执行攻击行为。BP_Muffin包含一个IsAttacking变量,当变量设置为true时,蘑菇会执行一次攻击,因此我们添加如下高亮节点:

如果这个任务节点在这里就结束了,那行为树执行就会卡在这个节点上,因为行为树并不知道该节点已执行完毕了,所以我们要在节点链末端添加Finish Execute节点。

接着,启用Success,由于我们用的是Sequence,这样就能让BTTask_Attack的后续节点得以执行。

现在图表看起来应该是这样的:

小结:

  1. 当行为树激活BTTask_Attack节点时,Event Receive Execute AI节点就会一同触发执行。
  2. Cast To BP_Muffin节点会检查Controlled Pawn是否为BP_Muffin类型
  3. 如果是,则设置IsAttacking变量为true
  4. 通过Finish Execute节点退出当前节点,让行为树继续往下执行

点击Compile并关闭BTTask_Attack

现在,我们需要将BTTask_Attack节点添加到行为树中。

行为树添加攻击行为

打开BT_Muffin,随后,将BTTask_Attack节点添加到Sequence节点后面。

接着,将Wait节点添加到Sequence节点后面,并将Wait Time设置为2。确保蘑菇小人不会攻击个不停。

回到主编辑器点击Play运行游戏,像上次一样生成两个蘑菇小人。蘑菇小人会朝着敌人走去。随后,它会尝试攻击,然后休息两秒。当它发现另一个敌人时,又会重复以上行为。

在最后一部分,我们要将攻击和移动两颗子树合并在一起。

合并子树

为了合并子树,我们要用上Selector组合节点。类似于Sequence节点,它也是按从左向右的顺序执行的。然而,Selector节点会在子节点返回成功而非失败时停止执行。利用这个特性,就可以确保行为树每次只执行一颗子树。

打开BT_Muffin并在Root节点下创建Selector节点。随后,如下图连接两个子树:

这样同一时间只有一颗子树会得到执行,下面是每颗子树的执行情况:

  • 攻击: Selector节点会首先运行第一颗子树,如果所有任务都成功了,Sequence节点也会返回执行成功。Selector节点得知执行成功,就会停止执行后面的节点,这样就不会再执行移动节点。

  • 移动: Selector节点会尝试运行前面的攻击子树,如果Enemy还没有值,MoveTo节点就会执行失败,Sequence节点也就同样失败。由于第一个子树失败了,Selector节点就会执行后续这颗移动子树。

回到主编辑器,按下Play运行游戏,生成一些蘑菇小人试试看吧!

“等等,为什么图中这个蘑菇小人没有马上攻击另一只呢?”

在传统的行为树设计里,行为树每帧都会从根节点开始执行,意味着每帧更新,它都会尝试执行第一颗攻击子树,然后再执行第二颗移动子树,这也意味着当Enemy值发生变化时,行为树就会马上切换执行另一颗子树。

然而,Unreal的行为树并不是这样设计执行的。在Unreal里,行为树会继续执行上一帧选中的那颗子树。图中由于AI感知没有马上感知到另一只蘑菇小人的存在,行为树开始执行移动子树,于是行为树就只能乖乖等待移动子树执行完毕,才能重新评估确定执行攻击子树。

为了解决这个问题,我们需要用上最后一种类型节点:装饰(decorators)节点。

创建装饰节点

类似于服务节点,装饰节点也依附于任务或组合节点。通常而言,装饰节点用于做前置检查。如果检查结果为true,装饰节点就返回true,反之亦然。通过装饰节点,就能控制其依附节点是否能够执行。

装饰节点也有能力中止子树的运行,这意味着我们能实现一旦Enemy有设值,就立即中止移动子树。这样蘑菇小人就能在发现敌人的第一时间攻击敌人。

要实现中止功能,我们可以使用Blackboard装饰节点,这个节点只是简单地检查某个黑板键值是否有值。打开BT_Muffin,并在攻击子树的Sequence节点点击右键,从弹出菜单选中Add Decorator\Blackboard,这样Sequence节点就会添加上Blackboard节点。

接着,选中Blackboard装饰节点,并在Details面板将Blackboard Key设为Enemy

这样可以判断Enemy是否有值,如果没有值,节点返回失败,从而导致Sequence失败,从而让移动子树得到执行。

为了中止移动子树,我们需要用上Observer Aborts设置。

使用Observer Aborts

Observer Aborts能够实现所选中的黑板键值发生变化时,中止执行子树,这里分为两种类型的中止:

  1. Self: 该设置允许当Enemy值失效时,立即中止运行攻击子树,这种情况发生在攻击子树还未运行完毕,而Enemy又死亡的时候。
  2. Lower Priority:该设置允许当Enemy有值时,中止运行较低优先度的子树。由于移动子树放在攻击子树后面,它就是较低优先度子树。

我们将Observer Aborts设为Both,同时启用两种类型的中止。

现在,当AI已经没有敌人目标时,可以马上从攻击子树切换运行移动子树。同样的,当AI检测到敌人目标时,又能从移动子树切换运行攻击子树。

以下是完整的行为树图表:

攻击子树小结:

  1. Enemy有值,Selector开始运行攻击子树
  2. 一旦运行子树,角色开始朝敌人走去
  3. 随后,进行攻击
  4. 最后,角色原地停留2秒

移动子树小结:

  1. Enemy没有值,攻击子树运行失败时,Selector继续运行移动子树
  2. BTService_SetRandomLocation生成一个随机位置
  3. 角色朝指定位置移动
  4. 随后,角色原地停留5秒

关闭BT_Muffin并按下Play运行游戏,生成一些蘑菇小人进行一场你死我活的决斗吧!

后续学习

你可以在这里下载完整项目。

如你所见,制作简单AI还算一件不难的事。如果你想创建一个更加高级的AI,请查阅场景查询系统,这个系统允许AI收集场景数据并作出相应的反馈。

如果你还想继续学习引擎其他内容,点击下篇教程,将教你如何制作一个简单的第一人称射击游戏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容