@@ -0,0 +1,153 @@
---
tags:
- Buff
---
source:[(2 封私信 / 4 条消息) 如何实现一个强大的MMO技能系统——BUFF - 知乎 ](https://zhuanlan.zhihu.com/p/150812545?utm_psn=1894773352150832417 )
---
## 前言
Buff模块可以说是技能中最核心又最复杂的系统了。一个优秀的[Buff系统 ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Buff%E7%B3%BB%E7%BB%9F&zhida_source=entity )能够让策划的创意得到最大限度的发挥, 大幅增强游戏的战斗深度和可玩性, 并且同时也能让开发者轻易的扩展维护, 支持更多的效果和功能。本章将为你详细讲述一个强大的Buff系统是如何实现的。( 长文预警)
## 正文
### 第一节: Buff定义
首先我们将Buff系统分为三个层次, 具体继承关系如下:

**Buff: **所有Buff的基类, 包含各类成员函数和基本接口。
** [Modifier ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Modifier&zhida_source=entity ): **继承于Buff, 代表这个Buff是一个修改器, 它可以用来修改当前目标的各种属性, 状态等等。抽象Modifier这个类的目的是出于性能优化的考虑。因为当Buff修改角色的属性或者状态时, 会导致重新计算角色的动态属性, 而在游戏中我们很多的Buff并不需要修改角色的属性状态, 仅仅用来提供一段逻辑。那么如果它是一个Buff不是Modifier, 就不需要重新计算角色的动态属性。
** [MotionModifier ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=MotionModifier&zhida_source=entity ): **继承于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 ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffAwake&zhida_source=entity )** 事件 。 如果存在某种Buff的效果是 : 受到负面效果时 , 驱散当前所有负面效果 , 并给自己加一个护盾 。 那么这个时候就需要监听BuffAwake事件了 , 此时会给自己加护盾 , 并且把所有负面Buff驱散 。** 这意味着一个Buff可能还未生效之前即销毁了 ( 小心Buff的生命周期 )。**
3. 当Buff生效时 ( 加入到Buff容器后 ) , 我们提供给策划一个抽象接口 ** [OnBuffStart ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffStart&zhida_source=entity )**, 由策划配置具体效果 。
4. 当Buff添加时存在相同类型且Caster相等的时候 , Buff执行刷新流程 ( 更新Buff层数 , 等级 , 持续时间等数据 )。 我们提供给策划一个抽象接口 ** [OnBuffRefresh ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRefresh&zhida_source=entity )**, 由策划配置具体效果 。
5. 当Buff销毁前 ( 还未从Buff容器中移除 ) , 我们提供给策划一个抽象接口 ** [OnBuffRemove ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRemove&zhida_source=entity )**, 由策划配置具体效果 。
6. 当Buff销毁后 ( 已从Buff容器中移除 ) , 我们提供给策划一个抽象接口 ** [OnBuffDestroy ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffDestroy&zhida_source=entity )**, 由策划配置具体效果 。
7. Buff还可以创建定时器 , 以触发间隔持续效果 。 通过策划配置时调用 ** StartIntervalThink ** 操作 , 提供 ** OnIntervalThink ** 抽象接口供策划配置具体效果 。
8. Buff还可以通过请求改变运动来触发相关效果 。 通过策划配置时调用 ** ApplyMotion ** 操作 , 提供 ** [OnMotionUpdate ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionUpdate&zhida_source=entity )** 和 ** [OnMotionInterrupt ](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionInterrupt&zhida_source=entity )** 接口供策划配置具体效果 。
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状态除了上面提到几种高度凝练抽象的状态外 , 我们还可以根据具体游戏的需求去扩展各种特殊状态 , 以满足策划的需求 , 同时方便开发者管理逻辑 。
### 第四节: Buff修改属性(ModifyAttribute)
在游戏中Buff的添加与移除是一个频繁的过程 。 而玩家的属性来源有很多 , 如等级 , 装备 , 成就 , 任务 , 时装等等各种各样的来源 。 相比于Buff , 这些模块修改属性的频率要远低于Buff , 所以我们一般将玩家的属性划分为两层 , 第一层时Core ( 核心层 ) , 第二层是External ( 外部层 )。 Core层是玩家各个其他模块的属性总和 , 而External层则是Buff修改属性的总和 。 两者相加既为玩家的实时属性 。
### 第五节: Buff修改运动(ModifyMotion)
现在的MMO中为了增加动作表现力 , 经常会有很多位移效果 , 如突进 , 翻滚 , 千斤坠 , 击退 , 击飞 , 拖拽 , 吸引等等 。 那么这些效果该如何实现呢 ? 而且有时候会遇到各种复杂的运动打断效果 , 比如击飞时不能被击退 , 击飞过程又能被冰冻效果定住 , 然后又有破冰技能击退冰冻物体并解除冰冻效果 。 面对这些复杂的情况 , 我们该如何设计呢 ?
在我们的系统中 , 运动都是统一通过MovementComponent来管理 。 因此通过使用MotionModifier来与MovementComponent交互 。 MovementComponent中有一个CustomMotion , 用来具体实现各种运动位移 。 具体运动实现相关细节我们将在后面的运动章节讲述 。
在MotionModifier中 , **我们会提供一个接口ApplyMotion ( motionTypeId , priority , forceInterrupt ) 来向运动组件请求运动效果 。** 同时通过设置回调UpdateBeforeMovement和UpdateAfterMovement来触发运动前和运动后的Buff效果 。 下面我们初步介绍下ApplyMotion函数的三个参数 :
- motionTypeId : 运动类型id , 配置项 。 包含运动位移参数及相关数据 。
- priority : 运动优先级 , 每个运动都有优先级 , 低优先级不能打断高优先级 。
- forceInterrrupt : 是否忽略优先级 , 强制打断当前的Motion 。
通过这三个参数 , 我们就能实现各类打断需求了 。
比如说击退的运动优先级是100 , 击飞的运动优先级是200 。 那么在击飞过程中 , 施加击退Buff调用ApplyMotion的时候会返回false , 这时可以销毁掉这个击退Buff , 即击飞时无法击退 。 如果击飞时被冰冻 , 且冻在半空中停止不动 , 那么我们就需要设计一个静止Buff : 运动优先级是300 , 作用效果是速度设置为0 , 不受重力影响 , 同时修改Stun状态并挂载冰冻特效 。 当破冰技消除冰冻效果时 , 则设置破冰Buff的位移效果为击退 , 设置运动优先级为100 , forceInterrupt为true 。 此时ApplyMotion强制打断运动 , 冰冻Buff会触发OnMotionInterrupt回调 , 在此接口中冰冻Buff自我销毁即可 。
** Buff修改运动仅代表修改运动轨迹 。** 比如说击退仅仅只是以直线移动一段距离 。 而击飞是以曲线移动一段距离 。 同理 ** 轻功的翻滚 , 突刺其实都与击退是相同的运动轨迹 。** 他们都是在一定的时间内以直线到达目标地点 , 且都设置Stun状态 。 它们不一样的地方其实仅仅只是动画层的表现的不同 。( 可能策划还会设置不同的Tag和ImmuneTag标记下 )
** 我们要牢牢记住 , 玩家看起来各种花哨的轻功击退击飞等位移效果实际上是State + Motion + Animation的组合 。** 掌握住了这一点 , 我们就可以通过简单的组合实现各种丰富的效果了 , 而不会被各种花哨的效果所迷惑 , 以为他们都是不一样的效果 , 导致最后设计出无比庞杂且难以维护的系统了 。
### 第六节: Buff监听事件
Buff可以通过监听各类事件 , 执行特定逻辑或者修改事件数据来实现各种效果 。
最常见的事件监听一般有 :
- ** OnAbilityExecuted , 监听某个主动技能执行成功 。** 常用于被动技能Buff , 比如说角色施法时有10 % 概率获得30 % 的攻速提升 。 那么我们通常是Buff-A监听OnAbilityExcuted事件 , 然后10 % 概率添加Buff-B 。 Buff-B的作用是修改玩家属性 , 增加30 % 攻速 。
- ** OnBeforeGiveDamage , OnAfterGiveDamage监听我方给目标造成伤害时触发 。** 比如说对目标造成的伤害有10 % 概率无法被闪避 , 那么这个效果我们就可以通过监听OnBeforeGiveDamage的流程来实现 。 当执行伤害流程时 , 在计算伤害前我们抛出一个事件event 。 event里面有当前伤害数据 。 Buff在调用OnBeforeGiveDamage ( event ) 时 , 修改event . Damage . DamageFlag |= DamageFlag_NotMiss , 标注该伤害无法被闪避就行了 。 又或者如果有一个需求是给目标造成伤害后有10 % 几率触发DOT伤害效果 , 那么我们在OnAfterGiveDamage的时候取出event . Target并给这个目标加个DOT类Buff即可 。
- ** OnBeforeTakeDamage , OnAfterTakeDamage监听我方受到伤害时触发 。** 如护盾类Buff通常在OnBeforeTakeDamage的时候修改伤害数据 。 又或者有某些Buff在受到伤害后可以触发各类效果就可以通过监听OnAfterTakeDamage事件来触发指定逻辑 。
- ** OnBeforeDead , OnAfterDead监听我方死亡时触发 。** 如免疫致死效果可以通过监听OnBeforeDead事件修改角色当前的Hp > 0, 从而让角色提前退出死亡流程以避免死亡。死亡后触发额外效果, 如爆炸或者召唤其他生物都可以通过监听OnAfterDead事件来执行。
- **OnKill事件, 监听我方击杀目标时触发。**如当击杀目标后获得治疗效果回复即可通过监听到Kill事件时给自己加一个HOT的Buff来实现。
开发者可以通过扩展各类事件列表, 让Buff通过监听对应事件就能执行任意逻辑。不需要与任何模块耦合, 只需要抛出事件, 监听事件, 执行逻辑即可获得Buff功能上的扩展。
## 总结
以上我们通过六个小节讲述了Buff系统主要模块的实现方法。通过这样的设计, 我们让Buff的深度和扩展性都能够得到了极大的提升, 几乎能实现各种各样的效果。足以让策划的创意得到最大限度的发挥。
## 示例
为了让读者便于直观理解,我会提出一些具体实现的例子以供参考:
- 问: Buff互斥效果也很常见, 怎么做?
- 答: BuffTag和BuffImmuneTag可轻松实现。比如说火系Buff和水系Buff互斥。无论策划的需求是存在水系Buff的时候无法添加火系Buff, 还是存在水系Buff的时候添加火系Buff会驱散水系Buff都可以实现。第一种情况最简单, 水系Buff配置Tag 为Water的时候配置ImmuneTag为Fire。此时存在水系Buff的时候即可免疫火系Buff了。第二中情况也好办。配置BuffTag为Water。当OnBuffStart的时候调用驱散接口DispelByTag(Water), 驱散掉所有火系Tag相关Buff即可。
- 问: 霸体效果怎么实现, 而且假如说存在破霸体效果又怎么实现, 而且Boss的霸体效果完全不受影响又怎么实现? 万一还存在特殊效果可以让Boss受到控制怎么办?
- 答: 我们可以定义两个BuffTag: WeakControl( 弱控制) 和StrongControl(强控制) , 普通霸体效果通过Buff配置ImmuneTag: WeakControl即可免疫控制效果。如果是破霸体效果, 我们给这个Buff的Tag标记StrongControl就行, 同时Boss的Buff配置ImmuneTag为WeakControl | StrongControl( 免疫弱控制和强控制) 就满足需求了。如果存在某个特殊的效果能让Boss受到控制效果的话, 那这个Buff的Tag不要标记WeakControl和StrongControl就行了, 这样它就无法被免疫掉了。看起来复杂的霸体破霸体效果实际实现就这么简单, 就这么清晰, 不需要引入任何新的系统。
- 问: Buff存盘那块如何处理跟施法者相关的属性数据? 如施法者可以给目标添加一个强力的毒Buff, 具体伤害数值有施法者属性决定, 离线后依旧生效, 直到Buff时间结束才移除。
- 答: 这块我们的处理依旧很简单, Buff依然设置bNoCaster=true。但是在Buff创建的Context里面我们设置Context.DamageValue为根据施法者属性计算出来的伤害数值。然后Buff持续造成伤害的时候直接取Context.DamageValue即可。至于说想要玩家离线再上线, Caster离线再上线后, 毒的伤害数值还能实时修改的话, 这样的需求是不存在的, 如果一定要做, 当然也能做, 只是麻烦一点而且也没有必要。这样的需求一般仅仅存在测试的大脑中, 策划是不会有这样的玩法需求了。
- 问: 常见的基于指定地点延迟触发的AOE效果怎么实现? 当技能施法成功后就延迟触发, 不会被打断AOE效果。( 如果能被打断, 我们可以用引导类技能轻松实现)
- 答: 我们将技能标记为可指定目标地点释放, 当技能Spell的时候我们先给自己加一个Buff, 这个Buff仅仅用于延迟效果( 当然可以有更多的可能性, 如监听到某种事件立即结束并触发AOE效果) , 当Buff持续时间到了的时候在OnBuffDestroy的时候创建AOE效果Buff。这个AOE Buff会调用StartIntervalThink函数, 在OnIntervalThink的时候通过Buff:GetAbility():GetCastPosition()为基准位置检查周围的敌方单位是否在AOE半径内, 如果是, 则施加作用效果。