前言

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

这一篇来说一下战斗系统中的状态系统,我们在游戏中的眩晕,移动,禁锢,灼伤,吟唱,攻击。。。几乎所有的行为和表现都可以抽象出一个状态,所以宏观来看,状态系统会和动画系统,技能系统,Buff系统产生交互,所以设计一个健壮,功能全面的状态系统是必要的。

需求分析

我们先来分析需求

动画系统

首先是最简单的,动画系统相关的状态设计,很多情况下,我们需要当前状态结束后回溯到前一个状态,例如Idel->Run->Idel,就是一个很好的例子,所以我们需要设计一个逻辑类似的状态容器(有学名的,叫“下推自动机”),在Remove一个状态的时候,前一个状态就会到栈顶,作为人物当前状态。

事实上这个栈式的逻辑在其他地方也适用,比如一个人物目前是冲刺状态,会一直朝着前方冲刺,但是会被禁锢技能给禁锢在原地,禁锢效果结束后继续往前冲刺。

技能系统

这里的技能系统指的是我们技能Canvas的“行为树区域”控制的那部分逻辑,也就是技能的运行逻辑,运行到某个节点,直接添加一个状态,比如开始释放技能,需要吟唱3S,就需要在“吟唱行为节点”中为施法者添加一个“技能引导状态”。比较容易理解,就不多嗦了。

Buff系统

然后我们深入一下,状态的冲突处理。之所以把状态冲突放在Buff系统分类里,因为绝大部分的状态冲突都发生在Buff系统,当然技能系统也会有一些,比如释放一些技能的时候会无法移动,这其实就隐含了一个无法移动的状态在里面,在此期间移动指令将会被忽略。

最简单的,例如玩家当前处于移动状态,被一个眩晕技能击中,就需要中断这个移动状态,进入眩晕状态,因为眩晕状态优先级要高于移动状态。在眩晕状态下,就无法切换到移动状态,因为移动状态与眩晕状态互斥。这个互斥状态的集合要配置在移动状态类中,而不是眩晕状态中,因为我们是眩晕状态下无法移动和移动会被眩晕打断,所以是移动状态在添加时会判断是否有眩晕这一互斥状态,而眩晕是无条件添加的,当然,凡事有例外,继续往下看。

接着上面说,更复杂一点的,涉及到谁的技能判定或者技能效果比较霸道的,同样也是优先级的概念,但是这个优先级概念与上文优先级作用区域是不一样的,上文的优先级是已经确定要添加状态的情况下对状态进行排序,而这里的优先级是还未确定是否要添加状态前的检测阶段。举个例子,英雄A有一个禁锢技能,大部分英雄释放位移技能的时候只要被这个禁锢技能击中,就要乖乖罚站,但是偏偏有一个英雄B的位移是可以无视这个禁锢的,也就是说这个英雄B的位移更加霸道,这就不是单纯的状态排斥问题了,还需要加上优先级的考量。

当然你可能会说这个英雄B释放位移技能的时候除了位移状态再加一个禁锢免疫状态不就行了吗?那么,如果这个英雄B的位移会被另一个英雄C的禁锢技能给打断呢(比如永恩的二段E本身是不可阻挡,但会被蝎子的R打断)?治标不治本罢了,需要找到通用的解决方案。

问题解决

多方考量的同时来一个个的解决问题

首先是我们的状态容器的选择,因为会有Buff持续时间/净化Buff的存在,需要我们直接移除部分状态,所以我们不能直接使用栈容器,而是需要自己使用链表模拟一个栈容器出来。

1
2
3
4
/// <summary>
/// 用于内部轮询,切换的状态链表
/// </summary>
private LinkedList<AFsmStateBase> m_FsmStateBases = new LinkedList<AFsmStateBase>();

因为我们一个状态可能包含多个原子状态,比如一些技能释放时释放者不可移动,就是一个释放技能状态和一个禁锢状态,而且一些Buff所带来的状态是有持续时间的,持续时间一到就得移除Buff和状态,所以需要一个string做标识,还有我们上面提到的优先级概念,所以要单独抽象一个状态类,而不是简单地一个枚举完事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 状态类型
/// </summary>
public StateTypes StateTypes;

/// <summary>
/// 状态名称
/// </summary>
public string StateName;

/// <summary>
/// 状态的优先级,值越大,优先级越高。
/// </summary>
public int Priority;

/// <summary>
/// 获取自身排斥的状态
/// </summary>
/// <returns></returns>
public abstract StateTypes GetConflictStateTypeses();

然后很多状态在进入/退出的时候都有逻辑要运行,比如移动,攻击状态在进入/退出/移除时我们有逻辑需要执行,所以要设计一下状态生命周期

1
2
3
public abstract void OnEnter(StackFsmComponent stackFsmComponent);
public abstract void OnExit(StackFsmComponent stackFsmComponent);
public abstract void OnRemoved(StackFsmComponent stackFsmComponent);

动画系统相关的栈式逻辑实现很简单,在新增(比较简单就不贴代码了)/移除状态的时候做一下处理就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 从状态机移除一个状态(指定名称),如果移除的是栈顶元素,需要对新的栈顶元素进行OnEnter操作
/// </summary>
/// <param name="stateName"></param>
public void RemoveState(string stateName)
{
AFsmStateBase temp = GetState(stateName);
if (temp == null)
return;
//移除的状态是否为当前状态
bool theRemovedItemIsFirstState = this.CheckIsFirstState(temp);
//清除数据
this.m_States[temp.StateTypes].Remove(temp);
this.m_FsmStateBases.Remove(temp);
//执行OnRemoved
temp.OnRemoved(this);
if (theRemovedItemIsFirstState)
{
//如果移除的状态是当前状态,就得执行前一状态的OnEnter
this.GetCurrentFsmState()?.OnEnter(this);
}
}

技能系统因为和Buff系统的处理比较类似,就放在一起解决了

一方面是技能Canvas的数据配置

image-20210907125223443

因为考虑到状态的组合,所以把原子状态枚举使用System.Flag打了标记,这样就可以利用位运算来提高性能和便捷性

1
2
3
4
5
6
7
8
9
10
[Flags]
public enum StateTypes
{
/// <summary>
/// 行走
/// </summary>
[LabelText("行走")]
Run = 1 << 1,
...
}

效果如下

image-20210907125232870

另一方面是状态系统的冲突处理

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
32
//返回true代表切换成功,false为失败
public virtual bool TryEnter(StackFsmComponent stackFsmComponent)
{
//这里将状态本身作为参数传入,方便做判断处理
//如果当前人物身上没有与此状态互斥的状态
//如果当前人物身上已经有了与此状态排斥的状态,并且已有状态的优先级高于此状态优先级,将无法切换至此状态
if (!stackFsmComponent.CheckConflictState(this))
{
return true;
}
else if(!CompareConflictStatePriorityHigher(this))
return true;
return false;
}

//检查自身是否有与aFsmStateBase互斥的状态,如果有返回true,否则返回false
public bool CheckConflictState(AFsmStateBase aFsmStateBase)
{
if(包含互斥状态)
return true;
else
return false;
}

//检查互斥状态优先级是否大于自身,如果是返回true,否则返回false
public bool CompareConflictStatePriorityHigher(AFsmStateBase aFsmStateBase)
{
if(自身与aFsmStateBase互斥的状态的优先级大于aFsmStateBase)
return true;
else
return false;
}

然后就是一些周边功能的开发,比如我们有一个Buff效果是移除人物身上所有的减速效果,就需要以减速状态为基准来获取需要移除的状态,又或者我们在添加Buff的时候做排斥判断,也需要类似的功能。每次遍历我们的链表查找状态着实有些不妥,所以我们需要再维护一个Dic:

1
2
3
4
5
/// <summary>
/// 当前持有的状态,用于外部获取对比,减少遍历次数
/// Key为状态类型,V为具体状态类
/// </summary>
private Dictionary<StateTypes, List<AFsmStateBase>> m_States = new Dictionary<StateTypes, List<AFsmStateBase>>();

整个状态系统的状态插入流程如下

后记

读者可能会发现状态系统部分项目代码可能与文章描述不符,这是因为在写文章的时候有了新的思路和改进方案,但是代码没有及时更新。