在这篇文章中,我们将看看Mermaid.js如何帮助你将简单的标记转化为适合说明有限状态机、分层状态机或软件系统的标准复杂性的状态图。
什么是有限状态机(FSM)?
一两年前,我做了一个小游戏原型,其中有一个与螃蟹怪物的BOSS战,该怪物由有限状态机驱动。这个怪物等着玩家进入它的竞技场,然后从天花板上下来,发出挑战的吼声,并开始与玩家战斗。
该怪物只有在完成下降后才会受到伤害。受到足够的伤害会使怪物在再次攻击前做出痛苦的反应。伤害足够多的怪物会导致它死亡。
那么,什么是有限状态机(FSM)?
有限状态机是一组相互关联的状态,通过在不同的状态之间以可控的方式移动来对事件做出反应。
在这个例子中,老板可能处于的状态包括下降、攻击、对疼痛的反应和死亡。
这个老板的战斗可以用下面的美人鱼状态机来表示:
在这个状态机中,我们从最左边的黑圈开始,移动到下降状态,然后在各状态之间移动,直到我们到达死亡状态和图中右边缘的双圈。一旦我们到达最后一个圆圈,状态机就终止了,不再继续评估。
用Mermaid.js构建简单的有限状态机
你可以用Mermaid.js和markdown相当容易地建立一个类似上面的状态机。
使用Mermaid.js,你需要使用一个与Mermaid兼容的环境,如GitHub markdown、Polyglot Notebooks、在线实时编辑器或Obsidian。一旦进入该环境,你可以开始一个代码块,并指定编程语言为mermaid ,然后像下面这样输入markdown:
stateDiagram-v2
[*] --> Descending
Descending --> Attack
Attack --> Pain
Pain --> Attack
Attack --> Dead
Pain --> Dead
Dead --> [*]
进入全屏模式 退出全屏模式
这个markdown生成了以下Mermaid.js的有限状态机图:
在这里,我们通过指定
stateDiagram-v2 ,声明我们想要一个状态图。
接下来,我们通过写出状态的名称和它可以转换的状态来声明状态之间的各种转换。状态可以过渡到多个其他状态。例如,attack 状态可以过渡到pain 或dead 。
第一个状态用箭头左边的[*] 来表示,最后一个状态用箭头右边的[*] 来表示。
请注意,这个图和我们之前看到的图是一样的,只是它是从上到下排列的,而不是从左到右。如果你想生成一个从左到右的Mermaid.js有限状态机图,你可以在stateDiagram-v2 行之后添加一行direction LR 。
在Mermaid.js的有限状态机中突出显示关系
如果你想明确说明从一个状态转移到另一个状态的原因,你可以通过添加: ,然后在关系的右边添加额外的注释来为每个过渡添加可选描述,如以下标记所示:
stateDiagram-v2
[*] --> Descending : Player entered arena
Descending --> Attack : After roar animation
Attack --> Pain : Hurt a lot
Pain --> Attack : Finished animation
Attack --> Dying : Ran out of health
Pain --> Dying : Ran out of health
Dying --> [*] : After death animation
进入全屏模式 退出全屏模式
这些标签往往会产生更多的图,但额外的文字也可以增加有价值的信息。
用Mermaid.js构建层次化的有限状态机(HFSM)。
传统的有限状态机的一个问题是,当你向你的有限状态机添加新的状态时,你会发现状态之间的关系几乎呈组合式爆炸。
为了解决这个问题,你可以将状态机嵌套在其他状态机的内部,以创建一个类似的层次结构。
由于这些状态机的层次性,我们把这些嵌套的状态机称为层次化的有限状态机,简称HFSMs。
嵌套的有限状态机可以使不同的状态更容易管理,同时也使较大的转换更加明显,如下图所示:
在Mermaid.js中声明分层的有限状态机有点直接,尽管涉及的语法有点不同:
stateDiagram-v2
direction LR
state intro {
[*] --> Descending
Descending --> Roar
Roar --> [*]
}
state combat {
[*] --> Attacking
Attacking --> Pain
Pain --> Attacking
}
state defeated {
[*] --> Dying
Dying --> Dead
Dead --> [*]
}
[*] --> intro
intro --> combat
combat --> defeated
defeated --> [*]
进入全屏模式 退出全屏模式
在这里,我们声明了3个大的根级状态,分别名为intro、combat和defeated。
在每个状态中,我们列出了该大状态中的各种状态,以及它们之间的转换方式。
我们还在标记的底部列出这三个状态之间的关系。在这种情况下,这三个状态形成了一个序列,但状态之间的循环往往比这更频繁。
在上图中,每个状态都链接到序列中的下一个状态,但你也可以通过明确提及父状态来链接到父状态内部的状态,如下面的标记中所示:
stateDiagram-v2
direction LR
state intro {
Descending --> Roar
Roar --> Attacking
}
state combat {
Attacking --> Pain
Pain --> Attacking
}
state defeated {
Dying --> Dead
}
[*] --> Descending
combat --> Dying
Dead --> [*]
note left of combat: The boss is damageable in this state
进入全屏模式 退出全屏模式
在这里,各种状态显得明显更简单,因为我们更少地依靠
[*] 节点来交流状态的进入和退出,而更多地依靠状态之间的直接转换。
这个图仍然有一个从整个combat 状态到defeated 内的dying 状态的转换。这是为了表明,如果需要,combat 内的任何状态可以直接转换到dying 。
还要注意的是,你可以在任何状态的左边或右边声明一个note ,以注释需要特别注意的事情。
最后,在同一个图中,可以使用分层的有限状态机和消息进行状态间的转换,虽然结果会有点乱:
stateDiagram-v2
direction LR
state intro {
Descending --> Roar : Movement Finished
Roar --> Attacking : Animation Finished
}
state combat {
Attacking --> Pain : Took Enough Damage
Pain --> Attacking : Animation Finished
}
state defeated {
Dying --> Dead : Animation Finished
}
[*] --> Descending : Spotted player
combat --> Dying : Took enough damage
Dead --> [*] : AI Stopped
note left of combat: The boss is damageable in this state
进入全屏模式 退出全屏模式
最后的思考
我认为Mermaid.js的有限状态机图相当有趣,有助于表达系统或代理可能处于的状态。
Mermaid.js中的状态机图不仅仅可以模拟有限状态机和分层的有限状态机,我鼓励你阅读Mermaid.js的文档,了解诸如决策、分叉甚至并发等功能。
如果你喜欢这些图表的一些功能,但又想获得额外的灵活性,你可能想看看Mermaid.js的流程图,而不是。
至于我,我计划在下次设计人工智能代理或系统时,使用Mermaid.js的状态图,其状态和状态转换有足够的复杂性。