8-8 GameRunルーチン

ここからはゲームのメインルーチンについて説明します。
メインルーチンとは入力・判定・表示というように、ゲームの全ての動作を処理するルーチンとなります。

ここでは経過した時間からBMSカウント値を求め、これを使ってBGMチャンネルの再生を行ってみます。
なお、この処理はこれ以降に説明する判定処理などにも使われるため、確実に理解しておいてください。

■GameRunの戻り値

GameRunの戻り値についてここではint型を返す仕様としています。
そして現状GameRunの戻り値には以下の意味を持たせています。

-1 ESCなどでアプリ自体を終了させる場合に返す
0 ゲーム中であることを返す(次もGameRunを呼び出す)
1 曲が終了した場合に返す


■経過した時間から進んだBMSカウント値を算出

現在のBMSカウント値を算出するためには、まずはゲーム開始時を0秒とした経過時間が必要です。

前回の説明でGameInitの最後に現在の時間を開始時の時間としてllStartTimeに保存していましたが、
GameRunにて毎フレーム現在の時間を取得し、この開始時の時間を差し引くことで経過時間を算出することができます。

なお、ここでは高解像度カウンタを利用しているため取得した値は実際には時間ではなくクロック数となっています。
そのためこれを時間に変換するにはマシンの1秒間のカウント数で割る必要があります。
ちなみにマシンの1秒間のカウント数もGameInitにてllGlobalFreqに保存してあるので、
単純にこの値で割れば時間に変換することが出来ます。

これを考慮した経過時間の算出方法は以下のようになります。

///////////////////////////////////////////////////////
// ゲーム実行ループ
///////////////////////////////////////////////////////
int CGame::GameRun( BOOL demo )
{
    // 開始時から経過した時間を算出
    LARGE_INTEGER li;
    QueryPerformanceCounter( &li );
    dElapsedTime = (double)(li.QuadPart - llStartTime) / llGlobalFreq;


dElapsedTimeは開始からの時間が秒で保存されますが、
これは高解像度カウンタから計算された値なので、結果の経過時間も高精度な値となっています。

ここで求めた経過時間をCBmsProクラスに実装した時間からBMSカウント値を算出する関数に渡すことで、
その瞬間での正確なBMSカウント値を取得することが出来ます。

    // 経過した時間から進んだBMSカウント値を算出
    LONG now_count = bms.GetCountFromTime( dElapsedTime );


■曲の終了判定

現在のBMSカウント値が分かったところで、ここでは先に曲の終了を判定してみます。

CBmsProクラスにはその曲の最大BMSカウンタ値が記録されているので、
この値と現在のBMSカウント値を比較することで、その曲が終わったかどうかが判断できます。

ちなみにCBmsProクラスの最大BMSカウント値は、実はギリギリ分の値しか持っていません。
そのためここではさらに+1小節、つまり+9600カウントを加算した値が
現在のBMSカウント値を超えていないかで判断してみます。

    // BMSカウンタが曲の最大カウント+1小節を超えたら終了
    if( bms.GetMaxCount()+BMS_RESOLUTION<=now_count )
        return 1;

※1小節のカウント数はBMS_RESOLUTIONとして定義されているので、ここではこの定数を指定しています

■BGMチャンネルの再生処理

ここではBMSデータのBGMチャンネル(0x01番)の自動再生処理について説明します。

CBmsProクラスには各チャンネルの情報を取得するための関数として、GetObjeNumとGetObjeが用意されています。
この関数の引数にチャンネル番号を指定することで、そのチャンネル内にあるデータの数と、
そのデータ1つずつの情報を取得することが出来るようになっています。

たとえば以下のような使い方です。

    CBmsPro bms;                // クラス
    bms.Load( "sample.bms" );   // ファイルをロード

    DEBUG( "オブジェ数        : %d\n",bms.GetObjeNum(BMS_BACKMUSIC) );
    DEBUG( "1つ目のカウント値 : %d\n",bms.GetObje(BMS_BACKMUSIC,0)->lTime );
    DEBUG( "2つ目のカウント値 : %d\n",bms.GetObje(BMS_BACKMUSIC,1)->lTime );
    DEBUG( "3つ目のカウント値 : %d\n",bms.GetObje(BMS_BACKMUSIC,2)->lTime );

BMS_BACKMUSIC0x01と定義されています

これを利用することでBGMチャンネルの再生を行うことが出来ますが、
以下はそのプログラムの参考例となります。

    // BGMをタイミングにあわせて再生する
    for( i=0;i<bms.GetObjeNum(BMS_BACKMUSIC);i++ ) {
        LPBMSDATA bf = bms.GetObje( BMS_BACKMUSIC,i );
        if( bf->bFlag ) {
            if( now_count>=bf->lTime ) {
                bf->bFlag = FALSE;
                ds.Reset( bf->lData );
                ds.Play( bf->lData );
            }
        }
    }


この例ではBGMチャンネルに存在するデータ全てをチェックするためのforループを定義しています。

まずはGetObjeNumを使うとBGMチャンネル内のデータ数が分かるのでこれをループ回数に指定します。
そしてGetObjeを使って全データを取り出し、この時データに保存されているBMSカウント値より
経過カウント値の方が大きかった場合、そのデータに記載されているサウンドIDを再生しています。

なお、データにはbFlagというBMSのロード直後にTRUEに初期化されている変数を見ることで、
まだ再生されていないデータかどうかを判断しています。
再生を行ったらFALSEに設定することで次回からこのデータをスキップさせることが出来ます。


この時のBGMチャンネルの各データの中身はだいたい以下のようになります。


■最適化1

BGMチャンネルのデータは本来1回だけ再生するものです。
そのため上ではbFlag変数を使って2回目以降はスキップさせていました。

しかしいくらスキップされるからと言っても、
forで全データを毎回チェックしているのはとても無駄な処理と言えます。

また全データをチェックするということは、データ数が増えれば増えるほど
処理的にも重くなっていくのが分かると思います。
※前に書いたとおりCPU的には1000個や10000個レベルなら全然問題は無いですが、
 これはあくまでもC言語の話であって、VBとかFlash、特にJavaScriptなどの
 スクリプト言語ではこの問題は特に大きくなります

ということでこのforループ処理をもっと軽く出来ないかを考えてみます。

さて、BMSクラスでロードされたデータは必ずlTimeの小さい順に並び替えられます。
つまりあとの方にあるデータは絶対にそれより小さい値になることはありません。

それを踏まえてもう一度上で紹介したBGMチャンネルのデータのイメージ画像を見てみましょう。
now_countというのは現在の時間から算出されたBMSカウント値ですが、
よく考えると音が出ているのはのnow_countを過ぎたデータのみです。

ということはnow_countが到達していない後方のデータは、
別にチェックしなくても良いように見えませんか?

つまりfor内でnow_countよりデータのBMSカウント値が大きければ、
forを抜けてしまっても問題無いと言うことです。

この判定を先ほどのBGM再生処理に追加してみると以下のようになります。

    // BGMをタイミングにあわせて再生する
    for( i=0;i<bms.GetObjeNum(BMS_BACKMUSIC);i++ ) {
        LPBMSDATA bf = bms.GetObje( BMS_BACKMUSIC,i );
        if( now_count<bf->lTime )
            break;
        if( bf->bFlag ) {
            if( now_count>=bf->lTime ) {
                bf->bFlag = FALSE;
                ds.Reset( bf->lData );
                ds.Play( bf->lData );
            }
        }
    }


これで途中でループを抜けられるようになりましたが、
曲の終盤になればなるほど結局はforループを最後まで回すことになるため、
結果的には完全な最適化とは言えません。

■最適化2

そしてもう1つ最適化出来るところがありますが、それは実はforの開始位置です。

もう一度上のイメージ画像をよく見てください。
データはBMSカウント値の小さい順で並べられているため、
now_count以前のデータというのは必ず再生が終了しているはずです。

ということはforでチェックすべきデータというのは、
最後に再生したデータの次のデータからで良いことになります。

これを踏まえforの開始を変数に変えた場合のプログラムは以下のようになります。

    // BGMをタイミングにあわせて再生する
    for( i=iStartNum[BMS_BACKMUSIC];i<bms.GetObjeNum(BMS_BACKMUSIC);i++ ) {
        LPBMSDATA bf = bms.GetObje( BMS_BACKMUSIC,i );
        if( now_count<bf->lTime )
            break;
        if( bf->bFlag ) {
            if( now_count>=bf->lTime ) {
                bf->bFlag = FALSE;
                ds.Reset( bf->lData );
                ds.Play( bf->lData );
                iStartNum[BMS_BACKMUSIC] = i + 1;
            }
        }
    }


ここで使用したiStartNumとはヘッダに定義していたint型の配列です。
この配列は256チャンネル分確保されており、ゲームの開始時に0クリアされた状態となっています。
そしてここではBGMチャンネルに使用するということで、配列番号としてBMS_BACKMUSICを指定しています。


この処理について詳しく説明すると、まずiStartNum[BMS_BACKMUSIC]は0に初期化されているので、
ゲーム開始直後のforでは0番目のデータから参照されるようになっています。

そして時間が経過しBGMが再生されると、そのデータはもうチェックがいらなくなるため
同時にiStartNum[BMS_BACKMUSIC]を更新します。
この時の値は処理したデータの次のデータとするため「i + 1」をセットしています。

これで次のフレームではそのデータからforが開始されるようになり、
それまでのデータのチェックは一切行われなくなるので、完全にCPU負荷を0にすることが出来ます。


そして最適化1とこの最適化2を組み合わせることで、forのループ回数は実質データが再生される分だけとなり、
逆にもしそのフレームで再生するデータが1つも無かった場合は、1度もループせずに即座にbreakで抜けられるため、
処理落ちをまったく気にせず、さらにいくらでもデータを追加することが出来ます。

なお、これらの最適化手法は以降全ての処理で使うことになるため、
ここで原理を十分理解しておいてください。