119ドット絵エディタ「EDGE2」、MIDI音楽編集ソフト「Domino」、楽曲などを配布している個人サイトです。ゲーム制作基礎編サンプルコードのダウンロードでは、まずサンプルコードをダウンロードし、解凍してください。C++でプロジェクトファイルを開いてください(「game_11.dsw」をダブルクリックすれば開けます)。圧縮ファイルに含まれる「game_11.exe」をダブルクリックし、実行してみてください。?画面が切り替わり、フルスクリーン化すると思います。どこからか弾が沸き、自機(スライム)の方向へ飛んできますよね?というわけで、今回は角度の算出とアニメーションについて説明していきたいと思います。その1第4回では、三角関数としてsin、cos、tanを説明しました。これだけで、敵から、自機への角度を算出する事は出来ません。そこで、今回はatan(アークタンジェント)というものを使用します。普通のtanはtanΘ=b/aでしたが、アークタンジェントはΘ=atan(b/a)となります。要するに、タンジェントの逆算みたいな感じですね。その2というわけで、基本が解れば、あとは当てはめるだけですね。Θ=atan((sy-my)/(sx-mx))とすればいいですよね?ただし、y/xが例えば1/1、-1/-1の場合、値は一緒ですよね?という事で、atanの返値Θも半分の180度周期になってしまいます。(ちなみにsin,cosは360度周期,sin(0)=sin(360)=sin(360*n))そこで、x,yの符号を利用して、ある条件の時はΘに180を足すと言った処理が必要になってきます。xが0の場合、y/0=∞となってしまい、エラーとなってしまいます。これも回避しなければなりません。上でも書いたように、返ってくる角度Θは180度周期だったり、xが0だった場合にエラーとなったりします。いう関数が用意されています。この、yとxに値を引き渡してやると、360度周期で角度を返してくれます。xが0の時の処理もしっかりやってくれるので便利です。atanもそうですが、atan2の返値の単位はラジアンです(0~360度=0~2π)。atan2の場合は-π~+πまでが返ってくるので、atan2の返値をπで割れば、-1~+1までになりますよね?そしてπは180°ですから、その値に180を掛けてやると、値は-180°~+180°になりますね?これに、360°を足し、360で割った余りを出すと、0~359°の値が出てきます。angle;tmp=atan2(-double(sy-my),double(sx-mx));tmp/=3.1415926535;tmp*=180;tmp+=360;angle=(short)tmp%360;こんな感じかな。キー入力によって弾を発射」を少し改良し、ランダムに弾を作成し、自機の方向へ飛ばすようにします。弾の移動等、詳しい事は第9回を参照してください。今回は、ついでに弾をアニメーションさせる事にするので、_bullet構造体にanimationという変数を追加しておきます。angle;//進む角度}bullet[BMAX];では、メインループを見てみます。GameLoop(void){while(1){WaitSet();//現在の時間(単位:ミリ秒)取得MovePlayer();//自機移動Shoot();//どこからともなく沸く弾作成MoveBullet();//弾移動Show();//画像表示Wait(1000/60);//メッセージループへ}}第9回と見た目が同じですが、Shoot()とMoveBullet()、Show()の中身を変更しています。x,y,angle;if(rand()%100==0){x=rand()%640;//x,y座標はランダムy=rand()%480;angle=GetAngle(x+8,y+8,(int)player.x+16,(int)player.y+16);//自機への角度取得CreateBullet((float)x,(float)y,angle);}}ここでは、1/100の確立で適当な位置を出し、自機への角度を取得して、弾を作成しています。rand()という関数は0~RAND_MAXまでの乱数を返してくれるので、適当な値pで割った余りを出せば、0からpのランダム値が出せます(度数分布が崩れるので本当はあまりよろしくない)。sy);関数は自作関数で、mx,myからsx,syまでの角度を算出します。short(atan2(-double(sy-my),double(sx-mx))/3.141592*180+360)%360;}この自作関数は上の方で説明したので解りますね(^^;で、弾をアニメーションさせるためには、弾移動中にanimationの値をどんどん増やしていきます。弾のアニメーションは12パターンありますよね?ですから、animationの値は0~11の間を繰り返し変化するようにプログラムしてやります。後は、このanimationの値をうまく画像に照らし合わせて画面に表示してやればいいわけです。i;//弾表示for(i=0;i<BMAX;i++)if(bullet[i].enable==1)BltClip(lpBack,(int)bullet[i].x,(int)bullet[i].y,16,16,lpWork,32+16*bullet[i].animation,0,1);//自機転送BltClip(lpBack,(int)player.x,(int)player.y,32,32,lpWork,0,0,1);//フリップFlip();}このアニメーションは、繰り返し行われる弾やら敵に限りますね。自機のようなキー入力によって画像を変えたいときには使えません。ゲーム制作基礎編サンプルコードのダウンロードでは、まずサンプルコードをダウンロードし、解凍してください。C++でプロジェクトファイルを開いてください(「game_10.dsw」をダブルクリックすれば開けます)。圧縮ファイルに含まれる「game_10.exe」をダブルクリックし、実行してみてください。?画面が切り替わり、フルスクリーン化すると思います。スペースキーを押すと、スライムがジャンプすると思います。いうわけで今回は、ジャンプについて解説したいと思います。慣性の時と処理が似ている(というかほとんど一緒)のですが、違うのは、Y座標に進む値(今回はsy)に重力として毎回一定の値を足すだけです。サンプルコードを見ていきます。今回は画面下が地面という設定にします。cy==448.0f)sy=-12.0f;//ジャンプ力cy+=sy;//慣性(?sy+=0.3f;//重力を加えるif(cy>=448)//画面より下に行かないようにするcy=448.0f;//キャラクタ表示BltClip(lpBack,cx,(int)cy,32,32,lpWork,0,0,1);Flip();//フリップWait(1000/60);//メッセージループへ}自然落下は、空から地面に向かって重力によって加速しながら進んでいきますよね?車で言えば、どんどんアクセル踏んでスピード出して加速していくようなものです。今回のプログラムの場合、syがスピードのようなものになり、重力によってそのスピードがどんどん増加していく形になります。ゲーム制作基礎編サンプルコードのダウンロードでは、まずサンプルコードをダウンロードし、解凍してください。C++でプロジェクトファイルを開いてください(「game_09.dsw」をダブルクリックすれば開けます)。圧縮ファイルに含まれる「game_09.exe」をダブルクリックし、実行してみてください。?カーソルキーで自機が移動し、スペースキーを押すと、弾が発射されると思います。いうわけで今回は、弾処理について解説したいと思います。構造化プログラミング基礎ここのプログラミング講座を見ている人は、ほとんどが初心者だと思います。初心者にプログラムを組ませてみると、高い確率で、WinMain(またはmain)関数内にひたすら書きまくるようです。たしかに、それでも動くには動くのですが、プログラムが複雑になってくると、どうしても管理がしにくくなってきますよね?そこで、ある程度固まった処理、例えば「キー入力でキャラを動かす」だとか「キャラクターを表示する」というような処理を区切って記述してやります。する事で、プログラムを管理しやすくなったり、バグを発見しやすくなります。ようするに役割ごとにサブルーチン化するって事です。弾の発射から消えて無くなるまでの大まかな流れは以下の通りです。弾用の変数(構造体等)を複数、用意するキー入力があったら、弾データを作成する弾を移動させる弾が画面外に出たら、弾データを消す弾表示2へ重要なポイントは、弾のデータを作ったら、その後、弾のデータを移動させたり削除したりする事でしょうかねぇ。実際にプログラムを組むでは、判りやすい(たぶん)ように、順番に説明していきたいと思います。構造化プログラムという事で、必要な変数等をグローバル変数として宣言してやります(ソースの出来るだけ上に書く。angle;//進む角度}bullet[BMAX];こうする事で、どこでも(どの自作関数内でも)これらの変数を参照する事が出来るようになります。今まで、プレイヤーの座標はWinMain関数内でcx,cyと宣言し、それらを使用していましたが、今回は構造体(_player)として宣言します。_bullet構造体は、弾の処理に使用します。今回使う変数はとりあえず、使用・未使用を表すフラグ(enable)と座標(x,y)と、進む方向(angle)だけです。最初に実行するWinMain関数を見てみましょう。nCmdShow){//ウィンドウ作成if(!InitWindow(hInstance,hPrevInstance,nCmdShow))Quit("ウィンドウ作成に失敗しました。");//DirectDraw開始if(!StartDirectDraw(hw))Quit("DirectDrawの初期化に失敗しました。");//ゲーム初期化InitGame();//ゲームループへGameLoop();return(FALSE);}いやぁ、今まではここ(↑)にひたすら書いていたので、今回のプログラムは結構すっきり見えますね。InitWindow()はウィンドウを作成する処理を書いてあるだけの自作関数なので解説省略。DirectDrawを初期化したらInitGame();を呼び出していますね?これはゲームに必要な処理を先にしておく自作関数です。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();を呼び出していますよね?この関数はゲームのループ用の自作関数です。コードは次のようになっています。GameLoop(void){while(1){WaitSet();//現在の時間(単位:ミリ秒)取得MovePlayer();//自機移動Shoot();//弾発射判定MoveBullet();//弾移動Show();//画像表示Wait(1000/60);//メッセージループへ}}whileの中の最初と最後のWaitSet()、Wait()については既にやっているので解説省略。MovePlayer()関数を呼び出していますよね?こいつはキー入力により自機を動かす処理をする自作関数です。mv=4.0f;//移動量if(angle!=-1){player.x+=fcos[angle]*mv;player.y-=fsin[angle]*mv;}}この関数内でやっていることは第5回で説明したので、みりゃ判りますね?ここの処理が終わったら、GameLoopの方へ戻って、次にShoot()関数が呼ばれています。Shoot()関数では、スペースキーが押されているか判定し、押されていれば弾を作成します。ここでは、シューティングゲームで使うような、押しっぱなしでも連射できる処理をやります。{//キーが押されていない場合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にすると、いつでも発射キーを押した瞬間に弾が出せます(実際にやってみてください)。)自作関数を呼び出しています。いった処理をしているのか見てみましょう。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()を呼び出しています。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()が呼び出されます。これは、ディスプレイに一連の表示をする自作関数です。コードを見てみましょう。弾は使用しているものだけ表示してやります。これで説明は終わりです(^^;今回のプログラムの流れを図で表すと、こんな感じになるのかも矢印は関数の呼び出しです。おまけ今回は弾のデータ構造に構造体の配列を使用しましたが、この場合、配列中に使用しているデータと使用していないデータが両方含まれていたり、弾数に制限があったりして少し不便ですよね?そんなときはリスト構造を使用すると結構スッキリ記述できます。Soldierは双方向リスト構造でやってました。ゲーム制作基礎編サンプルコードのダウンロードでは、まずサンプルコードをダウンロードし、解凍してください。C++でプロジェクトファイルを開いてください(「game_08.dsw」をダブルクリックすれば開けます)。圧縮ファイルに含まれる「game_08.exe」をダブルクリックし、実行してみてください。?画面が切り替わり、フルスクリーン化すると思います。何かキーを押すと、そのキーコードが画面に表示されると思います。いうわけで今回は、キー入力について解説したいと思います。右の図のように、座標上に2つの長方形が存在したとします。ここで、この2つの物体(長方形)が重なっているか(当たっているか)を調べるときに、次のような式をよく使います。ay2<by1)当たっていないelse当たっているこれが正しい式となります。当たった時の判定だけしたいのならば、「!」を使えば直ぐに出来ますね。ay2<by1))当たっている実際にゲームに使用する場合は、キャラクターの全体もしくは、その一部を当たり判定とし、その部分を長方形として扱い、上の式へ代入します。この方法はシューティングゲーム等のような、「あまり精度にはこだわらないから速度の速い方法がいい」という場合に使用してください。レッツプログラミングではサンプルコードを見ていきます。キー入力によって複数のキャラクタを動かす」の最後の方へちょこっと加え、2匹のスライムが重なった(当たった)時にそのメッセージを表示するようにします。cy[0]>cy[1]+31))DdTextOut(lpBack,0,16,"当たっている",255);Flip();//フィリップWait(1000/60);//メッセージループへ}解説するまでも無いですね。ゲーム制作基礎編サンプルコードのダウンロードでは、まずサンプルコードをダウンロードし、解凍してください。C++でプロジェクトファイルを開いてください(「game_07.dsw」をダブルクリックすれば開けます)。圧縮ファイルに含まれる「game_07.exe」をダブルクリックし、実行してみてください。?画面が切り替わり、フルスクリーン化すると思います。何かキーを押すと、そのキーコードが画面に表示されると思います。いうわけで今回は、キー入力について解説したいと思います。わからない解説ではサンプルコードを見ていきます。・<キー入力省略>//angleの値が変わっていたらキャラクタの座標を変更するif(angle!=-1){//座標を記憶しておく(バックアップfor(i=CMAX-2;i>=0;i--){cx[i+1]=cx[i];cy[i+1]=cy[i];}cx[0]+=fcos[angle]*mv;cy[0]-=fsin[angle]*mv;}//キャラクタ表示//オプション//後ろ(古い座標)から表示for(i=CMAX-1;i>0;i-=OSTEP)BltClip(lpBack,(int)cx[i],(int)cy[i],32,32,lpWork,32,0,1);//本体BltClip(lpBack,(int)cx[0],(int)cy[0],32,32,lpWork,0,0,1);Flip();//フィリップWait(1000/60);//メッセージループへ}グラディウスのオプションは、本体(自機)の通った跡を遅れて通りますよね?ですから、こういった処理は、自機の座標をいくつか記憶しておき、古い座標の所へオプションを配置してやればいいのです。キャラクタ(自機)の座標を配列でいくつか確保してやります。cx[CMAX],cy[CMAX];このとき、一番新しい座標をcx[0],cy[0]とし、一番古い座標をcx[CMAX-1],cy[CMAX-1]とします。ゲーム中、キー入力があり、自機の座標が変更されようとしたら、cx[0],cy[0]~cx[CMAX-2],cy[CMAX-2]の中身をcx[1],cy[1]~cx[CMAX-1],cy[CMAX-1]へコピーします(つまり1つ後ろへずらす)。それで、cx[0],cy[0]の値を変更してやればいいのです。if(angle!=-1){//座標を記憶しておく(バックアップfor(i=CMAX-2;i>=0;i--){cx[i+1]=cx[i];cy[i+1]=cy[i];}cx[0]+=fcos[angle]*mv;cy[0]-=fsin[angle]*mv;}座標を一つ後ろへずらす時、for(i=0;i<CMAX-1;i++){cx[i+1]=cx[i];cy[i+1]=cy[i];}こんなふうに前からコピーしてしまうと、cx[0]の値がcx[1]へ、cx[1]の値がcx[2]へ・・・というように全て同じ値になってしまうので、やめましょう(爆。後ろからコピーしていきましょう。自機の座標跡を作っていったら、次は表示ですね。20//オプションを配置する間隔<略>//後ろ(古い座標)から表示for(i=CMAX-1;i>0;i-=OSTEP)BltClip(lpBack,(int)cx[i],(int)cy[i],32,32,lpWork,32,0,1);//本体BltClip(lpBack,(int)cx[0],(int)cy[0],32,32,lpWork,0,0,1);オプション用の画像は自機の座標跡の古い所から表示してやります。優先度の問題なのですが、先に描いた方が画面の後ろに表示されますよね?なので、新しい座標の所程、後で描いてやります(自機は一番最後)上のようなプログラムだと、80,60,40,20番目の座標へ順番にオプションを配置し0番の座標へ自機を配置しています。自作の「ドット絵」を投稿することができます。ドッターさん同士の交流の場としてご利用ください。書き込むことができます。blockworks.このサイトについてサイトマップRSSフィード。