サンプルコードのダウンロード
では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_09.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_09.exe」をダブルクリックし、実行してみてください。
どうでしょう?カーソルキーで自機が移動し、スペースキーを押すと、弾が発射されると思います。
というわけで今回は、弾処理について解説したいと思います。
構造化プログラミング基礎
ここのプログラミング講座を見ている人は、ほとんどが初心者だと思います。
そして初心者にプログラムを組ませてみると、高い確率で、WinMain(またはmain)関数内にひたすら書きまくるようです。
たしかに、それでも動くには動くのですが、プログラムが複雑になってくると、どうしても管理がしにくくなってきますよね?
そこで、ある程度固まった処理、例えば「キー入力でキャラを動かす」だとか「キャラクターを表示する」
というような処理を区切って記述してやります。
そうする事で、プログラムを管理しやすくなったり、バグを発見しやすくなります。
ようするに役割ごとにサブルーチン化するって事です。
弾処理のアルゴリズム基礎
弾の発射から消えて無くなるまでの大まかな流れは以下の通りです。
- 弾用の変数(構造体等)を複数、用意する
- キー入力があったら、弾データを作成する
- 弾を移動させる
- 弾が画面外に出たら、弾データを消す
- 弾表示
- 2へ
重要なポイントは、弾のデータを作ったら、その後、弾のデータを移動させたり削除したりする事でしょうかねぇ。
実際にプログラムを組む
では、判りやすい(たぶん)ように、順番に説明していきたいと思います。
まず、構造化プログラムという事で、必要な変数等をグローバル変数として宣言してやります(ソースの出来るだけ上に書く。
//サイン、コサイン テーブル float fcos[360],fsin[360]; //自機用変数 struct _player{ float x,y; //座標 int renda; //連打用フラグ }player={320.0f-32,240.0f-32,0};//初期化 #define BMAX 100 //画面内に最大100弾とする(爆 //弾用変数 struct _bullet{ BOOL enable; //使用:1 未使用:0 float x,y; //座標 short angle; //進む角度 }bullet[BMAX];
こうする事で、どこでも(どの自作関数内でも)これらの変数を参照する事が出来るようになります。
今まで、プレイヤーの座標はWinMain関数内でcx,cyと宣言し、それらを使用していましたが、今回は構造体(_player)として宣言します。
_bullet構造体は、弾の処理に使用します。
今回使う変数はとりあえず、使用・未使用を表すフラグ(enable)と
座標(x,y)と、進む方向(angle)だけです。
では、一番最初に実行するWinMain関数を見てみましょう。
//----------[ メイン関数 ]---------------------------------------------------------------------- int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){ //ウィンドウ作成 if(!InitWindow(hInstance,hPrevInstance,nCmdShow)) Quit("ウィンドウ作成に失敗しました。"); //DirectDraw開始 if(!StartDirectDraw(hw)) Quit("DirectDrawの初期化に失敗しました。"); //ゲーム初期化 InitGame(); //ゲームループへ GameLoop(); return(FALSE); }
いやぁ、今まではここ(↑)にひたすら書いていたので、今回のプログラムは結構すっきり見えますね。
InitWindow()はウィンドウを作成する処理を書いてあるだけの自作関数なので解説省略。
で、DirectDrawを初期化したらInitGame();を呼び出していますね?
これはゲームに必要な処理を先にしておく自作関数です。中を見てみましょう。
//----------[ ゲーム初期化 ]-------------------------------------------------------------------- void InitGame(void){ //ビットマップからパレット読み込み LoadPalette("ss.bmp"); //オフスクリーンサーフェイス(lpWork)へビットマップ読み込み LoadBitmap(lpWork,"ss.bmp",0,0,72,64); //透明色指定(パレット0番の黒色を透明色とする) SetTransColor(lpWork,0); //弾データ初期化 for(int i=0;i<BMAX;i++) bullet[i].enable=0; //全て未使用データとする //サイン、コサインテーブル作成 for(i=0;i<360;i++){ fsin[i]=(float)sin(i*3.1415926535/180); fcos[i]=(float)cos(i*3.1415926535/180); } }
まず、作業用サーフェイス(lpWork)にキャラの画像を読み込んでおきます(左の絵)。
で、_bullet構造体のメンバ変数「enable」を0に設定しておきます(全て)。
enableが0だったら、その構造体は使用していないという事にします。
で、ここの処理が終わると、処理はWinMainの方へ戻って、次はGameLoop();を呼び出していますよね?
この関数はゲームのループ用の自作関数です。コードは次のようになっています。
//----------[ メインループ ]-------------------------------------------------------------------- void GameLoop(void){ while(1){ WaitSet(); //現在の時間(単位:ミリ秒)取得 MovePlayer(); //自機移動 Shoot(); //弾発射判定 MoveBullet(); //弾移動 Show(); //画像表示 Wait(1000/60); //メッセージループへ } }
whileの中の最初と最後のWaitSet()、Wait()については既にやっているので解説省略。
で、まずMovePlayer()関数を呼び出していますよね?こいつはキー入力により自機を動かす処理をする自作関数です。
//----------[ 自機移動 ]------------------------------------------------------------------------ void MovePlayer(void){ int angle=-1; //とりあえず角度を-1にしておく //[38]↑ [40]↓ [37]← [39]→ if(keyg[38] && keyg[39]) //右上 angle=45; else ・ ・ ・ (略) //angleの値が変わっていたらキャラクタの座標を変更する float mv=4.0f; //移動量 if(angle!=-1){ player.x+=fcos[angle]*mv; player.y-=fsin[angle]*mv; } }
この関数内でやっていることは第5回で説明したので、みりゃ判りますね?
ここの処理が終わったら、GameLoopの方へ戻って、次にShoot()関数が呼ばれています。
Shoot()関数では、スペースキーが押されているか判定し、押されていれば弾を作成します。
ここでは、シューティングゲームで使うような、押しっぱなしでも連射できる処理をやります。
//----------[ 弾発射判定 ]---------------------------------------------------------------------- void Shoot(void){ int i; //弾発射判定 if(keyg[32]){ //キーが押されている場合 if(!player.renda) //変数rendaが0なら打つ for(i=-2;i<=2;i++) //弾作成 CreateBullet(player.x+28,player.y-5,90+i*15); player.renda++; player.renda%=10; //打つ間隔 } else { //キーが押されていない場合 if(player.renda){ player.renda++; player.renda%=10; } } }
ここでは、ちょっとした工夫が必要ですかね。
スペースキーが押されていた場合に弾を作成するだけでは、
押している間、毎回弾が作成されてしまいますよね?
シューティングゲームのように、一定間隔で弾を撃ちたい場合は、一つ変数を用意し(今回はplayer.renda)、スペースキーが押されている間はこの変数に1を足し、10になったら0に戻してやります。
で、この変数が0の時だけ弾を作成すれば、処理的には10ループに1回弾を発射する事になります。
さて、ここで極端な例を挙げてみます。この弾を出す間隔が10では無く、1000だとしましょう。
普通、ゲームで発射キーを押しっぱなしにする場合、発射キーを押した瞬間に弾が発射され、一定間隔で弾がでますよね?
で、もし変数「player.renda」が仮に500で止まっていたらどうでしょう。
発射キーを押しても、player.rendaは0でないため、しばらく発射されませんね?
これではマズイので、発射キーが押されていない時、player.rendaが0で無い場合には、player.rendaに1を足し、ある数を越えたら0にする、といった処理を付け加えてやります。こうすれば、発射キーを離している間でもplayer.rendaの値が変化するので、発射キーを離し、しばらくして発射キーを押した瞬間に、弾を発射させる事が可能となります。
ついでですが、発射キーが押されていない時にplayer.rendaを0にすると、いつでも発射キーを押した瞬間に弾が出せます(実際にやってみてください)。
ここの関数内では弾を作成する時にCreateBullet( x座標 , y座標 , 角度(0~359) )自作関数を呼び出しています。
ではどういった処理をしているのか見てみましょう。
//----------[ 弾作成 ]-------------------------------------------------------------------------- void CreateBullet(float x,float y,short angle){ int i; int no=-1; //作成する弾のデータ番号 //構造体の中で、空いている(未使用)データを探す for(i=0;i<BMAX;i++) if(bullet[i].enable==0){ no=i; break; } //空いているデータが見つからなかったら作成せずに戻る if(no==-1) return; //データ詰める bullet[no].x=x; bullet[no].y=y; bullet[no].angle=angle; bullet[no].enable=1; //使用中にする }
さて、初めの方で呼び出したInitGame()内では_bullet構造体の全てのenableを0にし、未使用データとしましたね?
ですから、ここ(CreateBullet)では、構造体の配列の0番からBMAX-1(今回は99)までを順に参照し、enableが0(つまり未使用となっているデータ)を探します。
そんでもって、空いているデータが見つかったら、その番号の構造体に情報を詰め込んでやります。
最後にenableを1とし、no番の構造体を使用データという事にしていますよね?
ですから、次にCreateBullet関数内で空いているデータを順に探した時も、no番のenableは0では無いので
上から新しくデータが詰められる事はありません。
でShoot()関数へ戻り、GameLoop()の方へ戻り、次にMoveBullet()を呼び出しています。
//----------[ 弾移動 ]-------------------------------------------------------------------------- void MoveBullet(void){ int i; float mv=6.0f; for(i=0;i<BMAX;i++){ if(bullet[i].enable==0) //未使用データだったら次の処理へ continue; //移動 bullet[i].x+=fcos[bullet[i].angle]*mv; bullet[i].y-=fsin[bullet[i].angle]*mv; //画面外(今回は画面上)に出たら未使用データにする if(bullet[i].y<-32) bullet[i].enable=0; } }
ここでは、構造体_bulletの配列を0から順番に参照し、enableが0でないデータ(つまり使用しているデータ)だけを処理します。
角度による移動自体は第4回で説明したので省略。
そして、画面外に出たらenableを0にし、未使用データとしてやります。
GameLoop()に戻り、次にShow()が呼び出されます。これは、ディスプレイに一連の表示をする
自作関数です。
コードを見てみましょう。
//----------[ 画像表示 ]------------------------------------------------------------------------ void Show(void){ ClearScreen(lpBack); //バックバッファ初期化 DdTextOut(lpBack,0,0,"カーソルキーで移動 spaceキーで弾発射 pauseキーで終了",255); int i; //弾表示 for(i=0;i<BMAX;i++) if(bullet[i].enable==1) BltClip(lpBack,(int)bullet[i].x,(int)bullet[i].y,8,32,lpWork,64,0,1); //自機転送 BltClip(lpBack,(int)player.x,(int)player.y,64,64,lpWork,0,0,1); //フリップ Flip(); }
まぁ、ここは見りゃ判りますね(爆
弾は使用しているものだけ表示してやります。
とりあえず、これで説明は終わりです(^^;
今回のプログラムの流れを図で表すと、こんな感じになるのかも
矢印は関数の呼び出しです。
おまけ
今回は弾のデータ構造に構造体の配列を使用しましたが、この場合、配列中に使用しているデータと使用していないデータが両方含まれていたり、弾数に制限があったりして少し不便ですよね?
そんなときはリスト構造を使用すると結構スッキリ記述できます。
Space Soldierは双方向リスト構造でやってました。