アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

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で抜けられるため、処理落ちをまったく気にせず、さらにいくらでもデータを追加することが出来ます。