ここではユーザーがプレイする場合の判定の仕方について説明します。
この判定処理が入ることでいよいよゲームとしての機能がすべて組み込まれたことになります。
キーアサインとはキーの割り当てという意味です。
このサンプルではキーボードの以下のキーを使用することを前提としています。
またジョイスティックの場合は以下のボタンを使用します。
※USBコンバーターによりボタン番号は変わります
ここではスクラッチはキーボードの左シフトとスペースの2つどちらも使用可能とします。
また専用コントローラーの場合、スクラッチの右回りと左回りはアナログの上下に
割り当てられているのでこれらも入力対象としておきます。
ちなみに実際に製品版としてゲームを作る場合、これらのキーを任意に設定出来るようにしなければなりません。
これはキーボードによっては上記のキーを同時に押したときに入力判定が行えないものが存在します。
そのため各プレイヤーが持っているキーボードに合わせてキーアサインをさせなければなりません。
なおここではアサイン関連の詳しい説明はしないので、これは各自で考えて実装してください。
※ESCなどのキーは終了に使ったりするため、このようなシステムキーは登録出来ないなどの処置が必要です
このサンプルではキーボードとジョイスティックの両方を同時に扱えるように実装します。
ただし、ジョイスティックに関してはOSが認識している1つ目のジョイスティックのみの対応とし、
アナログの上下値はON/OFFのボタンとして処理します。(詳しくは後述)
このサンプルではDirectInputのラッパークラスCDIPro81を使用しているため、
全ての入力情報はこのクラスから取得出来ます。
また今までの説明で既にCDIPro81は初期化されているはずなので、
入力処理は取得関数を呼び出すだけで完了です。
以下はキーボードの入力を行っています。
// キーボード入力
BYTE key[256];
di.GetKeyboard( key );
GetKeyboardを呼び出すことでその瞬間のキーボードの状態がkeyに返されます。
次に1台目に接続されたジョイスティックの入力を行います。
// 1つ目のジョイスティックの情報を取得 DIJOYSTATE2 js; ZeroMemory( &js,sizeof(js) ); di.GetJoystick( &js,0 );
GetJoystickには取得したデータを格納する構造体のポインタと、
1番目のジョイスティックを指定して呼び出しています。
もしジョイスティックが1台も接続されていなかったりIDが不正だったりした場合でも、
最初に構造体を初期化してから渡しているため、無視してそのまま判定に利用することが出来ます。
このあたりの説明は詳しくはCDIPro81のページを確認してください。
このサンプルではキーボードとジョイスティックを同時に使用出来るようにしますが、
ゲームの仕様的にはそれぞれ押された瞬間、または回された瞬間というのが必要です。
これを1つのキーごとに判定することも出来なくは無いですが、
キーボードとジョイスティックという異なるハードウェアを判定に使用するとなると、
それぞれが絡み合うため複雑な処理を考えなければなりません。
そこでここではまず仮想ハードウェアという概念を考えてみます。
要は逆の発想でゲームに合わせた入力装置をいったん作ってしまうわけです。
そして実際の判定処理はこの仮想ハードウェアに対して行います。
これで判定処理にキーボードやジョイスティックを考えなくてもよいといった利点や、
さらに他の機器からの入力をこの仮想ハードウェアに適用するだけで、
判定処理を変えずとも簡単に機器を追加出来るといった利点があります。
※ハードウェアからの入力を一度統一されたインターフェースに変換するドライバーを作るような感じ
それではこのゲームの仮想ハードウェアについて考えてみましょう。
このゲームではチャンネルごとにキーが押された瞬間にオブジェとの判定を行う仕様となります。
ということは、全てのチャンネルに対して押された瞬間かどうかを記録する変数を用意すれば良いことになります。
以下はここで作成する仮想ハードウェアの仕様です。
番号 | 用途 | 型 | 説明 | |
0 | 白鍵(左) | BOOL | ボタンが押された瞬間のみTRUEとなる。 | |
1 | 黒鍵(左) | BOOL | ボタンが押された瞬間のみTRUEとなる。 | |
2 | 白鍵(中) | BOOL | ボタンが押された瞬間のみTRUEとなる。 | |
3 | 黒鍵(右) | BOOL | ボタンが押された瞬間のみTRUEとなる。 | |
4 | 白鍵(右) | BOOL | ボタンが押された瞬間のみTRUEとなる。 | |
5 | スクラッチ | BOOL | 左右どちらかの回転が発生した瞬間のみTRUEとなる。 また回転中に反対方向へ切り返した時もTRUEとなる。 |
ここではキーボードやジョイスティックの生のデータから、
仮想入力ハードウェアへの変換方法について順番に説明しています。
なんだかものすごく難しそうなサブタイトルですが、
ここでは単に上で決めた仮想入力ハードウェア用のテンポラリ変数を作成するだけです。
具体的にはこの変数は6つのBOOL型の配列として定義するだけです。
// 仮想入力ハードウェア(押された瞬間だけTRUEとなる配列) BOOL press[6]; // 5Key+スクラッチ ZeroMemory( &press,sizeof(press) );
※この配列は1フレームごとに生成されるため特にグローバル変数である必要はありません
まずはこの配列にスクラッチ以外の鍵盤の押下状態を設定します。
鍵盤の押下状態はキーボードの特定のキーが押されているか、
またはジョイスティックの特定のボタンが押されているかで判断しますが、
ここでは対応するキーなどの情報を別途配列で作成しておき一気にループで処理してみます。
// 鍵盤の処理 static const int KEYID[5] = { // キーのリスト DIK_Z, // ch11に割り当てるキー DIK_S, // ch12に割り当てるキー DIK_X, // ch13に割り当てるキー DIK_D, // ch14に割り当てるキー DIK_C, // ch15に割り当てるキー }; static const int JOYID[5] = { // ジョイスティックのボタンリスト 3, // ch11に割り当てるボタン 5, // ch12に割り当てるボタン 1, // ch13に割り当てるボタン 7, // ch14に割り当てるボタン 2 // ch15に割り当てるボタン }; for( i=0;i<5;i++ ) { if( (key[KEYID[i]]&0x80) || (js.rgbButtons[JOYID[i]-1]&0x80) ) { // キーボードかジョイスティックの入力があった場合 if( !bOnKey[i] ) { // まだ押されていなければ押された瞬間とする press[i] = TRUE; bOnKey[i] = TRUE; } } else { // 押されていなければフラグをリセット bOnKey[i] = FALSE; } }
鍵盤はここでは5キー分あるのでキーボードとジョイスティックのボタンもそれぞれ5個ずつ用意しています。
※この配列をキーアサインなどで動的に生成することで、自由にキーをチャンネルに割り当てるといったことが出来たり、
さらに多重配列とすることで同時に割り当てられるキーやボタンの数を増やすことも可能です
さてDirectInputではキーボードやジョイスティックの押下状態を検出するには、
上位ビットが立っているかをチェックする必要があるため、ここでは0x80をAndして判定しています。
なお、ジョイスティックのボタンのインデックスに-1をしているのは、
デバイスとプリンターのジョイスティックから確認できるボタン番号が1から始まっているからで、
これをDirectInputの順番に合わせるため視覚的に-1しています。
※このあたりの処理は各自で修正してみてください
スクラッチは右回転と左回転があり、それぞれどちらに回しても
回した瞬間かどうかを判断しなければなりません。
また例えば右回転から瞬間的に左回転を行った場合も回した瞬間と判断する必要があるため、
ここでは少し複雑な処理を行う必要があります。
まずは現在のスクラッチの回転状態を保持する変数をクラスに用意します。
これは既にクラスヘッダに定義済みです。
int iScratchStatus; // スクラッチの回転方向(-1=左,0=停止,1=右)
この変数はスクラッチの現在の状態として以下の値を保持します。
値 | 意味 |
-1 | 左回転中 |
0 | 停止中 |
1 | 右回転中 |
ゲームの開始時に停止状態とするため、この変数をGameInit内で初期化しておきます。
BOOL CGame::GameInit( void )
{
:
iScratchStatus = 0;
:
}
ではまず右回転についての処理を考えます。
右回転はキーボードの左シフトキーか、ジョイスティックのアナログY軸が
マイナス方向に行っている場合に押されていると判断します。
そして何も操作していない状態から右回転した時はiScratchStatusは0(停止)から1(右回転中)に、
もしくは左回転から即座に右回転を行った場合は-1(左回転中)から1(右回転中)に切り替わり、
この時に仮想ハードウェアのスクラッチボタンが押された瞬間と判断し、
スクラッチ用の配列press[5]にTRUEを設定します。
ちなみにこの状態で続けて右に回していた場合、既にiScratchStatusは1(右回転中)の状態となっているので、
この時は押された瞬間とはなりません。
同様に左回転を考えてみます。
左回転はキーボードのスペースキーか、ジョイスティックのアナログY値が
プラス方向に行っている場合に押されていると判断します。
そして右回転と同じく以前が左回転でない場合のみスクラッチが押された瞬間とみなし、
press[5]にTRUEを設定し、iScratchStatusを-1(左回転中)に更新します。
最後に右回転も左回転も行っていなければiScratchStatusを0(停止)にし、
次の右回転や左回転待ち状態とします。
これらをまとめてプログラムにすると以下のようになります。
// スクラッチの処理 if( (key[DIK_LSHIFT]&0x80) || js.lY<-500 ) { // 右回転 if( iScratchStatus!=1 ) { // 以前が停止か左回りなら回した瞬間とする press[5] = TRUE; iScratchStatus = 1; } } else if( (key[DIK_SPACE]&0x80) || js.lY>500 ) { // 左回転 if( iScratchStatus!=-1 ) { // 以前が停止か右回りなら回した瞬間とする press[5] = TRUE; iScratchStatus = -1; } } else { // 回転停止 iScratchStatus = 0; }
これで仮想入力ハードウェアで定義した押された瞬間だけTRUEとなるpress配列が完成したので、
次はこの配列を使ってオブジェとの当たり判定を行います。
キーの入力方法が分かったところでまだ肝心な判定方法を説明していませんでした。
ということで、ここではキーが押された時にどのように譜面との当たり判定を行えば良いのかを詳しく説明します。
当たり判定は実はそれほど難しくはありません。
簡単に言うと現在のカウンタとオブジェのカウンタの差が0に近いほど良い当たりと判定するだけです。
それでは以下の画像を順に見ていきましょう。
まずゲームが進行して現在のカウンタが4000だったとします。
この時の差分は4800-4000で800もあるため、もしここでキーを押した場合はBADと判定すれば良いでしょう。
ちなみに必要なのは差なので、引く順番は関係なく絶対値として判定します。
時間が経過して現在のカウンタが4500になったとします。
すると4800-4500で差分が300となったので、ここでキーを押していたらGOODと判定すれば良いでしょう。
そしてさらに時間が経過し現在のカウンタが4800になったとします。
この時の差は4800-4800で0となり、ここでキーを押していれば最上級のピカGREATと判定すれば良いでしょう。
その後、現在のカウンタがオブジェを通り越して5000になったとします。
この時の差分は5000-4800で200となり先ほどの300よりは少ないため、
ここではGOODより上のGREATと判定すれば良いでしょう。
これを踏まえ上記のオブジェ1の判定範囲は以下のように見ることが出来ます
そしてこのエリアの幅を変更することでゲーム自体の難易度を変えることが出来るわけです。
ちなみにこの画像内の判定外という部分ですが、これは実際にキーを押しても何も反応しない部分です。
この判定外を無くしてしまうと何もない箇所でキーを押してもBAD判定になってしまい、
プレイヤーが自由にアレンジプレイをすることがを出来なくなります。
※このゲームでは空押しした時には何も鳴りませんが、一番近いオブジェの音を鳴らすなど
自分で好きなように実装してみてください
以下はnow_countが進むと各オブジェの判定結果がどのように変化するかを表しています。
このようにnow_countが徐々に進んでいくとオブジェのカウントに近くなるほど良い判定になりますが、
代わりに良い判定ほどタイミングがシビアになっているのが分かると思います。
ちなみにこの例では途中で判定領域が2つのオブジェに重なっています。
普通は判定を行うオブジェは先に見つかったオブジェに対して行われるため、
2つ重なった状態で後方の方を押したつもりでも、1つ目の方が優先されBADとなってしまうことがありますが、
作り方によっては当たっている方を優先してOKとすることも出来ます。
このあたりは各自で考えて実装するとよいでしょう。
ここではこのゲームの最重要な判定ルーチンについて説明します。
その前にまずは判定時の差分カウント値を変数として定義して置きます。
直接判定時の値をプログラム中に埋め込むことも出来ますが、
あとでゲーム中にリアルタイムに変更したり出来るように、
ここでは定数ではなく変数として定義してみました。
const LONG PKGREAT_RANGE = BMS_RESOLUTION / 48; // GREATと判定する中心からの範囲(前後合わせて24分音符内) const LONG GREAT_RANGE = BMS_RESOLUTION / 32; // GREATと判定する中心からの範囲(前後合わせて16分音符内) const LONG GOOD_RANGE = BMS_RESOLUTION / 8; // GOODと判定する中心からの範囲(前後合わせて4分音符内) const LONG BAD_RANGE = BMS_RESOLUTION / 4; // BADと判定する中心からの範囲(前後合わせて2分音符内) const LONG POOR_RANGE = BMS_RESOLUTION / 2; // POOR判定する中心からの範囲(前後合わせて1小節内)
ちなみにここで定義した値は以下の範囲をあらわしています。
※上記の値とこの画像は一致していません
ここでは新たに1つ定義したものとしてPOOR範囲と言うものがありますが、
これは簡単に言えばキーの入力判定が行われる範囲となり、
その範囲以上のオブジェの判定は行わないようにするための定義です。
このゲームではBADまでの範囲をそのオブジェの判定に使い、
その間にボタンが押されたときはそのオブジェは判定済みとして次の判定から削除されます。
しかしPOORの範囲だった場合は、そのオブジェは削除されずまだ残った状態としています。
これにより、このPOOR範囲で何度もボタンを押すと失敗判定が重なりゲージがどんどん減っていくことになるため、
適当にプレイするとすぐにゲームオーバーにさせることが出来るようになります。
それではこれらの値を使ってオブジェを判定する処理を考えてみます。
実は当たり判定も今まで説明したオブジェの描画処理とほとんど似たような処理を行います。
以下は範囲内に存在するオブジェに対して入力判定を行うプログラムです。
// 判定対象のチャンネルのオブジェをチェック for( i=iStartNum[j+0x11+0x20];i<bms.GetObjeNum(0x11+j);i++ ) { LPBMSDATA b = (LPBMSDATA)bms.GetObje( 0x11+j,i ); if( b->bFlag ) { // まだ未判定のオブジェなら if( b->lTime<(now_count-GOOD_RANGE) ) { // 良判定を過ぎたら全て見逃し扱いとする b->bFlag = FALSE; // オブジェを消す // 判定オブジェをその次からに変更 iStartNum[j+0x11+0x20] = i + 1; // 次のオブジェをチェック continue; } // オブジェが判定外なら抜ける if( (now_count+POOR_RANGE)<=b->lTime ) break; // オブジェが判定内ならキーが押された瞬間かをチェック if( press[j] ) { // キーを押した瞬間なら精度判定 : } } }
まずオブジェのチェックのためのループ処理には既に最適化手法を取り入れており、
この時に使用しているiStartNumのインデックス番号はチャンネル番号に0x20を加算した値を指定しています。
これはコンピュータプレイで使用した配列と同じで、描画と判定を分離するために指定しています。
次にオブジェが存在していた場合の処理ですが、まず最初に見逃したオブジェに対する処理を行っています。
見逃したオブジェであればこのオブジェを終わらせ、次のループではそのオブジェの次から判定出来るように
最適化インデックスの変数を更新しています。
以下は見逃しによりPOORとなるタイミングのイメージです。
ここではオブジェのカウント値が現在のカウント値に対してGOOD範囲を過ぎていたら見逃しとしていますが、
何故ここがBAD範囲を超えた時では無くGOOD範囲を超えた時としているのかについて説明すると、
このサンプルではPOOR範囲が広く取られているため、POOR範囲を過ぎるまで見逃し判定がされないことになり、
つまり見逃し時の表示が即座に行われないという問題が発生します。
また、もしGOOD以降のBADやPOOR範囲内でキーを押したとしても、結局は失敗判定しか行われないため、
これらを考慮してGOOD範囲を超えたら見逃しとしています。
次にオブジェがまだ判定内まで到達していない場合は、このループを抜けるようになっています。
これはオブジェのカウント値が現在のカウント値に対してPOOR範囲より外側かどうかにより判断していますが、
これを描画の処理に例えるとしたら、画面の上部より上だったなら表示をもうやめるという処理の流用です。
そして、これらの判定が全て通らなかった場合というのが実際に判定内にあるオブジェということになり、
ここでキー入力があった場合にそのオブジェの精度判定を行います。
なお、キーの入力判定は上の方で作成した仮想入力ハードウェアの変数、
つまり押された瞬間が記録されているpress配列を参照しています。
press配列はチャンネル順になっているため、単純にforのインデックスを指定するだけで
鍵盤かスクラッチか関係なく同一の処理で判定出来るようになっています。
// 全チャンネル分を処理 for( j=0;j<6;j++ ) { // 判定対象のチャンネルのオブジェをチェック for( i=iStartNum[j+0x11+0x20];i<bms.GetObjeNum(0x11+j);i++ ) { LPBMSDATA b = (LPBMSDATA)bms.GetObje( 0x11+j,i ); if( b->bFlag ) { : // オブジェが判定内ならキーが押された瞬間かをチェック if( press[j] ) { : } } } }
このように仮想入力ハードウェアの概念を取り入れることで、
わざわざこの処理の中でデバイスごとに判定を入れ込む必要が無くなり、
処理を簡略化出来たりして見た目にもスッキリとしたプログラムを作ることが出来ます。
オブジェに対して判定を行う場合は、オブジェのカウント値と現在のカウント値との差を取って精度判定を行います。
また、この差が小さいほど良い判定ということになります。
なお、上記の通りここではBAD範囲より外側でキーを押した場合はPOORとし、そのオブジェは削除せずまだ存在するという仕様にしています。
これはBAD範囲以内ならそのオブジェに対して演奏をしようとしたが、それより外側はそのオブジェに対して弾いたのではなく、
間違えて弾いてしまったというようなイメージで考えてください。
以下はキーが押された時の現在のカウント値とオブジェのカウント値との差を判定し、
BAD以内ならそのオブジェを処理して次から判定をしないようにするプログラムです。
for( i=iStartNum[j+0x11+0x20];i<bms.GetObjeNum(0x11+j);i++ ) { LPBMSDATA b = (LPBMSDATA)bms.GetObje( 0x11+j,i ); if( b->bFlag ) { : // オブジェが判定内ならキーが押された瞬間かをチェック if( press[j] ) { // キーを押した瞬間なら精度判定 LONG sub = abs( now_count-b->lTime ); // オブジェとの差を絶対値で取得 int jadge = 0; // 判定値(0=POOR、1=BAD、2=GOOD、3=GREAT、4=PKGREATなど) if( sub<=PKGREAT_RANGE ) { jadge = 4; } else if( sub<=GREAT_RANGE ) { jadge = 3; } else if( sub<=GOOD_RANGE ) { jadge = 2; } else if( sub<=BAD_RANGE ) { jadge = 1; } if( jadge>=1 ) { // BAD以上ならオブジェを処理 b->bFlag = FALSE; // オブジェを消す // そのオブジェの音を再生 ds.Reset( b->lData ); ds.Play( b->lData ); // 判定オブジェをその次からに変更 iStartNum[j+0x11+0x20] = i + 1; // フラッシュ画像の定義 iFlashCount[j][iFlashIndex[j]] = 45; iFlashIndex[j]++; if( iFlashIndex[j]>2 ) iFlashIndex[j] = 0; // 判定オブジェをその次からに変更 iStartNum[j+0x11+0x20] = i + 1; break; } } } }
ここではまず最初に現在のカウント値からオブジェのカウント値の差分を取得します。
次にその差分がそれぞれの判定値以内に収まっているかで精度を判定しています。
この時注意しなければならないのが、判定は範囲の小さいものから行うことです。
これは広い方を先に判定してしまうと次の判定に行かなくなってしまうためです。
※もし広い方から判定するならばifを入れ子にすることで可能ではありますが、
この場合はあとで面倒なことになるのであまりお勧め出来ません
最後にその結果がBAD以上であればそのオブジェの処理を行いますが、
ここではそのオブジェを鳴らしてそのオブジェを終了とし、
同時に演出用のフラッシュカウンタをセットしています。
なおここでは判定結果による画面への表示は行っていないため、
この辺は自分でGREATなどを表示してみたり、またスコア処理などを実装してみると良いと思います。
ここではカウント値の差分を取って判定を行っていますが、これだとテンポによってカウント値の進み具合が変わってしまうため、
速い曲では判定がシビアになってしまったり、逆に遅い曲では判定が簡単になってしまったりします。
例えばオブジェの位置情報から再生までの時間を逆算し、経過時間との差分で判定することで、
テンポに依存しない判定を行うことが可能です。
ここではキーやスクラッチを操作した時レーンの背景が光る演出を行ってみます。
このサンプルでは白鍵盤、黒鍵盤、スクラッチの3種類のレーンがありますが、
それぞれのレーンの光り方は以下のような感じです。
このバックライトの仕様はキーを押しているとずっと光った状態となるようにし、
キーを離したらフェードアウトする処理とします。
まずはこのフェードアウト用にカウンタ変数が必要ですが、
これは既に以下の用にヘッダに定義されています。
class CGame { : int iBackKeyCount[6]; // キーを離した時の後ろのバックライトの演出用カウンタ : };
またGameInit内にて毎回このカウント変数をリセットするようにしています。
ZeroMemory( &iBackKeyCount,sizeof(iBackKeyCount) );
この変数はキーが押されたら30が入るようになっており、
メインループではこの変数が0より大きな値なら1ずつ減らすといった処理を行います。
ただしここではこのカウントを減らす際にフレームレートに依存しないよう、
60フレーム間隔で処理を行うようにしています。
// 60FPSでのデータ操作 int lp = tm.Run(); for( k=0;k<lp;k++ ) { : // 後ろのバックライト演出 for( i=0;i<6;i++ ) { if( iBackKeyCount[i]>0 ) iBackKeyCount[i]--; } : }
なおキーが入力されているかどうかはpress配列ではなく、
ここではbOnKey配列とiScratchStatusを使います。
press配列は押した瞬間しか分からないため判定でしか使えないので、
ここでは現在の状態を常に記録しているこれらの変数を使うことにしています。
// 鍵盤のバックライト for( i=0;i<5;i++ ) { if( bOnKey[i] ) { // キーが押された状態ならカウンタをリセット iBackKeyCount[i] = 30; } } // スクラッチのバックライト if( iScratchStatus!=0 ) { // 右か左に回している状態ならカウンタをリセット iBackKeyCount[5] = 30; }
これでキーをずっと押している場合は毎フレーム30が設定されるため、
フェードアウトすることがありません。
そして最後にこの変数を見て描画を行います。
// キーのバックライト演出 dd.SetRenderState( D3DRS_DESTBLEND,D3DBLEND_ONE ); // 加算合成 for( i=0;i<6;i++ ) { if( iBackKeyCount[i]>0 ) { dd.SetPutStatus( obj_kind[i]+10,(float)iBackKeyCount[i]/30.0f,1.0f,0 ); // 徐々にフェードアウト dd.Put( obj_kind[i]+10,obj_x[i],0 ); // レーンの種類ごとにバックライト画像を表示 } }
なおこのバックライトはレーンより上でオブジェより下に描画されるように、
オブジェの描画の前に行う必要があります。
ちなみにこのバックライトは光る効果を使うため、
最初に加算合成を行うようにデバイスを設定しています。
これで音ゲーの基本的な作り方の説明は終了となります。
ただ、実はここでは説明していないスクロール幅の変更機能がソースコードの方には既に実装されているのですが、
このあたりはソースコードの方を見て、どのような処理を行っているのか自分で解析してみてください。
また基本的にはこのサイトのサンプルはまだゲームというものではありませんが、
このサンプルをベースにタイトル画面や選曲画面、結果画面を入れて
きちんとしたゲームにしてみるなど、やれることはまだいっぱいあるので
このあたりは自分できちんと仕様を考えて実装してみてください。
見た目的にまったく異なったゲームを作ることも可能ですが、
判定処理や曲データの管理方法はここで紹介したものと基本的には変わらないので、
このサイトの内容を正しく理解出来ていれば、
もうどんな音ゲーでも作れるようになっていると思います。