GoogleAdsence

DXライブラリとC#での音楽ゲームの作り方その8 ステージ初期化、計算処理、最適化

前回の説明で音ゲーを作るうえで必要な考えはすべて書きました。
では実際にそれらを使うためのシーン別のクラスを作ってみましょう。
ステージの管理については以下を参考にしてください。

初期化処理

  1. ファイル名を引数file_nameから受け取りBmsLoader.Load関数でBmsデータを読み込む
  2. それぞれの変数を初期化
  3. GetNowHiPerformanceCountで開始した時間を記録しておく(マイクロ秒)
public class CStage : CScene
{
    string FileName;
    CBmsLoader BmsLoader = new CBmsLoader();
    private int[] BmsNum = new int[BMSMAXBUFFER]; // BMS演奏で計算開始する配列番号(処理を軽くするため)

    private bool AutoPlay = false; // オートプレイ
    private bool MovieStartFlag = false; // 動画再生フラグ
    private long NowCount = 0; //Bmsカウントを保存
    private long StartTime = 0; // 開始した時の秒数(マイクロ秒)
    private float fScrMulti = 1.0f; // 小説スケール
    int Time = 0; // ゲージ(目盛り)を揺らすためのカウンター
    int ImageID = 0;			// 現在表示されているアニメーションインデックス番号
    int TempScore = 0, Gauge = 0;
    public CStage(string file_name)
    {
        fScrMulti = 1.0f;
        ImageID = 0;
        FileName = file_name;
        if (!BmsLoader.Load(file_name))
        {
            MessageBox.Show("Bmsファイルを読み込めませんでした");
        }
        for (int i = 0; i < BMSMAXBUFFER; ++i)
        {
            BmsNum[i] = 0;
        }
        StartTime = DX.GetNowHiPerformanceCount(); // 開始時刻を記録(マイクロ秒)
    }
}

計算処理

GetCountFromTimeで経過した時間から開始した時間を引いた時間を渡して進んだBMSカウント値を算出します。この時の単位がマイクロ秒なので0.000001掛ける事によって秒に単位を変換してます。(下のソース9行目)
ちなみに、割り算よりも掛け算の方が計算コストが低いので、掛け算を使ってます。

曲の終了判定

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

ちなみにCBmsProクラスの最大BMSカウント値は、実はギリギリ分の値しか持っていません。
そのためここではさらに+1小節、つまり+9600カウントを加算した値が、現在のBMSカウント値を超えていないかで判断してみます。(10~16行目)
※1小節のカウント数はBMS_RESOLUTIONとして定義されているので、ここではこの定数を指定しています

そして、18~33行目までのコードでBMS_BACKMUSICのCBmsDataを取り出して、その時間がNowCountより大きければ、ループを終了します。
そうでなく、フラグがtrue(まだ鳴らされてない)でNowCountがbf.Times以上なら、BmsLoader.PlayMusic(bf.iData);で音を鳴らします。

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

同じ要領でBMS_BACKANIMEでアニメーション画像を任意のタイミングでセットして、BMS_MOVIEでムービーも任意のタイミングで再生します。

public class CStage : CScene
{
~省略~
   public override void Move()
   {
       float[] obj_x = { 55, 73, 87, 105, 119, 22, 0, 136, 150 };    // オブジェ表示X座標

       // 経過した時間から進んだBMSカウント値を算出
       NowCount = BmsLoader.GetCountFromTime((double)(DX.GetNowHiPerformanceCount() - StartTime)*0.000001);
       // BMSカウンタが曲の最大カウント値(+1小節分)を超えたら
       if (BmsLoader.GetMaxCount() + BMSDATA_RESOLUTION <= NowCount)
       {
           Terminate();
           Life = 0;
           return;
       }

       // BGMが1つでも設定されている場合はタイミングにあわせて再生
       for (int i = BmsNum[BMS_BACKMUSIC]; i < BmsLoader.GetObjeNum(BMS_BACKMUSIC); i++)
       {
           CBmsData bf = BmsLoader.GetObje(BMS_BACKMUSIC, i);
           if (NowCount < bf.Times)
               break;
           if (bf.Times != 0)
           {
               if (NowCount >= bf.Times)
               {
                   bf.Flag = false;
                   BmsLoader.PlayMusic(bf.iData);
                   BmsNum[BMS_BACKMUSIC] = i + 1;
               }
           }
       }

       // アニメーション画像のセット
       for (int i = BmsNum[BMS_BACKANIME]; i < BmsLoader.GetObjeNum(BMS_BACKANIME); i++)
       {
           CBmsData bf = BmsLoader.GetObje(BMS_BACKANIME, i);
           if (NowCount < bf.Times)
               break;
           if (bf.Flag)
           {
               if (NowCount >= bf.Times)
               {
                   bf.Flag = false;
                   ImageID = bf.iData;
                   BmsNum[BMS_BACKANIME] = i + 1;
               }
           }
       }

       // ムービーデータが設定されている場合はタイミングにあわせて再生
       for (int i = BmsNum[BMS_MOVIE]; i < BmsLoader.GetObjeNum(BMS_MOVIE); i++)
       {
           CBmsData bf = BmsLoader.GetObje(BMS_MOVIE, i);
           if (NowCount < bf.Times)
               break;
           if (bf.Flag)
           {
               if (NowCount >= bf.Times)
               {
                   bf.Flag = false;
                   BmsNum[BMS_MOVIE] = i + 1;
                   DX.PlayMovieToGraph(BmsLoader.GetMovieHandle());
                   MovieStartFlag = true;
               }
           }
       }
   }
   void Terminate()
   {
       TempScore = 0;
       MG.Score = 0;
       MG.Combo = 0;
       MG.FirstLoop = true;
       MG.Effect.Clear();
       BmsLoader.Clear();
       MG.SceneBuf.Add(new CResult());
   }
~省略~
}

最適化1

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

しかしいくらスキップされるからと言っても、forで全データを毎回チェックしているのはとても無駄な処理と言えます。また全データをチェックするということは、データ数が増えれば増えるほど、処理的にも重くなっていきます。

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

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

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

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

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

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

// BGMが1つでも設定されている場合はタイミングにあわせて再生
for (int i = BmsNum[BMS_BACKMUSIC]; i < BmsLoader.GetObjeNum(BMS_BACKMUSIC); i++)
{
    CBmsData bf = BmsLoader.GetObje(BMS_BACKMUSIC, i);
    if (NowCount < bf.Times)
        break;
    if (bf.Times != 0)
    {
        if (NowCount >= bf.Times)
        {
            bf.Flag = false;
            BmsLoader.PlayMusic(bf.iData);
            BmsNum[BMS_BACKMUSIC] = i + 1;
        }
    }
}

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

最適化2

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

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

ということはforでチェックすべきデータというのは、最後に再生したデータの次のデータからで良いことになります。これを踏まえforの開始を変数に変えた場合のプログラムは以下のようになります

// BGMが1つでも設定されている場合はタイミングにあわせて再生
for (int i = BmsNum[BMS_BACKMUSIC]; i < BmsLoader.GetObjeNum(BMS_BACKMUSIC); i++)
{
    CBmsData bf = BmsLoader.GetObje(BMS_BACKMUSIC, i);
    if (NowCount < bf.Times)
        break;
    if (bf.Times != 0)
    {
        if (NowCount >= bf.Times)
        {
            bf.Flag = false;
            BmsLoader.PlayMusic(bf.iData);
            BmsNum[BMS_BACKMUSIC] = i + 1;
        }
    }
}

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

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

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

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

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