見出し画像

std::function+ラムダ式でシーケンシャルな状態遷移を手軽に書きたい!

C++11からSTLにfunctionが追加されました。これは関数やクラスのメソッドなどを保持できるオブジェクトです。C#で言うSystem.ActionとかFuncと同じです。典型的な使い方一例はこちら:

#include <functional>

int add( int a, int b ) { return a + b; }

int main() {
    std::function< int( int , int ) > func = add;   // 関数を代入
    int sum = func( 100, 200 );   // addが呼ばれる
}

テンプレート引数の関数の型を記述すると、その型の関数を保持できるようになります。

このfunctionにはこちらもC++11で使えるようになったラムダ式も代入できます。ラムダ式というのは、ざっくり言えばコードの中に埋め込む事が出来る名前の無い関数(無名関数)です:

int main() {
    std::function< int( int , int ) > func =
              []( int a, int b ){ return a + b; };  // ラムダ式
    int sum = func( 100, 200 );
}

上の謎な[](...){...}というのがラムダ式です。(...)が引数定義で、{...}が実装部。[...]はキャプチャー式という部分で、実装部で使う外部変数を指定します。この詳細は別の機会で(^-^;。ラムダ式をfunctionに代入すると、その式を関数と同じ書式で呼び出す事が出来ます。これが恐ろしく便利なのですが、そのお話もどこかの機会で(^-^;;;

前置きが長くなりましたが、今回の新しい状態遷移の武器はこのfunctionとラムダ式を使って一過性で一直線な状態遷移を書く方法です。

こんな書き方を目指します!

例えばキャラクタに固定的で連続的な動きを付けたいなと思った時に、

void charaAction0001( Actor &actor ) {
    
    State< Actor > state( actor );
    state.next([]( int f, Actor &act ) {
        // 1. 毎フレーム右へ0.5ずつ進む
        act.addPos( 0.5f, 0.0f );
        return ( f == 10 );  // 10フレーム後次へ
    
    }).next( []( int f, Actor &act ) {
        // 2. 毎フレーム上へ0.5ずつ進む
        act.addPos( 0.0f, 0.5f );
        return ( f == 15 );  // 15フレーム後次へ
    
    }).next( []( int f, Actor &act ) {
        // 3. くるっと回って
        act.addTurn( 10.0f );
        return ( f == 72 );  // 72フレームで2回転
    
    }).next( []( int f, Actor &act ) {
        // 4. てーん!
        act.pause();
        return true;
    });

    addTask( state );   // 遷移開始
}

こんな書き方ができるといいんじゃないかなと。Stateテンプレートクラスのオブジェクトを一つ作り、対象となるオブジェクトを登録。nextメソッドに渡したラムダ式の中で状態を記述します。このnextメソッドを連結で書いていきます。一連の遷移を書き終えたStateオブジェクトをaddTask関数でシステムに登録すると、最初のnextに書いたラムダ式の状態が回り始めます。

変数fには呼ばれた回数がやってきます。こういう時間情報って大概必要です。第2引数はStateのコンストラクタで渡したオブジェクトです。ラムダ式がfalseを返した場合は状態維持で次回も自分が呼ばれます。一方trueを返すと遷移発生。次のnextメソッドに渡したラムダ式に移ります。で、最後まで行ったらおしまいっと。

どうでしょうか?Unityのコルーチンみたいですよね(^-^)

Stateクラスの中身は簡単

これを実現できるStateテンプレートクラスがこちら:

template< class OBJ >
class State {
public:
	State( OBJ &obj ) : cur_(), frame_(), obj_( obj ) {}
	State &next( const std::function< bool( int f, OBJ &ibj ) > &func ) {
		funcs_.push_back( func );
		return *this;
	}
	bool update() {
		if ( cur_ >= funcs_.size() )
			return false;
		frame_++;
		if ( funcs_[ cur_ ]( frame_, obj_ ) ) {
			cur_++;
			frame_ = 0;
		}
		return true;
	}

	std::vector< std::function< bool( int f, OBJ &ibj ) > > funcs_;
	int cur_;
	int frame_;
	OBJ &obj_;
};

nextメソッドは配列にfunctionオブジェクトを登録しているだけですが、ポイントは「*this」を返している所。こうする事で連続的にnextメソッドを定義していけます。updateメソッドはシステムが毎フレーム呼びます。中身は至極簡単で、現在のfunctionオブジェクトがtrueシグナルを返したら次のfunctionオブジェクトにスイッチしているだけ。cur_インデックスが最後まで到達したらupdateメソッドがfalseを返すので、システムはこのStateオブジェクトごと破棄します。

addTask関数の先で登録したStateオブジェクトを更新する所は、まぁ分かりますよね。登録されたオブジェクトを毎フレームクルクル回して生死判断するだけです。main級のトップレベルな位置で行うのが楽です。

メリットは宣言祭りの回避

このお手軽遷移、メリットは何と言っても「関数とかクラスをいちいち定義しなくて良い」点です。もし上と同じ状態遷移を関数やクラスで行うと、一つ一つ名前を決めて宣言と実装をして呼び出して…みたいなことになります。大きな状態遷移ではそれが大切だと思いますが、上のような極々ちいさくて寿命が極めて短い些末な状態遷移はとにかくテキパキと書きたいのです。Stateクラスはそれを可能にしてくれます。

工夫次第で色々便利になるStateクラス

上の実装は触りなのでnextメソッドしかありませんが、これは工夫次第でいかようにも便利に出来ます。例えば状態を途中で終わらせられるようにするとか、finishメソッドを設けて最後に必ずそのfunctionを呼び出すようにするとか。分岐はごっちゃになるのでお勧めしませんが、やろうと思えばできますし、nextメソッドに別のStateを登録できるようにしたっていいですよね。僕はC#でこの実装をして、実際使いまくってます(^-^)

という事で、functionとラムダ式を使ったお手軽な状態遷移のお話でした。

この記事が気に入ったらサポートをしてみませんか?