0%

好不容易熬完期末周
不想睡觉,
半夜正在折腾arch服务器…

Capoo半夜突然出阳台来看雪
于是我出阳台来看Capoo
冷🥶

祝生物钟倒转的三天新年快乐🎆


基于上次的状态机:

之前说过我当时写出这个状态机时,是从最初的if-else重构过来的。

所以当时新接触这个状态机时,我也尝试过从if-else的角度去反向理解这个教简单的状态机,然后误打误撞就发现这其实就是分层状态机(HFSM)的思想

从if-else角度看状态机

事实上把陌生和友好两个状态合并起来就是对应着本的’false’,而逃跑状态则对应原本的’true’

或者换句话说: if-else本身就是一种广义上的二元状态机

同时,在我们的这个例子中,我们用抽象的思维看,其实’陌生和友好’本身也是一个二元的状态机,它作为一个状态机被封装成了大的if-else二元状态机下的一个状态。

也就是说:
状态机中的状态可以是另一个状态机

这也就是分层状态机HFSM

我们把之前的FSM重构为HFSM,它总体上大致长这样
粗略的HFSM

hfsm的运行逻辑是先进入父状态机,然后根据过渡条件选择对应的子状态机,再运行子状态机并进入子状态机下的状态。

各层级内需要做好抽象屏障,子状态机的状态不应与父状态机的状态存在任何通信。

以下是HFSM的一个通用模型

对于我们这个例子:

父状态机是‘逃跑-不逃跑’,存在的子状态机是‘陌生-友好’。

在由原先的FSM重构为HFSM的过程中,
状态之间原本都是直接通信
为了防止破坏层级抽象
实际上对于这个例子具体实现上需要一点特殊处理

即通过外部的一个中转Boolean来让‘不逃跑’跳转到‘逃跑’,而不是直接从‘友好’或者‘陌生’跳转到‘逃跑’。

当然,需要这样去操作也说明在构建hfsm时‘友好’和‘陌生’就不适合被分到同一状态机下,而且这个例子其实也没必要使用HFSM,只是为了从FSM引出HFSM而已。

HFSM真正的适用场景是在FSM的状态数量已经达到非常多时,难以管理,遂把各个小状态依据关联性分类并组装成一个个子状态机,然后嵌入到父状态机中,可以进行很多层的封装。

本篇基于Modo在写Flee On Sight时的感想

对于较复杂的状态判断,有一种更好的方法

为什么不用if-else ?:

首先,对于大量子状态的判定会在这种boolean的判定中越来越冗长,如果是写了辅助方法来包装则会有层层嵌套的问题,而且对于几个不相关的子boolean强行包装成一个辅助boolean会导致可读性下降,最终整体的状态判定会很难维护

同时,最最危险的一点在于if-else的各种子状态判定一旦存在耦合,会变得非常非常麻烦
举个例子,我们判断一只羊是否应该逃跑(记为isFleeing), 假设isFleeing这个boolean由两个子状态A和B来判定:

A 代表空间判定(包含视角判定,空间距离判定)

B 代表食物引诱判定(即羊不应在受吸引时逃跑)
    

或许这个时候你会想这么写

1
2
3
if (A && B) {
isFleeing = true;
}

然而事实是B中实际上嵌套了A中的部分空间逻辑🤓,这意味着 A 与 B 并不是两个独立条件,这就非常头疼了

通常接下来你有两种常规的处理这玩意的思路:

第一种是打补丁:在A中嵌套B联合一些其他的空间状态判定,从而在耦合的那块地方强行用B覆盖A(假如决策系统稍微再复杂一点点甚至会需要A与B互相嵌套)。
最终A与B之间的耦合度越来越高导致会严重破坏抽象,可以想象调试这个逻辑时在两个子状态判定之间反复横跳的场景。

第二种是强行拆散耦合,直接不要这两个较大的子状态,把他们拆散成一个个小的boolean判定。
但这就没有封装了,可读性就不要想了,面对一堆boolean维护起来也是极其困难。

那么 ,该到我们的FSM有限状态机登场了!

Modo被羊的if-else折磨得死来活去时,突然发现了有限状态机FSM这个好东西

从羊的角度切入:

    有限状态机相当于把羊赋予一个’状态‘,状态可以有有限个种类

    但是一只羊同时有且仅有一个状态

    每个状态之间有精确的进入和退出条件

    我们把羊的状态分为如下三个种类:陌生,逃跑,友好

我们现在先根据之前我们想要通过if-else达到的目的效果来想想各个状态是什么意思:

    陌生:
        对玩家存在警觉,但不是逃跑
    逃跑:
        字面意义,即处于逃跑状态
    友好:
        对玩家毫无防备
    

接下来我们根据各个状态间的关系添加进入/退出条件即可
(注意,这些是进入/退出条件,也就是说状态是可以持续的,不改变的)

    每个tick中,羊仅处于一种状态中,

    只有当状态为逃跑时,羊才会调用逃跑方法

以下为具体代码实现:

1
2
3
4
5
public enum State {
DEFAULT_EMPTY,
FLEEING,
FRIENDLY
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch (mobState.currentState){
case DEFAULT_EMPTY:
if(FOVcheck(animal, player) && player.isHolding(Items.WHEAT) && distance <= 8.2){
mobState.currentState = FRIENDLY;
}
else if(distance <= 8 && FOVcheck(animal, player)){
mobState.currentState = FLEEING;
}
break;

case FRIENDLY:
if(animal.getAttacker() == player){
mobState.currentState = FLEEING;
}
break;

case FLEEING:
if(distance >= 20){
mobState.currentState = DEFAULT_EMPTY;
}
break;
}

我们当然也可以用if-else去做到这件事,
但是比起用if-else系统,状态机的可读性和可维护性会好很多。

实际上上述提到的这个羊的行为逻辑只是较规模小的一点实现,一旦决策系统更加庞大之后,状态机的优势就会更好地体现出来

原因在于:

- 我们每时每刻只处于一个状态中

- 我们只要管好局部每个状态时的进入和退出条件即可,可以忽略其他状态之间的交互细节,极大程度上降低了耦合度

杂谈:

  • 上述状态机的具体实现只是其中的一种简单形式, 在Java中即是使用enum配合switch来构成状态机,对于更重型的状态机,有更严谨和标准的方式通过接口来实现(一个真正意义上完整的状态机包含:状态,转换,和执行,其中执行可以在特定状态下也可以在转换时)

  • 状态机可以应付复杂的状态判定中,所以被大量运用在游戏ai逻辑处理中, 包括你可以在mc的反编译源码中找到它

  • 推荐一篇极好的状态机讲解 https://youtu.be/-ZP2Xm-mY4E?si=Qg5BXm2E3TQxlltM