簡単なステートマシンを作ってみた

# はじめに

これはArbor3とうまく自分の作成中のゲームを組み合わせられず, でもとりあえず簡単なステートマシンだけ欲しかった人が簡易的にステートマシン(有限オートマトン)を実装してみたので, それの記事です。

以下がそのコードです。

```csharp:StateMachine.cs
using System;
using System.Collections.Generic;
using UniRx;

namespace Assets.Scripts.StateMachine
{
    public abstract class State<T> where T: struct{


        protected Dictionary<T, State<T>> NextState = new Dictionary<T, State<T>>();

        public void Connect(T message, State<T> state)
        {
            NextState[message] = state;
        }

        public readonly string Name;

        /// <summary>
        /// このStateに入ったときに発行されます。イベントの値は前のStateです
        /// </summary>
        /// <returns></returns>
        public abstract IObservable<State<T>> OnEnterAsObservable();

        /// <summary>
        /// このStateにから出る時に発行されます。イベントの値は次のStateです
        /// </summary>
        /// <returns></returns>
        public abstract IObservable<State<T>> OnExitAsObservable();

        protected State(string name)
        {
            Name = name;
        }

        public override string ToString()
        {
            return $"{Name} State({typeof(T)})";
        }
    }
    
    public class StateMachine<T> where T : struct
    {
        private class InnerState : State<T>
        {


            public Subject<State<T>> OnEnter;
            public Subject<State<T>> OnExit;

            public override IObservable<State<T>> OnEnterAsObservable() => OnEnter = OnEnter ?? new Subject<State<T>>();

            public override IObservable<State<T>> OnExitAsObservable() => OnExit = OnExit ?? new Subject<State<T>>();

            public InnerState(string name) : base(name)
            {
            }

            public InnerState SendMessage(T message)
            {
                return (InnerState)NextState[message];
            }
        }

        public readonly State<T> Entry;

        private InnerState current;
        public State<T> CurrentState => current;

        public string CurrentStateName => current?.Name;

        public StateMachine()
        {
            Entry = current = new InnerState(default);
        }

        /// <summary>
        /// メッセージを送出します
        /// </summary>
        /// <param name="message"></param>
        public void SendMessage(T message)
        {
            var next = current.SendMessage(message);
            var prev = current;

            current.OnExit?.OnNext(next);
            current = next;
            next.OnEnter?.OnNext(current);
        }

        private Dictionary<string, InnerState> states = new Dictionary<string, InnerState>();

        public State<T> Create(string name)
        {
            var c = new InnerState(name);

            states.Add(name, c); // 例外出すため

            return c;
        }

        public void Jump(string name)
        {
            var next = states[name];
            var prev = current;

            current.OnExit?.OnNext(next);
            current = next;
            next.OnEnter.OnNext(current);
        }

        public State<T> GetState(string name)
        {
            return states[name];
        }



    }
}
```

## 使い方

ステートマシンの定義

1. メッセージ用のenumを作成します
2. Createメソッドで状態を作成します
3. State.ConnectメソッドでそのStateの状態でメッセージが来た場合に遷移する状態を定義します。


```csharp:PlayerStateMachine.cs
using Assets.Scripts.StateMachine;

namespace Assets.Scripts.StateMachine
{
    public enum PlayerMessage
    {
        Start,
        TakeDamage,

    }

    class PlayerStateMachine : StateMachine<PlayerMessage>
    {
        public State<PlayerMessage> Alive { get; private set; } 
        public State<PlayerMessage> Dead { get; private set; }

        public PlayerStateMachine()
        {
            Alive = Create("Alive");
            Dead = Create("Dead");


            Entry.Connect(PlayerMessage.Start, Alive);

            Alive.Connect(PlayerMessage.TakeDamage, Dead);
        }
    }
}
```

ステートマシンの使用

1. ステートマシンは必ずEntryから開始されます
2. StateにはOnEnterAsObservableとOnExitAsObservableという状態になった時状態から遷移したときのIObservableを入れています
3. SendMessageメソッドでメッセージを送って遷移します

```csharp:UseStateMachine.cs

var playerState = new PlayerStateMachine();
playerState.Dead.OnEnterAsObservable().subscribe(_ =>{/*死んだ*/});

playerState.SendMessage(PlayerMessage.Start);

// playerがダメージをうけたら
playerState.SendMessage(PlayerMessage.TakeDamage);

```

## まとめ

というわけで簡単なステートマシン作ってみました。拡張はしやすいかも？？