Edited at

ステート処理はナァラティブに

More than 1 year has passed since last update.


はじめに

私が初めてコンピュータに触れたのは中学の頃でした。

そして、ゲーム業界に足を踏み入れてから早2X年。

未だ、現役のゲームプログラマです。

長く活躍するために、私はプログラムを行う上で一つの流儀を持っています。

『プログラムは物語を語るように記述する』

連続した処理は、連続して記述する。

そうすることで理解しやすく、メンテナンスが簡単なプログラムを書くことができます。

ゲームの最たるは、『如何にステートを記述するか』です。

様々な都合ではなく、『人間の読み物としてプログラムする』

その手法の紹介をします。


ゲームプログラムはステートのカタマリ

『何フレームあとには何して』

『あれが完了したら何して』

…等というのが延々と続くのがゲームプログラム。

ゲームをプログラムするってことはステート処理を書き続けること。

そう、プログラマは書いて、読み直して、修正して…と

完成までずっとステート処理と付き合い続けます。


一般的なステート処理

自分の書き方を伝える前に、一般的なステート処理の紹介をします。

//ステートに名前を付け、enumで列挙する。

enum STATE
{
S_NONE,
S_START,
S_STEP1,
S_STEP2,
S_STEP3,
S_END
};

//現在のステートと次のステートを保持する2つの変数を用意する。

STATE m_curState = S_NONE;
STATE m_nextState = S_NONE;

//描画と同期したUpdate関数内にステート単位の処理を書く。

void Update()
{
if (m_curState!=m_nextState)
{
    m_curState = m_nextState;
switch(m_curState)
{
case S_START: (S_STARTの初期処理)    break;
case S_STEP1: (S_STTEP1の初期処理)   break;
case S_STEP2: (S_STTEP2の初期処理)   break;
case S_STEP3: (S_STTEP3の初期処理)   break;
case S_END: (S_ENDの初期処理)    break;
}
}
else
{
switch(m_curState)
{
case S_START: (S_STARTの更新処理)    break;
case S_STEP1: (S_STTEP1の更新処理)   break;
case S_STEP2: (S_STTEP2の更新処理)   break;
case S_STEP3: (S_STTEP3の更新処理)   break;
case S_END: (S_ENDの更新処理)    break;
}
}
};

//スタート用の関数 Start()を書く
void Start()
{
m_nextState = S_START;
}

以上がよく事例でも登場する、『switch』を使ったステート処理の例です。

switchを使う場合、ステートの記述が処理によって分散し、解りにくいという欠点があります。

そのため、先述の『物語のように書く』ことはできません。


現場で好まれる理由

オブジェクト指向の教本には『switchを使うな』と書かれており、実際それ(switch)を嫌う方もいます(私もその一人です)。

しかし担当者が初級レベルである場合も多く、無理をした属人的手法がとられるより、本手法の方が現場において安心できる側面があります。

代替手法を提供するものは、このことを肝に銘じる必要があります(つまり初級者でも理解できるものが必要)。


ステート関数

私が使う手法の要は、ステートを1関数に見立てる方法です。

これを便宜上、『ステート関数』と勝手に呼んでいます。

以下のように記述します。


void S_HOGE(bool bFirst) // ステートHOGEの関数
{
if (bFirst)
{
//初期処理
}
     else
{
//更新処理
if (終了?)
{
//終了処理
Goto(次のステート);
}
}
}

ステート関数の引数はbool1個のみです。

ステート関数は、引数がtrueの時のみ初期処理を行い、falseの時は更新処理を行います。更新処理内で終了検知し、終了処理をします。

このように関数単位でステートとなれば、大変見やすくなります。

ステートが連続して動作する場合は、その関数をステートの順に羅列して書くことで大変解り易く、プログラミング作業を捗らせることができます。

仕様書のステート図を、1クラス内にそのままの形で記述する事が可能となります。

私自身、これがメンテナンス上の大きなアドバンテージとなりました。


ステート管理クラス

ステート関数の呼出しをするのがステート管理クラスです。

ステート管理クラスはステートのリクエスト提供と呼び出しを行います。

クラスが持つメンバ変数は現在用と次のステート用の二つで、ステートを関数ポインタ(デリゲート)として扱います。

関数も、リクエスト用とフレームワーク接続用のUpdate関数の二つです。

リクエスト用は単純で、リクエストがあったステート関数のポインタを次のステートに格納するだけです。

Update関数では、次のステートがあった場合は現在のステートと入れ替え、引数にtrueを入れてステートを呼び出します。

そうでない場合は引数にfalseを入れて呼び出します。


管理クラスの記述例 (Unity)

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


public class StateManager
{
Action<bool> m_curstate;
Action<bool> m_nextstate;

//リクエスト
public void Goto(Action<bool> func)
{
m_nextstate = func;
}

//更新
public void Update()
{
if (m_nextstate!=null)
{
m_curstate = m_nextstate;
m_nextstate = null;
m_curstate(true);
}
else
{
m_curstate(false);
}
}
}


使用例 (Unity)

Unityでの使用例は以下のようになります。

ステート管理クラスをメンバとして、Start()にて初期化し、Update()にてステート管理クラスのUpdateを呼び出します。

ステートは最初に、Start()でのGoto()で、S_IDLEに設定されます。

S_IDLE内では、初期処理で現時刻+2秒をm_limitに設定し、本処理で現時刻がm_limitに達するのを待ち、S_WORKへ移動します。そして、最後にS_ENDへ移動します。


public class Hoge : MonoBehaviour {

StateManager m_sm;

void Start()
{
m_sm = new StateManager();
m_sm.Goto(S_IDLE);
}

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

// ステート処理
float m_limit;
void S_IDLE(bool bFirst)
{
if (bFirst)
{
Debug.Log("S_IDLE Initialization");
m_limit = Time.time + 2;
}
else
{
if (m_limit < Time.time)
{
m_sm.Goto(S_WORK);
}
}
}

void S_WORK(bool bFirst)
{
if (bFirst)
{
Debug.Log("S_WORK Initialization");
}
else
{
Debug.Log("S_WORK Update");
m_sm.Goto(S_END);
}
}

void S_END(bool bFirst)
{
}
}


C++の場合

C++の例を示します。

言語の特性上、関数ポインタの扱いは厳密にしなくてはなりません。

そのため、ステート管理クラスはテンプレートとして提供されます。


ステート管理クラス(C++)


template<typename T> class StateManager
{
typedef void (T::*STATE_T)(bool isFirst);

T *m_base;

STATE_T m_curState;
STATE_T m_nextState;

public:
StateManager(T *base)
:
m_base(base),
m_curState(NULL),
m_nextState(NULL) {}

void Goto(STATE_T state)
{
m_nextState = state;
}

void Update()
{
if (m_nextState != NULL )
{
m_curState = m_nextState;
m_nextState = NULL;
(m_base->*m_curState)(true);
}
else
{
(m_base->*m_curState)(false);
}
}
};


使用例 (C++)

利用する側は、ステート管理クラスに利用側のクラスをテンプレートの引数に設定します。

そしてステートの取り扱いを簡単にするため、STATEマクロを定義しています。


class HOGE {

#define STATE(X) (&HOGE::X)

StateManager<HOGE> m_sm;

public:
HOGE() : m_sm(this) {}

void Start() // 上位関数が本クラス初期化後すぐに呼び出すことを想定
{
m_sm.Goto(STATE(S_IDLE));
}

void Update() // 上位関数が更新時に呼び出すことを想定
{
m_sm.Update();
}

private:
float m_limit;
void S_IDLE(bool bFirst)
{
if (bFirst) {
printf("Initialize IDLE\n");
m_limit = time() + 2;
}
else
{
if (m_limit < time())
{
m_sm.Goto(STATE(S_WORK));
}
}
}
void S_WORK(bool bFirst)
{
if (bFirst) {
printf("Initialize WORK\n");
}
else
{
m_sm.Goto(STATE(S_END));
}
}
void S_END(bool bFirst)
{
}
};
float time()
{
clock_t t = clock();
long it = (long)t;
return (float)it / 1000.0f;
}


最後に

一読すればお分かりかと思いますが、とても単純な方法です。

しかしながら、単純ゆえに必要に応じた変更が容易であり、色々なケースで使う事ができます。

単純だから移植も簡単です。

これによって私は『ステート処理をナァラティブ』に書けるようになりましたし、そして多くのバリエーションを生み出しました。

この手法で得られた『読み易さ』、『メンテナンス性』と『汎用性』の恩恵は計り知れません。

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

以上