見出し画像

DXライブラリで2Dゲームを作ろう②

はじめに

この記事は、「DXライブラリで2Dゲームを作ろう①」の続きの記事となっています。

この記事では、プレイヤーが弾を発射できるようにする「弾うち」の仕組みと当たり判定を実装していきます。




3.弾うち実装

3-1 Shotクラスを作成する

まずは、弾を管理するShotクラスを作成します。
このクラスでは、弾の更新や描画などまとめて管理できるようにします。

Shotクラス作成

3-2 弾を表示する

続いて、画面上に弾が表示されるようにしていきます。

まずは弾の画像を準備します。
前回の2-2-1で作成したdataフォルダの中に、shot.pngを追加します。

今回は32×32のサイズの画像を使っています。
画像は好きなものを使ってもらって大丈夫です。
画像探すのがめんどうだな~って人は以下の画像を使ってください。

Shot.png

Shotクラスに画像を保存するメンバ変数と描画処理を追加します。
・Shot.h

#include "Vec2.h"

private:
	int m_handle; // 弾の画像
    Vec2 m_pos;   // 弾の位置

・Shot.cpp

#include "DxLib.h"
#include "Shot.h"

Shot::Shot():
	m_pos({0, 0})
{
	m_handle = LoadGraph("data/shot.png");
}

Shot::~Shot()
{
	DeleteGraph(m_handle);
}

void Shot::Draw()
{
	DrawGraph(m_pos.x, m_pos.y, m_handle, true);
}

次に、ボタンを押したら弾を発射できるように、Playerクラスに処理を追加します。
弾はコントローラーのBボタン(キーボードだとXキー)で発射できるようにします。
弾を発射する処理を作るうえで、「誰が弾を発射するか」「誰が弾を管理するか」という役割を分けて考えることが大切です。

今回は以下のように役割を分担しました。
・Playerクラス : ボタンが押されたかを判断し、「弾を撃ちたい」という意思だけを伝える。
・SceneMainクラス : 実際に弾を生成し、そのポインタを保持・管理する。

このように役割を分けることで、弾の生成や削除などの管理がしやすくなります。
・Player.h

class Shot;

public:
	/// <summary>
	/// 弾生成
	/// </summary>
	/// <returns>Shotポインタ</returns>
	Shot* CreateShot();

・Player.cpp

#include "Shot.h"

Shot* Player::CreateShot()
{
	if (Pad::IsTrigger(PAD_INPUT_2))
	{
		Shot* pShot = new Shot();

		return pShot;
	}

	return nullptr;
}

・SceneMain.h

class Shot;

class SceneMain
{
private:
	/// <summary>
	/// 弾更新
	/// </summary>
	void UpdateShot();

private:
	Shot* m_pShot;
};

・SceneMain.cpp

#include "Shot.h"

SceneMain::SceneMain():
	m_pShot(nullptr)
{
}

void SceneMain::Update()
{
	UpdateShot();
}

void SceneMain::Draw()
{
	if (!m_pShot) return;
	m_pShot->Draw();
}

void SceneMain::UpdateShot()
{
	Shot* newShot = m_pPlayer->CreateShot();
	if (newShot != nullptr)
	{
		m_pShot = newShot;  // 弾を保持
	}

	if (!m_pShot) return;
	m_pShot->Update();
}

これで実行してBボタン(キーボードはXキー)を押すと、画面の左上に弾が表示されるようになりました。

実行画面

ただし、今のままだと弾が左上に固定されて表示され続けてしまうため、弾が移動するような処理を追加していきます。


3-3 弾の移動

弾が発射された後、まっすぐ横に進むような処理をShotクラスに追加していきます。
・Shot.h

class Shot
{
private:
	Vec2 m_move;  // 移動量
}

・Shot.cpp

namespace
{
	constexpr float kSpeed = 10.0f; // 弾の移動速度
}

Shot::Shot():
	m_move({kSpeed, 0})
{
}

void Shot::Update()
{
	m_pos += m_move;
}

これで実行すると、弾が右方向に移動するようになりました。

実行画面

しかし現在の実装では、弾は画面の左上から表示され右方向にしか発射されません。
そこで、プレイヤーの位置から弾を発射しプレイヤーの向きに応じて発射方向が変わるように修正していきます。
具体的には、弾を生成する際に、プレイヤーの現在位置と向きの情報をShotクラスに渡すようにします。
Shotクラスに SetInfo() 関数を追加し、プレイヤー側から座標と向きを渡せるようにします。
・Shot.h

class Shot
{
public:
	/// <summary>
	/// 弾情報をセットする
	/// </summary>
	/// <param name="pos">座標</param>
	/// <param name="isRight">右方向に移動するか</param>
	void SetInfo(const Vec2& pos, bool isRight);
}

・Shot.cpp

void Shot::SetInfo(const Vec2& pos, bool isRight)
{
	m_pos = pos;

	if (isRight)
	{
		m_move.x = kSpeed;
	}
	else
	{
		m_move.x = -kSpeed;
	}
}

・Player.cpp

Shot* Player::CreateShot()
{
	if (Pad::IsTrigger(PAD_INPUT_2))
	{
		Shot* pShot = new Shot();
		pShot->SetInfo(m_pos, m_isRight);
		return pShot;
	}

	return nullptr;
}

再度実行すると、プレイヤーが右を向いているときは右に、左を向いているときは左に弾が発射されるようになりました。

実行画面

3-4 弾を消す

続いては、弾が画面外に出たり、使用後に削除できるようにします。
削除することで、メモリの無駄遣いや処理負荷を防ぐことができます。

この削除の処理はSceneMainクラスに追加していきます。
・SceneMain.h

class Shot
{
private:
	/// <summary>
	/// 弾削除
	/// </summary>
	void DeleteShot();
}

・SceneMain.cpp

void SceneMain::DeleteShot()
{
	if (!m_pShot) return;

	delete m_pShot;
	m_pShot = nullptr;
}

次に、弾が画面外に出たタイミングでDeleteShot()を呼び出す処理を実装します。
このままだと「本当に削除されたのか」が分かりづらいので、DeleteShot()が呼ばれたタイミングでPrintfDx()を使ってログを出力するようにします。
・SceneMain.cpp

#include "DxLib.h"

namespace
{
	constexpr float kScreenWidth = 1980.0f; // 画面幅
}

void SceneMain::UpdateShot()
{
	// 画面外に出たら削除する
	bool isDelete = false;
	isDelete = m_pShot->GetPos().x < 0 || m_pShot->GetPos().x > kScreenWidth;
	if (!isDelete) return;
	DeleteShot();
}

void SceneMain::DeleteShot()
{
	printfDx("弾削除\n");
}

これで実行してみると、弾が画面外に出たとき、左上にログが表示されるようになりました。
れで削除処理が正しく行われていることを確認できたので、コードのprintfDx("弾削除\n");は削除しておきます。

実行画面

3-5 弾を複数発射できるようにする

現在の実装では、弾を1発しか撃てない状態です。
これは、Shot* m_pShotという1つのポインタしか用意しておらず、新しい弾を発射するたびに前の弾が上書きされてしまうからです。

そこで、複数の弾を同時に扱えるように、Shot* m_pShot[kShotMax] とポインタを配列で管理するように変更します。
配列はコンストラクタで nullptrに初期化し 、発射処理では空いているスロットにのみ新しい弾を追加するようにします。
・SceneMain.h

class SceneMain
{
	/// <summary>
	/// 弾削除
	/// </summary>
	/// <param name="index">インデックス</param>
	void DeleteShot(int index);

private:
	Shot* m_pShot[3];
};

・SceneMain.cpp

namespace
{
	constexpr int kShotMax = 3; // 1度に撃てる最大弾数
}

SceneMain::SceneMain()
{
	for (int i = 0; i < kShotMax; i++)
	{
		m_pShot[i] = nullptr;
	}
}

void SceneMain::Draw()
{
	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;
		m_pShot[i]->Draw();
	}
}

void SceneMain::UpdateShot()
{
	Shot* newShot = m_pPlayer->CreateShot();
	if (newShot != nullptr)
	{
		for (int i = 0; i < kShotMax; i++)
		{
			if (m_pShot[i] == nullptr)
			{
				m_pShot[i] = newShot;
				break;
			}
		}
	}

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;

		m_pShot[i]->Update();

		// 画面外に出たら削除する
		bool isDelete = false;
		isDelete = m_pShot[i]->GetPos().x < 0 || m_pShot[i]->GetPos().x > kScreenWidth;
		if (isDelete)
		{
			DeleteShot(i);
		}
	}
}

void SceneMain::DeleteShot(int index)
{
	if (!m_pShot[index]) return;

	delete m_pShot[index];
	m_pShot[index] = nullptr;
}

これで実行してみると、1画面に3発まで撃てるようになりました。
連続でボタンを押しても前の弾が消えなくなったのが確認できました。

実行画面

4.当たり判定実装

ここからは、キャラクターや弾の当たり判定を実装していきます。
当たり判定はすべて矩形(四角形)で作成します。
矩形同士の当たり判定の仕組みについては、以下のサイトを確認してください。


4-1 Rectクラスを作成する

まずは、当たり判定を管理するRectクラスを作成します。
このクラスでは、矩形の位置やサイズを設定・取得したり他の矩形との衝突判定を行えるようにします。

Rectクラス作成

ここから、当たり判定を行うために必要な処理を追加していきます。
・Rect.h

#pragma once
#include "Vec2.h"

/// <summary>
/// 矩形を管理するクラス
/// </summary>
class Rect
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Rect();

	/// <summary>
	/// デストラクタ
	/// </summary>
	~Rect();

	/// <summary>
	/// 矩形の描画
	/// </summary>
	/// <param name="color">矩形の色</param>
	/// <param name="isFill">太さ</param>
	void Draw(unsigned int Color, bool isFill);

	/// <summary>
	/// 左上座標と幅高さを指定
	/// </summary>
	/// <param name="left">左</param>
	/// <param name="top">上</param>
	/// <param name="width">幅</param>
	/// <param name="height">高さ</param>
	void SetLT(float left, float top, float width, float height);

	/// <summary>
	/// 中心座標と幅高さを指定
	/// </summary>
	/// <param name="x">中心のX座標</param>
	/// <param name="y">中心のY座標</param>
	/// <param name="width">幅</param>
	/// <param name="height">高さ</param>
	void SetCenter(float x, float y, float width, float height);

	/// <summary>
	/// 矩形の幅を取得
	/// </summary>
	/// <returns>矩形の幅</returns>
	float GetWidth() const;

	/// <summary>
	/// 矩形の高さを取得
	/// </summary>
	/// <returns>矩形の高さ</returns>
	float GetHeight() const;

	/// <summary>
	/// 矩形の中心座標を取得
	/// </summary>
	/// <returns>矩形の中心座標</returns>
	Vec2 GetCenter() const;

	/// <summary>
	/// 矩形の左上のX座標を取得
	/// </summary>
	/// <returns>左上のX座標</returns>
	float GetLeft() const { return m_left; }

	/// <summary>
	/// 矩形の左上のY座標を取得
	/// </summary>
	/// <returns>左上のY座標</returns>
	float GetTop() const { return m_top; }

	/// <summary>
	/// 矩形の右下のX座標を取得
	/// </summary>
	/// <returns>右下のX座標</returns>
	float GetRight() const { return m_right; }

	/// <summary>
	/// 矩形の右下のY座標を取得
	/// </summary>
	/// <returns>矩形の右下のY座標</returns>
	float GetBottom() const { return m_bottom; }

	/// <summary>
	/// 矩形の当たり判定
	/// </summary>
	/// <param name="rect">矩形情報</param>
	/// <returns>当たってるかどうか</returns>
	bool IsCollision(const Rect& rect);

public:
	float m_left;	// 左上のX座標
	float m_top;	// 左上のY座標
	float m_right;	// 右下のX座標
	float m_bottom;	// 右下のY座標
};

・Rect.cpp

#include "DxLib.h"
#include "Rect.h"
#include <cassert>

Rect::Rect():
	m_left(0.0f),
	m_top(0.0f),
	m_right(0.0f),
	m_bottom(0.0f)
{
}

Rect::~Rect()
{
}

void Rect::Draw(unsigned int color, bool isFill)
{
	DrawBox(static_cast<int>(m_left), static_cast<int>(m_top), static_cast<int>(m_right), static_cast<int>(m_bottom), color, isFill);
}

void Rect::SetLT(float left, float top, float width, float height)
{
	m_left = left;
	m_top = top;
	m_right = left + width;
	m_bottom = top + height;
}

void Rect::SetCenter(float x, float y, float width, float height)
{
	m_left = x - width / 2;
	m_top = y - height / 2;
	m_right = x + width / 2;
	m_bottom = y + height / 2;
}

float Rect::GetWidth() const
{
	assert(m_right >= m_left);	// 左右の座標入れ替わりチェック
	return m_right - m_left;
}

float Rect::GetHeight() const
{
	assert(m_bottom >= m_top);	// 上下の座標入れ替わりチェック
	return m_bottom - m_top;
}

Vec2 Rect::GetCenter() const
{
	// 中心座標
	float x = (m_left + m_right) / 2;
	float y = (m_top + m_bottom) / 2;
	return Vec2{ x,y };
}

bool Rect::IsCollision(const Rect& rect)
{
	// 絶対に当たらないパターンをはじいていく
	if (m_left > rect.m_right) return false;
	if (m_top > rect.m_bottom) return false;
	if (m_right < rect.m_left) return false;
	if (m_bottom < rect.m_top) return false;

	// 当たらないパターン以外は当たっている
	return true;
}

ここでは、当たり判定の範囲を設定する方法として、
左上の座標から指定する SetLT() 、中心座標から指定する SetCenter()
の2種類を用意しています。
ゲーム内のキャラクターや弾などは、中心座標で管理する方が扱いやすく、位置計算もしやすいため、今回は SetCenter() を使って当たり判定を設定していきます。


4-2 キャラクターに当たり判定をつける

続いて、キャラクターの位置に合わせた当たり判定の矩形を追加します。
まずは、Characterクラスにキャラクターの位置とサイズに合わせて当たり判定を設定します。
・Character.h

#include "Rect.h"

class Character
{
protected:
	Rect m_colRect;	// 当たり判定用の矩形
};

・Character.cpp

namespace
{
	constexpr float kCharaSize = 64.0f;	// キャラクターサイズ
}

void Character::Update()
{
	// 当たり判定更新
	m_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize, kCharaSize);
}

kCharaSizeで当たり判定のサイズを調整できます。
今回は画像サイズと同じ64×64の大きさで当たり判定を作成しています。

制作中は、当たり判定が正しく表示されているか確認したい場面がたくさんあります。そこで、プレイヤーと敵の Draw() 関数内でそれぞれ矩形の描画を行います。
プレイヤーには青(0x0000ff)、敵には赤(0xff0000)の当たり判定を表示するようにします。

ただし、当たり判定の矩形はリリース時には表示したくありません。
そういった場合には、以下のように記述を行うことでデバッグ時のみ処理を実行させることができます。

#ifdef _DEBUG
  // デバッグ中だけ行いたい処理
#endif

・Player.cpp

void Player::Draw()
{
#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0x0000ff, false);
#endif
}

・Enemy.cpp

void Enemy::Draw()
{
#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0xff0000, false);
#endif
}

これで一旦実行します。

実行画面

キャラクターの近くに当たり判定の矩形が表示されました。
しかし、今のままだとキャラクターの描画位置と当たり判定の位置がずれてしまっているため、修正していきます。

このズレの原因は、キャラクターの位置(m_pos)が中心座標であるのに対してDrawGraphF()画像の左上を基準に描画するためです。
そのため、描画位置をキャラクター画像のサイズの半分だけ左上に補正する必要があります。
・Character.cpp

void Character::Draw()
{
	float drawX = m_pos.x - kCharaSize * 0.5f;
	float drawY = m_pos.y - kCharaSize * 0.5f;

	if (m_isRight)
	{
		DrawGraphF(drawX, drawY, m_handle, true);
	}
	else
	{
		DrawTurnGraphF(drawX, drawY, m_handle, true);
	}
}

これで再度実行してみると、キャラクターと当たり判定の位置が一致して描画されるようになりました。

実行画面

4-3 弾に当たり判定をつける

弾にも当たり判定を実装していきます。実装方法はキャラクターと同じです。
弾の当たり判定は緑色(0x00ff00)で表示します。
・Shot.h

#include "Rect.h"

class Shot
{
private:
	Rect m_colRect;	// 当たり判定用の矩形
};

・Shot.cpp

namespace
{
	constexpr float kShotSize = 32.0f;	// 弾のサイズ
}

void Shot::Update()
{
	// 当たり判定更新
	m_colRect.SetCenter(m_pos.x, m_pos.y, kShotSize, kShotSize);
}

void Shot::Draw()
{
	float drawX = m_pos.x - kShotSize * 0.5f;
	float drawY = m_pos.y - kShotSize * 0.5f;

	DrawGraph(drawX, drawY, m_handle, true);

#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0x00ff00, false);
#endif
}

次に、敵に弾が当たったときに弾を削除する処理を実装します。
当たり判定のチェックには、2-1で作成したRectクラスのIsCollision()関数を使用します。
GetColRect()関数でキャラクターや弾の当たり判定情報を取得し、ぶつかっているかどうかをチェックします。
・Character.h

class Character
{
public:
	/// <summary>
	/// 当たり判定を取得
	/// </summary>
	/// <returns>当たり判定情報</returns>
	Rect GetColRect() const { return m_colRect; }
};

・Shot.h

class Shot
{
public:
	/// <summary>
	/// 当たり判定を取得
	/// </summary>
	/// <returns>当たり判定情報</returns>
	Rect GetColRect() const { return m_colRect; }

private:
	Rect m_colRect;	// 当たり判定用の矩形
};

・SceneMain.cpp

void SceneMain::UpdateShot()
{
	for (int i = 0; i < kShotMax; i++)
	{
		// 弾が敵に当たった場合
		bool isColEnemy  = m_pShot[i]->GetColRect().IsCollision(m_pEnemy->GetColRect());
		// 画面外に出た場合
		bool isOffScreen = m_pShot[i]->GetPos().x < 0 || m_pShot[i]->GetPos().x > kScreenWidth;
		
		if (isOffScreen || isColEnemy) DeleteShot(i);
	}
}

SceneMain::UpdateShot()内の削除条件を少し書き換えました。
条件ごとにbool変数を作ることで処理の内容がわかりやすくなり、可読性も上がります。

実行すると、敵に弾が当たったタイミングで弾が正しく消えることが確認できました。

実行画面

4-4 HPを実装する

ここからは、キャラクターのHPを実装していきます。
まずはCharacterクラスにHPを管理する変数を追加します。
今回、最大HPは10にしました。
・Character.h

class Character
{
protected:
	int m_hp; // HP
};

・Character.cpp

namespace
{
	constexpr float kMaxHp = 10; // 最大HP
}


Character::Character():
	m_hp(kMaxHp),
{
}

4-4-1 ダメージを受けたらHPを減らす

キャラクターがダメージを受けたときにHPが減る処理を追加していきます。
今回は以下の場合にHPが減るようにします。
 ①敵に弾が当たった時、敵のHPを減らす
 ②プレイヤーが敵に当たった時、プレイヤーのHPを減らす

まずは、ダメージを受けた際の処理をCharacterクラスに追加していきます。
・Character.h

class Character
{
public:
	/// <summary>
	/// ダメージを受けた場合の処理
	/// </summary>
	void OnDamage();
};

・Character.cpp

void Character::OnDamage()
{
	m_hp--;
}

デバッグ用としてHPを画面左上に表示しておきます。
・Enemy.cpp

void Enemy::Draw()
{
#ifdef _DEBUG
	// HP表示
	DrawFormatString(0, 20, 0xffffff, "敵HP:%d", m_hp);
#endif
}

・Player.cpp

void Player::Draw()
{
#ifdef _DEBUG
	// HP表示
	DrawFormatString(0, 0, 0xffffff, "プレイヤーHP:%d", m_hp);
#endif
}

次に、①敵に弾が当たった時、敵のHPを減らす処理 を実装していきます。
処理はSceneMain.cppに追加していきます。
・SceneMain.cpp

void SceneMain::UpdateShot()
{
	for (int i = 0; i < kShotMax; i++)
	{
		// 弾が敵に当たった場合
		bool isColEnemy  = m_pShot[i]->GetColRect().IsCollision(m_pEnemy->GetColRect());
	
		if (isColEnemy) m_pEnemy->OnDamage();
	}
}

実行します。
敵に弾が当たると、左上の敵HPの数字が減るようになりました。

実行画面

続いて、②プレイヤーが敵に当たった時、プレイヤーのHPを減らすを実装していきます。
・SceneMain.cpp

void SceneMain::Update()
{
	// プレイヤーが敵に当たった場合
	bool isDamage = m_pPlayer->GetColRect().IsCollision(m_pEnemy->GetColRect());
	if (isDamage) m_pPlayer->OnDamage();
}

実行します。
敵に当たったときHPが減るようになりました。

実行画面

4-4-2 無敵時間を作る

現状はプレイヤーが敵に触れている間ずっとHPが減ってしまう状態です。
そこで、ダメージを受けた後は一定時間ダメージを受けないようにする無敵時間を実装していきます。
今回は、無敵時間を30フレーム(0.5秒)に設定します。
・Character.h

class Character
{
protected:
    int m_damageFrame; // ダメージを受けてからの経過フレーム数
};

・Character.cpp

namespace
{
	constexpr int kInvincibleFrame = 30; // 無敵フレーム数
}

Character::Character():
	m_damageFrame(0)
{
}

void Character::Update()
{
	m_damageFrame--; // 無敵時間更新
}

void Character::OnDamage()
{
	// 無敵中は飛ばす
	if (m_damageFrame > 0) return;

	m_damageFrame = kInvincibleFrame;

}

これで、無敵時間中はHPが減らないようになりました。

実行画面

4-4-3 HPが0になった場合

現在の実装では、キャラクターのHPが0以下になっても存在し続けてしまい、その後も攻撃を受けることでHPがマイナスになるという不自然な状態になります。
そのため、HPが0になったらキャラクター削除する処理を追加していきます。
削除の処理は、3-4弾を消すと同じようにdeleteを使用します。
Character.hで現在のHPを取得できるようにしました。
・Character.h

class Character
{
public:
	/// <summary>
	/// HP取得
	/// </summary>
	/// <returns>HP</returns>
	int GetHp() const { return m_hp; }
};

SceneMain.cpp内で、キャラクターのHPが0以下であればdeleteを行います。
・SceneMain.h

class SceneMain
{
private:
	/// <summary>
	/// キャラクターが死亡したかチェックする
	/// </summary>
	void CheckCharacterDeath();
};

・SceneMain.cpp

void SceneMain::Update()
{
	if(m_pPlayer) m_pPlayer->Update();
	if(m_pEnemy) m_pEnemy->Update();
	UpdateShot();
	CheckCharacterDeath();

	// プレイヤーが敵に当たった場合
	bool isDamage = m_pPlayer->GetColRect().IsCollision(m_pEnemy->GetColRect());
	if (isDamage) m_pPlayer->OnDamage();
}

void SceneMain::Draw()
{
	if (m_pPlayer) m_pPlayer->Draw();
	if (m_pEnemy) m_pEnemy->Draw();

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;
		m_pShot[i]->Draw();
	}
}

void SceneMain::CheckCharacterDeath()
{
	if (m_pPlayer->GetHp() <= 0)
	{
		delete m_pPlayer;
		m_pPlayer = nullptr;
	}
	if (m_pEnemy->GetHp() <= 0)
	{
		delete m_pEnemy;
		m_pEnemy = nullptr;
	}
}

キャラクターが削除された後、nullptrを通じてアクセスすると、アクセス違反になりエラーが出ます。
そのため、nullptrチェックを行ってから処理を実行するように変更します。
・SceneMain.cpp

void SceneMain::Update()
{
	if(m_pPlayer) m_pPlayer->Update();
	if(m_pEnemy) m_pEnemy->Update();
	UpdateShot();
	CheckCharacterDeath();

	if (!m_pPlayer || !m_pEnemy) return;
}

void SceneMain::Draw()
{
	if (m_pPlayer) m_pPlayer->Draw();
	if (m_pEnemy) m_pEnemy->Draw();
}

void SceneMain::UpdateShot()
{
	Shot* newShot = nullptr;
	if (m_pPlayer)
	{
		newShot = m_pPlayer->CreateShot();
	}

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;

		m_pShot[i]->Update();

		// 弾が敵に当たった場合
		bool isColEnemy  = m_pEnemy && m_pShot[i]->GetColRect().IsCollision(m_pEnemy->GetColRect());

	}
}

void SceneMain::CheckCharacterDeath()
{
	if (m_pPlayer && m_pPlayer->GetHp() <= 0)
	{
		delete m_pPlayer;
		m_pPlayer = nullptr;
	}
	if (m_pEnemy && m_pEnemy->GetHp() <= 0)
	{
		delete m_pEnemy;
		m_pEnemy = nullptr;
	}
}

これで再度実行してみると、敵のHPが0になった瞬間に敵が消えるようになりました。
nullptrチェックにより、エラーやクラッシュも発生しなくなりました!

実行画面

まとめ

今回は弾うちと当たり判定を実装しました!
次の記事では5.マップスクロールと6.マップチップ追加を行っていきます。

ここまでのソースコードを下に張り付けておきます。
・Character.h

#pragma once
#include "Vec2.h"
#include "Rect.h"

/// <summary>
/// キャラクターの基底クラス
/// </summary>
class Character
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Character();

	/// <summary>
	/// デストラクタ
	/// </summary>
	virtual ~Character();

	/// <summary>
	/// 初期化
	/// </summary>
	virtual void Init();

	/// <summary>
	/// 更新
	/// </summary>
	virtual void Update();

	/// <summary>
	/// 描画
	/// </summary>
	virtual void Draw();

	/// <summary>
	/// ダメージを受けた場合の処理
	/// </summary>
	void OnDamage();

	/// <summary>
	/// 当たり判定を取得
	/// </summary>
	/// <returns>当たり判定情報</returns>
	Rect GetColRect() const { return m_colRect; }

	/// <summary>
	/// HP取得
	/// </summary>
	/// <returns>HP</returns>
	int GetHp() const { return m_hp; }

protected:
	/// <summary>
	/// 重力計算
	/// </summary>
	void Gravity();

protected:
	int m_handle;			// キャラクターの画像
	int m_jumpFrame;		// ジャンプ中のフレーム数
	int m_hp;				// HP
	int m_damageFrame;		// ダメージを受けてからの経過フレーム数
	bool m_isJumpPreparing; // ジャンプ準備中か
	bool m_isRight;			// キャラクターが右を向いているか
	bool m_isGround;		// 地面についているか
	Vec2 m_pos;				// 座標
	Vec2 m_move;			// 移動量
	Rect m_colRect;			// 当たり判定用の矩形
};

・Character.cpp

#include "DxLib.h"
#include "Player.h"
#include "Enemy.h"
#include "Shot.h"
#include "SceneMain.h"

namespace
{
	constexpr float kScreenWidth = 1920.0f; // 画面幅
	constexpr int kShotMax = 3; // 1度に撃てる最大弾数
}

SceneMain::SceneMain()
{
	for (int i = 0; i < kShotMax; i++)
	{
		m_pShot[i] = nullptr;
	}

	m_pPlayer = new Player;
	m_pEnemy = new Enemy;
}

SceneMain::~SceneMain()
{
}

void SceneMain::Init()
{

}

void SceneMain::Update()
{
	if(m_pPlayer) m_pPlayer->Update();
	if(m_pEnemy) m_pEnemy->Update();
	UpdateShot();
	CheckCharacterDeath();

	if (!m_pPlayer || !m_pEnemy) return;
	// プレイヤーが敵に当たった場合
	bool isDamage = m_pPlayer->GetColRect().IsCollision(m_pEnemy->GetColRect());
	if (isDamage) m_pPlayer->OnDamage();
}

void SceneMain::Draw()
{
	if (m_pPlayer) m_pPlayer->Draw();
	if (m_pEnemy) m_pEnemy->Draw();

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;
		m_pShot[i]->Draw();
	}
}

void SceneMain::UpdateShot()
{
	Shot* newShot = nullptr;
	if (m_pPlayer)
	{
		newShot = m_pPlayer->CreateShot();
	}
	if (newShot != nullptr)
	{
		for (int i = 0; i < kShotMax; i++)
		{
			if (m_pShot[i] == nullptr)
			{
				m_pShot[i] = newShot;
				break;
			}
		}
	}

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;

		m_pShot[i]->Update();

		// 弾が敵に当たった場合
		bool isColEnemy  = m_pEnemy && m_pShot[i]->GetColRect().IsCollision(m_pEnemy->GetColRect());
		// 画面外に出た場合
		bool isOffScreen = m_pShot[i]->GetPos().x < 0 || m_pShot[i]->GetPos().x > kScreenWidth;
		
		if (isColEnemy) m_pEnemy->OnDamage();
		if (isOffScreen || isColEnemy) DeleteShot(i);
	}
}

void SceneMain::DeleteShot(int index)
{
	if (!m_pShot[index]) return;

	delete m_pShot[index];
	m_pShot[index] = nullptr;
}

void SceneMain::CheckCharacterDeath()
{
	if (m_pPlayer && m_pPlayer->GetHp() <= 0)
	{
		delete m_pPlayer;
		m_pPlayer = nullptr;
	}
	if (m_pEnemy && m_pEnemy->GetHp() <= 0)
	{
		delete m_pEnemy;
		m_pEnemy = nullptr;
	}
}

・Player.h

#pragma once
#include "Character.h"

class Shot;

/// <summary>
/// プレイヤークラス
/// </summary>
class Player : public Character
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Player();

	/// <summary>
	/// デストラクタ
	/// </summary>
	virtual ~Player();

	/// <summary>
	/// 初期化
	/// </summary>
	virtual void Init() override;

	/// <summary>
	/// 更新
	/// </summary>
	virtual void Update() override;

	/// <summary>
	/// 描画
	/// </summary>
	virtual void Draw() override;

	/// <summary>
	/// 弾生成
	/// </summary>
	/// <returns>Shotポインタ</returns>
	Shot* CreateShot();

private:
	/// <summary>
	/// 移動
	/// </summary>
	void Move();

	/// <summary>
	/// ジャンプ
	/// </summary>
	void Jump();
};

・Player.cpp

#include "DxLib.h"
#include "Pad.h"
#include "Shot.h"
#include "Player.h"

namespace
{
	constexpr float kCharaSize = 64.0f;		// プレイヤーサイズ
	constexpr float kSpeed = 20.0f;			// 移動速度
	constexpr float kJumpPower = 30.0f;		// ジャンプ力
	constexpr int kSmallJumpFrame = 6;		// 小ジャンプのフレーム数
	constexpr int kMediumJumpFrame = 12;	// 中ジャンプのフレーム数
	constexpr float kSmallJumpHeight = 0.5f;	// 小ジャンプの高さ
	constexpr float kMediumJumpHeight = 0.7f;	// 中ジャンプの高さ
	constexpr float kBigJumpHeight = 1.0f;		// 大ジャンプの高さ
}

Player::Player()
{
	m_handle = LoadGraph("data/player.png");
}

Player::~Player()
{
	DeleteGraph(m_handle);
}

void Player::Init()
{
}

void Player::Update()
{
	Character::Update();

	Move();
	Jump();

	m_pos += m_move;
}

void Player::Draw()
{
	Character::Draw();

#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0x0000ff, false);
	// HP表示
	DrawFormatString(0, 0, 0xffffff, "プレイヤーHP:%d", m_hp);
#endif
}

void Player::Move()
{
	if (Pad::IsPress(PAD_INPUT_LEFT))
	{
		m_move.x = -kSpeed;
		m_isRight = false;
	}
	else if (Pad::IsPress(PAD_INPUT_RIGHT))
	{
		m_move.x = kSpeed;
		m_isRight = true;
	}
	else
	{
		m_move.x = 0.0f;
	}
}

void Player::Jump()
{
	// ジャンプ中は飛ばす
	if (!m_isGround) return;

	// ボタンを押したらフレーム数を計測し始める
	if (Pad::IsTrigger(PAD_INPUT_1))
	{
		m_isJumpPreparing = true;
	}

	if (!m_isJumpPreparing) return;
	m_jumpFrame++;

	float jumpHeight = kJumpPower; // ジャンプの高さを決める

	// ボタンを離した瞬間にジャンプする
	if (!Pad::IsRelease(PAD_INPUT_1)) return;
	
	// 小ジャンプ
	if (m_jumpFrame < kSmallJumpFrame)
	{
		jumpHeight = kSmallJumpHeight;
	}
	// 中ジャンプ
	else if (m_jumpFrame < kMediumJumpFrame)
	{
		jumpHeight = kMediumJumpHeight;
	}
	// 大ジャンプ
	else
	{
		jumpHeight = kBigJumpHeight;
	}

	m_move.y = -kJumpPower * jumpHeight;
	m_isGround = false;
	m_isJumpPreparing = false;
}

Shot* Player::CreateShot()
{
	if (Pad::IsTrigger(PAD_INPUT_2))
	{
		Shot* pShot = new Shot();
		pShot->SetInfo(m_pos, m_isRight);
		return pShot;
	}

	return nullptr;
}

・Enemy.h

#pragma once
#include "Character.h"

/// <summary>
/// 敵クラス
/// </summary>
class Enemy : public Character
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Enemy();

	/// <summary>
	/// デストラクタ
	/// </summary>
	virtual ~Enemy();

	/// <summary>
	/// 初期化
	/// </summary>
	virtual void Init() override;

	/// <summary>
	/// 更新
	/// </summary>
	virtual void Update() override;

	/// <summary>
	/// 描画
	/// </summary>
	virtual void Draw() override;
};

・Enemy.cpp

#include "DxLib.h"
#include "Enemy.h"

Enemy::Enemy()
{
	m_handle = LoadGraph("data/enemy.png");
}

Enemy::~Enemy()
{
	DeleteGraph(m_handle);
}

void Enemy::Init()
{
}

void Enemy::Update()
{
	Character::Update();

	m_pos += m_move;
}

void Enemy::Draw()
{
	Character::Draw();

#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0xff0000, false);
	// HP表示
	DrawFormatString(0, 20, 0xffffff, "敵HP:%d", m_hp);
#endif
}

・Shot.h

#pragma once
#include "Vec2.h"
#include "Rect.h"

/// <summary>
/// 弾管理クラス
/// </summary>
class Shot
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Shot();

	/// <summary>
	/// デストラクタ
	/// </summary>
	~Shot();

	/// <summary>
	/// 初期化
	/// </summary>
	void Init();

	/// <summary>
	/// 更新
	/// </summary>
	void Update();

	/// <summary>
	/// 描画
	/// </summary>
	void Draw();

	/// <summary>
	/// 弾情報をセットする
	/// </summary>
	/// <param name="pos">座標</param>
	/// <param name="isRight">右方向に移動するか</param>
	void SetInfo(const Vec2& pos, bool isRight);

	/// <summary>
	/// 座標取得
	/// </summary>
	/// <returns>座標</returns>
	Vec2 GetPos() const { return m_pos; }

	/// <summary>
	/// 当たり判定を取得
	/// </summary>
	/// <returns>当たり判定情報</returns>
	Rect GetColRect() const { return m_colRect; }

private:
	int m_handle; // 弾の画像
	Vec2 m_pos;	  // 弾の位置
	Vec2 m_move;  // 移動量
	Rect m_colRect;	// 当たり判定用の矩形
};

・Shot.cpp

#include "DxLib.h"
#include "Shot.h"

namespace
{
	constexpr float kSpeed = 30.0f;		// 弾の移動速度
	constexpr float kShotSize = 32.0f;	// 弾のサイズ
}

Shot::Shot():
	m_pos({0, 0}),
	m_move({kSpeed, 0})
{
	m_handle = LoadGraph("data/shot.png");
}

Shot::~Shot()
{
	DeleteGraph(m_handle);
}

void Shot::Init()
{
}

void Shot::Update()
{
	m_pos += m_move;

	// 当たり判定更新
	m_colRect.SetCenter(m_pos.x, m_pos.y, kShotSize, kShotSize);
}

void Shot::Draw()
{
	float drawX = m_pos.x - kShotSize * 0.5f;
	float drawY = m_pos.y - kShotSize * 0.5f;

	DrawGraph(drawX, drawY, m_handle, true);

#ifdef _DEBUG
	// 当たり判定を表示
	m_colRect.Draw(0x00ff00, false);
#endif
}

void Shot::SetInfo(const Vec2& pos, bool isRight)
{
	m_pos = pos;

	if (isRight)
	{
		m_move.x = kSpeed;
	}
	else
	{
		m_move.x = -kSpeed;
	}
}

・SceneMain.h

#pragma once

class Player;
class Enemy;
class Shot;

/// <summary>
/// シーン管理クラス
/// </summary>
class SceneMain
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	SceneMain();

	/// <summary>
	/// デストラクタ
	/// </summary>
	~SceneMain();

	/// <summary>
	/// 初期化
	/// </summary>
	void Init();

	/// <summary>
	/// 更新
	/// </summary>
	void Update();

	/// <summary>
	/// 描画
	/// </summary>
	void Draw();

private:
	/// <summary>
	/// 弾更新
	/// </summary>
	void UpdateShot();

	/// <summary>
	/// 弾削除
	/// </summary>
	/// <param name="index">インデックス</param>
	void DeleteShot(int index);

	/// <summary>
	/// キャラクターが死亡したかチェックする
	/// </summary>
	void CheckCharacterDeath();

private:
	Player* m_pPlayer;
	Enemy* m_pEnemy;
	Shot* m_pShot[3];
};

・SceneMain.cpp

#include "DxLib.h"
#include "Player.h"
#include "Enemy.h"
#include "Shot.h"
#include "SceneMain.h"

namespace
{
	constexpr float kScreenWidth = 1920.0f; // 画面幅
	constexpr int kShotMax = 3; // 1度に撃てる最大弾数
}

SceneMain::SceneMain()
{
	for (int i = 0; i < kShotMax; i++)
	{
		m_pShot[i] = nullptr;
	}

	m_pPlayer = new Player;
	m_pEnemy = new Enemy;
}

SceneMain::~SceneMain()
{
}

void SceneMain::Init()
{

}

void SceneMain::Update()
{
	if(m_pPlayer) m_pPlayer->Update();
	if(m_pEnemy) m_pEnemy->Update();
	UpdateShot();
	CheckCharacterDeath();

	if (!m_pPlayer || !m_pEnemy) return;
	// プレイヤーが敵に当たった場合
	bool isDamage = m_pPlayer->GetColRect().IsCollision(m_pEnemy->GetColRect());
	if (isDamage) m_pPlayer->OnDamage();
}

void SceneMain::Draw()
{
	if (m_pPlayer) m_pPlayer->Draw();
	if (m_pEnemy) m_pEnemy->Draw();

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;
		m_pShot[i]->Draw();
	}
}

void SceneMain::UpdateShot()
{
	Shot* newShot = nullptr;
	if (m_pPlayer)
	{
		newShot = m_pPlayer->CreateShot();
	}
	if (newShot != nullptr)
	{
		for (int i = 0; i < kShotMax; i++)
		{
			if (m_pShot[i] == nullptr)
			{
				m_pShot[i] = newShot;
				break;
			}
		}
	}

	for (int i = 0; i < kShotMax; i++)
	{
		if (!m_pShot[i]) continue;

		m_pShot[i]->Update();

		// 弾が敵に当たった場合
		bool isColEnemy  = m_pEnemy && m_pShot[i]->GetColRect().IsCollision(m_pEnemy->GetColRect());
		// 画面外に出た場合
		bool isOffScreen = m_pShot[i]->GetPos().x < 0 || m_pShot[i]->GetPos().x > kScreenWidth;
		
		if (isColEnemy) m_pEnemy->OnDamage();
		if (isOffScreen || isColEnemy) DeleteShot(i);
	}
}

void SceneMain::DeleteShot(int index)
{
	if (!m_pShot[index]) return;

	delete m_pShot[index];
	m_pShot[index] = nullptr;
}

void SceneMain::CheckCharacterDeath()
{
	if (m_pPlayer && m_pPlayer->GetHp() <= 0)
	{
		delete m_pPlayer;
		m_pPlayer = nullptr;
	}
	if (m_pEnemy && m_pEnemy->GetHp() <= 0)
	{
		delete m_pEnemy;
		m_pEnemy = nullptr;
	}
}

いいなと思ったら応援しよう!

この記事が参加している募集