オブジェクト指向を意識してC++でシューティングゲームを作る(6)

前回「オブジェクト指向を意識してC++でシューティングゲームを作る(5) - 怠惰な人間のゲーム制作日記」で敵を出現させるところまでやりました。

今回は実際に敵に弾を当ててみましょう。

まず、当たり判定は円と円の当たり判定で計算します。
ちなみに、円と円の当たり判定のやり方は
f:id:haina817:20170413222802p:plain

(自機の座標(青) - 敵の座標(赤))で敵から自機のベクトル(黄)が出るので
x、y要素を二乗してから足して距離を出し(√でもいいが処理が重たいので非推奨)
そこに自機と敵の半径を足して二乗した(上の計算で√した場合はそのまま)数と比べて足した方が大きければ当たっていると判断できます。

もう少しわかりやすく式を書くとこんな感じ。(学校で最初に習うやり方は多分こっち)
(自機のX座標(青) - 敵のX座標(赤))^2 + (自機のY座標(青) - 敵のY座標(赤))^2 <= (自機と敵のあたり判定の和)^2

とにかく実装していきます。

今から当たり判定関数を作りたいのですが、当たり判定を行う際にそれに必要な情報を揃えないといけません。
ちなみに必要な情報は以下の通りです。

  • 自機と敵(又は弾)の座標
  • 自機と敵(又は弾)の当たり判定の合計
  • 当たった時にどうするかの処理(boolで返すだけでいい場合はなくてもok)

この3つを楽に取得しようとするならば敵、弾、自機全ての親クラス、Objectクラスを呼び出すのが一番楽です。

Object.h

#pragma once

class CObject
{
protected:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int *graphic;
	//あたり範囲
	float range;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CObject(){};
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	virtual ~CObject(){};
	//描画
	virtual void Render() = 0;
	//更新
	virtual void Update() = 0;

	//posのアドレスを渡す関数
	VECTOR *GetPos(){ return &pos; }
	//rangeの値を返す関数
	float GetRange(){ return range; }
	//当たった時の処理(全て処理が違うので仮想関数にする)
	virtual void HitAction(){};
};

これでGetPos関数で各オブジェクトの座標を、GetRange関数で当たり範囲を、HitActionで当たった時の挙動を扱うことが出来ます。
それでは次に当たり判定の関数を作っていきます。
今回は小規模のゲームなのでObject.cppに書いていきますが、本当はファイルを分けることをお勧めします。
Object.cpp

void ChackHitCircle(CObject *obj1, CObject *obj2)
{
	//やってることはこれ
	//((obj1.pos.x - obj2.pos.x)*(obj1.pos.x - obj2.pos.x) + (obj1.pos.y - obj2.pos.y)*(obj1.pos.y - obj2.pos.y))
	float distance = VSquareSize(VSub(*obj1->GetPos(), *obj2->GetPos()));
	float range = (obj1->GetRange() + obj2->GetRange())*(obj1->GetRange() + obj2->GetRange());
	
	//HIT
	if (distance < range)
	{
		obj1->HitAction();
		obj2->HitAction();
	}
}

プロトタイプ宣言もしておきます。
Object.h

void ChackHitCircle(CObject *obj1, CObject *obj2);

これで当たり判定関数の完成です。

次にどこで当たり判定をするかですが、この当たり判定関数はObjectクラスを継承したクラスのみしか受け付けてくれません。
つまりManagerクラスでもダメだし、プロトタイプ宣言のみのEnemyクラスもエラーが出ます。

ではどうするか...

BulletManager.cpp

CObject *CBulletManager::GetBullet(int num){ return (CObject*)bullet[num]; }

はい、こうします(笑)
何をやっているかと言うと、ObjectクラスでCEnemy型のポインタをキャストしています。
ちなみにこれに限らず、ポインタ型は、明示的にキャストすることで、他のポインタ型に変換できます(便利ですよね~ww)

これをEnemyManagerにも同じようにします。

EnemyManager.h

#pragma once
class CEnemy;
//敵の数
#define ENEMY_NUM 100
class CEnemyManager
{
	CEnemy *enemy[ENEMY_NUM];
	//敵の画像
	int enemyGraphic;
public:
	CEnemyManager();
	~CEnemyManager();
	void Update();
	void Render();
	CObject *GetEnemy(int num){ return (CObject*)enemy[num]; }
private:
	//敵の生成
	void Spawn();
};

次に当たり範囲を設定します。
これは各クラスのコンストラクタで定義します。

Enemy.cpp

CEnemy::CEnemy(int *tex, VECTOR &position)
{
	graphic = tex;
	pos = position;
	flag = true;
	hp = 5;
	range = 10;
}

Player.cpp

CPlayer::CPlayer(CManager *pManager)
{
	//CManagerのアドレス格納
	manager = pManager;
	//画像データ格納
	graphic = new int;
	*graphic = LoadGraph("graphic/Player/player.png");
	bulletGraphic = LoadGraph("graphic/Bullet/playerBullet.png");
	//位置を初期化
	pos = VGet(320, 240, 0);
	range = 10;

}

Bullet.cpp

CBullet::CBullet(int *tex, VECTOR &position, VECTOR &vPosition)
{
	graphic = tex;
	pos = position;
	vPos = vPosition;
	flag = true;
	range = 10;

}


次に当たった際の処理を書いていきます
今回は当たったら消す処理をします
Bullet.h

#pragma once
//弾の管理
class CBullet :public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
	//弾の生存フラグ(true = 生きている)
	bool flag;
public:
	CBullet(){};
	CBullet(int *tex, VECTOR &position, VECTOR &vPosition);
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
	//弾の生存フラグを取得
	bool GetFlag(){ return flag; };
	void HitAction(){ flag = false; }

};

Enemy.h

#pragma once

class CEnemy : public CObject
{
protected:
	//敵のHP
	int hp;
	//進む方向ベクトル
	VECTOR vPos;
	//敵の生存フラグ(true = 生きている)
	bool flag;
public:
	CEnemy(){};
	CEnemy(int *tex, VECTOR &position);
	~CEnemy();
	//描画
	void Render();
	//更新
	void Update();
	//敵の生存フラグを取得
	bool GetFlag(){ return flag; };
	void HitAction(){ flag = false; }
};

と、ここで明らかな間違いを見つけてしまったので修正します

Bullet.cpp

void CBullet::Update()
{
	//最初にフラグを倒す
	flag = false;
	pos = VAdd(pos, vPos);
	//画面内にいるか確認
	if (pos.x < GAME_SCREEN_WIDTH && pos.x > 0)
	{

		if (pos.y < GAME_SCREEN_HEIGHT && pos.y > 0)
		{
			//画面内ならフラグを元に戻す
			flag = true;
		}

	}
}

これ間違いなくflagを消しても復活してきますよね、ごめんなさい(笑)
なのでこう修正しました
Bullet.cpp

void CBullet::Update()
{
	pos = VAdd(pos, vPos);
	//画面内にいるか確認
	if (pos.x > GAME_SCREEN_WIDTH || pos.x < 0 ||
		pos.y > GAME_SCREEN_HEIGHT || pos.y < 0)
	{
		//画面外なら消す
		flag = false;
	}
}


んでEnemyの方も同じような感じで作っていたのでそこも修正
Enemy.cpp

void CEnemy::Update()
{
	//最初にフラグを倒す
	//flag = false;
	//下に移動
	vPos = VGet(0, 1, 0);
	pos = VAdd(pos, vPos);
	//画面の縦の長さ+100の数値まで敵が来たら消すようにする
	if (pos.y < GAME_SCREEN_HEIGHT + 100)
	{
		//画面内ならフラグを元に戻す
		flag = true;
	}
}

Enemy.cpp

void CEnemy::Update()
{
	//下に移動
	vPos = VGet(0, 1, 0);
	pos = VAdd(pos, vPos);
	//画面の縦の長さ+100の数値まで敵が来たら消すようにする
	if (pos.y > GAME_SCREEN_HEIGHT + 100)
	{
		//画面内ならフラグを元に戻す
		flag = false;
	}
}

では間違いなく出来ているか検証してみます。

Game.cpp

void CGame::Update()
{
	player->Update();
	bulletManager->Update();
	enemyManager->Update();
	//当たり判定
	for (int i = 0; i < BULLET_NUM; i++)
	{
		if (bulletManager->GetBullet(i) != NULL)
		{
			for (int j = 0; j < BULLET_NUM; j++)
			{
				if (enemyManager->GetEnemy(j) != NULL)
				{
					ChackHitCircle(bulletManager->GetBullet(i), enemyManager->GetEnemy(j));
				}
			}
		}
	}
}


f:id:haina817:20170414002824p:plain
f:id:haina817:20170414002830p:plain


弾と敵が消えました。

これで今回は終わりにします。
次は自機の弾と敵の弾の区別の処理を実装していきます