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

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

今回は敵の弾と操作キャラとの当たり判定について説明します。

アルゴリズムについては前回と同じで、CircleCollision関数を使います。
今回は敵の弾の座標を取得する必要があるので、
ENEMYクラスにもGetShotPosition関数を作成します。
また、当たった弾は当然消す必要があるので、そのための関数SetShotFlag関数、
さらに、弾によって当たり判定も違うので、敵の弾の種類を取得するための関数GetShotType関数も作成します。

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

int ENEMY::GetShotType()
{
	return stype;
}


GetShotPosition関数は操作キャラの弾を取得するときのGetShotPosition関数と変わりません。
対象の添え字と取得用のポインタを引数に渡すだけです。
弾のフラグが立っているときだけポインタ経由で値を代入し、trueを返します。
SetShotFlag関数も添字とbool値のフラグを渡すだけです。
GetShotType関数も弾の種類を返すだけです。

次に操作キャラに弾が当たったときの演出を考えなければありません。
私はあまりシューティングゲームに詳しくないので、今から説明する方法が一般的かどうかは分かりません。
流れとして、

●弾があたる

●操作キャラを消す

●点滅させながら、画面下から出てくる

●点滅をやめ、動かせるようにする

という流れでいくことにします。
そのため、プレイヤークラスのヘッダファイルは以下のように変更します。

class PLAYER{
private:
	//x座標,y座標
	double x,y;

	//画像幅
	int width,height;

	//グラフィックハンドル格納用配列
	int gh[12];


	//移動係数
	float move;

	//横方向と縦方向のカウント数。
	int xcount,ycount;
	//添字用変数
	int ix,iy,result;

	//プレイヤーのライフ
	int life;
	bool damageflag;
	bool endflag;
	//ダメージ中のカウント
	int dcount;


	//弾
	SHOT shot[PSHOT_NUM];

	//カウント
	int count;

	//サウンド関連フラグ
	//ショット音
	bool s_shot; 

private:
	void Move();
	void Draw();
	void Shot();

public:
	PLAYER();
	bool GetShotSound();
	bool GetShotPosition(int index,double *x,double *y);
	void SetShotFlag(int index,bool flag);
	void GetPosition(double *x,double *y);
	void SetDamageFlag();
	bool GetDamageFlag();
	void All();

};

以前lifeというbool値用の変数があったと思いますが、
int型にしてプレイヤーの残りライフ数として使うように変更します。
更に、プレイヤーがダメージを受けたことを示すフラグdamageflagと、
ライフ数がゼロに鳴って、ゲームーバーになったことを示すフラグendflag、
また、ダメージを受けて点滅している間のカウントを示すdcountをそれぞれ用意しました。
これらはコンストラクタでfalseまたは0に初期化してます。

次にDraw関数を変更します。

void PLAYER::Draw()
{

	//弾描画
	for(int i=0;i<PSHOT_NUM;++i){
		if(shot[i].flag){
			DrawGraph(shot[i].x-shot[i].width/2,shot[i].y-shot[i].height/2,shot[i].gh,TRUE);
		}
	}

	//生きてれば描画
	if(damageflag){
		if(dcount>20){
			if(dcount%2==0){
				SetDrawBlendMode(DX_BLENDMODE_ALPHA,140);
				DrawGraph(PLAYER_INITX-width/2,PLAYER_INITY-height/2+60-(dcount-20),gh[1],TRUE);
				SetDrawBlendMode(DX_BLENDMODE_NOBLEND,0);
			}else{
				DrawGraph(PLAYER_INITX-width/2,PLAYER_INITY-height/2+60-(dcount-20),gh[1],TRUE);
			}
		}
		++dcount;
		if(dcount==80){
			damageflag=false;
			dcount=0;
			//座標を初期値に戻す
			x=PLAYER_INITX;
			y=PLAYER_INITY;
			//上向きの画像にする
			result=1;
		}
	}else{
		//通常描画
		DrawGraph(x-width/2,y-height/2,gh[result],TRUE);
	}
}

まず、damageflagがtrueの時とfalseの時とで処理を分岐してます。
damageflagがfalseの時は通常描画なので、今までと変わりません。
damageflagがtrueの時は、ダメージを負っていて動かせない間の描画です。
このdamageflagというのは、後ほど説明するCONTROLクラスでの当たり判定の時に、
敵の弾が操作キャラに当たったときにtrueになります。
最初のif文でdcountが20以上になるまで、描画をしないように制限しています。
dcountとは弾が当たってからのカウント数を表してます。
つまり、弾が当たったら、20ループの間、操作キャラが消えることになります。
20ループ経過した後は、点滅して表示しながら、かつ画面下から操作キャラを出現させなければいけません。
点滅方法として、キャラを少し透過させて描画、透過させないで描画を繰り返せば点滅してるように見えるので、
その方法でいきます。
最初のif文でdcount%2==0としていますが、2で割って余りがゼロの時ということなので、
2カウントに一回だけ条件にマッチすることになります。
マッチしたときは透過させて描画、マッチしないときは通常描画をすることで、点滅させることができます。

透過して描画する部分のコードは、SetDrawBlendModeで透過モードに設定しています。
透過度は140にしていますが、この辺はお好みで結構です。
DrawGraph関数は、X座標は初期座標を示すPLAYER_INITXをそのまま指定しています。
width/2を引いてるのは、画像幅の半分の長さを引くことで画像の中心に座標が来るようにするためです。
今後この説明は省略します。
y座標は初期座標PLAYER_INITYにまず60を足してます。
初期座標は画面の一番下の当たりですから、60を足すことで画面外の座標に描画されることになります。
さらにその値から、(dcount-20)を引いてます。
dcountが20以上になったら、今のルーチンの処理が行われるので20引いてるだけです。
つまりカウントが増えていくと、徐々に引かれる値が大きくなり、段々キャラが下から現われてくるわけです。
グラフィックハンドルをgh[1]としているのは添え字の1番が丁度キャラの上向きの画像だからです。
最後にブレンドモードを元に戻しています。
elseの場合のコードは、ブレンドモードを設定してないだけで、描画部分のコードは一緒です。

次にその下の処理を見てください、
dcountが80になった時に、damageflagをfalseに、
dcountを0に、x,y座標を初期値に、resultを1に設定しています。
フラグやカウント、座標を初期値に戻しているだけです。
resultは現在の画像を現す添え字が入っている変数ですよね?
ですので上向きの1に設定しています。(復活時は上向きが望ましいため)
80でフラグを戻すということは、
先程の点滅描画部分のコードの計算式にこのカウントを当てはめてみると、
80-20で60になり、これを引いているわけですから、
60-60=0になり、初期座標PLAYER_INITYの座標になります。
最後の点滅描画の座標は初期値のPLAYER_INITYの座標と一致するわけです。
damageflagがfalseになれば通常描画に戻るので、
x,yの変数に入ってる座標に描画されますが、フラグが戻った直後では、
x,y座標も初期座標に設定していますし、点滅描画の最後も初期座標に描画されているので、
違和感なくゲームが再開されるということになります。

また、damageflagがtrueの時にキャラを動かしたり弾を撃てたりするのは変なので、
以下のようにShot関数とAll関数を変更してます。

void PLAYER::Shot()
{
	s_shot=false;

	if(!damageflag){
	
		//キーが押されててかつ、6ループに一回発射
		if(key[KEY_INPUT_Z]==1 && count%6==0){
			for(int i=0;i<PSHOT_NUM;++i){
				if(shot[i].flag==false){
					shot[i].flag=true;
					shot[i].x=x;
					shot[i].y=y;
					break;
				}
			}
			//ショットサウンドフラグを立てる
			s_shot=true;
		}
	}

	//弾を移動させる処理
	for(int i=0;i<PSHOT_NUM;++i){
		//発射してる弾だけ
		if(shot[i].flag){
			shot[i].y-=PSHOT_SPEED;

			//画面の外にはみ出したらフラグを戻す
			if(shot[i].y<-10){
				shot[i].flag=false;
			}
		}
	}


}
void PLAYER::All()
{
	//消滅してないときだけ実行
	if(!damageflag){
		Move();
	}
	Shot();
	
	Draw();

	++count;
}

Shot関数内のキーによる弾の発射を行っている部分のコードを、
damageflagが立っていないときだけしか実行しないようにif文で制御しています。
その下の弾の移動部分のコードはif文で囲っていないので、操作キャラがダメージを食らっても、
弾は移動し続けます。
また、All関数の方は、Move関数をdamageflagが立っていないときしか実行しないようにするために、
if文で囲っています。
こうすることで、ダメージを食らって復活するまでの間はキーによる移動をできなくすることができます。

これで弾が当たったときの操作キャラの描画の制御はできるようになったので、
後は当たり判定部分のコードだけです。
当たったときにdamageflagをtrueにするための関数と、damageflagを取得するための関数を先に作っておきます。

void PLAYER::SetDamageFlag()
{
	damageflag=true;
}

bool PLAYER::GetDamageFlag()
{
	return damageflag;
}

次にCONTROLクラスのCollisionAll関数のコードです。

void CONTROL::CollisionAll()
{
	double px,py,ex,ey;

	bool tempflag=false;

	//操作キャラの弾と敵との当たり判定
	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;
					}
				}
			}
		}
	}


	//敵の弾と操作キャラとの当たり判定
	//プレイヤーが生きてれば
	if(!player->GetDamageFlag()){
		player->GetPosition(&px,&py);
		for(int i=0;i<ENEMY_NUM;++i){
			if(enemy[i]!=NULL){
				for(int s=0;s<ENEMY_SNUM;++s){
					//弾フラグが立っていればtrueを返す
					if(enemy[i]->GetShotPosition(s,&ex,&ey)){
						//弾によって当たり判定が違うのでswitch文で分岐
						switch(enemy[i]->GetShotType()){

							case 0:
								//当たってれば
								if(CircleCollision(PLAYER_COLLISION,ESHOT0_COLLISION,px,ex,py,ey)){
									tempflag=true;
								}
								break;

							case 1:
								if(CircleCollision(PLAYER_COLLISION,ESHOT1_COLLISION,px,ex,py,ey)){
										tempflag=true;
								}
								break;

							case 2:
								if(CircleCollision(PLAYER_COLLISION,ESHOT2_COLLISION,px,ex,py,ey)){
										tempflag=true;
								}
								break;
						}
						if(tempflag){
							//操作キャラのdamageflagを立てる
							player->SetDamageFlag();
							//弾を消す
							enemy[i]->SetShotFlag(s,false);
							//プレイヤー消滅音フラグを立てる
							pdead_flag=true;
							//一時フラグを戻す
							tempflag=false;
						}
					}
				}
			}
		}
	}
}

下側の当たり判定部分の処理コードを見てください。
まず、プレイヤーのdamageflagを取得して、消滅してないときだけ当たり判定するように
if文で制御しています。
次にプレイヤーの座標を取得し、敵の数だけループさせてます。
さらにその下にforループ文を作り、敵の弾の数だけループさせるようにしています。
その下のif文でGetShotPosition関数で弾の座標を取得していますが、
戻り値がtrueの時しか処理しないようにしているので、弾のフラグが立っていないものは処理されません。
これで無駄な処理を省くことができます。
その下のswitch文を見てください。
GetShotType関数を使って、弾の種類ごとに当たり判定の処理を書き換えています。
当然弾ごとに当たり判定の半径は違いますよね?
ちなみにdefine.hで以下のように定義しています。

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

#define PSHOT_COLLISION 3
#define ESHOT0_COLLISION 10
#define ESHOT1_COLLISION 3
#define ESHOT2_COLLISION 2

弾ごとに違う当たり判定の値をCircleCollision関数に渡し、
当たっていれば一時変数tempflagをtrueにしてます。

その下のif文でそのtempflagがtrueなら、
プレイヤーのdamageflagを立て、敵の当たった弾のフラグを消してます。
これで先程のプレイヤー消滅時の演出が発生し、当たった弾も消えることになります。
またその下の処理で、プレイヤー消滅時の音のフラグを立て、一時フラグを元に戻してます。
プレイヤー消滅音の読み込み、再生等は、今までとまったく一緒なので省略します。

こうすることで、以下の動画のように敵の弾とプレイヤーの当たり判定を実装することができます。

まだエフェクトがないのでちょっと寂しいですけど、ゲームとして遊べるようにはなってきました。
今回の説明は以上です。
次回は敵消滅エフェクトをつけてみましょう。

>> 【敵の消滅エフェクトを作成しよう】に進む
>> シューティングゲーム作成入門トップに戻る