《Unity3D高级编程之进阶主程》第八章 AI(1)-状态机构架机器人行为

AI机器人在游戏中非常普遍,它们常以模仿人类的行为在游戏中活动。游戏中的怪物的自动行为比较普遍,这种简单的人工智能方式可以用几种不同的方式进行编写,这里我们介绍两种方式,一种为容易被人类思维接受的状态机AI编程,另一种为特别为机器人思考方式编写的行为树AI编程。其实还有很多方式来编写AI,比如和行为树差不多的决策树,以及相对比较复杂的神经网络,以及更复杂的机器学习,在此不多做介绍,由于过于复杂甚至超过了项目的周期,研发和维护成本太高,所以在游戏中的应用并不太多,很少有人尝试。

===

AI-状态机

状态机虽然是个比较简单的概念,但在实际编程中花样也是繁多的,不过最终都是围绕着状态这个基础变量来做变化的。

状态机最符合人类思考的方式,我们喜欢把事物以状态形式进行拆分成:当前状态,前置状态,下一个状态,状态变化需要的条件等。其中每个状态都有自己定义,比如行走,蹲下,躺下,攻击,防守,三连击,俯冲,左移,右移等。

人们习惯把人或动物的某些连贯的行为定义为状态,所以状态其实不只是一个动作,它可以是好几个动作,或者好几段位移,也就是好几段执行程序。

这种好几个动作和好几段位移一起组成的组合,在程序中用状态来表示最符合逻辑。每种状态下不只是一个动作或一种位移,而是由很多个动作多段位移组合而成。

在游戏里,智能的机器人由于都是人设计出来的,行为方式也最符合人类的逻辑,所以状态机的方式制作AI最符合人类思维,也最容易被接受。

从面向程序的角度来看,状态可以从父类构建开始,并且向不同的方向继承并衍生,最终再把所有的状态类集中到控制状态的状态控制器中。

下面我们就以图和文的方式构建一个 AI 状态机:

我们以 AIStateBase 为状态基础类,基础类中有一个识别状态类的变量,可以是整数,也可以是枚举,和三个必要的接口,更新函数 Update() ,进入状态事件引发的事件函数 OnEnter(),退出状态事件引发的事件函数 OnExit()。这三个函数足以概括状态的出、入、以及自我循环更新三个动作,状态机定义下也只有这三个动作可以做。

接下来,我们开始扩展状态机的功能,以跑步动作状态为例:

在进入跑步动作状态时,表现为,机器人有跑步动画,并且向前移动。

实现跑步状态,首先继承父类 AIStateBase 并且把跑步状态类命名为 AIRunState。然后再对 AIRunState 进行编写,在进入状态时也就是 OnEnter 函数中编写机器人播放跑步的动画并让动画不断循环播放,然后在更新函数 Update 中开启不断向前移动的位置变化。当不再需要移动时,也就是退出跑步状态事件中,OnExit 里调用停止播放动画,也可以不停止播放,因为下一个状态肯定会播放其他动画,不如让他们插值过度下动画看起来会更顺滑。

实现了跑步状态,可以引申出其他和它基本相同的状态,它们可以完全按照跑步状态的方式实现,比如后退,侧移,漫步,打坐,跳跃,攻击等,它们都是只用到播放自身动画和持续位移组合的方式完成状态。对其他复杂状态来说,算是比较基本的状态。

现在我们来实现稍微复杂点的状态‘追击’状态。追击中有两个动作,一个是追,一个是攻击,不止是这些,我们还必须首先锁定目标,然后寻找追击的路径。当怪物进入‘追击’状态时,会锁定目标,然后不断向目标跑去,当跑到攻击范围内时,进行攻击。

我们定义一个‘追击’状态类,AIAttackState 类同样继承 AIStateBase。当‘追击’状态开始时,进入 OnEnter 函数时先锁定目标,把目标保存下来,并且寻找追击目标的路径 Path Find,寻路解决方案会在地图与寻路章节中详细介绍。接下来就是‘追击状态’的更新函数,每帧都会调用更新函数 Update,在Update中,检查目标是否已在攻击范围内,如果是则立刻播放攻击动画,并且调用目标的伤害接口使目标受伤,如果不在范围内则检查锁定目标是否移动,如果有移动就重新寻找路径,并且根据路径来进行位移。整个追击状态,在追击中都是在 Update 函数中不断判断和位移的。

追击状态以追和攻击为主要行为。与追击状态类似的,也比较常用的还有‘巡逻状态’,当怪物进入这个‘巡逻状态’时,怪物会在一个范围内到处行走以查看敌人是否在周围,如果在视野范围内查看到敌人,则退出巡逻状态,进而进入追击状态。

我们来看看编写‘巡逻状态’的步骤。当进入‘巡逻状态’时,状态进入 OnEnter 函数,在 OnEnter 里找到最近的一个巡逻点,然后在更新函数 Update 中持续循环走向最近的巡逻点,当走到第一个最近的巡逻点后继续按照巡逻点的布置顺序继续往下一个巡逻点走,如此不断循环往复。同时更新每次移动时,都检查一下敌人是否在范围内,如果有敌人在检查范围内,当前‘巡逻状态’就结束,OnExit 被调用,转而进入‘追击状态’。

至于‘追击状态’返回‘巡逻状态’的条件可以设置为,当追击太远时重新转入’巡逻状态‘,比如在进入’追击状态‘ OnEnter 函数里记录起始追击位置,每次移动时都判断下,与起始位置的距离是否太远,如果太远则退出当前的’追击状态‘转而进入’巡逻状态‘。

游戏策划常设计Boss怪物进入‘疯狂状态’,例如,这个怪物突然变身,然后开始疯狂得从天空里召唤火球,这些火球像流星一般撞向地面,爆炸,并在爆炸范围内敌人受到伤害,结束后怪物又回到了普通的’追击状态‘。

我们来分析下这个’疯狂状态‘如何编写。当进入’疯狂状态‘时,状态类被调用进入状态事件函数 OnEnter ,函数里播放立刻Boss怪物的变身动画,因为 OnEnter 只调用一次,所以所有检查都在更新函数 Update 里进行,Update 中检查变身动画结束时更换怪物的模型为变身后的新模型,因为需要新模型播放自己的动画所以必须更换。紧接着更换完变身后的模型后,就用新的模型播放施法动画,同时不断按次序调让火球出现在上空的某个位置,并持续移动向地面,当火球的物理引擎检测碰撞到地面时,销毁火球并生成爆炸特效,同时在爆炸位置范围内,查找到是否与爆炸位置距离小于某个值的敌人,如果有则调用敌人的受伤接口使其受伤。当检测到所有火球都已经爆炸完毕,则完成了此次’疯狂状态‘,退出并转向’追击状态‘。

无论多少复杂的技能或者行为,都能在一个状态中体现出来,以状态形式的编写好用就在这里。有了众多复杂的状态还不够,我们还需要有控制这些状态的状态控制器。

状态控制器就是记录,这些状态的状态管理类,我们可以命名为 StateControl类,所有状态都在 StateControl 里有一份实例,而且状态控制器记录了一个变量是当前是什么状态,由于所有状态都是继承与 AIStateBase,所以这个当前状态变量可以为 AIStateBase 以支持所有类型的状态类。又由于所有的状态类的对外接口都是统一,所以只需要基类就能操作所有状态,而且状态的转换也完全可以各自状态的本身来决定。状态控制器,只是起到了对状态实例管理和转换的工具,所以状态控制器没有太多了的逻辑,所有AI逻辑都放在了每个状态类里面,由每个从 AIStateBase 父类继承而来的AI子类来决定如何行动,这种做法大大降低了逻辑的耦合,让代码逻辑更加清晰,维护性强,扩展性强。

用状态机编写AI有诸多优点,诸如可维护性强,可扩展性强,逻辑耦合清晰,符合人类思维逻辑易上手。但缺点也很大,由于每个状态都必须由设计人亲自制定,所以在每个状态时编写时要考虑到所有情况,每种情况都要有自己的处理方式,这样就导致当需要设计的AI行为过于复杂的时候,编写的逻辑复杂度和工期长度也呈现指数级的增长,到最后有可能无法承受太复杂的AI行为逻辑,比如人类在战场中的随机应变能力,对于各种各样的爆炸,攻击,冲锋,防御的应变能力需要表现出各种不一样的行为方式时,如果任然用状态机来编写,就会很容易陷入超出人类逻辑的复杂度。

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第八章 AI(1)-状态机构架机器人行为

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号