LoginSignup
4
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-31

前回「ステート処理はナァラティブに」の続きになります。

はじめに

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

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

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

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

ステートのバッチ処理

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

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

登録関数【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で作成した管理クラスを以下に示します。

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

StateSequencer.cs
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#によるプログラム例となります。

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++)

StateSequencer.hpp
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++)

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

最後に

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

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

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

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

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

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

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

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

以上

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5