ゲーム制作講座1−4

4.自機が沢山弾を撃てるようにしよう


 さて、前回は自機が弾を撃てるようにしました。ですが、まだ同時に1発しか撃てません。
最近のシューティングだったらやっぱり最低でも6連射、いえワイドショットなども
ありますから、とにかく沢山弾は撃ちたいものです。

 ではとりあえず2連射できるようにしてみましょう。
 前回と同じように処理枠の中に入れてやります

 ボール君を少し動かして描画→自機の弾1を少し動かして描画→自機の弾2を少し動かして描画→
             四角君を少し動かして描画→描画したものをプレイヤーにみせる


 プログラムは次のようになります。
#include "DxLib.h" // WinMain関数 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int BallX , BallY , BallGraph ; int SikakuX , SikakuY , SikakuMuki , SikakuGraph ; int Shot1X , Shot1Y , Shot1Flag , ShotGraph ; int Shot2X , Shot2Y , Shot2Flag ; // 画面モードの設定 SetGraphMode( 640 , 480 , 16 ) ; // DXライブラリ初期化処理 if( DxLib_Init() == -1 ) return -1; // グラフィックの描画先を裏画面にセット SetDrawScreen( DX_SCREEN_BACK ) ; // ボール君のグラフィックをメモリにロード&表示座標をセット BallGraph = LoadGraph( "Ball.png" ) ; BallX = 288 ; BallY = 400 ; // 四角君のグラフィックをメモリにロード&表示座標をセット SikakuGraph = LoadGraph( "Sikaku.png" ) ; SikakuX = 0 ; SikakuY = 50 ; // 弾のグラフィックをメモリにロード ShotGraph = LoadGraph( "Shot.png" ) ; // 弾1・2が画面上に存在しているか保持する変数に『存在していない』を意味する0を代入しておく Shot1Flag = 0 ; Shot2Flag = 0 ; // 四角君の移動方向をセット SikakuMuki = 1 ; // 移動ルーチン while( 1 ) { // 画面を初期化(真っ黒にする) ClearDrawScreen() ; // ボール君の操作ルーチン { // ↑キーを押していたらボール君を上に移動させる if( CheckHitKey( KEY_INPUT_UP ) == 1 ) BallY -= 3 ; // ↓キーを押していたらボール君を下に移動させる if( CheckHitKey( KEY_INPUT_DOWN ) == 1 ) BallY += 3 ; // ←キーを押していたらボール君を左に移動させる if( CheckHitKey( KEY_INPUT_LEFT ) == 1 ) BallX -= 3 ; // →キーを押していたらボール君を右に移動させる if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 ) BallX += 3 ; // スペースキーを押した場合は処理を分岐 if( CheckHitKey( KEY_INPUT_SPACE ) ) { if( Shot1Flag == 0 ) { // 弾1が画面に存在していない場合は弾1を出す int Bw, Bh, Sw, Sh ; // ボール君と弾の画像のサイズを得る GetGraphSize( BallGraph , &Bw , &Bh ) ; GetGraphSize( ShotGraph , &Sw , &Sh ) ; // 弾1の位置をセット、位置はボール君の中心にする Shot1X = ( Bw - Sw ) / 2 + BallX ; Shot1Y = ( Bh - Sh ) / 2 + BallY ; // 弾1は現時点を持って存在するので、存在状態を保持する変数に1を代入する Shot1Flag = 1 ; } else if( Shot2Flag == 0 ) { // 弾2が画面に存在していない場合は弾2を出す int Bw, Bh, Sw, Sh ; // ボール君と弾の画像のサイズを得る GetGraphSize( BallGraph , &Bw , &Bh ) ; GetGraphSize( ShotGraph , &Sw , &Sh ) ; // 弾2の位置をセット、位置はボール君の中心にする Shot2X = ( Bw - Sw ) / 2 + BallX ; Shot2Y = ( Bh - Sh ) / 2 + BallY ; // 弾2は現時点を持って存在するので、存在状態を保持する変数に1を代入する Shot2Flag = 1 ; } } // ボール君が画面左端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX < 0 ) BallX = 0 ; // ボール君が画面右端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX > 640 - 64 ) BallX = 640 - 64 ; // ボール君が画面上端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY < 0 ) BallY = 0 ; // ボール君が画面下端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY > 480 - 64 ) BallY = 480 - 64 ; // ボール君を描画 DrawGraph( BallX , BallY , BallGraph , FALSE ) ; } // 自機の弾1の移動ルーチン( 存在状態を保持している変数の内容が1(存在する)の場合のみ行う ) if( Shot1Flag == 1 ) { // 弾1を16ドット上に移動させる Shot1Y -= 16 ; // 画面外に出てしまった場合は存在状態を保持している変数に0(存在しない)を代入する if( Shot1Y < -80 ) { Shot1Flag = 0 ; } // 画面に弾1を描画する DrawGraph( Shot1X , Shot1Y , ShotGraph , FALSE ) ; } // 自機の弾2の移動ルーチン( 存在状態を保持している変数の内容が1(存在する)の場合のみ行う ) if( Shot2Flag == 1 ) { // 弾2を16ドット上に移動させる Shot2Y -= 16 ; // 画面外に出てしまった場合は存在状態を保持している変数に0(存在しない)を代入する if( Shot2Y < -80 ) { Shot2Flag = 0 ; } // 画面に弾2を描画する DrawGraph( Shot2X , Shot2Y , ShotGraph , FALSE ) ; } // 四角君の移動ルーチン { // 四角君の座標を移動している方向に移動する if( SikakuMuki == 1 ) SikakuX += 3 ; if( SikakuMuki == 0 ) SikakuX -= 3 ; // 四角君が画面右端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX > 576 ) { SikakuX = 576 ; SikakuMuki = 0 ; } // 四角君が画面左端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX < 0 ) { SikakuX = 0 ; SikakuMuki = 1 ; } // 四角君を描画 DrawGraph( SikakuX , SikakuY , SikakuGraph , FALSE ) ; } // 裏画面の内容を表画面にコピーする ScreenFlip() ; // Windows 特有の面倒な処理をDXライブラリにやらせる // -1 が返ってきたらループを抜ける if( ProcessMessage() < 0 ) break ; // もしESCキーが押されていたらループから抜ける if( CheckHitKey( KEY_INPUT_ESCAPE ) ) break ; } DxLib_End() ; // DXライブラリ使用の終了処理 return 0 ; // ソフトの終了 }

<実行図>

弾の絵

 いやおうなしに2連射してしまいますが、とりあえず今は沢山弾を出すことだけに
専念しましょう。とりあえず2つ弾を出すことには成功しました。
 上のプログラムでは今まで ShotX ShotY ShotFlag だったものを Shot1X Shot1Y
Shot1Flag
に変更し、さらに Shot2X Shot2Y Shot2Flag を追加しました。扱いかたは
全く同じで、殆ど同じプログラムがもう一つ増えただけです。

 …。
 この方法でどんどん増やしていくと考えますと、同時に20個の弾を画面上に飛ばすには
同じプログラムを20回書かなければなりません。とんでもないことです。

 そこで登場するのが変数配列です。
 変数配列とは int i ; が普通の変数だとすると int i[10] ; と表記されているのが
変数配列だということは御存知かと思います。

 まず普通の変数で10個の弾のデータを宣言するとしましょう、すると…

int Shot0X, Shot0Y, Shot0Flag ;
int Shot1X, Shot1Y, Shot1Flag ;
int Shot2X, Shot2Y, Shot2Flag ;
int Shot3X, Shot3Y, Shot3Flag ;
int Shot4X, Shot4Y, Shot4Flag ;
int Shot5X, Shot5Y, Shot5Flag ;
int Shot6X, Shot6Y, Shot6Flag ;
int Shot7X, Shot7Y, Shot7Flag ;
int Shot8X, Shot8Y, Shot8Flag ;
int Shot9X, Shot9Y, Shot9Flag ;

 となります。そして使う時もいちいち Shot0X = 0 ; Shot1X = 0 ; ... 以下略
と順順に記述してゆく必要があります。ああ、面倒くさいですね…
 次に変数配列で10個分の弾のデータを宣言するとしましょう。
 するとなんと

int ShotX[10] , ShotY[10] , ShotFlag[10] ;

 だけで済みます。おお、素晴らしく楽ですね。ちなみに配列とは int ShotX[xx] ;
表記した場合 int 型変数を xx 個宣言したことと同じになります。
ShotX[0] に入っている数値も ShotX[1] に入っている数値も ShotX[2] に入っている
数値もすべてモノは違い、独立しています。
 つまり今回の場合

ShotX[0]Shot0X と同じことを示していて ShotFlag[5]Shot5Flag と同じことを示しています。

 さて、でもここで10個のデータに対して普通の変数で処理していたとの同じように
ShotX[0] = 0 ; ShotX[1] = 0 ; ShotX[2] = 0 ; と配列の各要素を順順に処理して
いたのでは今までと全く変わりません。
 変数配列を使う最大の利点は、要素番号を ShotX[i] のように変数で指定して動的に
変化させることにより、一つのプログラムで複数のデータを処理できる
ことにあります。
 ……。
 説明していてもよくわかりませんが、とりあえずすべての弾のX座標を100にすることを
考えてみましょう。今までのように一つ一つに代入していったら

ShotX[0] = 100 ;
ShotX[1] = 100 ;
ShotX[2] = 100 ;
ShotX[3] = 100 ;
ShotX[4] = 100 ;
ShotX[5] = 100 ;
ShotX[6] = 100 ;
ShotX[7] = 100 ;
ShotX[8] = 100 ;
ShotX[9] = 100 ;

 となりますが、変数配列の要素番号指定を変数で指定することにより

int i ;
for( i = 0 ; i < 10 ; i ++ )
{
  ShotX[i] = 100 ;
}

 とするだけで済むのです。上のループではまず変数 iを代入しループの中では
ShotX 配列要素番号 i100 を代入しています。この i は毎ループごとに
インクリメント(1足すこと)されているので、つまり

ShotX[i] = 100  // i = 0 1回目のループ
ShotX[i] = 100  // i = 1 2回目のループ
ShotX[i] = 100  // i = 2 3回目のループ
ShotX[i] = 100  // i = 3 4回目のループ
ShotX[i] = 100  // i = 4 5回目のループ
ShotX[i] = 100  // i = 5 6回目のループ
ShotX[i] = 100  // i = 6 7回目のループ
ShotX[i] = 100  // i = 7 8回目のループ
ShotX[i] = 100  // i = 8 9回目のループ
ShotX[i] = 100  // i = 9 10回目のループ

 となり、10個のデータすべてに100を代入させることが出来るわけです。
 つまり変数配列同じような処理を複数のデータに対して行う場合にもってこい
なのです。
 ではとりあえず先程の2つまで弾を撃てるプログラムを変数をそれぞれ2こ用意して
処理するのではなく変数配列と、それを処理するための for 文を使って組んでみたいと
思います。
 出来たのが下記のプログラムです。

 
#include "DxLib.h" // WinMain関数 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int BallX , BallY , BallGraph ; int SikakuX , SikakuY , SikakuMuki , SikakuGraph ; int ShotX[2] , ShotY[2] , ShotFlag[2] , ShotGraph ; int i ; // 画面モードの設定 SetGraphMode( 640 , 480 , 16 ) ; // DXライブラリ初期化処理 if( DxLib_Init() == -1 ) return -1; // グラフィックの描画先を裏画面にセット SetDrawScreen( DX_SCREEN_BACK ) ; // ボール君のグラフィックをメモリにロード&表示座標をセット BallGraph = LoadGraph( "Ball.png" ) ; BallX = 288 ; BallY = 400 ; // 四角君のグラフィックをメモリにロード&表示座標をセット SikakuGraph = LoadGraph( "Sikaku.png" ) ; SikakuX = 0 ; SikakuY = 50 ; // 弾のグラフィックをメモリにロード ShotGraph = LoadGraph( "Shot.png" ) ; // 弾1・2が画面上に存在しているか保持する変数に『存在していない』を意味する0を代入しておく for( i = 0 ; i < 2 ; i ++ ) { ShotFlag[i] = 0 ; } // 四角君の移動方向をセット SikakuMuki = 1 ; // 移動ルーチン while( 1 ) { // 画面を初期化(真っ黒にする) ClearDrawScreen() ; // ボール君の操作ルーチン { // ↑キーを押していたらボール君を上に移動させる if( CheckHitKey( KEY_INPUT_UP ) == 1 ) BallY -= 3 ; // ↓キーを押していたらボール君を下に移動させる if( CheckHitKey( KEY_INPUT_DOWN ) == 1 ) BallY += 3 ; // ←キーを押していたらボール君を左に移動させる if( CheckHitKey( KEY_INPUT_LEFT ) == 1 ) BallX -= 3 ; // →キーを押していたらボール君を右に移動させる if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 ) BallX += 3 ; // スペースキーを押した場合は処理を分岐 if( CheckHitKey( KEY_INPUT_SPACE ) ) { // 画面上にでていない弾があるか、弾の数だけ繰り返して調べる for( i = 0 ; i < 2 ; i ++ ) { // 弾iが画面上にでていない場合はその弾を画面に出す if( ShotFlag[i] == 0 ) { int Bw, Bh, Sw, Sh ; // ボール君と弾の画像のサイズを得る GetGraphSize( BallGraph , &Bw , &Bh ) ; GetGraphSize( ShotGraph , &Sw , &Sh ) ; // 弾iの位置をセット、位置はボール君の中心にする ShotX[i] = ( Bw - Sw ) / 2 + BallX ; ShotY[i] = ( Bh - Sh ) / 2 + BallY ; // 弾iは現時点を持って存在するので、存在状態を保持する変数に1を代入する ShotFlag[i] = 1 ; // 一つ弾を出したので弾を出すループから抜けます break ; } } } // ボール君が画面左端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX < 0 ) BallX = 0 ; // ボール君が画面右端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX > 640 - 64 ) BallX = 640 - 64 ; // ボール君が画面上端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY < 0 ) BallY = 0 ; // ボール君が画面下端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY > 480 - 64 ) BallY = 480 - 64 ; // ボール君を描画 DrawGraph( BallX , BallY , BallGraph , FALSE ) ; } // 弾の数だけ弾を動かす処理を繰り返す for( i = 0 ; i < 2 ; i ++ ) { // 自機の弾iの移動ルーチン( 存在状態を保持している変数の内容が1(存在する)の場合のみ行う ) if( ShotFlag[ i ] == 1 ) { // 弾iを16ドット上に移動させる ShotY[ i ] -= 16 ; // 画面外に出てしまった場合は存在状態を保持している変数に0(存在しない)を代入する if( ShotY[ i ] < -80 ) { ShotFlag[ i ] = 0 ; } // 画面に弾iを描画する DrawGraph( ShotX[ i ] , ShotY[ i ] , ShotGraph , FALSE ) ; } } // 四角君の移動ルーチン { // 四角君の座標を移動している方向に移動する if( SikakuMuki == 1 ) SikakuX += 3 ; if( SikakuMuki == 0 ) SikakuX -= 3 ; // 四角君が画面右端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX > 576 ) { SikakuX = 576 ; SikakuMuki = 0 ; } // 四角君が画面左端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX < 0 ) { SikakuX = 0 ; SikakuMuki = 1 ; } // 四角君を描画 DrawGraph( SikakuX , SikakuY , SikakuGraph , FALSE ) ; } // 裏画面の内容を表画面にコピーする ScreenFlip() ; // Windows 特有の面倒な処理をDXライブラリにやらせる // -1 が返ってきたらループを抜ける if( ProcessMessage() < 0 ) break ; // もしESCキーが押されていたらループから抜ける if( CheckHitKey( KEY_INPUT_ESCAPE ) ) break ; } DxLib_End() ; // DXライブラリ使用の終了処理 return 0 ; // ソフトの終了 }

<実行図>

 実行してみても、内部の処理を変えただけなので見た目は全く同じだと思います。
 なお、今は画面上に弾は2つだけですので、最初の配列宣言や、for 文のマックス値を2に
していますが、次のように #define を使うことにより、下記のプログラムでいう
3行目の『#define SHOT 2』の『2』の値を変えてやるとにより、同時に出せる弾の数を
簡単に変えることが出来ます。試しに『#define SHOT 20』等にしてみてください、20個
まで弾が出せるようになると思います。

#include "DxLib.h" #define SHOT 2 // WinMain関数 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { int BallX , BallY , BallGraph ; int SikakuX , SikakuY , SikakuMuki , SikakuGraph ; int ShotX[SHOT] , ShotY[SHOT] , ShotFlag[SHOT] , ShotGraph ; int i ; // 画面モードの設定 SetGraphMode( 640 , 480 , 16 ) ; // DXライブラリ初期化処理 if( DxLib_Init() == -1 ) return -1; // グラフィックの描画先を裏画面にセット SetDrawScreen( DX_SCREEN_BACK ) ; // ボール君のグラフィックをメモリにロード&表示座標をセット BallGraph = LoadGraph( "Ball.png" ) ; BallX = 288 ; BallY = 400 ; // 四角君のグラフィックをメモリにロード&表示座標をセット SikakuGraph = LoadGraph( "Sikaku.png" ) ; SikakuX = 0 ; SikakuY = 50 ; // 弾のグラフィックをメモリにロード ShotGraph = LoadGraph( "Shot.png" ) ; // 弾が画面上に存在しているか保持する変数に『存在していない』を意味する0を代入しておく for( i = 0 ; i < SHOT ; i ++ ) { ShotFlag[i] = 0 ; } // 四角君の移動方向をセット SikakuMuki = 1 ; // 移動ルーチン while( 1 ) { // 画面を初期化(真っ黒にする) ClearDrawScreen() ; // ボール君の操作ルーチン { // ↑キーを押していたらボール君を上に移動させる if( CheckHitKey( KEY_INPUT_UP ) == 1 ) BallY -= 3 ; // ↓キーを押していたらボール君を下に移動させる if( CheckHitKey( KEY_INPUT_DOWN ) == 1 ) BallY += 3 ; // ←キーを押していたらボール君を左に移動させる if( CheckHitKey( KEY_INPUT_LEFT ) == 1 ) BallX -= 3 ; // →キーを押していたらボール君を右に移動させる if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 ) BallX += 3 ; // スペースキーを押した場合は処理を分岐 if( CheckHitKey( KEY_INPUT_SPACE ) ) { // 画面上にでていない弾があるか、弾の数だけ繰り返して調べる for( i = 0 ; i < SHOT ; i ++ ) { // 弾iが画面上にでていない場合はその弾を画面に出す if( ShotFlag[i] == 0 ) { int Bw, Bh, Sw, Sh ; // ボール君と弾の画像のサイズを得る GetGraphSize( BallGraph , &Bw , &Bh ) ; GetGraphSize( ShotGraph , &Sw , &Sh ) ; // 弾iの位置をセット、位置はボール君の中心にする ShotX[i] = ( Bw - Sw ) / 2 + BallX ; ShotY[i] = ( Bh - Sh ) / 2 + BallY ; // 弾iは現時点を持って存在するので、存在状態を保持する変数に1を代入する ShotFlag[i] = 1 ; // 一つ弾を出したので弾を出すループから抜けます break ; } } } // ボール君が画面左端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX < 0 ) BallX = 0 ; // ボール君が画面右端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallX > 640 - 64 ) BallX = 640 - 64 ; // ボール君が画面上端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY < 0 ) BallY = 0 ; // ボール君が画面下端からはみ出そうになっていたら画面内の座標に戻してあげる if( BallY > 480 - 64 ) BallY = 480 - 64 ; // ボール君を描画 DrawGraph( BallX , BallY , BallGraph , FALSE ) ; } // 弾の数だけ弾を動かす処理を繰り返す for( i = 0 ; i < SHOT ; i ++ ) { // 自機の弾iの移動ルーチン( 存在状態を保持している変数の内容が1(存在する)の場合のみ行う ) if( ShotFlag[ i ] == 1 ) { // 弾iを16ドット上に移動させる ShotY[ i ] -= 16 ; // 画面外に出てしまった場合は存在状態を保持している変数に0(存在しない)を代入する if( ShotY[ i ] < -80 ) { ShotFlag[ i ] = 0 ; } // 画面に弾iを描画する DrawGraph( ShotX[ i ] , ShotY[ i ] , ShotGraph , FALSE ) ; } } // 四角君の移動ルーチン { // 四角君の座標を移動している方向に移動する if( SikakuMuki == 1 ) SikakuX += 3 ; if( SikakuMuki == 0 ) SikakuX -= 3 ; // 四角君が画面右端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX > 576 ) { SikakuX = 576 ; SikakuMuki = 0 ; } // 四角君が画面左端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する if( SikakuX < 0 ) { SikakuX = 0 ; SikakuMuki = 1 ; } // 四角君を描画 DrawGraph( SikakuX , SikakuY , SikakuGraph , FALSE ) ; } // 裏画面の内容を表画面にコピーする ScreenFlip() ; // Windows 特有の面倒な処理をDXライブラリにやらせる // -1 が返ってきたらループを抜ける if( ProcessMessage() < 0 ) break ; // もしESCキーが押されていたらループから抜ける if( CheckHitKey( KEY_INPUT_ESCAPE ) ) break ; } DxLib_End() ; // DXライブラリ使用の終了処理 return 0 ; // ソフトの終了 }
 さて、弾を沢山出すことは出来るようになりました。
 そろそろ敵の当たり判定を行いたいとおもうのですが、その前に今のショットボタンを
押すと必ず連射になってしまう現象
を何とかしたいと思います。

戻る