見出し画像

ひとつの王道、関数ポインタによる状態遷移

状態遷移には名前の通り「状態」と「遷移」があります。通常は何らかの状態を維持していますが、ある条件が揃うと別の状態にぽんっと遷移します。遷移はゼロタイムなので、実は状態遷移のほぼすべては「状態の維持」に費やされます。例えばタイトル画面はプレイヤーがボタンを押すなどしない限りはずーっとタイトル画面です。そんなタイトル画面の維持は中々に大変。背景を動かしたり、ボタンをぴょんぴょんさせたり、プレイヤーのボタン押し下げを監視したりとやる事は沢山!状態とは同じ時刻を共有する様々な振る舞いの塊なんです。ですからそういう振る舞いは関数にまとめる事になります。

C言語の関数ポインタ

タイトルの振る舞いはupdateTitle関数に、ゲーム中の振る舞いはupdateGame関数に、など各状態単位で振る舞いを関数化していく時、多くの場合関数名は違うけど関数の型はどれも一緒に出来ます:

void updateLogo1( int param );    // ロゴ1表示
void updateLogo2( int param );    // ロゴ2表示
void updateTitle( int param );    // タイトル表示
void updateGame( int param );     // ゲーム
...

引数のparamは「環境変数」と呼ばれるグローバル的な値で、別に構造体でも何でも構いません。無くても良いですが、大概必要になります。ここで大切なのは環境変数じゃなくて「これらの関数はみんな同じ型」という所です。

C言語は引数型と戻り値の型が同じ関数は同一の関数型であると認識してくれます。int型とかchar型同様に、関数もC言語的には型なんです。ただしその型はポインタです。ポインタ苦手な方はうーとなるかもしれませんが、上の関数は次のように同じ型の関数ポインタに代入できます:

void (*func)( int param ) = 0;
func = &updateTitle;

関数ポインタ…僕実は苦手です。1行目の括弧に埋もれているfuncが変数の名前で「void *(int param)」という関数ポインタ型になっています。不思議な宣言の仕方ですが、仕様なのでしゃーない。ちなみに、これどうしてfuncが括弧でくくられているかというと、括弧をはずすと全く別の意味になっちゃうからです:

void (*func)( int param );   // void *( int param )型の関数ポインタ
void *func( int param );   // void*を返す関数の宣言

関数ポインタ苦手な方も何度か書いていると慣れてきますので、サンプル作って遊んでみて下さい。

関数ポインタ変数funcにupdateTitle関数のアドレスを代入すると、次のような呼び出しでupdateTitle関数をコールできてしまいます:

func( 10 );   // updateTitle(10)とコールしたのと一緒

もしfuncに別の関数ポインタを代入すれば同じ呼び出しで別の関数がコールされます。この辺ではは~んと思って来たんじゃないでしょうか。そう、この入れ替えを利用して状態遷移を実現するんです。新しい状態遷移の武器「関数ポインタによる状態遷移」です^^

state関数ポインタへ入れ替え差し替え&ガンガン更新

では関数ポインタを用いた状態遷移のプログラム例を挙げます:

void (*state)( int ) = 0;   // 状態遷移用の関数ポインタ

void updateLogo1( int param ) {
    ... // ロゴの表示等行う
    if ( finish == true )
        state = &updateLogo2;
}

int main() {
    int param_g = 0;
    state = &updateLogo1;
    while( true ) {
        state( param_g );
    }
}

グローバル変数としてstate関数ポインタを宣言し、一番最初の遷移であるロゴ1表示(updateLogo1)をセットしておきます。メインでstateを毎フレームガンガン更新すると、そこに代入されているupdateLogo1関数が呼ばれ続けます。そのロゴ1の表示が終わった瞬間次のロゴ2の状態関数(updateLogo2)をstate変数に代入すると、そこでupdateLogo1の更新は終わり、その次のフレームからupdateLogo2関数が呼び出されるようになります。
後はこれの連続。updateLogo2関数で2つ目のロゴ表示が終わったらstateにupdateTitle関数を代入すれば、今度はタイトルへ勝手に遷移します。各状態関数の中でstateの中身を差し替えるだけで勝手に状態遷移が起こるわけです!これ素敵ですよね(^-^)/

この関数ポインタを用いた状態遷移はswitch~case文などとは一線を画す画期的な方法で、一つの王道と言えます。if文やswitch~case文で必要だった状態を表す列挙型を用意する必要もありません。この利便性の高さから様々な局面で広く利用されてきました。

関数ポインタ変数が溢れ出る!

「よっしゃ、これガンガン使うぜ」と思った方、ちょっとお待ちを。ロゴやタイトルなど単独な状態遷移はstate変数一つで表現できました。しかしゲーム中となると主人公やエネミー、背景やパーティクルなど登場物が盛り沢山です。そしてそれぞれが独自に状態を遷移させたがっています。これはその状態遷移の分だけstateのような関数ポインタ変数が必要で、その関数ポインタすべてを更新し続けなければならない事を意味しています。またゲーム中の登場物は神出鬼没です。主人公が必殺技を出した時に出るパーティクルやオブジェクト、特定の時間が来た時に現れるエネミーなど、生成タイミングや数は状況によって異なるため、それ専用の関数ポインタ変数をあらかじめ用意しておくことは難しいんです。さらに、エネミーが死んでメモリから消えたら、そのエネミーに紐づいていたstate関数ポインタは決して更新してはいけません。この多量な関数ポインタの更新と生死をどう制御したら良いのか?そこを良く考えないとこの武器はかえって混乱を招いてしまいます。新しい武器は強力ですが諸刃でじゃじゃ馬なんです。

この問題の一つの解決策が「タスクシステム」です。そこで次回はタスクシステムを利用した大量オブジェクトの状態遷移について取り上げます。C言語ベースでタスクシステムを実現します。「ん~もうオブジェクト指向でぇ~(>_<)」とやきもきしているC++やC#などに慣れている皆さん。済みませんがもう少しお待ち下さい(^-^;

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