## 前言

基于行为树的Moba技能系统系列文章总目录:https://www.lfzxb.top/nkgmoba-totaltabs/

本篇文章主要讲一下Buff系统的设计。Buff系统是战斗系统中最为重要的一个部件,我们技能效果就是依靠Buff系统实现的,比如伤害,治疗,破甲,眩晕,护盾,斩杀等,也就是说一个技能真正的核心就是组成它的那些Buff,这一点其实在《可视化节点技能编辑器的制作》一文中的示例中有体现。

这就可以引申出一个“万物皆Buff”的思想,所有的行为/效果都可以用一个Buff来实现,常规的比如一个持续伤害Buff,特殊的比如一个播放特效Buff,往客户端同步数据Buff。

指导思想有了,并且经过《可视化节点技能编辑器的制作》文中Buff系统相关介绍,我们可以知道这种方式确实可行,那么具体怎么抽象出一个健壮的Buff系统就是我们需要考虑的事情了。

本文更多的是介绍Buff系统Runtime的架构设计,Editor的架构设计可从下图得知,更详细的内容在《可视化节点技能编辑器的制作》中:

正文

基类抽象

首先我们Runtime下的Buff需要有数据载体,用于记载此Buff的数据(也就是我们在Editor下配置的Buff数据)

其次一个Buff必定是有头有主的,所以必须要有两个字段记录这个Buff来自谁,作用在谁身上,有了这两个字段,我们的Buff就可以获取所有想要的人物的数据,并且执行相应操作

然后就是Buff层数,状态这种即时数据了,所以就有以下基础字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// Buff当前状态,是给下面的生命周期函数使用的
/// </summary>
public BuffState BuffState;

/// <summary>
/// 当前叠加数
/// </summary>
public int CurrentOverlay;

/// <summary>
/// 最多持续到什么时候
/// </summary>
public long MaxLimitTime;

/// <summary>
/// Buff数据
/// </summary>
public BuffDataBase BuffData;

/// <summary>
/// 来自哪个Unit
/// </summary>
[DisableInEditorMode]
public Unit TheUnitFrom;

/// <summary>
/// 寄生于哪个Unit
/// </summary>
[DisableInEditorMode]
public Unit TheUnitBelongto;

生命周期

因为大部分Buff是有时效性的,而且在不同的阶段都有不同的逻辑,所以生命周期也是必不可少的了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// 初始化buff数据
/// </summary>
/// <param name="buffData">Buff数据</param>
/// <param name="theUnitFrom">来自哪个Unit</param>
/// <param name="theUnitBelongto">寄生于哪个Unit</param>
public abstract void OnInit(BuffDataBase buffData, Unit theUnitFrom, Unit theUnitBelongto);

/// <summary>
/// Buff触发
/// </summary>
public abstract void OnExecute();

/// <summary>
/// Buff持续
/// </summary>
public virtual void OnUpdate()
{
}

/// <summary>
/// 重置Buff用
/// </summary>
public abstract void OnFinished();

/// <summary>
/// 刷新,用于刷新Buff状态
/// </summary>
public virtual void OnRefresh()
{
}

一个Buff被添加到人物身上后,实际上是被添加到人物的BuffManagerComponent中,BuffManagerComponent会根据具体Buff状态执行上面的生命周期函数,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void Update()
{
...
this.m_Current = m_Buffs.First;
//轮询链表
while (this.m_Current != null)
{
ABuffSystemBase aBuff = this.m_Current.Value;
if (aBuff.BuffState == BuffState.Waiting)
{
aBuff.OnExecute();
}
else if (aBuff.BuffState == BuffState.Running)
{
aBuff.OnUpdate();
this.m_Current = this.m_Current.Next;
}
else
{
aBuff.OnFinished();
this.m_Next = this.m_Current.Next;
m_Buffs.Remove(this.m_Current);
this.m_Current = this.m_Next;
}
}
}

监听Buff

之所以把监听类型的Buff单独拉出来,是因为他与常规的Buff相比更特殊一些,因为它会涉及到事件的订阅,而就是因为这个事件的全局订阅机制,我们需要做一下额外处理的

试想这样一个场景,某英雄的一个技能组成中,包含了一个监听Buff A,这个监听Buff A会订阅一个事件B,这个事件B由另一个Buff C分发,假如我们不做任何处理,一场战斗中如果只有一个此英雄,那是没问题的。但如果对面也有一个这个英雄,就不行了,因为对面英雄的Buff C也会触发事件B,这样会导致两人的技能都会响应这个事件,就乱套了。

所以我们需要为事件加上唯一性标识,我的做法是在事件订阅/分发时加上Buff来源英雄的Id,就像这样

1
2
3
4
//订阅事件
Game.Scene.GetComponent<BattleEventSystem>().RegisterEvent($"{temp.EventId}{this.TheUnitFrom.Id}", temp.ListenBuffEventNormal);
//分发事件
Game.Scene.GetComponent<BattleEventSystem>().Run($"{eventId}{this.TheUnitFrom.Id}", this);

走一遍Buff的完整流程

前面的内容比较零散,所以我来带大家走一遍一个Buff的完整生命流程

1
2
3
4
5
6
7
8
//获取一个Buff,并执行Buff的OnInit自动初始化相关数据,将Buff状态设置为就绪状态,执行OnExcute
ABuffSystemBase nextBuffSystemBase = BuffFactory.AcquireBuff(dataId, buffNodeId, theUnitFrom, theUnitBelongTo,theSkillCanvasBelongTo);
-------------------------------
//执行OnExcute函数,执行相关逻辑,抛出事件(如果有的话)
//执行OnUpdate函数(如果是持续性Buff的话)
//执行OnFinish函数,执行相关逻辑,重置Buff数据
//回收Buff
BuffFactory.ReleaseBuff

特殊的,如果一个Buff在存在期间又被添加了一次,就会执行OnRefresh函数