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

プレイヤーのショットを強化(追跡弾その2)

前回は追跡弾の発射部分である光の玉を表示させるところまで説明したので、
今回は実際に追跡弾を発射させて完成させましょう。

追跡弾ということは敵に向かって玉が飛んでいかなければならないので、
敵の座標が必要になります。
ですので、まずCONTROLクラスに敵の弾を取得するための関数を作ります。

bool CONTROL::GetEnemyPosition(int index,double *x,double *y)
{
	double tempx,tempy;

	if(enemy[index]==NULL || enemy[index]->GetDeadFlag())
		return false;


	//指定した添字の敵の座標を取得
	enemy[index]->GetPosition(&tempx,&tempy);

	//代入
	*x=tempx;
	*y=tempy;

	return true;

}


GetEnemyPosition関数です。
引数には敵の添え字、座標受け取り用のポインタ二つです。
中の処理は、最初のif文で、該当の添え字の敵クラスのポインタがNULLか、
消滅フラグ(Deadflag)が立っている場合はfalseを返すようにします。
もうその敵は消滅してるということを表すためです。

もし生きていれば、敵クラスのGetPosition関数を使って座標を取得し、
仮引数のポインタ経由で値を呼び出し元の変数に代入し、trueを返します。

次にプレイヤークラスを変更します。
追跡弾を発射するにはまずどの敵に発射するのかを決める必要があります。
今回はプレイヤーから見て、一番近い距離に居る敵を標的とします。
そのための関数、NearEnemySearch関数と追跡弾をセットするための関数、BallShotSet関数を
作成します。

int PLAYER::NearEnemySearch()
{

	CONTROL &control = CONTROL::GetInstance();
	int nearindex=-1;
	double nearresult=0;
	double ex,ey,tx,ty;

	for(int i=0;i<ENEMY_NUM;++i){
		if(!control.GetEnemyPosition(i,&ex,&ey))
			continue;
		tx=ex-x;
		ty=ey-y;
		
		if(nearindex==-1){
			nearindex=i;
			nearresult=tx*tx+ty*ty;
			continue;
		}
		//比較して小さければそれを最小値とする
		if(nearresult>tx*tx+ty*ty){
			nearindex=i;
			nearresult=tx*tx+ty*ty;
		}
	}
	return nearindex;
}


まず前提として、これらの関数を書いてあるball.cppではcontrol.hを取り込んでいます。
NearEnemySearch関数から説明します。
操作キャラから見てどの距離が一番近いかを調べるには、
今出現してる敵の座標を調べて、お互いの距離を比較する必要があります。
操作キャラと敵との距離は、敵のショットの時も説明しましたが、ピタゴラスの定理が使えますよね?
分からない人は敵のショットパターンを増やそうを確認して下さい。
まず最初に、CONTROLクラスの参照用の変数を宣言し、GetInstance関数を使って、
CONTROLクラスのインスタンスを取得しています。
変数nearindexは一番近い敵を示す添え字で、初期値は-1にしてます。
nearresultはピタゴラスの定理で言う斜辺以外の2辺をそれぞれ2乗して足した値を
代入しておくための変数です。
斜辺の長さはこの値の平方根になるので、この値が一番小さいものが、
操作キャラから見て一番近い敵ということになります。

まずforループ文で敵の数だけループさせてます。
GetEnemyPositionで敵の座標を取得してます。
この関数は最初の方で説明したとおり、敵が消滅してるか、該当の敵クラスのポインタがNULLなら、
falseを返します。
その場合はcontinueして、次のループに進んでます。
敵の座標を取得できたら、敵の座標とプレイヤーの座標のX,Y座標それぞれの差を求め、
一時変数に代入してます。
次のif文でnearindexが-1なら、それを最小値として、
nearindexにその添え字を代入し、
nearresultにピタゴラスの定理で言う斜辺以外の2辺(tx,ty)をそれぞれ2乗して足した値を
代入してます。
次のループからは、nearindexは-1じゃないので、2つ目のif文に処理が移ります。

2つ目のif文は先ほど代入されたnearresultと新しい値が代入されたtx,tyを2乗して足した値とを
比較して、小さければ、それを最小値としています。
このループが終われば、一番操作キャラに近い敵の添え字がnearindexに入ってることになります。
その値を最後に戻り値として戻しています。

void PLAYER::BallShotSet(int index)
{
	double ty;
	double trad,ex,ey;
	static 	int toggle=1;
	int tindex;
	
	CONTROL &control = CONTROL::GetInstance();


	ty=ball.GetPosition();


	tindex=NearEnemySearch();
	//戻り値が-1なら敵はもう居ないので、まっすぐ前に発射
	if(tindex==-1){
		trad=-PI/2;
	}else{
		//一番近い敵と弾との角度を取得
		control.GetEnemyPosition(tindex,&ex,&ey);
		trad=atan2(ey-ty+BALL_INITY,ex-x+(toggle*BALL_INITX));
	}

	shot[index].flag=true;
	shot[index].x=x+BALL_INITX*toggle;
	shot[index].y=ty+BALL_INITY;
	shot[index].rad=trad;
	shot[index].type=1;



	if(toggle==1){
		toggle=-1;
	}else{
		toggle=1;
	}
}

次にBallShotSet関数の説明です。
まず最初にCONTROLクラスのインスタンスとBALLクラスのGetPosition関数を呼び出して、
光の弾のy座標を取得しています。
その後、先ほどのNearEnemySearch関数を使って一番近い敵の添え字を取得しています。
NearEnemySearch関数の戻り値が-1なら敵は現在居ないことになるので、
その場合は単純にまっすぐ発射するようにするため、-PI/2の値を一時変数に代入してます。
-1じゃない場合は、一番近い敵の添え字を返してきているので、
その敵の座標を取得し、光の弾の位置との角度をatan2関数で計算し一時変数に代入しています。
光の弾と敵との角度なので、光の弾の座標を出さなければなりません。
光の弾のy座標はBallクラスのGetPosition関数で取得したy座標に、BALL_INITYを足したものです。
x座標については、光の弾はプレイヤーの脇に二つあるのでプレイヤーのx座標にBALL_INITXを足したものと
引いたものを用意する必要があります。
そこでBALL_INITXを逆数にするためにtoggleという静的変数を使っています。
下の方にいるif文を見てもらうと分かるとおり、
toggleが-1だと1にし、1だと-1にしています。
つまり関数が呼び出される毎にBALL_INITXの値が反転するので、
2回呼び出すと左右それぞれの光の弾の座標が取得できるわけです。
その座標を元に、光の弾と敵との角度をatan2関数で計算し、tradに代入しています。
その下で、引数で指定された添え字のSHOT構造体の配列に、
計算された角度や座標等の値を代入しています。
SHOT構造体は弾の種類を示すtypeというint型のメンバを増やしています。

struct SHOT{
	bool flag;//弾が発射中かどうか
	double x;//x座標
	double y;//y座標
	int gh;//グラフィックハンドル
	int width,height;//画像の幅と高さ
	double rad;//角度
	int type;//弾の種類(0なら通常、1なら追跡弾)
};

0なら通常の前方発射の弾、1なら追跡弾です。
x,y座標は先ほど説明した角度を出したときの座標の計算方法と同じで代入しています。
これで後は、プレイヤークラスのshot関数を改良するだけです。

void PLAYER::Shot()
{
	s_shot=false;
	int num=0;

	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){
					if(power<5){
						shot[i].flag=true;
						shot[i].x=x;
						shot[i].y=y;
						shot[i].rad=-PI/2;
						shot[i].type=0;
						break;

					}else if(power>=5 && power<10){
						//0の時が前方発射
						if(num==0){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.57;
							shot[i].type=0;
						}else if(num==1){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.69;
							shot[i].type=0;
						}else if(num==2){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.45;
							shot[i].type=0;
						}

						++num;


						if(num==3){
							break;
						}
					}else if(power==10){
						//0の時が前方発射
						if(num==0){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.57;
							shot[i].type=0;
						}else if(num==1){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.69;
							shot[i].type=0;
						}else if(num==2){
							shot[i].flag=true;
							shot[i].x=x;
							shot[i].y=y;
							shot[i].rad=-1.45;
							shot[i].type=0;
						}else if(num>2){
							BallShotSet(i);
						}


						++num;


						if(num==5){
							break;
						}

					}

				}
				
			}
			//ショットサウンドフラグを立てる
			s_shot=true;
		}
	}

	//一番近い敵との角度
	double trad;
	//一番近い敵の添え字
	int index;
	double ex,ey;
	//controlクラスの参照変数
	CONTROL &control = CONTROL::GetInstance();

	//一番近い敵の添え字取得
	index=NearEnemySearch();
	

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

			}else if(shot[i].type==1){
				
				//戻り値が-1なら敵はもう居ないので、まっすぐ前に発射
				if(index==-1){
					trad=-PI/2;
				}else{
					//一番近い敵との角度を取得
					control.GetEnemyPosition(index,&ex,&ey);
					trad=atan2(ey-shot[i].y,ex-shot[i].x);
				}
				
				shot[i].rad=trad;
				shot[i].x+=cos(trad)*PSHOT_SPEED;
				shot[i].y+=sin(trad)*PSHOT_SPEED;
			}

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


まず上側の弾をセットする部分のコードを見てください。
追跡弾ではない弾は、SHOT構造体のtypeメンバに0を代入するように変更しています。
さらに、powerが10の時のコードを追加してます。
numが3以上になったら、
BallShotSet関数を実行して、追跡弾をセットしています。
numが5になるとforループを抜けるので、
2発追跡弾をセットするとループを抜ける仕組みです。

次に下側にある弾を移動させる部分のコードを見てください。
少し上に宣言してる変数は全て一時変数として使ってますので、
特に大きな意味は持ってません。
その下のif文ではtypeの種類ごとに処理を分岐させただけです。
typeが0の場合は通常弾なのでコードも今までどおりです。
typeが1の場合は追跡弾です。
変数indexはこのforループに入る前に、NearEnemySearch関数を実行し、
その戻り値を代入してます。
-1の場合は敵が居ないことになるのでそのまままっすぐ発射させるために、
-PI/2を一時変数tradに代入してます。
もしindexが-1じゃなければ、一番近い敵の添え字が入ってることになるので、
その添え字をGetEnemyPositionに渡して、一番近い敵の座標を取得しています。
その後atan2関数で弾の座標と敵の座標から弾と敵との角度を計算して一時変数tradに代入してます。
最後にSHOT構造体にそれらを代入しています。
さらにその下にある、弾が画面の外にはみ出した場合の処理ですが、
追跡弾によって画面のどの方向からも弾がはみ出す可能性が出てきたので、
全ての方向に対して判定を行っています。

こうすることで以下の動画のように追跡弾を発射することが出来ます。
※動画では分かりやすくするために、真ん中の3方向発射の弾を消してます。

今回の説明は以上です。
次回からはボスの作成に取り掛かっていきましょう。

>> 【ボスを作ろう1(出現)】に進む
>> シューティングゲーム作成入門トップに戻る