はじめに
これはArbor3とうまく自分の作成中のゲームを組み合わせられず, でもとりあえず簡単なステートマシンだけ欲しかった人が簡易的にステートマシン(有限オートマトン)を実装してみたので, それの記事です。
以下がそのコードです。
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];
}
}
}
使い方
ステートマシンの定義
- メッセージ用のenumを作成します
- Createメソッドで状態を作成します
- State.ConnectメソッドでそのStateの状態でメッセージが来た場合に遷移する状態を定義します。
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);
}
}
}
ステートマシンの使用
- ステートマシンは必ずEntryから開始されます
- StateにはOnEnterAsObservableとOnExitAsObservableという状態になった時状態から遷移したときのIObservableを入れています
- SendMessageメソッドでメッセージを送って遷移します
UseStateMachine.cs
var playerState = new PlayerStateMachine();
playerState.Dead.OnEnterAsObservable().subscribe(_ =>{/*死んだ*/});
playerState.SendMessage(PlayerMessage.Start);
// playerがダメージをうけたら
playerState.SendMessage(PlayerMessage.TakeDamage);
まとめ
というわけで簡単なステートマシン作ってみました。拡張はしやすいかも??