原文: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系列教程的其中一篇:
- Part 1:入门
- Part 2:蓝图
- Part 3:材质
- Part 4:UI
- Part 5:制作简单游戏
- Part 6:动画
- Part 7:音频
- Part 8:粒子系统
- Part 9:AI
- Part 10:制作简单FPS游戏
起步入门
下载示例项目并解压。进入项目文件夹,双击MuffinWar.uproject打开项目。
按下Play运行游戏,在围栏内点击左键生成蘑菇小人。
在本例中,我们将创建一个能四处走动的AI,当其他蘑菇小人进入AI的视野时,AI会追逐对方并进行攻击。
要创建一个AI角色,我们需要三个元素:
- 身体:这个是角色的物理表现,在本例中,蘑菇小人就是身体
- 灵魂:这个是控制角色行为的实体,既能是玩家本身,也可以是AI
- 大脑: 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个新面板:
- Behavior Tree:这个图表面板用于创建行为树节点
- Details:展示选中节点的参数
- Blackboard:展示黑板的所有键值(后续讲解)和其对应数值。只有在游戏运行时才会有显示
像蓝图一样,行为树也是由节点构成的。行为树有4类节点,前两种分别是任务(tasks)和组合(composites)节点。
什么是任务和组合节点?
顾名思义,任务节点负责完成具体任务,可以是表现一套连招这样的复杂任务,也可以是原地等待这样的简单任务。
要完成多个任务,我们就要用上组合节点。一个行为树由许多分支(行为)组成。每个分支的根节点,都是一个组合节点。不同类型的组合节点,执行其子节点的方式也各不相同。
比如,我们有一组如下序列的行为:
要按顺序执行每个行为,我们就要用上Sequence组合节点,因为Sequence节点能够从左至右的执行子节点,图表看起来是这样的:
注意:从组合节点衍生出来的节点可以称为子树(subtree)。通常来说,这些节点就统称为一个行为。比如,Sequence,Move To Enemy,Rotate Towards Enemy和Attack就统称为“攻击敌人”行为。
如果Sequence的任意节点执行失败,整个Sequence节点就会停止执行。
比如,如果角色无法移动到敌人身边,Move To Enemy节点就执行失败了,这样Rotate Towards Enemy和Attack节点也就无法继续执行了。反之,如果角色成功移动到敌人边上,就能执行随后两个节点。
后续我们还会学习Selector组合节点,不过现在先让我们用Sequence节点实现角色随机移动到某个位置并原地停留。
随机移动位置
首先,创建Sequence节点并与Root节点相连。
接着,我们需要让角色移动起来,创建MoveTo节点与Sequence节点相连,这个节点可以驱动角色移动到特定位置或Actor。
随后,创建Wait节点与Sequence节点相连,确保将其放置在MoveTo节点右边,放置顺序非常重要,因为子节点是按照从左到右的顺序执行的。
注意:你可以通过每个节点右上角的数字确认其执行顺序。数字越小执行顺序越高。
恭喜你,你刚刚创建了你的第一个行为!它将会驱动角色移动到指定位置并原地停留数秒。
为了让角色移动,我们还需要指定要移动的位置。由于MoveTo节点只接受由黑板提供的数值,我们要先创建一个黑板。
创建黑板
黑板是一个单纯用来存放变量(键值)的资源。我们可以将其理解为AI的内存。
虽然黑板不是必须使用的,但它确实为我们读取,存取数据提供了极大便利,这么说的原因是很多行为树节点只接受黑板键值作为参数输入。
要创建一个黑板,我们在Content Browser选择新建Add New\Artificial Intelligence\Blackboard,将其命名为BB_Muffin并打开。
黑板编辑器
黑板编辑器由2个面板组成:
- Blackboard:展示所有键值列表
- 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,缩放其大小为理想可达区域。
接下来,我们需要将位置数据存储到黑板里。有两种方式指定要存放的键值:
- 我们可以使用Make Literal Name节点指定键值名字
- 我们可以将变量暴露给行为树,这样就能在行为树里通过下拉列表选中变量
这里我们使用第二种方法。创建类型为Blackboard Key Selector的变量。将其命名为BlackboardKey并启用Instance Editable,这样行为树里的服务就会出现对应变量。
随后,创建如下高亮节点:
小结:
- Event Receive Activation AI节点会在其父类(本例中的MoveTo节点)激活时执行
- GetRandomPointInNavigableRadius节点返回角色500单位半径内的一个随机可达目标点
- Set Blackboard Value as Vector节点将一个黑板键值(BlackboardKey)数值设为随机位置点
点击Compile并关闭BTService_SetRandomLocation。
接着,我们需要让行为树来使用这个黑板值。
使用黑板
打开BT_Muffin并确保没有选中任何东西。在Details面板的Behavior Tree设置处,将Blackboard Asset设为BB_Muffin。
然后MoveTo和BTService_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个主要设置:
- Sight Radius:蘑菇小人的最远视觉范围,将其设置为3000。
- Lose Sight Radius:如果蘑菇小人已经看到了敌人,那敌人要逃离小人视野的距离,将其设置为3500。
- 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只接受Vector或Actor类型的键值。
为了解决这点,我们选中Enemy并展开Key Type,将Base Class设置为Actor。这样行为树就能将Enemy识别为Actor了。
关闭BB_Muffin,现在,我们要创建一个行为让AI向敌人走去。
朝敌人移动
打开BT_Muffin并断开Sequence和Root连接。我们可以通过按住Alt键点击连线来做到,并将移动子树移到一边。
接着,创建如下高亮节点,并将Blackboard Key设置为Enemy:
这样角色就会朝Enemy走去。有时候,角色不会刚好面对着它的目标,所以我们还需要用上Rotate to face BB entry节点。
现在,我们需要在AI感知检测到其他蘑菇时,将其设置为Enemy的值。
设置敌人键值
打开AIC_Muffin并选中AIPerception组件,添加Perception Updated事件。
只要感官发生更新,这个事件就会触发执行。在本例中,当AI获得或丢失了某物体的视野,这个事件就会执行,并提供了其当前所能感知到的Actor列表。
添加如下高亮节点,并确保将Make Literal Name节点设置为Enemy。
这样就可以判断AI目前有没有敌人对象,如果没有,我们就要给它设置一个敌人,因此添加如下节点:
小结:
- IsValid节点负责判断Enemy键值是否有值
- 如果还没设置,遍历当前所有检测到的Actor
- Cast To BP_Muffin节点负责检查Actor是否为蘑菇
- 如果是蘑菇,进一步判断是否已死亡
- 如果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的后续节点得以执行。
现在图表看起来应该是这样的:
小结:
- 当行为树激活BTTask_Attack节点时,Event Receive Execute AI节点就会一同触发执行。
- Cast To BP_Muffin节点会检查Controlled Pawn是否为BP_Muffin类型
- 如果是,则设置IsAttacking变量为true
- 通过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能够实现所选中的黑板键值发生变化时,中止执行子树,这里分为两种类型的中止:
- Self: 该设置允许当Enemy值失效时,立即中止运行攻击子树,这种情况发生在攻击子树还未运行完毕,而Enemy又死亡的时候。
- Lower Priority:该设置允许当Enemy有值时,中止运行较低优先度的子树。由于移动子树放在攻击子树后面,它就是较低优先度子树。
我们将Observer Aborts设为Both,同时启用两种类型的中止。
现在,当AI已经没有敌人目标时,可以马上从攻击子树切换运行移动子树。同样的,当AI检测到敌人目标时,又能从移动子树切换运行攻击子树。
以下是完整的行为树图表:
攻击子树小结:
- 当Enemy有值,Selector开始运行攻击子树
- 一旦运行子树,角色开始朝敌人走去
- 随后,进行攻击
- 最后,角色原地停留2秒
移动子树小结:
- 当Enemy没有值,攻击子树运行失败时,Selector继续运行移动子树
- BTService_SetRandomLocation生成一个随机位置
- 角色朝指定位置移动
- 随后,角色原地停留5秒
关闭BT_Muffin并按下Play运行游戏,生成一些蘑菇小人进行一场你死我活的决斗吧!
后续学习
你可以在这里下载完整项目。
如你所见,制作简单AI还算一件不难的事。如果你想创建一个更加高级的AI,请查阅场景查询系统,这个系统允许AI收集场景数据并作出相应的反馈。
如果你还想继续学习引擎其他内容,点击下篇教程,将教你如何制作一个简单的第一人称射击游戏。