ステート関数でバッチ処理　～続・ステート処理はナァラティブに～

前回[「ステート処理はナァラティブに」](http://qiita.com/tohmas/items/4fde6c9370dac5f19e55)の続きになります。

#はじめに

私の信条は『プログラムを物語のように書く』です。

難読、または冗長になりやすいステート処理を記述する上で欠かせないのが『ステート関数』です。

『ステート関数』はゲームでのステート処理を簡潔に表現でき、複数のステートを一つのクラス内に纏めて記述でき、ステート間の共有データを簡単に保持できる便利な手法です。

今度はこの『ステート関数』をもう一工夫し、【バッチ処理】へ対応する手法を紹介します。

#ステートのバッチ処理

ステートのバッチ処理は、あらかじめ一連のステートをパラメータ付きで登録しておき、それを自動で一気に処理します。

※ただしステートは同時に複数を実行するのではなく、ステートの完了を待って次のステートを実行する【同期処理】とします。

### 登録関数【Command】

バッチ処理でのステート登録は、ステート名とパラメータが一組となり登録されます。

その登録関数がCommandです。

書式は次のようになります。

```
    Command(ステート名,[パラメータ[,...]]);
```

例えば、以下のような戦闘アクションの指定があるとします。

① カメラフォーカスを"マリー"に設定し、0.2秒でフォーカス完了
② 武器"UZI" で２回攻撃
③ 武器"手裏剣" で１回攻撃
④ ターン制御へ

Command関数を使い、以下のような記述で処理を表す事が出来ます。

```c#:
   Command(S_FOCUS,"MARRY",0.2);      //カメラフォーカスを"マリー"に設定し、0.2秒でフォーカス完了
   Command(S_ATTACK,"UZI",2);         //武器"UZI" で２回攻撃
   Command(S_ATTACK,"SHURIKEN",1);    //武器"手裏剣" で１回攻撃
   Command(S_TURN_CONTROL);           //ターン制御へ
```

上記の通り分かり易く表現でき、ステートがパラメータを扱うことで、汎用性の高いプログラミングが可能となります。

次にバッチ用のステート関数と管理クラスを説明します。

#『バッチ用ステート関数』

前回紹介した『ステート関数』をバッチ処理用に改造します。

### ステート処理時間を引数に

前回の『ステート関数』の『初回呼出しを知らせる'bool値'』が、『ステート時間'float値'』に変わります。

ゲームにおいてはタイミングを扱うことが多いため、『ステート時間（ステート処理の経過時間）』があると何かと便利なのです。

最初の更新時を０秒とし、その後は経過時間が設定されます。

### パラメータ引数の追加

登録時に設定されたパラメータを受けるための引数を追加します。

### ステート時間０が初期処理のタイミング

廃止されたbool値に代わり、ステート時間が０の時に初期化処理を行います。

### 終了通知の追加

ステートの終了通知関数【Done】を追加します。

バッチ処理では管理クラスが次のステートを決定します。よって『前ステート関数』の次ステート指定関数【Goto】は廃止します。

### バッチ用ステート関数

結果、以下のように表記します。

```
    void S_HOGE(float t,string p1, string p2) //例としてstring型パラメータを２個に限定
    {
        if (t == 0)
        {
            //初期処理
        }
        else
        {
            //更新処理

            if (終了？){
                Done(); //ステート完了通知
            }
        }
    }
```

#バッチ用にステート管理クラスを改造

管理クラスに『ステート登録と実行機能』を追加し、『バッチ用ステート関数』用の変更を行います。

### 『ステート登録と実行機能』の追加

ステートとパラメータを一組のアイテムとし、その登録キューが用意されます。

登録関数【Command】が、登録キューへの登録を行います。

更新関数【Update】は、実行中ステートがない場合に、登録キューから次アイテムを取り出します。


次アイテムのステートとパラメータが実行用として取り扱われます。

### 『バッチ用ステート関数』への対応

ステート処理（経過）時間(m_elapsed)がメンバ変数に追加します。
ステートの初回更新時に０、それ以降は更新時間の差分を加算した値に設定されます。

ステートの完了通知をするDone関数が用意されます。

### バッチ用管理クラス【ステートシーケンサー】の記述例 (Unity)

例としてUnityで作成した管理クラスを以下に示します。

ソース上ではバッチ用管理クラスを、都合上『ステートシーケンサー』と命名しています。

```c#:StateSequencer.cs
public class StateSequencer {

    public class Item {
        public Action<float, string, string> state; //例としてパラメータは２個
        public string p1;
        public string p2;
    }

    Queue<Item> m_queue; //保持用キュー
    float m_elapsed;     //ステート経過時間

    Item m_curState;     //現在実行中セット
    Item m_nextState;    //次実行のセット

    public StateSequencer()
    { 
        m_queue = new Queue<Item>();
        m_curState  = null;
        m_nextState = null;
    }

    //実行登録
    public void Command(Action<float,string,string> func,string p1=null, string p2=null)
    {
        var i = new Item();
        i.state = func;
        i.p1 = p1;
        i.p2 = p2;
        m_queue.Enqueue(i);
    }

    //更新処理　上位関数から更新時呼び出しを想定
    public void Update()
    {
        m_elapsed += Time.deltaTime;

        if (m_curState == null)
        {
            if (m_queue.Count != 0)
            {
                m_nextState = m_queue.Dequeue();
            }
        }
        if (m_nextState != null)
        {
            m_curState  = m_nextState;
            m_nextState = null;
            m_elapsed = 0;
        }
        if(m_curState != null)
        {
            m_curState.state(m_elapsed, m_curState.p1, m_curState.p2);
        }
    }

    //ステートの終了告知用
    public void Done()
    {
        m_curState = null;
    }
}

```


### 使用例 (Unity)

サンプルの概要は以下の通りです。

Start()にて、実行するステートを登録。
以下の４つステートをCommandで呼び出す。

S_START : 引数なし、１秒待ち
S_WORK1 : 引数１個、引数表示、１秒待ち　…　引数を変え２回呼出し
S_WORK2 : 引数２個、引数表示、２番目引数の指定時間待ち　…　引数を変え２回呼出し
S_END   : 引数なし、引数表示、１秒待ち

Update()にてシーケンサーの更新関数(Update)を呼び出します。

以下、Ｃ＃によるプログラム例となります。

```c#:Hoge.cs
public class Hoge : MonoBehaviour {

    StateSequencer m_ss;

    void Start()
    {
        m_ss = new StateSequencer();

        // 処理の登録
        m_ss.Command(S_START);

        m_ss.Command(S_WORK1, "1st");
        m_ss.Command(S_WORK1, "2nd");
        m_ss.Command(S_WORK2, "3rd","0.5");
        m_ss.Command(S_WORK2, "4th", "0.2");

        m_ss.Command(S_END);

    }

    void Update()
    {
        m_ss.Update();
    }

    // 以下、ステート関数

    void S_START(float t,string p1, string p2)
    {
        if (t == 0)
        {
            Debug.Log("S_START Initailize");
        }
        else if (t > 1)
        {
            m_ss.Done();
        }
    }

    void S_WORK1(float t, string p1, string p2)
    {
        if (t == 0)
        {
            Debug.Log("S_WORK1 Initialize :" );
            Debug.Log(">PARAM=" + p1);
        }
        else if (t > 1)
        {
            m_ss.Done();
        }
    }

    void S_WORK2(float t, string p1, string p2)
    {
        string s = p1;
        float wait = float.Parse(p2);

        if (t == 0)
        {
            Debug.Log("S_WORK2 Initialize :");
            Debug.Log(">PARAM=" + s + "," + wait);
        }
        else if (t > wait)
        {
            m_ss.Done();
        }
    }

    void S_END(float t, string p1, string p2)
    {
        if (t == 0)
        {
            Debug.Log("S_END Initailize");
        }
        else if (t > 1)
        {
            m_ss.Done();
        }
    }
}

```

# C++の場合

C++の例を示します。

### ステートシーケンサー (C++)


```cpp:StateSequencer.hpp
template<typename  T> class StateSequencer
{
public:
	typedef void (T::*STATE_T)(float t, char* p1, char *p2);//例としてchar*型パラメータ２個
	struct Item { 
		STATE_T state; 
		char *p1; 
		char *p2; 
	};

private:
	T *m_base;

	queue<Item*>    m_queue;
	float          m_elapsed;

	Item           *m_curState;
	Item           *m_nextState;

public:
	StateSequencer(T *base)
		:
		m_base(base),
		m_queue(),
		m_elapsed(0),
		m_curState(NULL),
		m_nextState(NULL) {}
        
        // ステート登録
	void Command(STATE_T state, char *p1 = NULL, char *p2 = NULL)
	{
		Item* i = new Item();
		i->state = state;
		i->p1    = p1;
		i->p2    = p2;
		m_queue.push(i);
	}
        //更新関数　上位関数から呼び出される事を想定
	void Update()
	{
		m_elapsed += 0.016f; // 1/60 sec

		if (m_curState == NULL)
		{
			if (!m_queue.empty())
			{
				m_nextState = m_queue.front();
				m_queue.pop();
			}
			if (m_nextState != NULL)
			{
				m_curState = m_nextState;
				m_nextState = NULL;
				m_elapsed = 0;
			}
		}

		if (m_curState != NULL)
		{
			(m_base->*(m_curState->state))(m_elapsed, m_curState->p1, m_curState->p2);
		}
	}

	void Done()
	{
		if (m_curState != NULL)
		{
			free(m_curState);
			m_curState = NULL;
		}
	}
};
```

###使用例 (C++)

```cpp:Hoge.cpp
#include <stdio.h>
#include <stdlib.h>
#include <queue>
#include <Windows.h>

using namespace std;

#include "StateSequencer.h"


class HOGE
{
	StateSequencer<HOGE> m_ss;
#define STATE(X)    (&HOGE::X) 

public:

	HOGE() : m_ss(this) {}

	void Start()
	{
                // 処理の登録

		m_ss.Command(STATE(S_START));

		m_ss.Command(STATE(S_WORK1), "1st");
		m_ss.Command(STATE(S_WORK1), "2nd");
		m_ss.Command(STATE(S_WORK2), "3rd", "0.5");
		m_ss.Command(STATE(S_WORK2), "4th", "0.2");

		m_ss.Command(STATE(S_END));
	}
	void Update()
	{
		m_ss.Update();
	}

private:

        // 以下、ステート関数

	void S_START(float t, char* p1, char *p2)
	{
		if (t == 0)
		{
			printf("S_START Initialize\n");
		}
		else
		{
			if (t>1) m_ss.Done();
		}
	}
	void S_WORK1(float t, char *p1, char *p2)
	{
		if (t == 0) {
			printf("S_WORK Initialize\n");
			printf(">PARAM=%s\n", p1);
		}
		else
		{
			if (t > 1) m_ss.Done();
		}
	}
	void S_WORK2(float t, char *p1, char *p2)
	{
		char *s = p1;
		double wait = atof(p2);

		if (t == 0) {
			printf("S_WORK2 Initialize\n");
			printf(">PARAM=%s,%f\n", s, wait);
		}
		else
		{
			if (t > wait) m_ss.Done();
		}
	}
	void S_END(float t, char *p1, char *p2)
	{
		if (t == 0) {
			printf("S_END Initialize\n");
		}
		else
		{
			if (t > 1) m_ss.Done();
		}
	}
};

int main()
{
	HOGE task;
	task.Start();

	while (true)
	{
		Sleep(16);
		task.Update();
	}

	return 0;
}

```



#最後に

### 『ステート関数のバッチ処理』について

バッチ処理は『ステート処理を部品化』し、『プログラム内にバッチとして記述』して、プログラム作業を捗らせる大変便利な手法です。

さらに外部テキストファイルからの登録を可能にすれば、スクリプトエンジンとしての応用も可能です。

スクリプト化は、プログラマ以外のチームメンバとのデータ連携を簡単にすることが出来ます。

### 『ステートコントロール』と『ステートシーケンサー』

前回の記事で紹介した手法を（管理クラス名から）『ステートコントロール』、今回を『ステートシーケンサー』と呼んでいます。

紹介した２つの手法を使うことで、大概のゲームのステート処理を実装することが出来ます。

よろしければ、どうぞご自由に活用ください。

以上