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

前回の「オブジェクト指向を意識してC++でシューティングゲームを作る(2)」で自機の描画と動かす所までやりました。
今回は弾を発射する所をやりたいと思います。

やること

  1. Bulletクラスを作る
  2. 基底クラスを作って共通部分をまとめる
  3. 弾を決まった方向に飛ばす
  4. BulletManagerクラスで全ての弾の管理をできるようにする
  5. 弾の発射


Bulletクラスを作る

弾の描画と更新を管理するクラスを作りたいと思います。
ではcppとhファイルを作成します。
f:id:haina817:20160930134523p:plain

cppファイルのほうにDxlibとBullet.hをインクルードします。

Bullet.hの方でBulletクラスを作ります。

Bullet.h

#pragma once
//弾の管理
class CBullet
{
private:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	CBullet();
	~CBullet();
};

Bullet.cpp

CBullet::CBullet()
{
}
CBullet::~CBullet()
{
}

と、ここで前回のCPlayerクラスでも同じ部分があることにお気づきでしょうか?

CPlayer.h

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

この中のメンバー変数posとgraphicは自機と弾には共通の用途なので、今から1つにまとめたいと思います。


基底クラスを作って共通部分をまとめる

新しくObjectクラスを作るため、cppとhファイルを作成します。
f:id:haina817:20160930135248p:plain

Bulletと同じようにcppファイルのほうにDxlibとObject.hをインクルードします。

Objectクラスを作成します。
Object.h

#pragma once
//座標と画像を持つオブジェクトの基底クラス
class CObject
{
protected:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CObject(){};
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	virtual ~CObject(){};
	//描画(純粋仮想関数)
	virtual void Render() = 0;
	//更新(純粋仮想関数)
	virtual void Update() = 0;
};

ここで「純粋仮想関数」というものが出てきました。
これは簡単に言うと継承で書き換えるのを前提として作る関数です。
座標と画像データが同一な為にCObjectにまとめましたが、各オブジェクト毎に描画の仕様や更新の内容が違ってきます。なので、「更新と描画はするけど、内容は各クラスで違うよ」と明記するためにこれを書きます。派生先の子クラスでUpdateとRender関数を書かないとエラーが出るので書き忘れ防止にもなります。

これをCPlayerとCBulletに派生させていきます。
Player.hとBullet.hがインクルードされている全てのファイルにObject.hをインクルードします。
f:id:haina817:20160930142558p:plain



次にCPlayerクラスとCBulletクラスにCObjectクラスを継承します。
Player.h

class CPlayer:public CObject
{
	CManager *manager;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

Bulletはついでに更新描画も作成しておきます。
Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
public:
	CBullet();
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

Bullet.cpp

void CBullet::Update()
{
}
void CBullet::Render()
{
}


これで座標と画像データをCObjectクラスにまとめるとこができました。
次に弾を飛ばす処理の部分を制作します。

弾を決まった方向に飛ばす
今度は弾を指定した方向に飛ばす処理を書いていきます。
弾を飛ばす処理には色々ありますが、今回は簡単に方向ベクトルを設定し、その方向に毎フレーム移動していきます。

最初に方向ベクトルを取得する変数を作成します。

Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet();
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

そしてUpdate関数内で自分の今の座標を足せば移動処理は完成です。
Bullet.cpp

void CBullet::Update()
{
	pos = VAdd(pos, vPos);
}

次に進む方向ベクトルを入れる構造を作ります。
方向ベクトルを取得する時は基本的にインスタンス生成時なので、
コンストラクタに弾の発生位置と方向ベクトルを引数に持たせたいと思います。

Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet(VECTOR &position, VECTOR &vPosition);
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

Bullet.cpp

CBullet::CBullet(VECTOR &position, VECTOR &vPosition)
{
	pos = position;
	vPos = vPosition;
}

これで弾の動きの処理は完成です。
それではこの弾を複数出す時に一括で管理出来るように弾の管理クラスを作ります。

BulletManagerクラスで全ての弾の管理をできるようにする
では弾を一括で管理するクラスBulletManagerクラスを作ります。
まずcpp、hファイルを作ってください。

f:id:haina817:20161005155710p:plain

BulletManagerクラスを作り、その中にBulletのポインタ配列を作ります。
Bulletのポインタを定義するため、上にBulletクラスをプロトタイプ宣言しておきます。
BulletManager.h

class CBullet;
#pragma once
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
};

このままだとエラーが出るのでcppファイルでインクルードしてください。

f:id:haina817:20161005183800p:plain

そしてコンストラクタの中でポインタ配列を全てNULLにし、デストラクタで全て解放します。

BulletManager.cpp

CBulletManager::CBulletManager()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		bullet[num] = NULL;
	}

}
CBulletManager::~CBulletManager()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		delete bullet[num];
	}
}

NULLを入れることにより一斉にdeleteする際、NULLが入ったポインタは無視されるためエラーが出なくなります。

では最後に弾の発射部分を作ります。
BulletManagerクラスにShot関数を作ります。
BulletManager.h

class CBullet;
#pragma once
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
	void Shot(VECTOR &pos, VECTOR &vPos);
};


Shotの中身はNULLのbulletを探してそのポインタを使って動的確保するだけ。

BulletManager,cpp

void CBulletManager::Shot(VECTOR &pos, VECTOR &vPos)
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		if (bullet[num] == NULL)
		{
			bullet[num] = new CBullet(pos,vPos);
			break;
		}
	}
}

これで後はbulletのNULLでないポインタ変数を全て更新、描画すれば完成です。

BulletManager.h

class CBullet;
#pragma once
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
	void Shot(VECTOR &pos, VECTOR &vPos);
	void Update();
	void Render();
};

BulletManager.cpp

void CBulletManager::Update()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		//NULLでない場合
		if (bullet[num] != NULL)
		{
			bullet[num]->Update();
		}
	}
}
void CBulletManager::Render()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		//NULLでない場合
		if (bullet[num] != NULL)
		{
			bullet[num]->Render();
		}
	}
}


そういえば弾のグラフィックをまだ読み込んでいなかったので今から読み込もうと思います。
ですか、数がかなり多いのでここは一つ一つ読み込まず、読み込む回数は一回にして、弾は全て読み込んだ画像データのアドレスを参照して読み込むことにします。
Objectクラスのgraphicをポインタ変数にします。
Object.h

#pragma once

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


するとPlayerの画像を格納している部分と描画している部分がエラーを吐くので、最初にgraphicを動的確保し、実態を得たgraphic変数に画像を格納します。
動的確保したため、デストラクタを使って解放し、描画のところのgraphic変数をポインタ先にします。
ついでに弾の画像をPlayer自身に持たせたいため、弾の画像の格納も一緒にやります。
Player.h

#pragma once
class CManager;
class CPlayer:public CObject
{
	CManager *manager;
	//弾の画像
	int bulletGraphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

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);
}
CPlayer::~CPlayer()
{
	delete graphic;
}
void CPlayer::Render()
{
	// 読みこんだグラフィックを回転描画
	DrawRotaGraph(pos.x, pos.y, 1.0, 0, *graphic, TRUE);
}

これでエラーが消えたと思います。
次にBulletクラスのコンストラクタの引数にint型のポインタ変数を定義します
Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet();
	CBullet(int *tex,VECTOR &position, VECTOR &vPosition);
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

そしてgraphicに代入し、Render関数で描画します。

CBullet::CBullet(int *tex,VECTOR &position, VECTOR &vPosition)
{
	graphic = tex;
	pos = position;
	vPos = vPosition;
}
void CBullet::Render()
{
	// 読みこんだグラフィックを回転描画
	DrawRotaGraph(pos.x, pos.y, 1.0, 0, *graphic, TRUE);
}

BulletManagerのShot関数で弾を生成するためこちらにも引数にint型のポインタ変数を入れてBulletのコンストラクタの引数に渡します。
BulletManager.h

#pragma once
class CBullet;
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];

public:
	CBulletManager();
	~CBulletManager();
	void Shot(int *tex, VECTOR &pos, VECTOR &vPos);
	void Update();
	void Render();
};

BulletManager.cpp

void CBulletManager::Shot(int *tex,VECTOR &pos, VECTOR &vPos)
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		if (bullet[num] == NULL)
		{
			bullet[num] = new CBullet(tex,pos,vPos);
			break;
		}
	}
}

では最後に弾の発射をして終わりたいと思います。

弾の発射

弾の発射はPlayerで行います。

PlayerクラスにBulletManagerクラスのポインタ変数と、アドレスを取得する関数を作ります。

Player.h

#pragma once
class CManager;
//これがないとエラーが出る
class CBulletManager;
class CPlayer:public CObject
{
	CManager *manager;
	//弾の画像
	int bulletGraphic;
	CBulletManager *bulletManager;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
	//bulletManagerのアドレスを取得
	void SetBulletManager(CBulletManager *bullet){ bulletManager = bullet; }
};

cppファイルの上にBulletManagerをインクルードします。

f:id:haina817:20161005221809p:plain

更新の関数の所で発射部分を作ります。
Player.cpp

void CPlayer::Update()
{
	//移動する向き
	VECTOR vPos = VGet(0, 0, 0);
	//弾の発射を抑制するトリガー
	static int cnt = 0;
	//移動していないときは正規化する必要がないのでこれで移動しているか確認する
	bool moveFlag = false;
	if (manager->GetKey()[KEY_INPUT_LEFT] > 0)
	{
		vPos.x -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_RIGHT] > 0)
	{
		vPos.x += 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_UP] > 0)
	{
		vPos.y -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_DOWN] > 0)
	{
		vPos.y += 1.0f;
		moveFlag = true;
	}
	if (moveFlag)
	{
		vPos = VNorm(vPos);
		vPos = VScale(vPos, PLAYER_SPEED);
		//現在の座標に移動量を加算する
		pos = VAdd(pos, vPos);
	}
	//弾の発射
	if (manager->GetKey()[KEY_INPUT_SPACE] > 0)
	{
		cnt++;
	}
	else
	{
		cnt = 0;
	}
	if ((cnt % 10) == 1)
	{
		//上に弾を飛ばす
		bulletManager->Shot(&bulletGraphic, pos, VGet(0, -1, 0));
	}
}

最後にGameクラスでBulletManagerを動的確保と更新描画、Playerクラスにアドレスを取得させれば完成です。
Game.h

#pragma once
class CPlayer;
class CBulletManager;
class CGame:public CScene
{
	//自機のポインタ変数
	CPlayer *player;
	//弾の全体管理しているクラスのポインタ変数
	CBulletManager *bulletManage;
public:
	CGame(CManager *pManager);
	~CGame();
	void Update();
	void Render();
};

BulletManagerのインクルードを忘れずに。

Game.cpp

#include"DxLib.h"
#include"Manager.h"
#include"Game.h"
#include"Object.h"
#include"Player.h"
#include"BulletManager.h"

CGame::CGame(CManager *pManager) : CScene(pManager)
{
	//プレイヤーを動的確保
	player = new CPlayer(pManager);
	//弾の全体管理しているクラスを動的確保
	bulletManage = new CBulletManager();
	//CBulletManagerクラスのアドレス取得
	player->SetBulletManager(bulletManage);
}
CGame::~CGame()
{
	//動的確保したものを解放する
	delete player;
	delete bulletManage;
}
void CGame::Update()
{
	player->Update();
	bulletManage->Update();
	
}
void CGame::Render()
{
	player->Render();
	bulletManage->Render();
}

それでは実行してみましょう。


f:id:haina817:20161005223558p:plain

こんな感じで弾を飛ばすことに成功しました。

今回はスクショするために遅くしましたが

//弾の発射
	if ((cnt % 10) == 1)
	{
		//上に弾を飛ばす
		bulletManager->Shot(&bulletGraphic, pos, VGet(0, -1, 0));
	}

のVGet(0,-1,0)の部分をもう少し大きくすればもっと速い球が出せるようになります。

今回はここまでにします。

このままでは弾が100発しか出せないようになっているので、次はいらなくなった弾の解放からやりたいと思います。

次回(オブジェクト指向を意識してC++でシューティングゲームを作る(4) - hainaのゲーム制作日記

※追記。
キー取得の関数gpUpdateKeyに不具合があったので修正いたしました

//キーの入力状態を更新する
int gpUpdateKey(char *key){
	//現在のキーの入力状態を格納する
	char tmpKey[KEY_NUM];
	//全てのキーの入力状態を得る
	GetHitKeyStateAll(tmpKey);
	for (int i = 0; i<KEY_NUM; i++){
		if (tmpKey[i] != 0)
		{//i番のキーコードに対応するキーが押されていたら加算
			key[i]++;
		}
		else 
		{//押されていなければ0にする
			key[i] = 0;
		}
	}
	return 0;
}

//キーの入力状態を更新する
int gpUpdateKey(char *key){
	//現在のキーの入力状態を格納する
	char tmpKey[KEY_NUM];
	//全てのキーの入力状態を得る
	GetHitKeyStateAll(tmpKey);
	for (int i = 0; i<KEY_NUM; i++){
		if (tmpKey[i] != 0)
		{//i番のキーコードに対応するキーが押されていたら加算
			if (key[i] == 120)
			{
				key[i] = 0;
			}
			key[i]++;
			
		}
		else 
		{//押されていなければ0にする
			key[i] = 0;
		}
	}
	return 0;
}