前回「ステート処理はナァラティブに」の続きになります。
はじめに
私の信条は『プログラムを物語のように書く』です。
難読、または冗長になりやすいステート処理を記述する上で欠かせないのが『ステート関数』です。
『ステート関数』はゲームでのステート処理を簡潔に表現でき、複数のステートを一つのクラス内に纏めて記述でき、ステート間の共有データを簡単に保持できる便利な手法です。
今度はこの『ステート関数』をもう一工夫し、【バッチ処理】へ対応する手法を紹介します。
ステートのバッチ処理
ステートのバッチ処理は、あらかじめ一連のステートをパラメータ付きで登録しておき、それを自動で一気に処理します。
※ただしステートは同時に複数を実行するのではなく、ステートの完了を待って次のステートを実行する【同期処理】とします。
登録関数【Command】
バッチ処理でのステート登録は、ステート名とパラメータが一組となり登録されます。
その登録関数がCommandです。
書式は次のようになります。
Command(ステート名,[パラメータ[,...]]);
例えば、以下のような戦闘アクションの指定があるとします。
① カメラフォーカスを"マリー"に設定し、0.2秒でフォーカス完了
② 武器"UZI" で2回攻撃
③ 武器"手裏剣" で1回攻撃
④ ターン制御へ
Command関数を使い、以下のような記述で処理を表す事が出来ます。
Command(S_FOCUS,"MARRY",0.2); //カメラフォーカスを"マリー"に設定し、0.2秒でフォーカス完了
Command(S_ATTACK,"UZI",2); //武器"UZI" で2回攻撃
Command(S_ATTACK,"SHURIKEN",1); //武器"手裏剣" で1回攻撃
Command(S_TURN_CONTROL); //ターン制御へ
上記の通り分かり易く表現でき、ステートがパラメータを扱うことで、汎用性の高いプログラミングが可能となります。
次にバッチ用のステート関数と管理クラスを説明します。
『バッチ用ステート関数』
前回紹介した『ステート関数』をバッチ処理用に改造します。
ステート処理時間を引数に
前回の『ステート関数』の『初回呼出しを知らせる'bool値'』が、『ステート時間'float値'』に変わります。
ゲームにおいてはタイミングを扱うことが多いため、『ステート時間(ステート処理の経過時間)』があると何かと便利なのです。
最初の更新時を0秒とし、その後は経過時間が設定されます。
パラメータ引数の追加
登録時に設定されたパラメータを受けるための引数を追加します。
ステート時間0が初期処理のタイミング
廃止されたbool値に代わり、ステート時間が0の時に初期化処理を行います。
終了通知の追加
ステートの終了通知関数【Done】を追加します。
バッチ処理では管理クラスが次のステートを決定します。よって『前ステート関数』の次ステート指定関数【Goto】は廃止します。
バッチ用ステート関数
結果、以下のように表記します。
void S_HOGE(float t,string p1, string p2) //例としてstring型パラメータを2個に限定
{
if (t == 0)
{
//初期処理
}
else
{
//更新処理
if (終了?){
Done(); //ステート完了通知
}
}
}
バッチ用にステート管理クラスを改造
管理クラスに『ステート登録と実行機能』を追加し、『バッチ用ステート関数』用の変更を行います。
『ステート登録と実行機能』の追加
ステートとパラメータを一組のアイテムとし、その登録キューが用意されます。
登録関数【Command】が、登録キューへの登録を行います。
更新関数【Update】は、実行中ステートがない場合に、登録キューから次アイテムを取り出します。
次アイテムのステートとパラメータが実行用として取り扱われます。
『バッチ用ステート関数』への対応
ステート処理(経過)時間(m_elapsed)がメンバ変数に追加します。
ステートの初回更新時に0、それ以降は更新時間の差分を加算した値に設定されます。
ステートの完了通知をするDone関数が用意されます。
バッチ用管理クラス【ステートシーケンサー】の記述例 (Unity)
例としてUnityで作成した管理クラスを以下に示します。
ソース上ではバッチ用管理クラスを、都合上『ステートシーケンサー』と命名しています。
public class StateSequencer {
public class Item {
public Action<float, string, string> state; //例としてパラメータは2個
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()にて、実行するステートを登録。
以下の4つステートをCommandで呼び出す。
S_START : 引数なし、1秒待ち
S_WORK1 : 引数1個、引数表示、1秒待ち … 引数を変え2回呼出し
S_WORK2 : 引数2個、引数表示、2番目引数の指定時間待ち … 引数を変え2回呼出し
S_END : 引数なし、引数表示、1秒待ち
Update()にてシーケンサーの更新関数(Update)を呼び出します。
以下、C#によるプログラム例となります。
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++)
template<typename T> class StateSequencer
{
public:
typedef void (T::*STATE_T)(float t, char* p1, char *p2);//例としてchar*型パラメータ2個
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++)
#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;
}
最後に
『ステート関数のバッチ処理』について
バッチ処理は『ステート処理を部品化』し、『プログラム内にバッチとして記述』して、プログラム作業を捗らせる大変便利な手法です。
さらに外部テキストファイルからの登録を可能にすれば、スクリプトエンジンとしての応用も可能です。
スクリプト化は、プログラマ以外のチームメンバとのデータ連携を簡単にすることが出来ます。
『ステートコントロール』と『ステートシーケンサー』
前回の記事で紹介した手法を(管理クラス名から)『ステートコントロール』、今回を『ステートシーケンサー』と呼んでいます。
紹介した2つの手法を使うことで、大概のゲームのステート処理を実装することが出来ます。
よろしければ、どうぞご自由に活用ください。
以上