MMORPG战斗系统随笔(三)、AI系统简介

时间:2021-02-08 06:21:18

  转载请标明出处http://www.cnblogs.com/zblade/

  在设计一款游戏的时候,如果我们是玩家,是希望自己能够操作角色畅玩游戏的。在一款MMORPG游戏中,大部分的实际游戏角色,是需要玩家来操作的,通过在游戏大世界相互完成游戏中的任务等等来体验游戏。在大世界交互场景中,不可避免的会有怪物的存在,也会有NPC,某些策划布置的场景角色怪物等等。同时,在常见的MMORPG游戏中,自动战斗是不可避免的一个功能。这些表面的角色或者功能的背后,其实就是游戏的AI机制。

  说到游戏的AI,和现在比较流行的人工智能有一定的区别。现在的人工智能是全面的AI,包含数据收集,数据分析,行为支配等等操作。而游戏中的AI,属于比较特定的一类,主要是为战斗系统提供的一种机制。现在主流的机制主要分为两类:状态机和行为树。这两类机制,各有其优势,在实际的应用中需要结合实际的需求来采用。我分别在两款游戏中采用了这两种机制,下面也算一个个人的总结吧。

一、状态机

  状态机这种机制,主要是针对战斗状态不太复杂的应用场景。如果我们的角色,在整个游戏过程中,只有少数几个可以列举的状态,比如:站立,寻敌,攻击,行走,受击这样可以枚举出来的几个状态,那么,我们可以考虑用状态机的方式实现这几种状态的相互切换。具体的关于状态机的实现,我这儿就不再赘述了,网上有很多详细的讲解,也有比较多开源的代码,大家可以参考github上的代码类似的实现自己的状态机。我就写几点个人的一些总结吧。

    1、从基础搭建开始

  网上比较多的状态机,都是结合其实际的应用场景,包含一些特定的实现设计在里面,我们在搭建自己的状态机的时候,可以从一个细小的状态开始搭建,先搭建一个节点,然后依据同样的设计,搭建类似的多个节点;

2、保持节点的简洁性和去耦性

  通常,在最初的节点搭建完后,是不可能一劳永逸的,后续反复的修改,都会对这些节点进行特定的修改和某些特定规则的设计。

  举个列子,射击这样一个状态节点,A英雄的设计是单发,B英雄是连续10发,那么如何保证对这两个英雄的同一个射击状态的兼容?一种设计方式,是将当前的英雄作为一个参数进行传递,那么在执行的时候,就读取当前传入参数的英雄的具体配置,进行相关的射击动作。把射击相关的逻辑封装在该英雄对应的攻击执行器中,那么其具体执行的射击单发还是射击10发,就可以作为一个循环执行,单发循环一次,10发循环10次。

  以前我的设计实现思路,是针对单一的英雄,特定重载实现其对应的射击节点,这样也算一种解决方法,只是这样的设计有一个弊端,就是随着英雄类型的增多,其对应的特定实现会不断的扩大,相应的重载实现的版本会增多。如果设计思路和文本清晰,那还可以维护,如果设计思路不清晰,那么就会带来维护的消耗。

3、做好节点的剥离,避免节点耦合太多

  通常状态机是用在比较简易的游戏类型中,角色本身的状态类型不会太多,那么对应的状态节点应该避免相互之间的耦合和功能交叉,状态的切换可以走相同的属性设置来实现交互。如果随着设计的变化,状态实现越来越多,可以考虑用行为树来实现,避免相互之间的耦合太多。

二、行为树

  在RPG游戏中,行为树是用的比较多的一种AI机制。就其本质而言,行为树是状态机的一种更高封装的实现。我个人的理解,行为树就是将特定的行为节点进行封装,做成叶子节点,这样可以实现任意节点的拼接。想象一下,如果我们把每个特定的状态机进一步的封装,做成一片片叶子,这样我们在搭建树的时候,就可以收集特定的叶子,来搭建特定的树,最后得到特定功能的行为树,这就是我对行为树的一个简易理解:D 如果用更为规范的说法,那么这些叶子节点,就是行为树中的行为节点,行为树的行为节点的执行结果,可以用枚举的方式列出:

RunningStatus =
{
INVALID = ,
SUCCESS = , --执行成功
FAILURE = , --执行失败
RUNNING = , --执行中
}

  一棵树的搭建,不能只有叶子,还需要有枝干,这就需要行为树中的一些特定的节点来搭建这棵树,这就是行为树中的控制节点(Control Node)的作用。注意一点,行为节点是和游戏实际关联的,在行为节点中,我们会去具体的定义如何攻击(shoot/attack),寻路(patrol),闲置(idle)等,但是在控制节点中,其具体运行的逻辑是和实际游戏数据没有关联的,其只需要负责基本的控制逻辑即可 。通过控制节点,我们可以清晰的知道整个行为树的执行逻辑,这就是行为树的一大优势:执行逻辑可见。 行为树主要有以下几种控制节点,举例说一下:

  4种Composite节点

   1、Sequence节点: 顺序执行控制节点,其执行的基本逻辑是:对其下面的所有叶子节点,均顺序执行,直到遇到返回为FAILURE的节点。(我对这种节点的理解,就是串联电路的思维,电流顺序走过每个电阻(叶子),如果遇到第一个无法流过的电阻,则返回,否则会一直流过去,直到所有电阻都流过)

2、Selector节点:选择执行控制节点,其执行的基本逻辑是:对其下面的所有叶子节点,顺序执行,直到遇到第一个返回为SUCCESS/RUNNING执行结果的节点。观察上面的Sequence节点,和Selector节点的区别仅仅在于选中的节点的返回结果的不同处理。在其他地方好像对选择节点进行了分类,可以分为:带优先级的选择节点,不带优先级的选择节点,带权值的选择节点。带优先级,是指在选择那个节点更新的时候,会根据优先级进行比较来选择。不带优先级的时候,一般会设置为每次更新的时候会沿用上一次的更新节点,因为一般更新的时候,会有一段时间持续处于某个节点的状态(目前我用的就是这样的选择节点),而对于带权重的选择节点,一般用来做多样的随机性,比如游戏中的宠物,需要表现一个交互动作,那么可以做多个交互动作,在每次做选择节点的时候,根据权重随机一个节点用来表示交互动作。

3、Parallel节点:并行执行控制节点,其执行的基本逻辑是:对其下面的所有叶子节点,各自执行一次,如果返回结果不为RUNNING,则分别统计SUCCESS和FAILURE的结果,一般结果会采用“与”和“或”的操作。比如当前Parallel节点的返回条件可以分为 全SUCCESS或者任意一个SUCCESS,全FAILURE或者任意一个FAILURE。在执行完所有叶子节点后,可以对比其执行的SUCCESS和FAILURE节点的个数,和其设置的返回结果对比,如果满足则返回SUCCESS(SUCCESS条件)或者FAIULURE(FAILURE条件),或者返回RUNNING。

 4、Detector节点:检测执行控制节点,其执行的基本逻辑是:对其下面的所有叶子节点,逐个执行,如果返回的不为RUNNING,则检测,如果为SUCCESS,则接着执行,为FAILURE,则执行结束。即遇到第一个返回为FAILURE的节点,就结束执行。

6种Decorator节点

1、Invert节点:反转装饰节点,其执行的基本逻辑是:只有一个叶子节点,如果返回为SUCCESS,则返回FAILURE;如果返回为FAILURE,则返回SUCCESS;否则返回RUNNING

2、Sucees节点:Success装饰节点,其执行的基本逻辑是:只有一个叶子节点,执行后,直接返回SUCCESS

3、SuccessToRunning节点:根据名字就可以知道,其叶子节点在执行完后,如果返回为SUCCESS,则修改其结果为RUNNING,其他的状态不改变,返回最终执行状态给上一层。

4、FailureTORunning节点:类似于上一个节点,其叶子节点在执行完后,如果返回为FAILURE,则修改其结果为RUNNING,其他的状态不改变,返回最终执行状态给上一层。

5、SuppressSuccess节点:suppress的意思是抑制,所以这个节点的功能,就是不准返回SUCCESS的执行结果,如果叶子节点返回为RUNNING则返回RUNNING,其他都返回为FAILURE给上一层

6、SuppressFailure节点:类似于上一个节点,该节点只会返回RUNNING或者SUCCESS这两种执行结果

除了常见的Compositor节点和Decorator节点,还有Condition节点,依据前面两种类型节点,不难推出后面的Condition节点的功能,就是在某些条件下才触发某些特定返回结果的一些节点。

简而言之,行为树就是在三个大类的控制节点:Compositor节点、Decorator节点、Condition节点的搭建下,结合各个行为叶子节点,拼接出一个基本的AI执行机制,举个例子:

bt_atk

<Root>
<Selector>
<Attack>
<Homing>
</Selector>
</Root>

  这是一个简单的站在原地攻击的行为树设计,首先执行Selector节点,然后顺序执行其下面的所有叶子节点,首先会执行攻击的叶子节点,如果返回为Success,则继续执行Homing节点。所以其基本的设计思想就是:触发一次攻击检测,如果有攻击对象,则执行攻击,返回SUCCESS,同时结束本次tick;否则返回FAILUER,那么就会执行下一个节点Homing。

  通过构建一颗基本的行为树,我们可以组件一颗更大的树,比如我们再构建一颗巡逻的行为树:

bt_patrol

<Root>
<Selector>
<Patrol>
<Homing>
</Selector>
</Root>

   通过这两颗基本的行为树,我们可以组建一个更大的行为树:

bt_monster

<Root>
<Selector>
<Sequence>
<Patrol>
<Homing>
</Sequence> <Sequence>
<Attack>
<Homing>
</Sequence>
</Selector>
</Root>

  当然我只是一个引申,更加具体的设计可以根据具体的设计来实现,有时候不只是叶子节点可以复用,某些树也可以整体作为一个叶子复用,这样可以实现设计的复用。

  在行为树中,一般的设计思路,是会构建一个行为树的模版,比如上面的bt_atk,在每个使用该模版的角色进行初始化行为树的时候,是从该模版缓存中取出一份,然后初始化相关的信息得到一份实例,在进行行为树更新的时候,是更新其对应的实例。

  而且为了通用各个树干和叶子节点,都是采用数据封包传递,在数据包中封闭当前角色相关的信息或者黑板信息,这样在每个节点更新的时候,都是从数据封包中获取当前角色相关的信息,从而进行对应的逻辑更新。这样,就避免了一些设计上的将数据封存在action行为节点上的问题,通过封包的传递,可以降低耦合,提高复用性。

  对于行为树的优化,我提一个优化点吧。大部分的行为树在具体的项目中应用都是结合具体的设计来实现的,所以我采用的优化未必适合于其他游戏的优化,但是可以采用一个更新频率的设置来降低行为树的更新频率,这个是可以通用的。我在测试服务器的代码性能的时候发现,当场景中的角色数量比较少的时候,大部分的游戏性能都被场景中的怪占用了,而怪物的更新中对于AI的更新又是一个很大的占用。我采用一种距离配置的方法进行优化,当怪物周边没有玩家的时候,降低怪物的更新频率或者就不执行怪物的AI更新,当怪物进入玩家的视野的时候,采用较高的更新频率,当怪物进入玩家的攻击范围的时候,采用正常的更新频率。通过不同距离检测设置不同的更新频率,可以较好的优化在玩家个数较少时的怪物AI性能。

总结:游戏中两种常见的状态机和行为树都做了一个简单的讲解,当然现在网上比较多相关的资料,如果想深入的学习,可以搜集相关的资料研究,有较多的开源代码也可以参考研究一下。当然我说的都是较为浅显的设计,具体的状态机和行为树节点的设计,其中具体逻辑的编写,是需要结合实际的游戏设计来实现的,这就需要程序和策划具体的商量和实现了。好了,今天AI的简介就说到这儿,下一篇再说说一些优化的总结吧:D