6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

状態管理を作る

ゲームを作る上で避けられないのがオブジェクトの状態管理です
レガシーな実装方法としては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);
	}
}

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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?