>> シューティングゲーム作成入門トップに戻る

操作キャラの弾と敵との当たり判定

今回は操作キャラの弾と敵との当たり判定を実装します。

当たり判定については、円形同士の当たり判定にします。
レーザーとかを実装するようになれば、四角形や円形の当たり判定などが必要になりますが、
今は弾も円形ですし、敵も円形なので円形で十分です。

当たり判定の仕組みですが、ピタゴラスの定理を使います。

ピタゴラスの定理とは直角三角形において、
図のような斜辺(c)の2乗は、その他の辺(a,b)の2乗を足したものと等しいという公式です。
ではこれをどう使うのでしょうか?
以下の図を見てください。

操作キャラの弾と敵が当たった瞬間の図です。
それぞれの当たり判定部分を円で表し、
それぞれの中心を線で結んだ後にそこから更に線を延ばすと、直角三角形が出来ます。
つまり、先程のピタゴラスの定理を使って、先程のCの値を求めれば、
衝突直後の距離を求めることができます。
もっと言うと、それぞれの円の半径を足したものよりも、
それぞれの円の中心と中心の距離(C)が短ければ、当たっているということになります。
イメージしにくい場合は、もっと円同士をめり込むぐらいに近づけてみてください。
お互いの円の半径を足した距離よりも、お互いの円の中心を結んだ距離の方が短くなりますよね?
a,bの距離については、お互いの座標がわかっていれば計算できます。

では実際に関数を定義しましょう。
当たり判定はCONTROLクラスで行います。
今後円形同士の当たり判定を他の物体同士でも使う可能性があるので、
円形専用の当たり判定関数、CircleCollision関数を作ります。

bool CONTROL::CircleCollision(double c1, double c2, double cx1, double cx2, double cy1, double cy2)
{

	double hlength=c1+c2;
	double xlength=cx1-cx2;
	double ylength=cy1-cy2;

	if(hlength*hlength >= xlength*xlength+ylength*ylength){
		return true;
	}else{
		return false;
	}
}

仮引数は左から、
円形1の半径,円形2の半径、円形1のx座標、円形2のx座標、円形1のy座標、円形2のy座標、
という順番です。

まず最初に一時変数に、半径同士を足しています。
この値よりも、先程の図で言うCの長さが短ければ当たっていることになります。
また、x,y座標から先程の図でいうa,bの距離を出しています。

この状態で、xlengthの2乗とylengthの2乗を足した値の平方根を求めれば、Cの長さは求まります。
その値とhlengthと比較して、当たり判定をチェックしてもいいのですが、
平方根を求めると若干処理が重くなるので、単純に2乗した値を比較するだけにします。
もし、当たっていれば、hlengthの2乗よりも、xlengthの2乗とylengthの2乗を足した値の方が小さくなるので、
これだけで判定できます。
if文の条件式は、今説明したことをそのまま書いているだけです。
当たっていればtrueを返し、当たっていなければfalseを返すようにしています。
これで円形用の当たり判定関数は出来ました。

次に、操作キャラの弾の座標を取得する関数を作る必要があります。
PLAYERクラスにGetShotPosition関数を作ります。

bool PLAYER::GetShotPosition(int index,double *x, double *y)
{
	if(shot[index].flag){
		*x = shot[index].x;
		*y = shot[index].y;
		return true;
	}else{
		return false;
	}

}

仮引数は、取得対象の弾の添え字、x座標取得用のポインタ、y座標取得用のポインタです。
当然発射している弾だけ取得できればいいので、
フラグが立っているときのみ、値をポインタ経由で代入して、trueを返すようにしています。
フラグが立っていなければfalseを返すようにします。

これで操作キャラの弾の座標は取得できるようになりました。
次に敵に弾があった時に、敵に弾を発射させないようにすることと、敵を表示させないようにします。
敵が消滅したかどうかを示すフラグdeadflagというものがあったと思いますが、
これが立っていないときだけ弾を発射するようにし、
また、弾が当たったらこのフラグをtrueに出来るようにするための関数を作ります。
まず、弾の制御をするために、shot関数を以下のように変更しています。

void ENEMY::Shot()
{
	//CONTROLクラスの参照
	CONTROL &control = CONTROL::GetInstance();
	double px,py;

	
	//発射タイミングになったら、フラグを立てる
	if(shot_time==g_count){
		sflag=true;
	}

	//フラグ立ってるときだけ
	if(sflag){
					
		//ショット音フラグを戻す
		s_shot = false;
		//敵が生きてるときだけ発射する。
		if(!deadflag){

			//プレイヤーの一取得
			control.GetPlayerPosition(&px,&py);

			//敵とプレイヤーとの座標の差から逆正接を求める。
			//初回だけ実行
			if(scount==0)
				rad=atan2(py-y,px-x);

			
			switch(s_pattern){
				//前方にショット
				case 0:
					//5ループに一回発射。20までなので5発発射。
					if(scount%5==0 && scount<=20){
						for(int i=0;i<ENEMY_SNUM;++i){
							//フラグが立ってない弾を探して、座標等をセット
							if(shot[i].flag==false){
								shot[i].flag=true;
								shot[i].x=x;
								shot[i].y=y;
								shot[i].rad=rad;
								break;
							}
						}
						s_shot=true;

					}
					break;

				//プレイヤーに向かって直線ショット
				case 1:
					//6ループに一回発射。54までなので10発発射。
					if(scount%6==0 && scount<=54){
						for(int i=0;i<ENEMY_SNUM;++i){
							//フラグが立ってない弾を探して、座標等をセット
							if(shot[i].flag==false){
								shot[i].flag=true;
								shot[i].x=x;
								shot[i].y=y;
								shot[i].rad=rad;
								break;
							}
						}
						s_shot=true;

					}
					break;

				//3直線ショット
				case 2:
					//10ループに一回発射。1ループに3発なので5ループさせると15発発射
					if(scount%10==0 && scount<=40){
						for(int i=0;i<ENEMY_SNUM;++i){
							//フラグが立ってない弾を探して、座標等をセット
							if(shot[i].flag==false){
								shot[i].flag=true;
								shot[i].x=x;
								shot[i].y=y;
								
								//0はキャラクターに向かって発射
								if(num==0){

									//敵とプレイヤーとの逆正接から30度引いたラジアンを代入
									shot[i].rad=rad-(10*3.14/180);

								}else if(num==1){
									//敵とプレイヤーとの逆正接を代入
									shot[i].rad=rad;
									
								}else if(num==2){
									//敵とプレイヤーとの逆正接から30度足したラジアンを代入
									shot[i].rad=rad+(10*PI/180);

								}
									++num;

								//3発発射したら,numを0にしてループを抜ける。
								if(num==3){
									num=0;
									break;
								}
							}
						}
						s_shot=true;

					}
					break;

				//乱射ショット
				case 3:
					//10ループに一回発射。42までなので15発発射。
					if(scount%3==0 && scount<=42){
						for(int i=0;i<ENEMY_SNUM;++i){
							//フラグが立ってない弾を探して、座標等をセット
							if(shot[i].flag==false){
								shot[i].flag=true;
								shot[i].x=x;
								shot[i].y=y;
								//初回だけ乱数初期化
								if(num==0)
									srand((unsigned int)time(NULL));

								shot[i].rad=atan2(py-y,px-x)-(60*PI/180)+((rand()%120)*PI/180);
								++num;
								break;
							}
						}
						s_shot=true;

					}
					break;
				}
			}
		
		
		//フラグが立ってる弾の数
		int s=0;

		//フラグ立ってる弾だけ、弾の移動を行う
		for(int i=0;i<ENEMY_SNUM;++i){
			if(shot[i].flag){
				switch(shot[i].pattern){
					//単純に下に発射
					case 0:
						shot[i].y+=shot[i].speed;
						break;

					case 1:
						shot[i].x+=shot[i].speed*cos(shot[i].rad);
						shot[i].y+=shot[i].speed*sin(shot[i].rad);
						break;
					case 2:
						shot[i].x+=shot[i].speed*cos(shot[i].rad);
						shot[i].y+=shot[i].speed*sin(shot[i].rad);
						break;
					case 3:
						shot[i].x+=shot[i].speed*cos(shot[i].rad);
						shot[i].y+=shot[i].speed*sin(shot[i].rad);
						break;
						
				}

				//弾が画面をはみ出たらフラグを戻す。
				if(ShotOutCheck(i)){
					shot[i].flag=false;
					continue;
				}
				++s;
			}
		}
		//sがゼロということは発射中の弾がない。
		//かつdeadflagがTRUEということはこの敵のクラスは消滅させてもよい
		if(s==0 && deadflag){
			//敵クラス消滅フラグをTRUEにする
			endflag=true;
		}
		
		++scount;

	}
	
}

敵の弾セット部分のコードを見てください。
弾発射フラグを初期化した後に、
if文でdeadflagがfalseの時しか弾セットの処理をしないようにしています。
こうすることで敵が消滅したら、弾の発射を止めることができます。
弾の移動に関してはこのif文で囲んでいないので、
敵が消滅しても弾は移動しつづけることになります。

次に敵のdeadflagをセットするための関数です。
また、deadflagを取得できるようにするための関数もついでに作っておきましょう。

void ENEMY::SetDeadFlag()
{
	deadflag = true;
}

bool ENEMY::GetDeadFlag()
{
	return deadflag;
}

単純に呼び出したら、deadflagをtrueにする関数SetDeadFlag関数と、
deadflagを戻り値として返すだけのGetDeadFlag関数です。

また、敵のDraw関数は変更ありません。
元から、弾発射フラグが立っているものだけ描画し、deadflagが立っていない敵だけ、
描画するようにしてるからです。
一応貼っておきます。

void ENEMY::Draw()
{
	int temp;

	//弾から最初に描画
	for(int i=0;i<ENEMY_SNUM;++i){
		if(shot[i].flag){
			if(stype==0 || stype==2){
				DrawGraph(shot[i].x-shot[i].width/2,shot[i].y-shot[i].height/2,shot[i].gh,true);
			}else{
				DrawRotaGraph(shot[i].x,shot[i].y,1.0,shot[i].rad-(90*PI/180),shot[i].gh,true);
			}
		}
	}


	if(!deadflag){

		temp= count%40/10;
		if(temp==3)
			temp=1;

		DrawGraph(x-width/2,y-height/2,gh[temp],TRUE);
	}
}

あとはこれらの関数を呼び出して、当たり判定を行うだけです。
CONTROLクラスに当たり判定全体を担う関数CollisionAll関数を作ります。

void CONTROL::CollisionAll()
{
	double px,py,ex,ey;
	//操作キャラの弾と敵との当たり判定
	for(int i=0;i<PSHOT_NUM;++i){
		if(player->GetShotPosition(i,&px,&py)){
			for(int s=0;s<ENEMY_NUM;++s){
				//敵クラスのポインタがNULLじゃない、かつdeadflagがfalse(死んでない&帰還してない)
				if(enemy[s]!=NULL && !enemy[s]->GetDeadFlag()){
					enemy[s]->GetPosition(&ex,&ey);
					//当たり判定
					if(CircleCollision(PSHOT_COLLISION,ENEMY1_COLLISION,px,ex,py,ey)){
						//当たっていれば、deadflagを立てる
						enemy[s]->SetDeadFlag();
						//当たった弾のフラグを戻す
						player->SetShotFlag(i,false);
						//敵消滅音フラグセット
						edead_flag=true;
					}
				}
			}
		}
	}
}

仕組みはそんなに難しくありません。
プレイヤーの弾の数全てを出現中の敵全てに対して、総当りで弾が当たってないかをチェックすればいいだけです。
まず最初のforループでプレイヤーの弾数だけループさせてます。
その次にGetShotPosition関数で弾の座標を取得します。
さらにその下の階層でまたforループ文を使ってます。
今度は敵の数だけループさせています。
こうすることでプレイヤーの弾一発に対して、敵全てとの当たり判定をチェックできます。
さらにその下のif文で、敵クラスのポインタがNULLじゃないことをチェックしてます。
敵クラスにMove関数を思い出して欲しいのですが、
敵が消滅または画面からはみ出している、かつ敵の弾が画面上に一発もない場合は、
敵クラスのendflagをtrueにしてました。

敵クラスのAll関数はそのフラグを戻り値として返し、それがtrueなら敵クラスを解放し、
そのポインタをNULLにしてましたよね?
つまり、消滅した敵はポインタがNULLになってるのでもうアクセスできません。
そのためのチェックです。
さらにこの条件かつ、GetDeadFlag関数の戻り値がfalseの時だけしか処理しないようにしてます。
deadflagがtrueだと敵は消滅しているので、当たり判定する必要がないからです。
このif文を通ると、敵の座標を取得し、先程作っておいた円形当たり判定用の関数にかけ、
当たっていれば、敵のdeadflagを立ててます。
なお、当たり判定用の半径はPSHOT_COLLISION,ENEMY1_COLLISIONという定数として、
define.hに以下のように定義しています。

//当たり判定用半径定義
#define PLAYER_COLLISION 6
#define ENEMY1_COLLISION 14

#define PSHOT_COLLISION 3
#define ESHOT1_COLLISION 12
#define ESHOT2_COLLISION 3
#define ESHOT3_COLLISION 2

今後の為にも、プレイヤーや敵ショットの当たり判定用の半径も定義してます。

その次に、操作キャラの当たった弾のフラグを戻しています。
当たった弾は消すほうが正しいですよね?
新たにPLAYERクラスでは弾のフラグをセットするための関数、SetShotFlag関数を定義しています。

void PLAYER::SetShotFlag(int index,bool flag)
{
	shot[index].flag=flag;
}

引数は対象の弾の添字と、セットするフラグのbool値です。
これを使い、フラグをfalseにすることで、弾は描画されなくなり、当たり判定の対象からも外すことができます。

最後に、敵が消滅したときの消滅音フラグもセットしています。
なお、ここの音声読み込みコード及び再生コードは、読み込みファイルが違うだけで、
弾ショット時のコードとまったく一緒なので省略します。

このCollisionAll関数をAll関数で以下のように呼び出します。

void CONTROL::All()
{
	//サウンドフラグを初期化
	eshot_flag=pshot_flag=edead_flag=false;

	//描画領域を指定
	SetDrawArea(MARGIN,MARGIN,MARGIN+380,MARGIN+460);

	back->All();
		
	player->All();
	//プレイヤーショットサウンドフラグチェック
	if(player->GetShotSound()){
		pshot_flag=true;
	}
	
	for(int i=0;i<ENEMY_NUM;++i){
		if(enemy[i]!=NULL){
			//敵ショットサウンドフラグチェック
			if(enemy[i]->GetShotSound()){
				eshot_flag=true;
			}

			if(enemy[i]->All()){
				delete enemy[i];
				enemy[i]=NULL;
			}
		}
	}
	//当たり判定
	CollisionAll();

	SoundAll();

	++g_count;
}

これらを実行すると、以下の動画のように操作キャラの弾と敵との当たり判定が実装できます。

まだエフェクト等がないのでちょっと地味ですがゲームっぽくなってきたと思います。
今回の説明は以上です。
次回は敵の弾と操作キャラとの当たり判定を実装します。

>> 【敵の弾と操作キャラとの当たり判定】に進む
>> シューティングゲーム作成入門トップに戻る