如何设计一套好的技能buff(一)

简介: 如何设计一套好的技能buff(一)

前言

Buff模块可以说是技能中最核心又最复杂的系统了。一个优秀的Buff系统能够让策划的创意得到最大限度的发挥,大幅增强游戏的战斗深度和可玩性,并且同时也能让开发者轻易的扩展维护,支持更多的效果和功能。本章将为你详细讲述一个强大的Buff系统是如何实现的。(长文预警)

正文

第一节:Buff定义

首先我们将Buff系统分为三个层次,具体继承关系如下:

Buff:所有Buff的基类,包含各类成员函数和基本接口。

Modifier:继承于Buff,代表这个Buff是一个修改器,它可以用来修改当前目标的各种属性,状态等等。抽象Modifier这个类的目的是出于性能优化的考虑。因为当Buff修改角色的属性或者状态时,会导致重新计算角色的动态属性, 而在游戏中我们很多的Buff并不需要修改角色的属性状态,仅仅用来提供一段逻辑。那么如果它是一个Buff不是Modifier,就不需要重新计算角色的动态属性。

MotionModifier:继承于Modifier,代表此类Buff提供修改玩家运动效果的功能。因为牵涉到与运动组件的交互,所以抽象出一个新的类。

Buff类层次结构划分了之后,那么Buff需要包含那些成员数据呢?

我们提供BuffTypeId(Buff类型Id), Caster(Buff施加者),Parent(Buff当前挂载的目标), Ability(Buff由哪个技能创建),BuffLayer(层数), BuffLevel(等级)BuffDuration(时长),BuffTag,BuffImmuneTag(免疫BuffTag)以及Context(Buff创建时的一些相关上下文数据)等等。

在这里,我将说明一下Caster,Ability以及Context这三个成员,这也可能是我们Buff系统中一些独特的点。

Caster代表Buff的施加者,它有可能为空,也有可能不为空,视具体构造时是否传Caster参数而定。但是Buff有一个配置项bNoCaster(是否强制设置Caster为空)。如果bNoCaster = true。则Buff的Caster一定为空。

为什么要有一个bNoCaster设置呢?那是因为我们的Caster不仅仅是一个成员项,它还关系到Buff合并问题。如果存在两个TypeId类型相同的Buff时候,当他们的Caster相同才可以走合并流程(Buff层数增加),如果Caster不同,则不能合并。当策划有一些玩法需求可以多人给BOSS叠Buff时就可以配置Buff的bNoCaster=true,这样就不需要开发者在写代码添加Buff的时候小心翼翼的设置Caster参数为空了。另外还有几种情况也需要设置bNoCaster=true,比如存在一个熔岩地图,或者冰雪地图,玩家每秒掉多少血量,这个时候也可以配置bNoCaster=true。再比如说一些活动buff,如双倍经验buff,红名惩罚buff,都可以由策划配置bNoCaster=true。类似于双倍经验,还有红名Buff这种所有需要存盘的Buff,我们都需要设置bNoCaster=true。也许会有人有疑问,这样能满足需求吗?完全可以,我会在最后的示例部分举出一个例子来解答这个疑问。

Ability代表Buff是由哪个技能创建,它有可能为空,也有可能不为空,视具体构造时是否传Ability参数而定。通过Ability这个成员类型,我们就将Buff与技能联系起来了,我们能在Buff中取得技能的各种数据,通过获取技能的数据,然后由Buff来实现各种各样的技能效果。

BuffTag,BuffImmuneTag由策划配置(基于标记位),标注这个Buff属于那些种类以及免疫哪些种类。策划可以定义一些Tag如下:

  1. Metal = 1 << 1 (金系)
  2. Wood = 1 << 2(木系)
  3. Water = 1 << 3(水系)
  4. Fire = 1 << 4(火系)
  5. Earth = 1 << 5(土系)

当策划配置BuffTag为Meta | Wood时,则代表这个Buff归属为金系和木系Buff。如果策划配置BuffImmuneTag为Wood | Fire时,则代表这个Buff可以免疫所有木系和火系Buff。由于Tag的实际定义由策划控制,策划可以根据他们的需求组合出各种各样的免疫效果。我将在后面的示例里面描述一些基于Tag和ImmuneTag用法的例子来让读者体会Tag和ImmuneTag者两个概念抽象的简洁之美。

Context代表Buff创建时候的一些上下文数据,它是一个不确定的项,通过外部传入各种自定义的数据,然后在Buff逻辑中使用这些自定义数据。

第二节:Buff执行流程

在Buff从创建到销毁的过程中,我们划分为如下几个阶段:

  1. Buff创建前检查当前Buff是否可创建。一般主要是检测目标身上是否存在免疫该Buff的相关Buff,如果被免疫则不会创建该Buff。
  2. Buff在实例化之后,生效之前(还未加入到Buff容器中)时会抛出一个OnBuffAwake事件。如果存在某种Buff的效果是:受到负面效果时,驱散当前所有负面效果,并给自己加一个护盾。那么这个时候就需要监听BuffAwake事件了,此时会给自己加护盾,并且把所有负面Buff驱散。这意味着一个Buff可能还未生效之前即销毁了(小心Buff的生命周期)。
  3. 当Buff生效时(加入到Buff容器后),我们提供给策划一个抽象接口OnBuffStart,由策划配置具体效果。
  4. 当Buff添加时存在相同类型且Caster相等的时候,Buff执行刷新流程(更新Buff层数,等级,持续时间等数据)。我们提供给策划一个抽象接口OnBuffRefresh,由策划配置具体效果。
  5. 当Buff销毁前(还未从Buff容器中移除),我们提供给策划一个抽象接口OnBuffRemove,由策划配置具体效果。
  6. 当Buff销毁后(已从Buff容器中移除),我们提供给策划一个抽象接口OnBuffDestroy,由策划配置具体效果。
  7. Buff还可以创建定时器,以触发间隔持续效果。通过策划配置时调用StartIntervalThink操作,提供OnIntervalThink抽象接口供策划配置具体效果。
  8. Buff还可以通过请求改变运动来触发相关效果。通过策划配置时调用ApplyMotion操作,提供OnMotionUpdateOnMotionInterrupt接口供策划配置具体效果。

Buff由于其有着生命周期可控,低耦合(通过监听事件修改逻辑),高内聚、易于扩展的特性,因此通过使用Buff来管理逻辑的话,不仅方便处理各种复杂的行为,同时还能有效的减少开发者的维护难度。

例如延迟触发伤害是游戏中非常常见的需求,在一些开发者的设计中就是直接给角色挂个定时器触发伤害。简单的游戏里这样做没什么大问题,但是如果技能逻辑稍微复杂点,这样就会带来很多问题。例如某天策划提出需求,如果受到控制效果时需要取消该延迟伤害。此时你怎么办,直接干掉timer?结果策划过了两天又提出了个新的需求,还是受到控制效果时,需要这个延迟伤害立即触发,你又怎么办?再又比如说,当角色受到伤害超过1000点时,这个延迟伤害立即触发,你又该怎么做?

这里就体现出Buff的方便之处了,我们可以直接添加一个持续时间为N秒的Buff。Buff销毁时触发伤害。如果需求变更为受到控制时取消伤害,那么我们就在Buff中检查当前是否包含有Tag为Control的Buff。如果有,则设置Buff.bTriggerDamage=false,同时自我销毁。然后在BuffDestroy触发的时候检查是否触发伤害,如果bTriggerDamage为false则不触发伤害。同理,当需求为Buff监听伤害超过1000点伤害立即触发时,我们只需要通过Buff监听OnTakeDamage事件,检查当前受到的伤害值是否大于1000点,如果是则销毁Buff,此时立即触发BuffDestroy并执行伤害效果。

从上面的例子我们可以看出整个控制逻辑都是在Buff内部完成的,不需要各种手动开启/取消定时器。只需要Buff扩展下逻辑检查即可,具有非常好的扩展性和高内聚性。

第三节:Buff修改状态(ModifyState)

Buff可以通过修改状态去影响角色行为逻辑。以下列举一些最常见的状态:

  1. Stun(眩晕状态——目标不再响应任何操控)
  2. Root(缠绕,又称定身——目标不响应移动请求,但是可以执行某些操作,如施放某些技能)
  3. Silence (沉默——目标禁止施放技能)
  4. Invincible (无敌——几乎不受到所有的伤害和效果影响)
  5. Invisible (隐身——不可被其他人看见)

这些状态是高度凝练的精华,抽象到极致的代表。非常多的游戏效果实际上都是这几种状态+运动+动画的组合。这里很多开发者都会有一个设计误区就是把Buff的状态跟运动和动画耦合在一块,比如:眩晕状态一定就是播个眩晕动画,然后击退状态就是击退位移+击退动画。这样最后导致的问题就是状态膨胀,而且各种逻辑耦合,Bug频出,最后维护成本大大提高。

以Stun为例,很多人第一眼看过去就觉得它是个Debuff,是个敌人给我方加的控制Buff。实际上并非如此,Stun可以用到的地方非常多。例如有个技能是野蛮冲撞,释放后2秒内向前移动10米并将敌人推开。那这个Buff的实现就是技能Spell的时候给角色加个Buff,这个Buff会有个Stun状态同时带位移突进效果。挂上这个Buff后,技能施放后角色2秒内就不会响应角色按键移动和释放其他技能的请求了,同时往前突进的效果由Buff控制,将来处理各种位移打断效果也很方便。再比如说有个技能叫寒冰屏障:你被一道寒冰屏障所笼罩,在十秒内不会受到任何物理和法术伤害,但这期间无法移动、攻击或施法。那这个技能的实现也很简单,就是一个十秒的Buff同时添加了眩晕和无敌这两个状态,如果还需要每秒回血,则StartIntervalThink(interval),然后OnIntervalThink的时候Heal当前角色即可。

除了各类战斗效果之外,我们的Buff甚至可以扩展到一些其他场景。比如说打BOSS前有个播过场动画的需求,此时策划希望隐藏Boss和玩家的血条和姓名。那么此时我们完全可以做个Buff,这个Buff扩展个状态HideHpBar,当有这个状态时即隐藏血条和名字就行了。而且我们还可以让这个Buff加上无敌状态,毕竟播过场动画的时候我们不希望玩家或者BOSS真的受到什么伤害。

总而言之,Buff状态除了上面提到几种高度凝练抽象的状态外,我们还可以根据具体游戏的需求去扩展各种特殊状态,以满足策划的需求,同时方便开发者管理逻辑。

相关文章
|
4月前
|
程序员 C++
malloc与free的内存管理奥秘:技术分享
【8月更文挑战第22天】在软件开发过程中,内存管理是一个至关重要的环节。特别是在使用C或C++这类语言时,程序员需要手动管理内存的分配与释放。malloc和free函数是这一过程中的核心工具。本文将深入探讨malloc如何分配内存,以及free如何知道释放多少内存,帮助你在工作学习中更好地掌握这一技术干货。
101 4
|
4月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
123 7
|
4月前
|
5G UED
5G NR中的寻呼过程是如何工作的?
【8月更文挑战第31天】
131 0
|
7月前
|
存储 算法 C语言
结构体:编程之基石
结构体:编程之基石
|
开发者
如何设计一套好的技能buff(二)
如何设计一套好的技能buff(二)
239 0
|
7月前
|
存储 C语言
深入浅出 C 语言:学变量、掌控流程、玩指针,全方位掌握 C 编程技能
C 语言介绍 C 语言的特性 C 语言相对于其他语言的优势 C 程序的编译 C 中的 Hello World 程序
78 2
|
图形学
unity里面的一套简单buffer和技能
unity里面的一套简单buffer和技能
109 0
|
存储 NoSQL Oracle
【软件设计】系统设计面试基础:CAP 与 PACELC
【软件设计】系统设计面试基础:CAP 与 PACELC
|
存储 C++
深入理解MMAP原理,大厂爱不释手的技术手段
深入理解MMAP原理,大厂爱不释手的技术手段
295 0
深入理解MMAP原理,大厂爱不释手的技术手段
|
C语言 C++
结构体的基础知识,足够详细
结构体的基础知识,足够详细
89 0
结构体的基础知识,足够详细