カテゴリー: BackEnd

FSMを使った状態管理をGoで実装する

はじめに

一般的なアプリケーションには、少なからず、何らかの状態変化を管理するコードが含まれていると思います。

それが、管理する状態が少なかったり簡単である場合は、 if や case を使って処理すれば良いですが、状態が多岐にわたり、状態遷移に制限があるような場合だと、途端に煩雑になってしまいます。

そのような状態管理をするには、FSM(finite state machine)を使うと、状態管理を簡潔明瞭に書くことができます。

今回は、GoでFSMを使った状態管理を実装してみます。

FSMとは

FSM(finite state machine)とは、有限状態機械、または、有限オートマトンなどと言われる、状態管理に用いられるモデルのことです。

このモデルは、有限個の状態(States)と遷移(Transitions)の動作を含み、状態の移行はイベントによって行われます。

FSMについてもっと詳しく知りたい方は、こちら等をご覧ください。

基本的な実装

FSMを実装するに当たり、looplab/fsm ライブラリを使用します。

実装する状態管理の概要

状態管理を実装する題材として、架空の横スクロールゲームを例に実装を行います。

このゲームには以下の状態があります。

  • Standing(静止中)
  • Walking(歩行中)
  • Running(走行中)
  • Jumping(ジャンプ中)
  • Attacking(攻撃中)

これらの状態は、それぞれ以下の状態に遷移させることができます。

to Standing to Walking to Running to Jumping to Attacking
from Standing NG OK OK OK OK
from Walking OK NG OK OK OK
from Running OK OK NG OK OK
from Jumping OK NG NG NG OK
from Attacking OK OK OK OK OK

状態管理を実装する

これをコードで表現すると、以下のようになります。

package main

import (
    "context"
    "fmt"

    "github.com/looplab/fsm"
)

// イベントの定義
const (
    stop   = "stand"
    walk   = "walk"
    run    = "run"
    jump   = "jump"
    attack = "attack"
)

// 状態の定義
const (
    standing  = "standing"
    walking   = "walking"
    running   = "running"
    jumping   = "jumping"
    attacking = "attacking"
)

func main() {
    // FSM定義
    fsm := fsm.NewFSM(
        standing,
        fsm.Events{
            {Name: stop, Src: []string{walking, running, jumping, attacking}, Dst: standing},
            {Name: walk, Src: []string{standing, running, attacking}, Dst: walking},
            {Name: run, Src: []string{standing, walking}, Dst: running},
            {Name: jump, Src: []string{standing, walking, running}, Dst: jumping},
            {Name: attack, Src: []string{standing, walking, running, jumping}, Dst: attacking},
        },
        fsm.Callbacks{},
    )

    fmt.Println(fsm.Current()) // standing

    err := fsm.Event(context.Background(), walk) // standing -> walking
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(fsm.Current()) // walking

    err = fsm.Event(context.Background(), run) // walking -> running
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(fsm.Current()) // running

    err = fsm.Event(context.Background(), jump) // running -> jumping
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(fsm.Current()) // jumping

    err = fsm.Event(context.Background(), run) // jumping -> running
    if err != nil {
        fmt.Println(err) // event running inappropriate in current state jumping
    }

    fmt.Println(fsm.Current()) // jumping
}

順番にコードを見ていきます。

まず初めに、FSMにどんな状態があり、どの状態に遷移できるかを設定します。

fsm := fsm.NewFSM(
    standing,
    fsm.Events{
        {Name: stop, Src: []string{walking, running, jumping, attacking}, Dst: standing},
        {Name: walk, Src: []string{standing, running, attacking}, Dst: walking},
        {Name: run, Src: []string{standing, walking}, Dst: running},
        {Name: jump, Src: []string{standing, walking, running}, Dst: jumping},
        {Name: attack, Src: []string{standing, walking, running, jumping}, Dst: attacking},
    },
    fsm.Callbacks{},
)

ここで設定するNameはイベント名、Srcはどの状態からの遷移を許可するか、Dstは遷移先の状態を表しています。

現在の状態は、Current()メソッドで取得することができます。

fmt.Println(fsm.Current()) // standing

Event()メソッドを使って状態を変化させることができます。

err = fsm.Event(context.Background(), run) // walking -> running
if err != nil {
    fmt.Println(err)
}

ただし、JumpingからRunningのような許可されていない状態遷移を行うと、エラーが発生します。

err = fsm.Event(context.Background(), run) // jumping -> running
if err != nil {
    fmt.Println(err) // event running inappropriate in current state jumping
}

基本的にはこれだけで状態管理を行うことができます。

次の項では、より実践的なFSMの実装を行っていきます。

structを定義した応用的な実装

先程の実装では、単に状態を遷移させただけで、状態管理を行う対象がありませんでした。

この項では、Character構造体を定義して、Characterの状態管理を行います。

サンプルコードは以下のとおりです。

package character

import (
    "context"
    "fmt"

    "github.com/looplab/fsm"
)

// イベントの定義
const (
    stop   = "stand"
    walk   = "walk"
    run    = "run"
    jump   = "jump"
    attack = "attack"
)

// 状態の定義
const (
    standing  = "standing"
    walking   = "walking"
    running   = "running"
    jumping   = "jumping"
    attacking = "attacking"
)

func New(name string, hp, atk, def, spd int) *Character {
    c := &Character{
        Name: name,
        HP:   hp,
        Atk:  atk,
        Def:  def,
        Spd:  spd,
    }

    c.fsm = fsm.NewFSM(
        standing,
        fsm.Events{
            {Name: stop, Src: []string{walking, running, jumping, attacking}, Dst: standing},
            {Name: walk, Src: []string{standing, running, attacking}, Dst: walking},
            {Name: run, Src: []string{standing, walking}, Dst: running},
            {Name: jump, Src: []string{standing, walking, running}, Dst: jumping},
            {Name: attack, Src: []string{standing, walking, running, jumping}, Dst: attacking},
        },
        fsm.Callbacks{
            "enter_attacking": func(ctx context.Context, e *fsm.Event) {
                c.enterAttacking(ctx, e)
            },
        },
    )

    fmt.Println(fsm.VisualizeWithType(c.fsm, fsm.GRAPHVIZ))

    return c
}

type Character struct {
    Name string
    HP   int
    Atk  int
    Def  int
    Spd  int

    fsm *fsm.FSM
}

func (c *Character) CurrentState() string {
    return c.fsm.Current()
}

func (c *Character) Stop() error {
    return c.fsm.Event(context.Background(), stop)
}

func (c *Character) Walk() error {
    return c.fsm.Event(context.Background(), walk)
}

func (c *Character) Run() error {
    return c.fsm.Event(context.Background(), run)
}

func (c *Character) Jump() error {
    return c.fsm.Event(context.Background(), jump)
}

func (c *Character) Attack() error {
    return c.fsm.Event(context.Background(), attack)
}

func (c *Character) enterAttacking(_ context.Context, e *fsm.Event) {
    // 攻撃時の処理
    fmt.Printf("event src: %s\n", e.Src)
    fmt.Printf("event dst: %s\n", e.Dst)
    fmt.Printf("event name: %s\n", e.Event)
    fmt.Println("attacking now!")
}
package main

import (
    "fmt"
    "fsm-sample/s2/character"
)

func main() {
    myCharacter := character.New("my character", 100, 10, 5, 20)
    fmt.Println(myCharacter.CurrentState()) // standing

    if err := myCharacter.Walk(); err != nil { // standing -> walking
        fmt.Println(err)
    }
    fmt.Println(myCharacter.CurrentState()) // walking

    if err := myCharacter.Run(); err != nil { // walking -> running
        fmt.Println(err)
    }
    fmt.Println(myCharacter.CurrentState()) // running

    if err := myCharacter.Jump(); err != nil { // running -> jumping
        fmt.Println(err)
    }
    fmt.Println(myCharacter.CurrentState()) // jumping

    if err := myCharacter.Run(); err != nil { // jumping -> running
        fmt.Println(err) // event running inappropriate in current state jumping
    }
    fmt.Println(myCharacter.CurrentState()) // jumping

    if err := myCharacter.Attack(); err != nil { // jumping -> attacking
        fmt.Println(err)
    }
    // event src: jumping
    // event dst: attacking
    // event name: attacking
    fmt.Println(myCharacter.CurrentState()) // attacking
}

先程のコードからの変更点としては、まず、Character構造体を定義し、非公開のフィールドとしてFSMを持つようにしました。

type Character struct {
    Name string
    HP   int
    Atk  int
    Def  int
    Spd  int

    fsm *fsm.FSM
}

次に、New()関数でCharacterを生成するようにして、FSMのコールバックを登録しました。

func New(name string, hp, atk, def, spd int) *Character {
    c := &Character{
        Name: name,
        HP:   hp,
        Atk:  atk,
        Def:  def,
        Spd:  spd,
    }

    c.fsm = fsm.NewFSM(
        standing,
        fsm.Events{
            {Name: stop, Src: []string{walking, running, jumping, attacking}, Dst: standing},
            {Name: walk, Src: []string{standing, running, attacking}, Dst: walking},
            {Name: run, Src: []string{standing, walking}, Dst: running},
            {Name: jump, Src: []string{standing, walking, running}, Dst: jumping},
            {Name: attack, Src: []string{standing, walking, running, jumping}, Dst: attacking},
        },
        fsm.Callbacks{
            "enter_attacking": func(ctx context.Context, e *fsm.Event) {
                c.enterAttacking(ctx, e)
            },
        },
    )

    return c
}

fsm.CallbaksのKeyにenter_attakingを指定していますが、これはprefix_eventの形式にする必要があります。

prefixとして指定できるものは以下のとおりです。

  • before
  • leave
  • enter
  • after

それぞれが実行されるタイミングに該当しています。

Character構造体の状態遷移は外部から行えないように隠蔽しています。

func (c *Character) Stop() error {
    return c.fsm.Event(context.Background(), stop)
}

func (c *Character) Walk() error {
    return c.fsm.Event(context.Background(), walk)
}

func (c *Character) Run() error {
    return c.fsm.Event(context.Background(), run)
}

func (c *Character) Jump() error {
    return c.fsm.Event(context.Background(), jump)
}

func (c *Character) Attack() error {
    return c.fsm.Event(context.Background(), attack)
}

大体は、この様にFSMを内部に隠蔽する形になるのではないかと思います。

FSMの可視化

設定したFSMの状態遷移図を様々なフォーマットで出力することができます。

fmt.Println(fsm.VisualizeWithType(c.fsm, fsm.GRAPHVIZ))

上記の様にフォーマットとしてgraphvizを指定すると、このような出力がされます。

digraph fsm {
    "attacking" -> "standing" [ label = "stand" ];
    "attacking" -> "walking" [ label = "walk" ];
    "jumping" -> "attacking" [ label = "attack" ];
    "jumping" -> "standing" [ label = "stand" ];
    "running" -> "attacking" [ label = "attack" ];
    "running" -> "jumping" [ label = "jump" ];
    "running" -> "standing" [ label = "stand" ];
    "running" -> "walking" [ label = "walk" ];
    "standing" -> "attacking" [ label = "attack" ];
    "standing" -> "jumping" [ label = "jump" ];
    "standing" -> "running" [ label = "run" ];
    "standing" -> "walking" [ label = "walk" ];
    "walking" -> "attacking" [ label = "attack" ];
    "walking" -> "jumping" [ label = "jump" ];
    "walking" -> "running" [ label = "run" ];
    "walking" -> "standing" [ label = "stand" ];

    "attacking";
    "jumping";
    "running";
    "standing" [color = "red"];
    "walking";
}

これを http://www.webgraphviz.com/ で可視化したものがこちらです。

 

graphviz以外のフォーマットでも出力することができます。

さいごに

サンプルの状態管理はシンプルなものでしたが、実際のアプリケーションではより複雑な制御が求められることが多々あると思いますので、その際にFSMの真価が発揮されるのではないでしょうか。

おすすめ書籍

   

Hiroki Ono

シェア
執筆者:
Hiroki Ono
タグ: golang

最近の投稿