技能系统之美_知识分享

一个MMORPG的常规技能系统

广义的的说,和战斗结算相关的内容都算技能系统,包括技能信息管理、技能调用接口、技能目标查找、技能表现、技能结算、技能创生体(buff/法术场/弹道)管理,此外还涉及的模块包括:AI模块(技能调用者)、动作模块、寻路/移动模块以及人物属性和伤害数值结算等。

先说下技能模块每个部分的职责和原理:

  • 技能信息管理:管理unit所拥有的技能以及技能的等级、cd等。在我们游戏中,这里还需要负责管理符文,符文会对技能信息进行修改。
  • 技能调用接口:AI或者UI操作触发技能,触发技能时可能选择了一个目标(AI),也可能并没有目标。
  • 技能流程管理:一个技能可能由多个子技能以移动的执行模式组合而成,而每一个最终执行的技能执行过程也存在一个流程,一般包括:前摇过程-结算点-后摇过程。技能在前摇结束时进入技能真正的结算流程,结算流程可能创建子弹,也可能触发buf或者创建法术场。
  • 技能目标查找:若技能触发时已经设置了技能目标unit(如怪物AI释放技能),则直接将其作为目标unit,否则需要根据一定的策略选择一个目标。此外,技能释放的时候还需要释放方向和释放位置等信息,也通过这个模块获取。
  • 技能表现:技能释放过程中,需要创建相应的特效以及执行相应的动作。
  • 技能创生体(buf/弹道/法术场)管理:buf挂在unit身上,可能影响unit的一些行为和状态;法术场一般由场景管理,影响场景中某范围内的unit;弹道就是技能创建的一个子弹,这个子弹可能以不同的路线移动(直线/抛物线/直接命中等)

0技能表

首先说下实现技能的基本思路。实现技能的基本思路就是通过策划填写表格,来配制成某些技能,在执行某个技能的时候,分别去根据这些表格中的内容,确定技能如何表现。基本的逻辑是:

if skillTable.get("技能动作"):
     paly 动作
if skillTable.get("特效"):
     播放特效
if skillTable.get("法术场"):
    创建法术场
....

1 技能信息管理

unit创建时,此模块管理unit可使用哪些技能,比如游戏中玩家可以选择使用哪些技能。

游戏中技能的升级、技能加点、技能池管理都在这个模块。

此模块还需要管理技能等级/符文/装备等外部模块对技能参数的修改。

2 技能调用接口

提供技能调用的接口供AI或玩家操作调用,调用时可以提供一个目标unit,也可以不提供让技能自己查找。

提供三个接口:

  • 技能开始skill_enter:开始执行技能,若技能不循环进行,则技能可以自动结束。
  • 技能结束skill_exit:有的技能不能自己结束,比如某些循环技能,对于循环技能玩家可以按住按钮一直释放。当玩家松开按钮,调用技能结束接口,告诉当前技能使其结束,此时技能到达后摇点时,技能不再继续执行。
  • 技能停止skill_stop:当技能被强制打断时,如被攻击、晕眩、蓝不足等,技能会被强制停止。

此外,当前一个技能正在执行时新的技能调用启动,此时新的技能调用信息会被保存。一般来说,并不会把所有新的技能调用信息保存下来,那样就成了一个技能执行的序列。我们游戏仅保存一个新的技能调用信息。

总的来说,技能模块提供尽量少的接口供AI/UI等上层逻辑使用,这样可以有效的与AI和UI进行解耦。

3 技能流程管理

技能流程这里分两点讨论:

  1. 一个技能可能由多个子技能以一定的模式组合起来。
    一个技能常常由多个子技能以一定的模式组合而成,比如三段击、比如冲锋斩(先冲锋、后斩)等,甚至还存在根据不同的环境选择执行不同的子技能。分析策划需求发现,技能可以分成一个树形结构,这个树形结构非常类似行为树,同样可以将节点分为控制节点和执行节点,甚至可以包括condition节点。为此,我们项目引入一个技能树概念来描述这种数据结构。

  2. 一个具体的技能(技能树执行节点)也有一个固定的执行流程。这个流程一般为:前摇过程、前摇过程结束=技能结算时间点、后摇时间点。

3.1 技能树

技能树参考传统行为树的设计,使用树形结构控制技能的执行流程。

技能树和行为树在结构上比较类似,但是在运行逻辑上有很大的不同。

首先,技能树的重点并不是根据上下文选择一个合适的节点执行,而是以一定的策略将技能树从头到尾遍历执行一遍。

其次,技能树没有tick的概念,而是基于回调的,比如一个顺序节点,顺序节点中一个子节点执行完毕后,马上通知顺序节点,顺序节点执行下一个子节点,直至顺序节点的最后一个子节点执行完毕,顺序节点就会通知父节点(如果有)它已经执行完毕。

此外,为了完成技能的一些需求,控制节点往往存储更多的控制信息来控制子节点的执行流程。具体的信息根据策划需求设置,比如顺序结点包括原子属性和循环属性。如果一个顺序节点具有原子属性,则这个顺树节点在执行的过程中并不会被end,只有全部子节点执行结束才可以end。

以我们游戏中战士普攻三段击为例:

 

技能系统之美_知识分享_02


Paste_Image.png

三段击本身是一个顺序节点,当技能开始时,此节点顺序执行三个子节点。对于第一个子节点,它依然是一个顺序节点,首先冲锋至目标单位身前,然后对目标单位进行挥砍。但是冲锋节点还包括了一个condition,若和目标的距离很近,则跳过冲锋节点,直接挥砍。

普攻是一个循环技能,这个技能只要玩家点着按钮不放开,技能就会一直执行,因此根节点(普攻)是一个具有循环属性的顺序节点。而对于子技能1(控制节点),他是一个具有原子属性的顺序技能,即当单位正在冲锋时,玩家松开按钮,单位也会执行完挥砍后才会推出技能。

!关于技能树的使用和思考

技能树开始的设计思路是,有些技能的执行流程和行为树类似,比如以一定的顺序执行一系列子技能,比如根据不同的上下文确定技能的执行流程。简单的说,技能树的引入有以下好处:1.使技能模块可以获得部分AI的能力,从而将和技能强相关的AI逻辑放在技能模块使技能模块和AI模块降低耦合,2.可以清晰的描述技能流程,3.使用树增加拓展性,策划可以设计出各种各样复杂的技能。

关于好处1,举个例子:屠夫boss的勾子技能可以将玩家拉过来,若成功的拉过来,boss会执行一个攻击子技能,否则不执行。通过这样可以将勾人和攻击作为两个子技能构成技能树,攻击子技能有一个condition过程,即判断上一个子技能是否成功。

技能树在使用后慢慢发现一些问题,首先,技能树的同步要求每个树节点都进行同步,增加同步负担,其次,技能本身并不会有太复杂的控制结构。

为此,后来我们对技能树进行了优化:
1.简化同步信息,不再同步所有节点的enter/exit信息(具体参考文章《技能模块的同步》)。
2.取消并行节点,通过拓展表头实现一个技能同时执行多件事情。

最终的技能树基本上是只有顺序/随机两种控制类型节点,节点拥有较轻度的condition功能。

3.2 执行节点的技能流程

一般来说,技能的执行流程包括:

  • 前摇时间:技能开始,但是技能真正的结算流程还没开始。技能开始以后,机能相关的特效和动作就开始播放。
  • 前摇时间结束:技能前摇结束时技能开始真正的释放以及结算,等技能前摇结束以后,技能真正的释放并结算。释放包括创建相应的弹道/法术场和buff。
  • 技能后摇点:技能播放到后摇点时间时,技能真正的结束。这时,技能对应的特效以及人物动作可能还会继续播放,但是技能流程已经正式结束了。也就是说,下一个技能可以执行。

4 技能目标查找

技能释放时,目标可能已经由AI传给了技能模块,也有可能没有一个目标,如玩家控制单位。

技能在释放法术场、弹道的时候,重要的是技能的方向而不是技能目标一般来说,技能获得一个目标对象以后,技能的方向就是释法者到目标的方向。

此外,技能方向可能需要一些配置,如前摇锁定(前摇过程中目标移动,技能方向不变),UI可控制(技能释放过程中,玩家可以通过控制UI控制技能的释放方向)。

5技能表现

技能的表现包括动作、特效、shader、音效等。其中,特效比较复杂,需要配置的内容也比较多。比如,有些特效挂在模型上,有的特效挂在场景里。对于法术场的特效,分别可以分为法术场开始、结算、结束特效,分别在法术场开始时、结算时、结束时显示。对于buff也类似。

6 弹道、法术场和buff等技能创生体

狭义的来说,技能只是负责技能的执行流程(技能树管理以及技能流程管理),而技能真正的结算主要是由其创生体结算的。当技能前摇结束开始生效时,技能创建相应的弹道和法术场,法术场弹道击中敌人时又有可能产生相应的buff。

一般来说,法术场是一个场景的某块检测区域,每隔一段时间法术场检测此区域的敌人,并对其攻击结算。
弹道是一类子弹移动路径的抽象,创建一个弹道就表示一个子弹特效沿这个弹道移动并检测路径上的敌人。
buff就是挂在单位身上的一个具有持续时间的状态,状态对单位产生一些正面或者负面的影响,并且在此段时间内,每隔一段时间进行一次伤害结算 。

对于技能、法术场、buff之间的功能界定并不是很固定,比如技能能否直接对单位造成伤害,法术场能否对单位造成伤害,甚至技能只能创建法术场,法术场只能检测目标不能造成伤害,只能挂buff,而所有的伤害都是通过buff来结算。当然,这样并不一定好,一般来说,技能和法术场都可以对单位造成伤害。

总之,创生体功能的界定需要根据策划需求、效率考虑等因素调整。

6.1 Buff状态

Buff就是挂在单位身上持续一定时间的有益或者有害的状态,这里状态=buff。

Buff模块有个需要注意的是Buff之间的相互关系,如排斥(A状态在,B状态挂不上去),清除(A状态挂上去同时导致B状态消失)等。

为了实现以上功能,最简单的方式是在状态A中直接填写状态关系状态字段,如状态A排斥状态B/C/D/E...,A状态清除状态X/Y/Z...。

以上的实现方式有个问题,等游戏做到后期,我们有成千上万个buff状态,那么一个魔法免疫状态,策划需要填表的排斥状态可能成千上万。

为了解决这个问题,可以使用分类的思想解决。定义某类状态和另一类状态之间的规则。
基于以上思想,引入一个叫buff原子状态的概念,原子状态表示一类状态,如减速、禁魔、魔免、悬空、晕眩、变羊等等等。

在给单位挂一个新的buff的之前,查询此buff持有的原子状态和单位身上已经有的原子状态之间的关系,根据单位身上已有的原子状态判定新的原子状态应该使用何种行为处理。

此处的何种行为,代表的就是原子状态之间的规则,如排斥等。这些规则可以让策划填一个名字叫“原子状态关系”的表,此表是一个n*n的二维数组,n为游戏中所有的原子状态的数量。

原子状态的数量远远小于buff的数量,所以可以很容易的定义这些规则。

6.2 法术场

法术场描述对一块区域的影响,这块区域可以每隔一段时间进行一次检测,检测这块区域内的单位并且对单位进行结算。

法术场需要注意一个问题,就是一个法术场每次结算可能使用不同的参数进行结算,比如一个技能,第一次结算对每个单位进行晕眩,第二次结算对单位进行伤害。

解决这种问题比较直接的方式是技能直接创建两个法术场,每个法术场结算一次,第二个法术场创建具有延迟时间。但是这种方式有个问题,有可能策划需求做一个结算十次而且每次结算的参数都不同的法术场。那么,一个技能以一定的时间间隔创建是个法术场,同时法术场的管理具有一定的成本,从而导致效率的降低。

为解决这个问题,我们优化了法术场结算的实现机制,增加了一种新的法术场:序列法术场。这类法术场策划可以配置法术场每次结算之间的时间间隔以及每次结算所使用的法术场参数。

 

技能系统之美_知识分享_03

 

技能系统的同步机制

这里所说的技能系统包括:技能流程和技能创生体(法术场、弹道和buff)

首先介绍authority和proxy的概念,这两个概念是基于单位unit的基础上进行的区分。

authority表示单位的主控端,即此单位是由客户端和还是服务端控制。对于玩家avatar,玩家本地的客户端就是主控端。而对于怪物,他们的行为由服务端控制,主控端就是服务端。

proxy表示代理端,表示被主控端控制。如对于怪物来说,所有的客户端都是proxy;对于玩家A来说,服务端和其他玩家的客户端都是proxy。

0 技能同步的原则

1.客户端先行

对于玩家控制的单位来说,玩家点击按钮释放一个技能,客户端首先响应,单位播放动作以及相应的技能特效。

据我了解,有的已上线游戏并没有做客户端先行,而是所有的技能执行请求都发给服务端,然后由服务端发起。

这种模式技能流程控制会比较简单,但是在网络环境差的情况下,体验可能差一些。但是,目测也是可以接受的。

 

2.技能流程以authority为发起端

玩家单位技能发起是由她的客户端,怪物的技能发起是由AI也就是服务端。

3.技能结算在服务端发起。

技能真正的结算,比如法术场检测、buff结算、伤害计算以及扣血等,统一在服务端处理。

1 技能执行流程的同步

这里所说的技能执行流程指的是技能树的一个执行节点的流程。

技能流程负责动作、特效以及技能结算,其中技能结算包括:释放法术场、弹道或buff。

一个技能执行节点的执行流程中,需要同步的有两个时间点:

  • 技能开始:技能开始播放动作
  • 技能结算:前摇结束,即能进入结算逻辑。这类同步消息往往并不是由技能本身去同步,而是技能生成了法术场、弹道等,他们去做相应的同步。

以玩家点击技能按钮开始释放技能为例介绍技能同步流程,如图所示:


 

技能系统之美_知识分享_04

 

1.主控端点击技能按钮,技能开始播放动作,主控端告诉服务端技能开始。

2.服务端广播给所有的客户端(多玩家场景),告知其他所有的客户端此玩家开始执行技能。其他客户端收到指令后可是播放技能表现。

3.服务端延迟一段时间后,服务端开始进行技能结算,并且将结算结果通知客户端。

延迟时间=技能前摇时间-上行-下行,下行一半不能确定,所以默认为上行=下行

另一种中庸的计算方式是:延迟时间=技能前摇时间-上行,防止要求技能前摇时间过长

使用此同步流程的表现为:

1.要求技能前摇时间>2*网络延迟,若前摇时间短,则延迟时间=0,效果可能差一些

2.authority客户端表现完美。

3.proxy client表现一般,即玩家A看玩家B的效果为:玩家B刚开始执行技能动作,没到前摇时间就进行了技能结算。但是因为玩家一般也不会过分关注其他玩家的动作,所以是可以接受的。

2 技能树的同步

我上篇文章一个MMORPG的常规技能系统 - 知乎专栏已经介绍,我们游戏使用的是技能树来管理技能流程。那么就面临一个问题,技能树如何同步。

最简单最暴力的方式,是客户端和服务端同时管理技能树,并且将其状态同步。这样,客户端和服务端的技能树状态统一、完备。

后来发现,对于proxy端,并不需要完备的技能树信息,最节省的方式是proxy根本不接受技能树同步信息,只是接受播放动作、技能结算等信息。但这样需要告诉其他proxy播放什么动作、特效等。

在我们系统中,技能同步包括三类同步消息:

  • 技能根节点enter (root_enter): 表示一个大技能的进入
  • 技能叶子节点enter(action_enter): 表示一个技能树的执行节点的进入。
  • 根节点exit(root_exit) :表示大技能结束

根节点保存一个完整技能的信息,需要和技能模块外部交互,因此需要知道技能的开始和结束。

叶子节点的执行代表着技能真正的执行逻辑,也需要同步。

而对于其他节点,作为流程控制节点,只需要在主端确保技能流程无误即可

后来这里进行了进一步的优化,对于纯根节点,主控端(玩家控制的客户端)将信息同步给服务端,服务端不再同步给其他客户端。有的技能树只有一个节点,那么按照叶子节点的策略,主控端同步给服务端,服务端广播给所有的其他客户端。

3 技能结算的同步

技能结算包括创建法术场、buff、弹道、技能直接伤害等。

法术场、弹道的同步

法术场、弹道的同步比较类似,他们都作为一个entity(网络同步单元)在服务端创建,创建以后使用entity管理机制服务端通知客户端他们的创建和销毁。

以法术场为例,法术场的执行和同步流程:

  1. 服务端发起创建一个法术场,并且通知客户端
  2. 法术场每隔一段时间结算一次,注意,法术场结算并不需要同步,每隔一段时间服务端执行检测逻辑,客户端播放结算特效等。两个逻辑互不依赖,也不要求时间一致。
  3. 当法术场结算时检测到攻击目标时,服务端计算攻击伤害等信息,并将攻击信息发给客户端。
  4. 客户端收到伤害信息,客户端播放相应的表现,如法术场受击特效等。此处还包括属于通用模块的跳字等。
  5. 当服务端的法术场时间到了进行destroy时,使用entity的管理机制通知所有客户端destroy法术场。

弹道的同步类似,唯一的区别就是法术场在某一位置使用攻击盒检测目标,而弹道是一个移动的子弹,客户端表现是一个特效在飞,而服务端每隔一段时间根据飞行速度等使用胶囊攻击盒去检测目标碰撞。

由以上可以发现,法术场作为一个entity他的管理成本是比较高的,所以若策划想出一些需求需要使用多个法术场实现,一般通过拓展法术场功能使用一个法术场来实现。

比如,策划要做一个冰火两重天法术场,即法术场在每次结算时使用不同的参数,第一次结算使用火焰,第二次结算是冰霜。若这种需求较少,可以使用两个法术场,但是如果要冰火雷毒水电风魔奥术神圣*N重天,则代价太大。一般可以让法术场支持每次使用不同的结算参数来结算即可。

buff同步

buff是附加在unit身上的东西(没有unit就没有buff,但是没有unit可能有法术场),所以不需要使用entity来同步。

  1. 服务端确定buff是否可以挂在unit上面。
  2. 客户端和服务端都维护一个buff管理器,挂buff的消息通知所有客户端,客户端负责表现,服务端负责结算即可。

4 伤害、属性的同步

主要介绍下伤害的同步,顺便附带介绍下属性同步。

对于伤害结算来说,技能、buff、法术场和弹道都可能造成伤害,当服务端发现造成伤害时,服务端首先根据技能信息计算伤害值,计算以后将伤害信息发送给所有客户端,所有客户端接到信息后首先播放技能伤害相关的表现,如受击特效等,然后播放跳字等通用伤害客户端表现。

也就是说,伤害值的同步其实就是简单的rpc消息。

buff可能修改单位属性,如攻击力、攻击速度等。这些属性值的同步一般使用属性同步属性同步的意思是当一个值改变了,底层自动把这个值同步给客户端。

 

血量等信息值都是用属性同步方式。

 

伤害计算和属性模块是一个重要且比较复杂的模块(当然,若游戏数值非常简单也可以很简单),后面我会写文章详细介绍这个模块。