アマゾンバナーリンク

DXライブラリとC#での音楽ゲームの作り方その9 ステージ内での描画スクロール処理

2020年2月26日

前回は画面上に全てのオブジェを表示する方法について説明しましたが、
ここではこれをスクロールする方法について説明します。

スクロールカウンタ

ゲームの画面をスクロールするには時間から、BMSカウントの算出のページで説明した「経過した時間から進んだBMSカウント値」を使います。詳しくは↓を参照してください。

またこの値はクラス変数の「NowCount」と定義されており、毎フレーム関数の最初に算出しているため、この関数内で自由に参照出来るようになっています。

ではこの値を使って画面のオブジェをスクロールさせるにはどうするしたら良いかですが、まずオブジェ描画時に使用したTimesと NowCount という変数は同じ単位です。

そしてこの Times を使って最終的に画面の特定の位置にオブジェを描画していたわけですが、ということはこの Times からnow_countを差し引いた値でオブジェを描画すれば、 Times がどんどん小さくなっていくため結果的にスクロールしているように見えるわけです。

以下は1小節分のスクロールのサンプルですが、 NowCount が増えるとその分オブジェが下にスクロールします。

スクロールプログラム

それではこの処理を前回作成した描画部分に組み込んでみます。とりあえずここではオブジェ1個だけのスクロールについて考えてみましょう。

まず指定のオブジェ1つを表示する部分について、前回のプログラムから抜き出してみると以下のようになっています。

 CBmsData b = BmsLoader.GetObje( 0x11+index[j],i );
 int obj_y = (int)((double)b->Times / (BMS_RESOLUTION / 192));
 DX.DrawGraphF(obj_x[j], (float)(JUDGE_LINE - (int)( obj_y   + 0.5f)), MG.ImgBar[obj_kind[j]], 1); 

ここではオブジェのlTimeをそのまま使用して描画していますが、これに経過した分を差し引いた場合のプログラムは以下のようになります。

 CBmsData b =  BmsLoader.GetObje( 0x11+index[j],i );
 int off_y = (int)((double)(b->lTime - now_count) / (BMS_RESOLUTION / 192));
 DX.DrawGraphF(obj_x[j], (float)(JUDGE_LINE - (int)(off_y  + 0.5f)), MG.ImgBar[obj_kind[j]], 1);  

このように経過した分をオブジェのBMSカウント値から差し引くということは、オブジェのBMSカウンタ値がだんだん小さくなるということで、これでどんどん判定バーに近くなるため結果的にスクロールしているように見えるというわけです。

ちなみに、このままだと差し引き後のBMSカウント値がマイナスになってしまいますが、描画的には判定バーより下に表示されるだけなので何も問題ありません。

スクロールプログラム(改)

上記のプログラムは基本1小節を192ピクセルとして、この幅が2倍や3倍など整数倍の時だけきれいにスクロールしているように見えますが、これが整数倍ではない場合、スクロール時に全てのオブジェが同じ速度でスクロールするのではなく、1オブジェごとに微妙にピクピクとブレて表示されてしまいます。

これは上記の計算式が1オブジェごとにスクロール量を計算しているためで、この時int型にキャストする際に小数部分がカットされてしまいます。

これがもし全てのオブジェが同じ小数値となるのであれば、カット後の値も同じ差となるため特に問題は出ませんが、実際には1オブジェごとに小数誤差が異なるため、例えばあるオブジェでは計算結果が5.9だとすると小数部がカットされて5.0になり、あるオブジェでは計算結果が6.1としたら小数部がカットされて6.0になりますが、この時元の差が0.2だったものが計算後は1.0の差となっているため、このせいでそれぞれのオブジェが同じ速度でスクロールしていないように見えるため、ピクピクと上下に動いているように見えるわけです。

これを避けるため、ここではオブジェの位置とスクロール量を別々に算出することにします。そしてここで重要なのは、これらは全て整数値として算出します。

原理を説明すると、まずオブジェの位置というのはスクロールしていない状態でのスクリーン画面上での位置であり、これは前回のプログラムで既に実装済みのもので、原点から常に一定の位置を指しています。

次に、スクロール量というのはゲーム開始を0としたスクリーン座標上でのスクロール量で、これは時間の経過とともにスクロール量も変化しますが、最終的にこのスクロール量を全てのオブジェの位置から差し引くことで、結果的に全体がまったく同じ量だけスクロールすることになるため、各オブジェがピクピクしてしまう問題は発生しません。

以下はスクロール量の計算式です。

// スクリーン座標上でのスクロール量を算出
int scr_y = (int)((double)now_count / (BMS_RESOLUTION / 192));

この計算式は実はオブジェの位置を計算する処理とまったく同じもので、ここでは単に経過した時間からスクロールした分のピクセル数を求めているだけです。

そして、このスクロール量を各オブジェの位置から差し引くプログラムは以下のようになります。

CBmsData b = BmsLoader.GetObje( 0x11+index[j],i );
// スクロールを考慮しないスクリーン座標上での原点からの座標値を算出
int obj_y = (int)((double)bar->Times / (BMS_RESOLUTION / 192));
// スクロールを考慮した現在のY座標を算出
int off_y = obj_y - scr_y;
DX.DrawGraphF(obj_x[j], (float)(JUDGE_LINE - (int)( off_y  + 0.5f)), MG.ImgBar[obj_kind[j]], 1);

上記で説明したとおり、オブジェの位置は整数値であり、しかもオブジェのカウント値はゲーム中に変化はしないため、スクロールが無ければ常にスクリーン座標の固定の位置を指していることになります。
そしてこれにスクロールした分の整数値を差し引くことで、結果的にスクロール後の座標が算出出来ます。

ちなみにこのプログラムの欠点としては、前のスクロールプログラムではスクリーン座標に変換するための計算は1回だけでしたが、こちらのスクロール処理は実質2回の計算が必要となるため、処理的にはちょっと無駄なことをしているように見えます。

ただ、スクロール量の計算自体は全体で1回だけで良いのと、最終的な計算はint型で行えるため、一般的な言語を使用する分にはそこまで問題は無いと思います。※1小節の幅をリアルタイムに変更する必要が無いのであれば、オブジェごとに最初からスクリーン座標を計算しておくことで、さらにもっと高速化も可能です。

画面外の処理

これまでのプログラムでは全てのオブジェを描画していたため画面外への描画も行われています。

ちなみに画面外の描画とは実際には以下の赤い部分です。

上側(Y=0より小さい部分)
下側(Y=418より大きい部分)
 ※念のため勘違いしないように拡大

まずはこの中の下側の判定バーの描画部分について説明します。念のためこのゲームの仕様の確認ですが、まずオブジェに関しては原点を左上、高さは6pxとなっています。

そしてこのオブジェが判定ラインの上部ぴったりとなる位置をスクロールの原点としていました。

ちなみにこのゲームでは、オブジェは判定バーより下にはスクロールせず、
この時は表示を行わないものとしています。

表示無し

ではこのオブジェが下に行く時と言うのはどんな時かというと、上のプログラムでスクロール込みのオフセットがマイナスになった場合となります。

オブジェのlTimeは時間が進むとどんどん小さくなりますが、もしオブジェのlTimeと経過時間がまったく一緒の場合は、その差は0になるため計算後のオフセット値は0となります。さらに時間が経過するとオブジェのlTimeを過ぎてしまった状態となり、つまりオフセット値はマイナスになります。

このことから判定バーより上だけ表示させるには、オフセット値が0以上かどうかで判断すれば良いことになります。


次は上部の画面外処理について説明します。

まずこのゲームのオブジェの表示範囲は画面の一番上から判定バーまでとなります。またここでは判定バーの原点Y座標は413としています。

ということは逆にこの判定バーの原点を0とした場合、そこから上413ピクセルが表示範囲であると言えるので、これによりオブジェのオフセット値が413以下ならオブジェは画面内に表示されていると判断出来ます。

ただし実はこのままだと1つ問題があります。それはスクロール時にオブジェが画面内に入ってくる時ですが、もしオフセット値が413以下なら表示ということにすると、オブジェが突然出てくるように見えます。

これを避けるにはこのオブジェの高さ分をだけ前から表示する、つまり413+6以下であれば表示するようにします。ちなみに以下は画面の上部のサンプルです。

413以下の時413+6以下の時

よく観察すると左側は突然オブジェが出てくるように見えますが、右側はきちんと画面外からスクロールしているように見えると思います。

この範囲指定を上のプログラムに組み込んでみると以下のようになります。

CBmsData b =  BmsLoader.GetObje( 0x11+index[j],i );
// スクロールを考慮しないスクリーン座標上での原点からの座標値を算出
int obj_y = (int)((double)bar->lTime / (BMS_RESOLUTION / 192));
// スクロールを考慮した現在のY座標を算出
int off_y = obj_y - scr_y;
if( off_y>=0 && off_y<=413+6 ) {
    dd.Put( 15+obj_kind[index[j]],obj_x[index[j]],(float)(413-off_y) );
}

オブジェ表示の最適化

これで描画自体の無駄は省けるようにはなりましたが、そもそも毎回for分で全オブジェをチェックするという部分は変わっていません。ということはこれで根本的に速くなったとは言い切れません。

そこで、ここにも以前説明した最適化1と2を取り入れてみることにします。

まずは最適化1の方法で画面上部より上の部分をスキップしてみましょう。ちなみにこのゲームは上のオブジェほど後方にあることになるので、つまりオフセット値が413+6より大きければスキップしてしまいます。

for( j=0;j<6;j++ ) {
    for( i=0;i< BmsLoader.GetObjeNum(0x11+index[j]);i++ ) {
        CBmsDatab = bms.GetObje( 0x11+index[j],i );
        // スクロールを考慮しないスクリーン座標上での原点からの座標値を算出
        int obj_y = (int)((double)bar->lTime / (BMS_RESOLUTION / 192));
        // スクロールを考慮した現在のY座標を算出
        int off_y = obj_y - scr_y;
        // 画面の上より外ならばその先は全て描画スキップ
        if( off_y>413+6 )
            break;
        dd.Put( 15+obj_kind[index[j]],obj_x[index[j]],(float)(413-off_y) );
    }
}

※前のプログラムでは範囲内なら描画するとして「<=」としていましたが、今回は範囲外なら抜ける必要があるので、比較演算子は逆の「>」となります

次に最適化2の方法も取り入れてみます。これはオブジェが判定バーより下に行った場合、そのオブジェはもう表示する必要は無くなるので、最後に判定バーを過ぎたオブジェの1つあとのオブジェを、次のforの開始位置とすれば良いことになります。

for( j=0;j<6;j++ ) {
    for( i=iStartNum[0x11+index[j]];i<bms.GetObjeNum(0x11+index[j]);i++ ) {
        LPBMSDATA b = bms.GetObje( 0x11+index[j],i );
        // スクロールを考慮しないスクリーン座標上での原点からの座標値を算出
        int obj_y = (int)((double)bar->lTime / (BMS_RESOLUTION / 192));
        // スクロールを考慮した現在のY座標を算出
        int off_y = obj_y - scr_y;
        // 判定ラインより下ならもう表示はせず、次回からその次のオブジェから参照する
        if( off_y<0 ) {
            iStartNum[index[j]+0x11] = i + 1;
            continue;
        }
        // 画面の上より外ならばその先は全て描画スキップ
        if( off_y>413+6 )
            break;
        dd.Put( 15+obj_kind[index[j]],obj_x[index[j]],(float)(413-off_y) );
    }
}


なお、ここではoff_yが0より下であれば画面にはもう表示する必要が無いため、同時にこのオブジェをcontinueすることでスキップさせています。

これらの最適化により1フレーム内に表示されるオブジェ数は、基本的に画面内に存在するオブジェ数のみで済みます。

小節ラインの描画

ここでは小節ラインの描画を行ってみます。
オブジェの描画方法が理解できていれば小節ラインの描画はまったく難しくありません。
これは基本的にオブジェの1レーン分の表示処理をそのまま流用するだけです。

なお小節ラインはオブジェより下に表示する必要があるので、
小節ラインはオブジェより先に描画しなければなりません。

このためここでは先に画面上に表示される小節を先に全て描画し、
そのあとで各レーンのオブジェを描画するようにしています。


このゲームの仕様として小節バーはオブジェの上辺ではなく、下辺に表示されるものとします。

正しい×間違い

また小節バーは高さ1ピクセルで作成しています。

さて、小節ラインの情報はCBmsProクラスのGetBarNumGetBarから取得できます。そしてオブジェ描画ルーチンのループ処理を単純にこれに置き換えるだけでほぼ描画処理は完成となりますが、ここでは小節用として3点確認しなければならないことがあります。


まず1つはGetBarで取得出来る構造体はBMSBAR構造体となるので、オブジェ構造体のBMSDATAから置き換える必要があります。なおBMSBAR構造体はオブジェ構造体と同じく、
BMSカウント値としてlTimeが定義されているので、オフセット計算処理時は特に変数名を変える必要はありません。

もう1つは上で説明した小節バーの表示位置についてですが、もし同じBMSカウント値にオブジェと小節が存在する場合、計算したオフセット値を描画時にそのまま使用してしまうと、オブジェの上辺に小節バー表示されてしまいます。小節バーはオブジェの下辺に表示されなければならないので、そのためには小節バーの描画時にY軸をさらに+5して表示を行います。※オブジェの高さはここでは6pxのため下辺は5px目ということになります

最後の1つは最適化2用のforの開始位置を制御するための変数ですが、ここではStartNumの0番目を使用することにしています。これはゲーム的にチャンネル0というものは存在しないので、未使用の状態となっているためです。

以下はこれらを考慮して小節バーを描画するプログラムとなります。

    for( i=iStartNum[0];i<bms.GetBarNum();i++ ) {
        LPBMSBAR bar = bms.GetBar(i);
        // スクロールを考慮しないスクリーン座標上での原点からの座標値を算出
        int obj_y = (int)((double)bar->lTime / (BMS_RESOLUTION / (fScrMulti * 192)));
        // スクロールを考慮した現在のY座標を算出
        int off_y = obj_y - scr_y;
        // 判定ラインより下ならもう表示はせず、次回からその次の小節から参照する
        if( off_y<0 ) {
            iStartNum[0] = i + 1;
            continue;
        }
        // 画面の上より外ならばその先は全て描画スキップ
        if( off_y>413+2 )
            break;
        dd.Put( 2,1,413-off_y+5 );
    }

小節バーは切り抜き番号2に登録されており、また原点は一番左となっているため、
ここではPutに指定するX座標値は1となります。