はじめに
一般的なアプリケーションには、少なからず、何らかの状態変化を管理するコードが含まれていると思います。
それが、管理する状態が少なかったり簡単である場合は、 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 |
状態管理を実装する
これをコードで表現すると、以下のようになります。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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にどんな状態があり、どの状態に遷移できるかを設定します。
1 2 3 4 5 6 7 8 9 10 11 | 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()
メソッドで取得することができます。
1 | fmt.Println(fsm.Current()) // standing |
Event()
メソッドを使って状態を変化させることができます。
1 2 3 4 | err = fsm.Event(context.Background(), run) // walking -> running if err != nil { fmt.Println(err) } |
ただし、JumpingからRunningのような許可されていない状態遷移を行うと、エラーが発生します。
1 2 3 4 | 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の状態管理を行います。
サンプルコードは以下のとおりです。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 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!") } |
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 33 34 35 36 37 38 39 | 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を持つようにしました。
1 2 3 4 5 6 7 8 9 | type Character struct { Name string HP int Atk int Def int Spd int fsm *fsm.FSM } |
次に、
New()
関数でCharacterを生成するようにして、FSMのコールバックを登録しました。
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 | 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構造体の状態遷移は外部から行えないように隠蔽しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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の状態遷移図を様々なフォーマットで出力することができます。
1 | fmt.Println(fsm.VisualizeWithType(c.fsm, fsm.GRAPHVIZ)) |
上記の様にフォーマットとして
graphviz
を指定すると、このような出力がされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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の真価が発揮されるのではないでしょうか。