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

敵のショットパターンを増やそう

今回は敵のショットパターンを増やしてみます。

ショットパターンは色々とありますが、
基本はプレイヤーが居る方向に向かって打つのが基本です。
ですので、まず敵とプレイヤーとの角度を計算する必要があります。
その為には敵クラスからプレイヤーの座標を取得して計算しなければなりません。
ですがクラスが別なので現状ではアクセスできません。

それを解決するために、ENEMYクラスとPLAYERクラスのインスタンスを持つCONTROLクラスに、
座標を取得する関数を作ります。
そして、ENEMYクラスからその関数を呼び出して座標を取得するようにします。
そのためには、ENEMYクラスからCONTROLクラスの実体を取得する必要がありますので、
CONTROLクラスをシングルトンにします。

#include "player.h"
#include "back.h"
#include "enemy.h"

class CONTROL{

	//プレイヤークラス
	PLAYER *player;

	//背景クラス
	BACK *back;

	//敵クラス
	ENEMY *enemy[ENEMY_NUM];

private:
	CONTROL();
	~CONTROL();
public:
	void All();
	void GetPlayerPosition(double *x,double *y);
	void GetEnemyPosition(int index,double *x,double *y);
	static CONTROL& GetInstance(){
		static CONTROL control;
		return control;
	}
};

GetInstanceという静的関数を作り、CONTROLクラス自体はその関数内で、
static指定子をつけ、静的変数という形で宣言しています。
publicの静的関数なので、実体がなくても関数を呼び出せます。
これをENEMYクラス、また今後PLAYERクラスからも敵の座標を取得する機会が出てくるかもしれないので、
PLAYERクラスからも呼び出すようにします。

ENEMYクラス、PLAYERクラスのヘッダーは以下のようになっています。
●playerクラス

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

	//画像幅
	int width,height;

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


	//移動係数
	float move;

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

	//生きてるかどうかのフラグ
	bool life;

	//弾
	SHOT shot[PSHOT_NUM];

	//カウント
	int count;

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

public:
	PLAYER();
	void GetPosition(double *x,double *y);
	void All();

};

●enemyクラス

#include "pch.h"

#ifndef _ENEMY
#define _ENEMY

class ENEMY{
private:
	//座標とグラフィックハンドル
	double x,y;
	int gh[3];

	//画像サイズ
	int width,height;

	//出現、停止、帰還、発射タイミング
	int in_time,stop_time,out_time,shot_time;

	//敵の種類
	int type;
	//弾の種類
	int stype;
	//移動パターン
	int m_pattern;
	//ショットパターン
	int s_pattern;
	//HP
	int hp;
	//アイテム
	int item;

	//敵が出現してからのカウント
	int count;
	
	//発射した弾数
	int num;
	//発射直後のラジアン
	double rad;
	
	//敵消滅フラグ
	bool deadflag;
	//敵クラス消滅フラグ
	bool endflag;

	//弾構造体
	E_SHOT shot[ENEMY_SNUM];
	//ショットが撃てるようになったかのフラグ
	bool sflag;
	//ショットが打てるようになってからのカウント
	int scount;

private:
	void Move();
	void Shot();
	void Draw();
	bool OutCheck();
	bool ShotOutCheck(int i);

public:
	bool All();
	void GetPosition(double *x, double *y);
	ENEMY(int type,int stype,int m_pattern,int s_pattern,int in_time,int stop_time,int shot_time,int out_time,int x,int y,int speed,int hp,int item);
};

#endif

それぞれ新たにGetPositionという関数を作りました。
この関数は以下のような関数です。

void PLAYER::GetPosition(double *x,double *y)
{
	*x=this->x;
	*y=this->y;

}

ポインタを渡して、座標を代入させるだけの関数です。
そして、この関数を呼び出すためにCONTROLクラスに、
GetPlayerPosition関数と、GetEnemyPosition関数を作成します。

void CONTROL::GetPlayerPosition(double *x,double *y)
{
	double tempx,tempy;

	player->GetPosition(&tempx,&tempy);

	*x=tempx;
	*y=tempy;
}
void CONTROL::GetEnemyPosition(int index,double *x,double *y)
{
	double tempx,tempy;
	//指定した添字の敵の座標を取得
	enemy[index]->GetPosition(&tempx,&tempy);

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

GetPlayerPosition関数は、プレイヤークラスのGetPosition関数を実行し、
取得した座標を、引数で渡したポインタ経由で値を代入してます。
GetEnemyPosition関数は、引数が三つあります。
敵は複数居るので、どの添字の敵の座標を取得するかを指定する必要があるからです。
第一引数に添字、第二、第三にdouble型のポインタを指定します。
指定した添字のENEMYクラスのGetPosition関数を実行し、座標を取得したあと、
引数に渡されたポインタ経由で値を代入しています。

これで、ENEMYクラス、PLAYERクラスのcppファイルでこのCONTROLクラスをインクルードすれば、
これらの関数を呼び出して、お互いの座標を取得することができるようになりました。

次にこれらの関数を使って、ショットパターンを増やしますが、
最初に言ったとおり、ショットには角度が付くことになりますので、弾も当然傾きます。
そのためにE_SHOT構造体にもdouble型のradという変数を持たせます。

struct E_SHOT{
	bool flag;//弾が発射中かどうか
	double x;//x座標
	double y;//y座標
	double rad;//角度(ラジアン)
	int gh;//グラフィックハンドル
	int width,height;//画像の幅と高さ
	int pattern;//ショットパターン
	int speed;//弾スピード
};

角度はラジアンという単位で表します。
360度は2πラジアンです。
角度からラジアンに変換するには、

ラジアン = 角度 * π(3.14) / 180

という計算で求められます。
このまま公式として覚えるか、分からない人はググってみてください。

さて、ようやくMove関数の説明です。
以下のコードをご覧下さい。

void ENEMY::Shot()
{

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

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

	//フラグ立ってるときだけ
	if(sflag){

		//プレイヤーの一取得
		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;
						}
					}

				}
				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;
						}
					}

				}
				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){

								//敵とプレイヤーとの逆正接から10度引いたラジアンを代入
								shot[i].rad=rad-(10*3.14/180);
							//1の時はプレイヤー一直線
							}else if(num==1){
								//敵とプレイヤーとの逆正接を代入
								shot[i].rad=rad;
							//2の時は右より
							}else if(num==2){
								//敵とプレイヤーとの逆正接から10度足したラジアンを代入
								shot[i].rad=rad+(10*PI/180);

							}
								++num;

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

				}
				break;

			//乱射ショット
			case 3:
				//1ループ毎に1発発射

					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;
						}
					}

				}
				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;

	}
	
}

bool ENEMY::ShotOutCheck(int i)
{
	//弾が画面をはみ出たらフラグを戻す。
	if(shot[i].x<-20 || shot[i].x>420 || shot[i].y<-20 || shot[i].y>500){
		return true;
	}else{
		return false;
	}
}

まず最初に、controlクラスのGetInstance関数を使って、CONTROLクラスの実体の参照を取得しています。
次にif(sflag)内の処理を見てください。
まず最初に、GetPlayerPosition関数を使って、プレイヤーの座標を取得しています。

次に、scountがゼロ、つまり発射直後の時だけ、敵とプレイヤーによって表されるシータ角をラジアン単位で取得し、
変数radに代入してます。
分からないという人もいると思うので以下の図をご覧下さい。

(x0,y0)がプレイヤーの位置、(x1,y1)が敵の位置と思ってください。
そこを線で結んで、プレイヤーの位置から水平に、敵の位置から垂直に線を引くと直角三角形ができます。
直角三角形が出来たとき、以下の公式が成り立ちます。

x0 = cosθ × r
y0 = sinθ × r

つまり、θが求まれば、プレイヤーの座標が取得できるわけです。
もっと言えば、弾は当然プレイヤーまでの直線上にあるので、
rを弾のスピードの値にしてやれば、プレイヤーの方向に一定距離飛んだ弾の座標が取得できるというわけです。

c言語にはコレを簡単に取得できるatan2という関数があります。
これを使うにはmath.hをインクルードして下さい。
第一引数には、プレイヤーと敵とのy座標との距離、第二引数はそのx座標との距離を指定します。
図では、敵の座標からプレイヤーの座標を引いて距離を表してますが、
それだと逆方向のプラスの向きの角度が取得されてしまうので、
プレイヤーの座標から敵の座標を引いた距離を指定します。

rad=atan2(py-y,px-x);

これでθ角が求められました。
これを先程の公式に当てはめれば、プレイヤーの方向に向いたショットが撃てます。

その下のswitch文を見てください。
ショットパターンが0のものについては、
単純に座標を代入してるだけです。

パターン1については、
6ループに一回発射で、scountが54までなので10発発射してます。

パターン2については、
1回のループで3発発射させて、それを5ループさせてますので合計15発です。
numという変数が発射した弾数を表しています。
0の時はradから10度引いた角度なので、
プレイヤーが居る位置よりも若干左よりになります。
1の時はそのままプレイヤーの居る方向に発射。
2の時は10度足した位置なので、若干右よりになります。
計算式は先程ラジアンの求め方の公式を書いておいたのでわかりますよね?
numが3、つまり3発発射したらループを抜けるようにしています。


パターン3は、乱射です。
numが0、つまり初回だけ、srand関数で乱数を初期化してます。
その下の計算式は以下のようになっています。

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

角度は毎回変化させたいので、rad変数を使わずにatan2を使って直接計算させてます。
イメージとしてはプレイヤーの左右60度以内の間で弾を乱射させるようにします。
ですので、まず60度分のラジアンを引きます。
その60度分引いた位置から、右に120度広げた位置までが範囲になるので、
乱数を120で割って余りを求めます。
これでランダムに120より小さい値が取得できます。
これを同じようにラジアンに変換して、加算してやれば、プレイヤーの左右60度以内の範囲でのラジアンが
ランダムで求められます。

さらにその下にあるswitch文を見てください。
ここは実際に弾の移動を行う部分です。
パターン1については、そのまま下に打つだけなので、y座標に弾のスピード分の値だけ足しています。
それ以外のパターについては、先程の公式に当てはめて座標を代入しています。
C言語の関数にcosやsinに求めたラジアンを指定すれば、cosθやsinθが求まります。

これで弾の移動も問題なくできることになります。
その下のShotOutCheck関数は弾が画面からはみ出してないかをチェックする関数です。
単純に添字を渡して、その添字の弾の座標が画面からはみ出してないかチェックし、
bool値を返しているだけです。
はみ出してた場合はフラグをfalseにしています。

なお、弾の描画についてはDraw関数の該当部分を以下のように変更します。

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);
	}
}


弾の種類によって処理を分岐しています。
回転させる必要のない0と2の弾であれば単純にそのまま描画し、
それ以外であればDrawRotaGraph関数を使っています。
DrawRotaGraphの角度の指定で90度分のラジアンを足していますが、
こうしないと画像の向きが正しく向かなくなるからです。

さて、敵データを作って見ましょう。
今回は以下のようなデータを作りました。

また、以下二つの弾も追加しました。


これに伴って、ENEMYクラスのコンストラクタは以下のように変更してます。

ENEMY::ENEMY(int type,int stype,int m_pattern,int s_pattern,int in_time,int stop_time,int shot_time,int out_time,int x,int y,int speed,int hp,int item)
{
	

	//サイズ
	width=27;
	height=25;
	
	//敵の種類
	this->type=type;
	this->stype=stype;

	//移動パターンとショットパターン
	this->m_pattern=m_pattern;
	this->s_pattern=s_pattern;

	this->x=x;
	this->y=y;

	this->in_time=in_time;

	this->stop_time=stop_time;

	this->shot_time=shot_time;

	this->out_time=out_time;

	//hpとアイテム代入
	this->hp=hp;
	this->item=item;

	//敵画像読み込み
	if(type==0){
		LoadDivGraph("enemy.png",3,1,3,27,25,gh);
	}

	int temp;
	//弾画像読み込み
	switch(stype){
		case 0:
			temp=LoadGraph("enemyshot1.png");
			break;
		case 1:
			temp=LoadGraph("enemyshot2.png");
			break;
		case 2:
			temp=LoadGraph("enemyshot3.png");
			break;

	}

	int w,h;
	GetGraphSize(temp,&w,&h);

	for(int i=0;i<ENEMY_SNUM;++i){
		shot[i].flag=false;
		shot[i].gh=temp;
		shot[i].width=w;
		shot[i].height=h;
		shot[i].pattern=s_pattern;
		shot[i].speed=speed;
		shot[i].x=x;
		shot[i].y=y;
	}

	count=0;
	scount=0;
	num=0;
	rad=0;
	
	deadflag=false;
	endflag=false;
	sflag=false;

}

弾画像の読み込み部分をstypeが増えたので、
読み込む画像を追加しただけです。

この敵データを読み込むことで、以下の動画のように、ショットパターンを増やすことができます。

さらにゲームっぽくなってきましたね?
今回の説明は以上です。
次回は音を鳴らしてみましょう。

>> 【音を鳴らしてみよう】に進む
>> シューティングゲーム作成入門トップに戻る