オープンフィールドなRPG風ゲームを作る 4ワールド目

# 状態管理を作る

ゲームを作る上で避けられないのがオブジェクトの状態管理です
レガシーな実装方法としてはenum等で状態を列挙してswitch caseで分岐する等があります
が、C#はオブジェクト指向言語なので、きちんとオブジェクト指向な実装をしようと思います


# 有限状態機械 (finite state machine)

詳しいことはネットで調べてもらうとして
状態管理の概念としてはこれを使います

1つの状態を表すクラス(State)と状態のまとまりを管理するクラス(System)で構成します
コードはこんな感じ

```IFSMState.cs

//!	@brief	ステートインターフェイス
public interface IFSMState
{
	string	name	{ get; }		//!< ステート名

	//!	@brief	開始処理
	//!	@return	なし
	void entry();

	//!	@brief	実行処理
	//!	@param	[in]	fsm		ステートマシン
	//!	@return	なし
	void exec(FSMSystem fsm);

	//!	@brief	終了処理
	//!	@return	なし
	void exit();
}
```

```FSMSystem.cs
using UnityEngine;
using System.Collections.Generic;

//!	@brief	ステートマシン
public class FSMSystem
{
	private const int	INVALID_STATE_INDEX		= -1;	//!< 無効なステートインデックス

	//!	@brief	初期化
	//!	@param	[in]	states	ステート
	//!	@return	なし
	public void initialize(IFSMState[] states)
	{
		clear();

		if( states == null 
			|| states.Length <= 0
			) return;

		_states = states;
		for( var i = 0; i < _states.Length; ++i ) {
			var state = _states[i];
			if( state == null ) continue;

			var stateName = state.name;
			if( _stateIndexTable.ContainsKey(stateName) ) {
				Debug.LogWarning(
					"State \"" + stateName + "\" is duplicate !!!!!"
					+ "\nFailed initialize FSMSystem ..."
					);
				clear();
				return;
			}

			_stateIndexTable.Add(stateName, i);
		}

		_curStateIndex	= 0;
		_nextStateIndex	= 0;
		_isEntry		= false;
	}

	//!	@brief	クリア
	//!	@return	なし
	public void clear()
	{
		_stateIndexTable.Clear();
		_states = null;

		_curStateIndex	= INVALID_STATE_INDEX;
		_nextStateIndex	= INVALID_STATE_INDEX;
	}

	//!	@brief	実行
	//!	@return	なし
	public void execute()
	{
		var state = getState(_curStateIndex);
		if( _curStateIndex != _nextStateIndex ) {
			if( state != null ) {
				state.exit();
			}

			_curStateIndex = _nextStateIndex;
			state = getState(_curStateIndex);
			_isEntry = false;
		}

		if( !_isEntry
			&& state != null
			) {
			state.entry();
			_isEntry = true;
		}

		if( state != null ) {
			state.execute(this);
		}

		if( _curStateIndex != _nextStateIndex ) {
			if( state != null ) {
				state.exit();
			}
			_curStateIndex = _nextStateIndex;
			_isEntry = false;
		}
	}

	//!	@brief	次のステートの設定
	//!	@param	[in]	name	ステート名
	//!	@return	なし
	public void setNextState(string name)
	{
		if( _states == null ) return;

		var nextStateIndex = INVALID_STATE_INDEX;
		if( !_stateIndexTable.TryGetValue(name, out nextStateIndex) ) return;

		_nextStateIndex = nextStateIndex;
	}

	//!	@brief	次のステートへ
	//!	@return	なし
	public void next()
	{
		if( _states == null ) return;

		_nextStateIndex = Mathf.Min(_curStateIndex + 1, _states.Length);
	}

	//!	@brief	前のステートへ
	//!	@return	なし
	public void prev()
	{
		if( _states == null ) return;

		_nextStateIndex = Mathf.Max(_curStateIndex - 1, 0);
	}

	//!	@brief	ステートの取得
	//!	@param	[in]	stateIndex		ステートインデックス
	//!	@return	ステート
	private IFSMState getState(int stateIndex)
	{
		if( _states == null
			|| stateIndex < 0
			|| stateIndex >= _states.Length
			) return null;

		return _states[stateIndex];
	}

	private Dictionary<string, int>		_stateIndexTable	= new Dictionary<string, int>();		//!< ステートからステートインデックスへのテーブル
	private IFSMState[]					_states				= null;									//!< ステート

	private int							_curStateIndex		= INVALID_STATE_INDEX;					//!< 現在のステートインデックス
	private int							_nextStateIndex		= INVALID_STATE_INDEX;					//!< 次のステートインデックス
	private bool						_isEntry			= false;								//!< 現在のステートを開始したか
}
```

せっかくなのでテストコードも書いておきます

```FSMTest.cs
using UnityEngine;
using NUnit.Framework;
using System.Text;

//!	@brief	FSMテスト
public class FSMTest
{
	//!	@brief	テストステート
	private class FSMTestState : IFSMState
	{
		public string	name	{ get; }		//!< ステート名

		//!	@brief	コンストラクタ
		//!	@param	[in]	name	ステート名
		//!	@param	[in]	log		ログ
		public FSMTestState(string name, StringBuilder log)
		{
			this.name	= name;
			_log		= log;
		}

		//!	@brief	開始処理
		//!	@return	なし
		public void entry()
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".entry");
		}

		//!	@brief	実行処理
		//!	@param	[in]	fsm		ステートマシン
		//!	@return	なし
		public void execute(FSMSystem fsm)
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".exec");
		}

		//!	@brief	終了処理
		//!	@return	なし
		public void exit()
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".exit");
		}

		private readonly StringBuilder		_log		= null;		//!< ログ
	}

	//!	@brief	テストステート
	private class FSMTestStateToNext : IFSMState
	{
		public string	name	{ get; }		//!< ステート名

		//!	@brief	コンストラクタ
		//!	@param	[in]	name	ステート名
		//!	@param	[in]	log		ログ
		public FSMTestStateToNext(string name, StringBuilder log)
		{
			this.name	= name;
			_log		= log;
		}

		//!	@brief	開始処理
		//!	@return	なし
		public void entry()
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".entry");
		}

		//!	@brief	実行処理
		//!	@param	[in]	fsm		ステートマシン
		//!	@return	なし
		public void execute(FSMSystem fsm)
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".exec");
			fsm.next();
		}

		//!	@brief	終了処理
		//!	@return	なし
		public void exit()
		{
			if( _log == null ) return;

			_log.AppendLine(name + ".exit");
		}

		private readonly StringBuilder		_log		= null;		//!< ログ
	}

	//!	@brief	ステート
	private static class State
	{
		public static string	Test0	{ get { return "test0"; } }		//!< テストステート0
		public static string	Test1	{ get { return "test1"; } }		//!< テストステート1
		public static string	Test2	{ get { return "test2"; } }		//!< テストステート2
	}

	//!	@brief	FSMシステムテスト
	//!	@return	なし
	[Test] public void FSMSystemTest()
	{
		var log = new StringBuilder();
		var fsm = new FSMSystem();

		fsm.initialize(new IFSMState[] {
			new FSMTestState(State.Test0, log),
			new FSMTestStateToNext(State.Test1, log),
			new FSMTestState(State.Test2, log),
		});

		fsm.execute();

		log.AppendLine("setNextState(" + State.Test0 + ")");
		fsm.setNextState(State.Test0);
		fsm.execute();

		log.AppendLine("setNextState(" + State.Test1 + ")");
		fsm.setNextState(State.Test1);
		fsm.execute();

		fsm.execute();

		var testResult = new StringBuilder()
			.AppendLine(State.Test0 + ".entry")
			.AppendLine(State.Test0 + ".exec")
			.AppendLine("setNextState(" + State.Test0 + ")")
			.AppendLine(State.Test0 + ".exec")
			.AppendLine("setNextState(" + State.Test1 + ")")
			.AppendLine(State.Test0 + ".exit")
			.AppendLine(State.Test1 + ".entry")
			.AppendLine(State.Test1 + ".exec")
			.AppendLine(State.Test1 + ".exit")
			.AppendLine(State.Test2 + ".entry")
			.AppendLine(State.Test2 + ".exec")
			;

		Assert.AreEqual(testResult.ToString(), log.ToString());
	}

	//!	@brief	FSMシステム初期化失敗テスト
	//!	@return	なし
	[Test] public void FSMSysmteInitializeFailedTest()
	{
		var log = string.Empty;
		var logMsgRecieved = new Application.LogCallback((condition, stackTrace, type) => {
			log = condition;
		});

		Application.logMessageReceived += logMsgRecieved;

		var fsm = new FSMSystem();
		fsm.initialize(new IFSMState[] {
			new FSMTestState(State.Test0, null),
			new FSMTestState(State.Test0, null),
		});

		Application.logMessageReceived -= logMsgRecieved;

		var testLog = 
			"State \"" + State.Test0 + "\" is duplicate !!!!!"
			+ "\nFailed initialize FSMSystem ..."
			;
		Assert.AreEqual(testLog, log);
	}
}
```

これで状態管理の仕組みができました
次回はこれを使ってプレイヤーの状態を管理していこうと思います