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

#はじめに

私が初めてコンピュータに触れたのは中学の頃でした。
そして、ゲーム業界に足を踏み入れてから早２Ｘ年。

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


##一般的なステート処理

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


<pre>

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

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

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

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;
}
</pre>

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

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

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


### 現場で好まれる理由

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

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

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

# ステート関数

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

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

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

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

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

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

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

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

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

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

#ステート管理クラス

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

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

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

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

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

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


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

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

<pre>
public class StateManager 
{
    Action&lt;bool&gt; m_curstate;
    Action&lt;bool&gt; m_nextstate;
    
    //リクエスト
    public void Goto(Action&lt;bool&gt; 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);
        }
    }
}
</pre>

### 使用例 (Unity)

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

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

ステートは最初に、Start()でのGoto()で、S_IDLEに設定されます。
S_IDLE内では、初期処理で現時刻＋２秒をm_limitに設定し、本処理で現時刻がm_limitに達するのを待ち、S_WORKへ移動します。そして、最後にS_ENDへ移動します。

<pre>
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)
    {
    }
}
</pre>

# C++の場合

C＋＋の例を示します。
言語の特性上、関数ポインタの扱いは厳密にしなくてはなりません。
そのため、ステート管理クラスはテンプレートとして提供されます。

###ステート管理クラス(C++)
<pre>
template&lt;typename  T&gt; 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);
        }
    }
};
</pre>

### 使用例 (C++)

利用する側は、ステート管理クラスに利用側のクラスをテンプレートの引数に設定します。
そしてステートの取り扱いを簡単にするため、STATEマクロを定義しています。

<pre>
class HOGE {

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

    StateManager&lt;HOGE&gt; 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;
}
</pre>

# 最後に

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

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

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


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

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

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

以上