8-9 オブジェ描画

ここではゲーム画面でスクロールさせるオブジェの表示方法について説明します。

■2D描画の奥行きについて

一般的に2Dでの描画とは後から表示したものが上に来ます。

これは例えば紙にシールを貼っていくようなイメージで、
2Dの描画も紙のようなバックバッファに対して画像をどんどん描画してくと、
前に描画した画像はさらに上から描画された画像によって上書きされてしまいます。

なんだか難しいことを書いているようですが、
このサイトの音ゲーではこれが重要となる箇所が1箇所あります。
それは白鍵(白)と黒鍵(青)のオブジェの表示です。

下の画像を見てください。


①と②は重なっていないため描画順は特に関係ありませんが、
問題は③で同じタイミングに白と青が重なっています。

普通プログラムを作るとしたらオブジェクトを表示するため鍵盤の左側から、
「白→青→白→青→白→スクラッチ」という順番でforを使って回すことを考えると思います。

これはオブジェのチャンネル番号が11~16とちょうど連番になっていることも理由ですが、
このままだと実際には以下の③のように青の方が先に描画されるため結果的に奥に行ってしまいます。

※分かりやすいように黄色にしてみました。

これでは見た目的にもおかしいので、これに対応した描画を行う必要があります。

と、ここまで過剰に説明してとても難しそうに見えますが、
実は解決法は以外に簡単です。

このゲームの仕様をよく考えてみると青2つは必ず白より上に表示されます。
また、ここではそれより上に描画されるオブジェはありません。

ということは先に白3つを描画してから、あとで青2つを描画することにしてしまえばよいのです。
つまり「白→白→白→青→青→スクラッチ」という感じです。

ついでにこれをチャンネル番号的に見ると、「11→13→15→12→14→16」となります。

■1つのチャンネル内オブジェを全て表示してみる

ここでははじめから6チャンネル分の描画を考えるのではなく、
最初は1チャンネル分の描画について考えます。

なんでも一気に考えようとすると頭がこんがらがってくるので、
出来るだけ基本的なところから考えるというのがプログラミングのコツです。

ではまず一番左側の白鍵(チャンネル11番)の描画を考えてみましょう。
※以下はチャンネル11番のレーンを表しています
 

1つのチャンネル内には当然オブジェが1つしかないわけがありません。
つまりたくさんのオブジェがそのチャンネルに存在するわけなので、
これらを画面上に全て表示しなければなりません。

なおここでは画面上に描くと言っていますが、ゲーム自体はオブジェが上からスクロールしてくるため、
実際にはこの画面よりさらに上にも描画をしていることになります。

本当は画面外にいったら描画を行わないのが良いのですが、
とりあえずここでは最適化は考えずこのチャンネルを全て描画するプログラムを考えてみます。


ここで使用するのはお馴染みのCBmsProクラスのGetObjeNumとGetObjeです。
そして座標を算出するのに必要なのが各オブジェのBMSカウント値で、
これはオブジェ構造体のlTimeに入っています。


lTimeが0の時というのはゲーム画面上で言うと判定バーの上部となります。
ちなみにサンプルではこの座標はx=1、y=413としています。


この位置を時間0の時の原点位置とし、ここをベースとして上方向にオブジェを順番に表示していきます。


さてlTimeの値が0の場合とは最初の小節(0小節目)の先頭となり、上の画像の原点の座標ということになりますが、
それではlTimeが9600、つまり1小節目の先頭だった場合を考えてみましょう。

ちなみにlTimeの値をそのままピクセル数として使ってしまうと、
このオブジェは原点から9600ピクセル上に表示されることになってしまいます。
※ゲームとしてスクロールするようになるとものすごい速さで落ちてきます(笑)

そこでここではゲームの見易さなどを考慮して1小節を192ピクセルと定義し、
これに合わせるためように倍率を掛けることにします。

すると以下のように1小節ごとに192ピクセル区切りの見やすい画面になります。



それでは1小節を192ピクセルとする場合に計算式を求めてみましょう。

まずlTimeはBMSカウント単位で記録されています。
そしてBMSカウント値というのは1小節あたり9600と定義しています。

そして今回は9600という値を192ピクセルで表示したいわけなので、
「9600÷192px」で得られた値「50」が求められます。

そしてこの50を各オブジェのlTimeから割ってあげることで、
1小節を192ピクセルとした最終的なピクセル数に変換出来ます。

さて、画面上では上に行くほど時間的に後のほうのオブジェが表示されます。
本来2D画面というのは左上が原点で右下に行くほど値が大きくなりますが、
実際にオブジェを表示する際は上方向に行くために符号を反転してやる必要があります。

これらを踏まえ全てのオブジェを原点から上方向に表示するプログラムは以下のようになります。

    for( i=0;i<bms.GetObjeNum(0x11);i++ ) {
        LPBMSDATA b = bms.GetObje( 0x11,i );
        float off_y = (float)b->lTime / (BMS_RESOLUTION / 192);
        dd.Put( 15,1,413-off_y );
    }


注意点として一番左側のチャンネルは11番となりますが、
ここでは11をそのまま使うのではなく16進数として扱います。
このためGetObjeNumGetObjeに渡すチャンネル番号は、
実際には「0x11」と指定しなければなりません。
(CBmsProクラスはチャンネル番号を16進数として解析しているため)


ではこの処理について詳しく説明しますが、
まずはこのチャンネル内の全オブジェを対象とするためforループを構築しています。

次にそれぞれのオブジェの情報をGetObjeから取得します。

そして上で説明した式を使ってオブジェのカウント値からピクセル数に変換した値を計算します。

なお、ここでは計算した値はテンポラリ変数のoff_yに入りますが、
この値はまだプラス値のためそのままでは画面的に下方向に向かっていることになるので、
最後に原点(Y=413)から上方向になるように、413から先ほど計算したピクセル値を引くことで、
上方向の表示に変換されます。

ちなみに描画はCDDPro90クラスのPutを利用していますが、
このサンプルではGameInitにて切り抜きIDの15に白鍵用オブジェを設定しているので、
ここではそのIDを指定しています。


1つ重要な問題として、上記のプログラムはoff_yはfloat型になっていますが、
これはPutの引数がfloat型となっているためそれに合わせたものですが、
以降に説明するスクロール処理にて、オブジェと小節ラインの計算をする際に、
実はfloat誤差によりオブジェと小節ラインがうまく重ならずにチラチラとブレてしまうことがあるため、
ここでは計算結果を一度int型の整数として算出してから、Putに渡す時にfloatにキャストして渡しています。
なお、ここではまだ実装はしていませんが、小節幅を倍率により高精度で求められるようfloatではなくdoubleに変更しています。

    for( i=0;i<bms.GetObjeNum(0x11);i++ ) {
        LPBMSDATA b = bms.GetObje( 0x11,i );
        int obj_y = (int)((double)b->lTime / (BMS_RESOLUTION / 192));
        dd.Put( 15,1,(float)(413-obj_y) );
    }


ひとまずこれでこのチャンネル内のオブジェは全て表示されたことになります。


■全てのチャンネルを表示してみる

1つのチャンネルの表示方法が分かったところで、これを応用して全てのチャンネルを表示してみます。

上記のプログラムではチャンネル番号を0x11に固定にしていました。
つまりこれを動的に変えることで、全てのチャンネル(11~16)の表示を行うことが出来ます。

まず最初に考えられるのは、以下のように多重forとして定義することです。

    for( j=0;j<6;j++ ) {
        for( i=0;i<bms.GetObjeNum(0x11+j);i++ ) {
            LPBMSDATA b = bms.GetObje( 0x11+j,i );
            :
        }
    }


しかしこれだと最初に説明した通り、後から描いたものが手前に表示されるため、
譜面によっては黒鍵盤のあとに白鍵盤が表示されてしまいます。

これに対応するためこのforに細工をすることにします。

重なりを正すには描画する順番を変えればよいと説明しましたが、
具体的にはこの描画する順番を別に定義しておき、それを参照することで順番を間接的に変えることが出来ます。

上のプログラムだと「白(0)→青(1)→白(2)→青(3)→白(4)→スクラッチ(5)」という順番になりますが、
これを「白(0)→白(2)→白(4)→青(1)→青(3)→スクラッチ(5)」となるように、
まずは以下のようなインデックスリストを定義しておきます。

    static const int index[6]       = { 0,2,4,1,3,5 };              // インデックスリスト


そしてGetObjeNumとGetObjeにはこのインデックスリストを参照することで、
表示順に合わせたチャンネル番号が参照出来るようになります。

以下はこの処理を組み込んだプログラムです。

    for( j=0;j<6;j++ ) {
        for( i=0;i<bms.GetObjeNum(0x11+index[j]);i++ ) {
            LPBMSDATA b = bms.GetObje( 0x11+index[j],i );
            :
        }
    }


頭の回転が速い人はこのindex配列に直接「0x11,0x13,0x15,0x12,0x13,0x16」と、
チャンネル番号を入れておけば良いんじゃないかと思うかもしれません。
しかしまだ説明していませんがこの変数はこれ以外にも用途があるため、
ここでは敢えてチャンネル番号ではなく0からの番号としています。


■チャンネルごとのX座標とオブジェ画像

上で全てのチャンネルに対して描画を行うための土台は揃いましたが、実はこれだとまだ問題があります。
それはチャンネルごとに表示する座標が異なるのと、表示するオブジェ画像自体が異なるということです。

例えばレーンの幅が固定幅であれば単純に掛け算すれば求めることが出来ますが、
このゲームはレーンの幅がチャンネルごとに異なっています。
またそれに合わせて白鍵、黒鍵、スクラッチの3つの画像が必要ですが、
上のプログラムではとりあえず白鍵画像が表示されるようにしかなっていませんでした。


ということで上のプログラムにこれらの情報を取り込む方法としては、
インデックスリストと同じくチャンネルごとにX座標とオブジェの種類を配列として用意し、
これを参照することでレーンごとに座標とオブジェ画像を変えるように制御します。

ちなみにこれらの配列はindex変数の順番とは関係なく、
普通に「白→青→白→青→白→スクラッチ」として定義します。

まず各レーンのX座標ですが、ここでは以下のように定義しています。



またオブジェの種類ですが、ここでは以下のように定義しています。

0 白鍵
1 黒鍵
2 スクラッチ

これは切り抜き画像の順番と同一となっています。
※ID15、16、17が白鍵、黒鍵、スクラッチ

そしてこれらを実際に配列として定義すると以下のようになります。

    static const int obj_kind[6]    = { 0,1,0,1,0,2 };              // オブジェの種類
    static const float obj_x[6]     = { 1,18,30,46,59,92 };         // オブジェ表示X座標


画像の切り抜きIDは15からとなるため、この配列を参照して表示させる場合はこれに+15します。
※直接配列を「15、16、15、16、15、17」とすることも出来ますが、
 これもあとで流用するためここでは敢えてオフセット値としています

これを使って実際に表示を行う場合、これもインデックスリストの順番で参照されなければなりません。
つまり「白→白→白→青→青→スクラッチ」の順に参照するためには、
先にインデックスリストから今回の参照先を求めるような参照の参照という形になっています。

そしてこれらを全て含めたプログラムは以下のようになります。

    for( j=0;j<6;j++ ) {
        for( i=0;i<bms.GetObjeNum(0x11+index[j]);i++ ) {
            LPBMSDATA b = bms.GetObje( 0x11+index[j],i );
            int obj_y = (int)((double)b->lTime / (BMS_RESOLUTION / 192));
            dd.Put( 15+obj_kind[index[j]],obj_x[index[j]],(float)(413-obj_y) );
        }
    }


これで全てのオブジェが画面上(実際には見えない部分にも)に表示されたことになります。