【摸鱼向】UE4的AI模块探索手记(1)
前言
之前实现了自主创作的角色导入进UE4并成功控制其进行一系列动作,但目前的样子距离基本的游戏架构还差了一个很大的模块:NPC,而这部分是由电脑来进行自动控制,所以,我有一句话不知当讲不当讲(对,我又不满足了( •̀ ω •́ )✧)。由此,我又一次打开了官方文档,开始对UE4中比较难啃的AI模块进行探索。(前方少图,请放心加载(笑))
正文
一.构成
先说一下UE4中AI的构成,一般如果是对玩家有威胁的敌人角色或者是跟随玩家的npc角色,它们的配置一般有:1.行为树;2.黑板;3.AI控制器;4.AI角色;5.相关服务;6.相关修饰器;7.相关任务。本次以类似于《合金装备》里敌兵的AI构建为例,对各个配置进行说明与个人见解。
1.行为树
UE4的AI角色核心,属于决策层配置,大致样貌如下:
如果你有一些程序设计基础,那么大致通过名字就能判断行为树是由类似于if—else一类的决策组成的AI行动方案。并且行为树是一种可以进行类似深度优先遍历操作的数据结构,即比如在某个子树的行为已经运行完成后,状态改变,则此时根据改变的条件选择需要遍历的子树接着进行遍历与运行操作。而组成行为树的节点可并不是类似于普通树状结构中单一仅含有权重的“小圆圈”,行为树一般由以下几种节点组成:Root,Composites和Tasks。接下来分别描述一下三种节点:
a.Root节点
又名根节点,是整个行为树的核心与基础,一但当Agent在场景中的实例化被调用时,那么,Root就是行为树的调用入口,从此处开始AI行为逻辑。由于其固定的性质,所以Root节点不可进行编程。
b.Composites节点
直译过来是“复合体”,根据其节点的作用,又分为Sequence,Selector和Simple Parallel(此节点暂不做叙述),其中,Sequence(队列)是为了让其下的子节点或子树按照顺序标号的顺序进行执行的节点,而Selector则是行为树中最重要的“选择单元”,通过某些“特殊”的“玩意儿”实现对于状态(一般是用整型,浮点,或枚举表示状态)的判断,从而选择相关的子树。与Root同样含有固定的性质,所以不可编程。
c.Tasks节点
又名任务节点,即上述的“相关任务”,是相同的东西,这里具体记载着Agent的行为,当遍历执行到这里时,Agent便会做出行动,比如跑,跳等。由于AI的行为依据具体情况决定,所以在引擎默认创建的几个Tasks节点之外,开发者还可以编写新的Tasks节点,即Tasks节点是可编程的(行为树插图中紫色的部分)。
2.黑板
黑板是用来存储一系列Agent状态变量的一种结构体,比如此时Agent发现了玩家并且向玩家跑过来,所以它此时的状态至少就应该有这么几个:玩家此时的位置,Agent的移动类型(Idle或者Running)以及玩家最后一次出现的位置,这些东西都要用变量存储起来。然而各个任务节点与一系列的服务,修饰器之间是毫无联系且相互独立的,所以他们之间并不能直接修改对方的数据,这时候就要通过黑板里面的数据作为接口实现整个系统之间的通信。并且实际上这些数据并没有保存在黑板里面,黑板只是提供了一种定义方式,行为树会自行创建一个黑板,将所有数据存储在那里面,只是行为树创建的黑板会遵从原黑板的数据格式罢了。真不知道Epic官方是怎么起名的,叫“记忆体”或“数据坞”这种高大上狂拽酷炫屌炸天的名字它不香么?(或许Epic有这种想法的合理性吧,咱对人工智能的基础理论知之甚少,如果有哪位大佬知道,欢迎补充)
3.AI控制器与AI角色
这便是AI在游戏场景中的实例化,也是最终展现给玩家的样貌。由控制器控制角色进行相应的动作,由行为树作为多触手章鱼玩家来操作多个控制器从而控制多个角色。
4.服务
开发人员以及玩家们都希望在做出一系列的举止后(比如制造噪声,向敌人扔个小黄书等),Agent可以根据这种举止做出相应行动,即遵守图灵机的基本定义“输入一定的数据——处理数据——输出相应的处理结果”。试想,如果不接受玩家的输入从而在场景内乱跑,那岂不就与喵星人的扫地座驾以及辣个女人一样么?
所以,服务就是来干这事的。通常,服务一般用来接收此时场景内的情况输入,比如玩家进入视锥,并且玩家并没有躲在墙体后面,这时服务就会获取这些信息的输入,并且通过逻辑处理从而将黑板中的状态数据进行改变并更新。当然,由于一个Agent的行为在不同的游戏类型中(甚至不同关卡中)会接受多种不同的输入,并且黑板上的数据也并不是一定相同的,所以,服务完完全全是可编程的(行为树插图中青色的部分)。
5.修饰器
好的,这时候我们已经获取了环境的信息并改变了Agent的状态,如果我们希望Agent根据此时的状态做出相应的行为,那么就需要Selector进行选择。然而,如何让Selector进行选择,总不能随机选一个?如果是这样,那么Selector以及Agent的状态便毫无意义所以,这便是“那个玩意儿”——修饰器的作用:作为执行其负责的子树部分的判定条件,也就是if—else结构中括号里填写的那个条件,即定义的条件满足后再运行相应子树,若不满足则退回上一节点。一般都使用基于参考黑板数据的修饰器。当然,判定条件也有可能与关卡状态,玩家状态甚至是你的电脑状态等黑板中并没有存储的数据相关,所以,修饰器也是可编程的。(行为树插图中蓝色的部分)
二.联系
接下来以我最喜欢的PS2游戏之一《合金装备2:自由之子》(我绝对不是因为可以在厕所里看海报而喜欢的)其中一个场景进行说明(请结合行为树插图食用更佳),具体场景如下所示:
此时玩家操纵的蛇叔处在A点,他要前往的B点(rush B)附近有敌兵看守,此敌兵假如说就是我们描写的Agent,并且在相应的黑板中创建了这样几个变量State(Agent的状态,枚举类型:Idle,Combat,Searching),Player(准确的说是Agent前往的地点),PlayerMarker(玩家在Agent可视范围消失后最后一次出现的位置);这时,Agent在场景中实例化,所以,引擎会调用Agent所使用的行为树的Root节点,此时行为树开始工作。首先这时Agent面向墙壁,而在行为树中的EnemyTrial_SearchVision服务为Agent正方向创造了一个110度(角度)的视锥,此时玩家并没有在视锥范围内,所以EnemyTrial_SearchVision服务将此时Agent的State设置为Idle,目标前往地点与玩家最后一次的位置设置为空。接下来进行选择,Selector会通过自己所有分支中的修饰器进行选择,既然此时状态为Idle,那么State equal Idle修饰器符合条件,所以接下来执行此修饰器下的子树,然后此时Agent的行为由EnemyTrial_SetWalkSpeed(设置此时Agent的行走速度:巡逻速度)以及EnemyTrial_MoveToRandomPoint(让Agent随机移动到某点)定义并控制Agent执行。在执行的过程中,服务以及修饰器也没闲着,一旦此时Agent随机移动的点在A点附近,Agent需要转身,而此时玩家操作的蛇叔正好探头与Agent对上了眼(进入视锥)。那么,服务会立即改变此时Agent的State为Combat,而且将Player与PlayerMarker设置为此时玩家的位置,修饰器监视到State改变,所以会立即中断自身的行为,并将进度立即返还到Selector,然后由Selector分配给State equal Combat,接着执行Combat对应的Agent行为。但是,如果此时蛇叔打开了光学迷彩(这是不可能的,因为从桥上跳下来时已经摔坏了),在视锥中消失,这时服务将Player设置为空,并会检索黑板中的PlayerMarker,如果PlayerMarker非空,则Agent的Selector将进度交给State equal Searching,总不能Agent看不到敌人就放弃搜索吧(山猫:你已经被开除了),如果PlayerMarker为空,那么Agent的Selector会将进度交给State equal Idle(一般这种情况不可能,除非玩家开了修改器)。接着,当Agent到达PlayerMarker后,在其搜索的时间内如果没有在视锥内发现玩家,那么EnemyTrial_DestroyMarker任务便会清空Player与PlayerMarker,并且将State设置为Idle,这时修饰器监测到了State的改变,所以此时又会将进度返还给Selector,再由Selector将进度交给State equal Idle,实行Agent基础的巡逻状态。
结语
又开了一个坑……我也想好好做个游戏啊%&*#@!¥%%#(因言语过激而被踢出直播间),但是基础不足,身边也没有学习美工与音效的小伙伴,只能自己慢慢摸索了,C++也在补STL方面的基础,不过这倒是充足了网课后的课余时间(笑)。如果我的这篇文章里面的相关名词与解释有不正确或是容易引发歧义的地方,欢迎在评论区指出。而且如果你对于其中的一些解释还有不明白的地方,也欢迎骚扰。当然,老规矩,如果你认为这篇文章会对更多的人有帮助,欢迎转载,只不过请注明转载出处即可,以上。どうもありがとうございました!(都莫,阿里嘎多靠萨依玛希大!)