オブジェクト指向を意識してC++でシューティングゲームを作る(6)
前回「オブジェクト指向を意識してC++でシューティングゲームを作る(5) - 怠惰な人間のゲーム制作日記」で敵を出現させるところまでやりました。
今回は実際に敵に弾を当ててみましょう。
まず、当たり判定は円と円の当たり判定で計算します。
ちなみに、円と円の当たり判定のやり方は
(自機の座標(青) - 敵の座標(赤))で敵から自機のベクトル(黄)が出るので
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)); } } } } }
弾と敵が消えました。
これで今回は終わりにします。
次は自機の弾と敵の弾の区別の処理を実装していきます