状態管理を作る
ゲームを作る上で避けられないのがオブジェクトの状態管理です
レガシーな実装方法としては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);
}
}
これで状態管理の仕組みができました
次回はこれを使ってプレイヤーの状態を管理していこうと思います