見出し画像

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

はじめに

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

この記事では、マップスクロールマップチップを実装していきます。




5.マップスクロール

5-1Bgクラス作成

まずは、弾を管理するBgクラスを作成します。
このクラスでは、背景やマップチップを管理できるようにします。

Bgクラス作成

5-2 背景を表示する

次に、背景を表示してみます。
まずは背景の画像を準備します。
dataフォルダの中に、bg.pngを追加します。

今回は画面と同じ1920×1080のサイズの画像を使っています。
画像は好きなものを使ってください。
以下の画像を使ってもらっても大丈夫です。

bg.png

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

#include "Vec2.h"

class Bg
{
private:
	/// <summary>
	/// 背景表示
	/// </summary>
	void DrawBg();

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

・Bg.cpp

#include "DxLib.h"
#include "Bg.h"

Bg::Bg():
	m_pos{0, 0}
{
	m_bgHandle= LoadGraph("data/bg.png");
}

Bg::~Bg()
{
	DeleteGraph(m_bgHandle);
}

void Bg::Draw()
{
	DrawBg();
}

void Bg::DrawBg()
{
	DrawGraph(m_pos.x, m_pos.y, m_bgHandle, true);
}

SceneMainクラスでBgクラスのポインタを管理します。
・SceneMain.h

class Bg;

class SceneMain
{
private:
	Bg* m_pBg;
};

・SceneMain.cpp

#include "Shot.h"

SceneMain::SceneMain()
{
	m_pBg = new Bg;
}

void SceneMain::Update()
{
	m_pBg->Update();
}

void SceneMain::Draw()
{
	m_pBg->Draw();
}

実行すると、背景が表示されるようになりました。

実行画面

5-3 プレイヤーの移動と同時に背景を動かす

今のままだと、プレイヤーが横に進んでも背景は動かないため、プレイヤーが画面の外に出てしまいます。
そこで、「プレイヤーの位置を参照して背景画像の描画位置を決める」という方法で実装していきます。
具体的には、プレイヤーのX座標から画面の中央の値を引いたものを背景のスクロール量とします。
プレイヤーの座標情報が必要になるため、背景クラスのコンストラクタでプレイヤーのポインタを受け取るようにします。
・Bg.h

class Player;

class Bg
{
public:
	/// <summary>
	/// コンストラクタ
	/// </summary>
	Bg(Player* pPlayer);

	/// <summary>
	/// 横のスクロール量を決定する
	/// </summary>
	/// <returns>横スクロール量</returns>
	int GetScrollX();
	
	/// <summary>
	/// 縦のスクロール量を決定する
	/// </summary>
	/// <returns>縦スクロール量</returns>
	int GetScrollY();

private:
	Player* m_pPlayer;
};

・Bg.cpp

#include "Player.h"

namespace
{
	constexpr float kMapWidth = 5000.0f;	 // マップ全体の幅
	constexpr float kMapHeight = 1080.0f;	 // マップ全体の高さ
	constexpr float kScreenWidth = 1920.0f;	 // スクリーンの幅
	constexpr float kScreenHeight = 1080.0f; // スクリーンの高さ
}

Bg::Bg(Player* pPlayer):
	m_pPlayer(pPlayer)
{
}

Bg::~Bg()
{
	DeleteGraph(m_bgHandle);
}

int Bg::GetScrollX()
{
	int result = static_cast<int>(m_pPlayer->GetPos().x - kScreenWidth * 0.5);
	if (result < 0)
	{
		result = 0;
	}
	if (result > kMapWidth - kScreenWidth)
	{
		result = kMapWidth - kScreenWidth;
	}

	return result;
}

int Bg::GetScrollY()
{
	int result = static_cast<int>(m_pPlayer->GetPos().y - kScreenHeight * 0.5);
	if (result < 0)
	{
		result = 0;
	}
	if (result > kMapHeight - kScreenHeight)
	{
		result = kMapHeight - kScreenHeight;
	}

	return result;
}

void Bg::DrawBg()
{
	// 画像サイズを取得
	Size bgSize = { 0, 0 };
	GetGraphSize(m_bgHandle, &bgSize.width, &bgSize.height);

	int scrollBg = GetScrollX() % bgSize.width;

	DrawGraph(-scrollBg, m_pos.y, m_bgHandle, true);
}

・SceneMain.cpp

SceneMain::SceneMain()
{
    m_pPlayer = new Player;
	m_pBg = new Bg(m_pPlayer);
}

・Character.h

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

実行してみると、プレイヤーの移動に合わせて背景も動くようになりました。

実行画面

5-4 背景をループ表示する

ただし、このままだと背景の端が見切れたときに、画面に真っ暗な部分が表示されてしまいます。
そこで、背景がループして繋がるようにしていきます。
スクロール位置(scrollBg)が0以上になった場合、もう一枚画像を続けて描画するようにします。
・Bg.cpp

if (scrollBg > 0)
{
	DrawGraph(bgSize.width - scrollBg, kScreenHeight - bgSize.height, m_bgHandle, true);
}

これで実行すると、右に移動しても背景がループするため、画面に真っ暗な部分が表示されなくなりました。

実行画面

今のままだと、プレイヤーが移動し続けた際に画面外に出てしまいます。
そこで、基本的にはプレイヤーを画面中央に固定し、マップの端に近づいた場合のみプレイヤー自体が移動するように処理を変更します。

背景クラスで求めたスクロール量を取得したいので、BgクラスのポインタをCharacterクラスで取得できるようにして、Draw()関数内を書き換えていきます。
・Character.h

class Bg;

class Character
{
	/// <summary>
	/// Bgクラスのポインタをセットする
	/// </summary>
	/// <param name="pBg">Bgクラスポインタ</param>
	void SetBgPointer(Bg* pBg) { m_pBg = pBg; }

protected:
	Bg* m_pBg;
};

・Character.cpp

#include "Bg.h"

Character::Character():
	m_pBg(nullptr)
{
}

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

・SceneMain.cpp

SceneMain::SceneMain()
{
	m_pBg = new Bg(m_pPlayer);
	m_pPlayer->SetBgPointer(m_pBg);
    m_pEnemy->SetBgPointer(m_pBg);
}

実行します。
プレイヤーが移動しても画面中央に表示され、マップ端に近づいたときだけ
プレイヤーの表示位置が変わるようになりました。

実行画面

5-5 弾と当たり判定にもスクロールを反映させる

現在は、キャラクターにのみスクロール情報が適用されており、弾や当たり判定が画面スクロールに追従していません。
そのため、弾の描画や当たり判定にもスクロール量を反映させる処理を追加していきます。

まずは、弾にスクロール量を反映させます。
現在、プレイヤーがx=1920以上に移動すると弾が発射されなくなるため、SceneMainのUpdateShot()内の条件式を変更しておきます。
・SceneMain.cpp

namespace
{
	constexpr float kMapWidth = 3000.0f; // マップ幅
}

void SceneMain::UpdateShot()
{
	for (int i = 0; i < kShotMax; i++)
	{
		// 画面外に出た場合
		bool isOffScreen = m_pShot[i]->GetPos().x < 0 || m_pShot[i]->GetPos().x > kMapWidth;
	}
}

弾クラス内でBgクラスのポインタを扱いたいため、SetInfo()関数でBg クラスのポインタを受け取れるようにコードを修正します。
・Shot.h

class Bg;

class Shot
{
public:
	/// <summary>
	/// 弾情報をセットする
	/// </summary>
	/// <param name="pos">座標</param>
	/// <param name="isRight">右方向に移動するか</param>
	/// <param name="pBg">背景クラスポインタ</param>
	void SetInfo(const Vec2& pos, bool isRight, Bg* pBg);

private:
	Bg* m_pBg;
};

・Shot.cpp

#include "Bg.h"

Shot::Shot():
	m_pBg(nullptr)
{
}

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

void Shot::SetInfo(const Vec2& pos, bool isRight, Bg* pBg)
{
	m_pBg= pBg;
}

・Player.cpp

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

	return nullptr;
}

続いて、当たり判定にもスクロール情報を反映させます。
当たり判定の位置は、矩形の位置からスクロール量を引くことで計算できます。
そのため、Rectクラスにスクロール対応用の描画関数を新たに追加し、スクロール量を引いた位置で描画できるようにします。
・Rect.h

class Rect
{	/// <summary>
	/// スクロール情報を適用した矩形の描画
	/// </summary>
	/// <param name="scrollX">横スクロール量</param>
	/// <param name="scrollY">縦スクロール量</param>
	/// <param name="color">矩形の色</param>
	/// <param name="isFill">太さ</param>
	void DrawScroll(int scrollX, int scrollY, unsigned int color, bool isFill);
};

・Rect.cpp

void Rect::DrawScroll(int scrollX, int scrollY, unsigned int color, bool isFill)
{
	int drawL = static_cast<int>(m_left) - scrollX;
	int drawT = static_cast<int>(m_top) - scrollY;
	int drawR = static_cast<int>(m_right) - scrollX;
	int drawB = static_cast<int>(m_bottom) - scrollY;
	DrawBox(drawL, drawT, drawR, drawB, color, isFill);
}

これまで使用していたm_colRect.Draw()を、スクロール対応の関数m_colRect.DrawScroll()に変更します。
・Player.cpp

#include "Bg.h"

void Player::Draw()
{
#ifdef _DEBUG
	m_colRect.DrawScroll(m_pBg->GetScrollX(), m_pBg->GetScrollY(),0x0000ff, false);
#endif
}

・Enemy.cpp

#include "Bg.h"

void Enemy::Draw()
{
#ifdef _DEBUG
	m_colRect.DrawScroll(m_pBg->GetScrollX(), m_pBg->GetScrollY(), 0xff0000, false);
#endif
}

・Shot.cpp

void Shot::Draw()
{
#ifdef _DEBUG
	m_colRect.DrawScroll(m_pBg->GetScrollX(), m_pBg->GetScrollY(), 0x00ff00, false);
#endif
}

実行します。
キャラクターがマップ右端に移動しても、弾や当たり判定が正しく画面内に描画されていることが確認できました。

実行画面

これで、マップスクロールに関する実装は一通り完了です。


6.マップチップ実装

最後に、マップチップを使って地面や壁などのマップを作成していきます。
現在の状態では、プレイヤーが宙に浮いているように見えるため、マップに地面や壁を配置して、ゲームらしい見た目に仕上げていきます。

6-1 マップチップを配置する

まずはマップに表示するためのマップチップ画像を用意します。
dataフォルダの中に、mapChip.pngを追加します。

今回は320×64のサイズで、1チップは64×64の正方形として分割して使う想定です。
画像は好きなものを使ってください。
以下の画像を使ってもらっても大丈夫です。

mapChip.png

マップチップの描画はBgクラスで管理します。
とりあえず、マップチップの全体画像を画面左上に表示してみます。
・Bg.h

class Bg
{
private:
	/// <summary>
	/// マップチップ表示
	/// </summary>
	void DrawMapChip();

private:
	int m_mapHandle; // マップチップ画像
};

・Bg.cpp

Bg::Bg(Player* pPlayer):
	m_pos{0, 0},
	m_pPlayer(pPlayer)
{
	m_mapHandle = LoadGraph("data/mapChip.png");
}

Bg::~Bg()
{
	DeleteGraph(m_mapHandle);
}

void Bg::Draw()
{
	DrawMapChip();
}

void Bg::DrawMapChip()
{
	DrawGraph(0, 0, m_mapHandle, true);
}
実行画面

現在の状態では、マップチップ画像全体が描画されてしまうため、指定したマップチップだけを選んで表示できるようにします。
画像の中に何枚のチップがあるかを取得し、それを使って特定のチップを切り出せるようにします。

チップはわかりやすく、左から順に 0, 1, 2, 3, 4 と番号を振って管理することにします。

どの位置にどのチップを置くかは、二次元配列を使って管理します。
0番 : 何も表示しない
1,2番 : 地面
3,4番 : 壁
のように分類し、配置していきます。

マップチップ画像に何枚のチップがあるかを求めるため、DXライブラリの機能のGetGraphSize()を使います。

・Map.h

/// <summary>
/// 背景管理クラス
/// </summary>
class Bg
{
private:	
    // 画像に含まれるマップチップの数
	int m_graphChipNumX;
	int m_graphChipNumY;
};

・Map.cpp

namespace
{
    constexpr int kChipSize= 64; // マップチップ1つのサイズ
}

Bg::Bg(Player* pPlayer):
	m_graphChipNumX(0),
	m_graphChipNumY(0)
{
	// 画像のマップチップ数を数える
	int graphW = 0;
	int graphH = 0;
	GetGraphSize(m_mapHandle, &graphW, &graphH);

	m_graphChipNumX = graphW / kChipSize;
	m_graphChipNumY = graphH / kChipSize;
}

ここでは仮のマップデータとして、定数の二次元配列を使ってマップを定義します。
※この部分は後で外部ファイルから読み込むように変更していきます(6-3で解説予定)

横方向と縦方向にそれぞれ何マス分のマップチップを置くかを決めます。
今回は横幅を7マス分、縦方向は画面全体に表示されるようにします。
縦のマス数は、画面の高さ ÷ マップチップ1つのサイズで求めることができ、例えば画面が縦1024ピクセルで、1チップのサイズが64ピクセルの場合、16マスになります。

次に、配置するマップチップの番号を決めます。
今回は「左端に壁」「画面下部に地面」といった配置にしたいと考えています。
マップ情報はfor文を使って順番に描画していきます。
描画位置は次のように計算します。
X座標 = 列番号(x) × チップのサイズ - 横スクロール量
Y座標 = 行番号(y) × チップのサイズ - 縦スクロール量

実際に描画するチップの番号は、マップ情報の配列から取得します。
このようにして、キャラクターが歩ける床や進行を妨げる壁などを自由に配置できます。
なお、スクリーン外にあるチップは見えないため、描画の対象外とします。また、デバッグ用に当たり判定を緑色の枠線で表示するようにしています。

・Map.cpp

namespace
{
	constexpr float kChipScale = 1.0f; 	// マップチップ拡大率

	// チップを置く数
	constexpr int kChipNumX = 7;
	constexpr int kChipNumY = Game::kScreenHeight / kChipHeight;

	// マップチップの配列情報
	constexpr int kChipData[kChipNumY][kChipNumX] =
	{
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 1, 1},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 0, 0, 0},
		{3, 0, 0, 0, 1, 1, 1},
		{3, 0, 0, 1, 2, 2, 2},
		{3, 1, 1, 2, 2, 2, 2},
		{3, 2, 2, 2, 2, 2, 2}
	};
}

void Bg::DrawMapChip()
{
	// マップチップの描画
	for (int y = 0; y < kChipNumY; y++)
	{
		for (int x = 0; x < kChipNumX; x++)
		{
			int posX = static_cast<int>(x * kChipSize * kChipScale - GetScrollX());
			int posY = static_cast<int>(y * kChipSize * kChipScale - GetScrollY());

			// 画面外は描画しない
			if (posX < 0 - kChipSize) continue;
			if (posX > kScreenWidth) continue;
			if (posY < 0 - kChipSize) continue;
			if (posY > kScreenHeight) continue;

			// 設置するチップ
			int chipNo = kChipData[y][x];

			// マップチップのグラフィック切り出し座標
			int srcX = kChipSize * (chipNo % m_graphChipNumX);
			int srcY = kChipSize * (chipNo % m_graphChipNumY);

			// 描画
			DrawRectRotaGraph(posX, posY, srcX, srcY, 
            kChipSize, kChipSize, kChipScale, 0.0f, 
            m_mapHandle, true);

#ifdef _DEBUG
			// 当たり判定
			DrawBoxAA(posX, posY, posX + kChipSize * kChipScale, posY + kChipSize * kChipScale, 0x00ff00, false);
#endif
		}
	}
}

実行してみます。
このまま描画をすると、下記の画像のようにマップチップの表示位置が左上に寄ってしまう問題が発生します。

実行画面

これは DrawRectRotaGraph() 関数が、描画位置をチップの「中心点」として扱う仕様になっているためです。
正しく描画するには、描画位置にマップチップのサイズの半分を加えて調整します。
・Map.cpp

void Bg::DrawMapChip()
{
	// マップチップの描画
	for (int y = 0; y < kChipNumY; y++)
	{
		for (int x = 0; x < kChipNumX; x++)
		{
			DrawRectRotaGraph(
				static_cast<int>(posX + kChipSize * kChipScale * 0.5f),
				static_cast<int>(posY + kChipSize * kChipScale * 0.5f),
				srcX, srcY,
				kChipSize, kChipSize,
				kChipScale, 0.0f,
				m_mapHandle, true);
		}
	}
}

これで再度実行してみると、マップチップと当たり判定がそろって描画されるようになりました。

実行画面

6-2 キャラクターとマップチップの当たり判定

続いては、キャラクターが壁や床にめり込まないようにするために、マップチップとの衝突判定を実装していきます。

どのマップチップに当たっているか判別するため、Bgクラスに当たり判定用の関数を追加します。
この関数で、キャラクター周辺のマップチップを調べ、衝突しているかどうかを判断できるようにします。
0番のマップチップには当たり判定をつけないようにします。

・Bg.h

class Bg
{
public:
    /// <summary>
	/// 指定した矩形と当たっているか判定する
	/// </summary>
	/// <param name="rect">指定した矩形</param>
	/// <param name="chipRect">マップチップの矩形</param>
	/// <returns>当たっているか</returns>
	bool IsCollision(Rect rect, Rect& chipRect);
};

・Bg.cpp

bool Bg::IsCollision(Rect rect, Rect& chipRect)
{
	for (int y = 0; y < kChipNumY; y++)
	{
		for (int x = 0; x < kChipNumX; x++)
		{
			// マップチップ0番は当たり判定がないため飛ばす
			if (kChipData[y][x] == 0) continue;

			int chipLeft = static_cast<int>(x * kChipSize * kChipScale);
			int chipRight = static_cast<int>(chipLeft + kChipSize * kChipScale);
			int chipTop = static_cast<int>(y * kChipSize * kChipScale);
			int chipBottom = static_cast<int>(chipTop + kChipSize * kChipScale);

			// 絶対に当たらない場合
			if (chipLeft > rect.GetRight()) continue;
			if (chipTop > rect.GetBottom()) continue;
			if (chipRight < rect.GetLeft()) continue;
			if (chipBottom < rect.GetTop()) continue;

			// ぶつかったマップチップの矩形を設定する
			chipRect.m_left = static_cast<float>(chipLeft);
			chipRect.m_right = static_cast<float>(chipRight);
			chipRect.m_top = static_cast<float>(chipTop);
			chipRect.m_bottom = static_cast<float>(chipBottom);

			// いずれかのチップに当たっていたら終了する
			return true;
		}
	}

	return false;
}

実際の処理としては、
横方向から壁に当たった場合は、キャラクターを元のX座標に戻す
縦方向から地面に衝突した場合
は、キャラクターを地面の上に移動させる
といった感じで実装していきます。

Character.cppのUpdate関数内を書き換えます。

・Character.h

class Rect;

class Character
{
protected:
	/// <summary>
	/// マップチップとの当たり判定処理
	/// </summary>
	/// <param name="chipRect">マップチップの当たり判定</param>
	void CheckHitMap(Rect chipRect);
};

・Character.cpp

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

	Gravity();

	Rect chipRect; // 当たったマップチップの矩形
	CheckHitMap(chipRect);

	// 接地判定
	if (m_isGround)
	{
		// ジャンプ準備中は飛ばす
		if (m_isJumpPreparing) return;
		m_jumpFrame = 0;
		m_move.y = 0.0f;
	}
}

void Character::CheckHitMap(Rect& chipRect)
{
	// 横から当たったかチェックする
	m_pos.x += m_move.x;
	m_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize, kCharaSize);

	if (m_pBg->IsCollision(m_colRect, chipRect))
	{
		if (m_move.x > 0.0f)
		{
			m_pos.x = chipRect.GetLeft() - kCharaSize * 0.5f;
		}
		else if (m_move.x < 0.0f)
		{
			m_pos.x = chipRect.GetRight() + kCharaSize * 0.5f;
		}
		m_move.x = 0.0f;
	}

	// 縦から当たったかチェックする
	m_pos.y += m_move.y;
	m_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize, kCharaSize);

	if (m_pBg->IsCollision(m_colRect, chipRect))
	{
		if (m_move.y > 0.0f)
		{
			m_pos.y = chipRect.GetTop() - kCharaSize * 0.5f;
			m_move.y = 0.0f;
			m_isGround = true;
		}
		else if (m_move.y < 0.0f)
		{
			m_pos.y = chipRect.GetBottom() + kCharaSize * 0.5f;
			m_move.y *= -1.0f;
		}
	}
}

当たり判定の更新と座標の更新はマップチップとの当たり判定の前で更新したいので、プレイヤーと敵クラスに書いていた m_pos += m_move;は削除します。
そして、Characterクラスに書いていたm_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize, kCharaSize);はCheckHitMap()関数内に移動します。

わかりやすいようにキャラクターの初期位置を変更しておきます。
・Player.cpp

namespace
{
	const Vec2 kInitPos = { 300.0f, 600.0f };	// 初期位置
}

Player::Player()
{
	m_pos = kInitPos;
}

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

	Move();
	Jump();
}

・Enemy.cpp

namespace
{
	const Vec2 kInitPos = { 150.0f, 600.0f };	// 初期位置
}

Enemy::Enemy()
{
	m_pos = kInitPos;
}

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

これで実行します。

実行画面

これだと縦の当たり判定は問題なく動作していますが、横の当たり判定はうまくいっていません。
キャラクターが地面に接地している状態で横に移動すると、キャラクターがマップチップの端に弾かれるような挙動になります。

これは、当たり判定のサイズが原因です。
判定が少し大きいため、マップチップに触れていないのに衝突と判定されてしまいます。
そのため、マップチップとの当たり判定を行う際に、キャラクターの当たり判定を少しだけ縮小しておきます。(今回は当たり判定のサイズを-1することで対応しています。)

void Character::CheckHitMap(Rect& chipRect)
{
	// 横から当たったかチェックする
	m_pos.x += m_move.x;
	m_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize - 1, kCharaSize - 1);

	if (m_pBg->IsCollision(m_colRect, chipRect))
	{
		if (m_move.x > 0.0f)
		{
			m_pos.x = chipRect.GetLeft() - kCharaSize * 0.5f;
		}
		else if (m_move.x < 0.0f)
		{
			m_pos.x = chipRect.GetRight() + kCharaSize * 0.5f;
		}
		m_move.x = 0.0f;
	}

	// 縦から当たったかチェックする
	m_pos.y += m_move.y;
	m_colRect.SetCenter(m_pos.x, m_pos.y, kCharaSize - 1, kCharaSize - 1);

	if (m_pBg->IsCollision(m_colRect, chipRect))
	{
		if (m_move.y > 0.0f)
		{
			m_pos.y = chipRect.GetTop() - kCharaSize * 0.5f;
			m_move.y = 0.0f;
			m_isGround = true;
		}
		else if (m_move.y < 0.0f)
		{
			m_pos.y = chipRect.GetBottom() + kCharaSize * 0.5f;
			m_move.y *= -1.0f;
		}
	}
}

これで再度実行してみます。

実行画面

キャラクターが地面に立っている間は、左右に移動しても不自然な方向に移動することはなくなりました。
また、壁にぶつかったときも正常に動作しています。


6-3 外部エディタでマップを作る

マップチップの実装は今の状態でも問題ないのですが、マップを修正したいとなった場合、現在の方法では二次元配列に1つずつマップチップを手作業で並べていく必要があります。
そこで、外部のエディタを使ってマップを制作し、それを読み込むという方法を実装します。

マップ制作には、「Platinum」というエディタを使用します。
以下のURLからダウンロードできます。

ダウンロード後、フォルダを解凍し「Platinum.exe」を起動します。
いくつかウィンドウが表示されるので、
[ファイル] > [新規作成] を選択すると、新規マップの作成ができます。
今回は以下のサイズで作成しました。
横幅 : 46
高さ : 17
パーツの横幅・高さ : 64

マップ作成

次に、Platinumにマップチップの画像を読み込みます。
しかしこのエディタは.pngファイルを読み込めません…。
そのため、bmp形式かjpeg形式の画像を用意する必要があります。
今回は、bmp形式の画像を用意しました。
mapChip.bmpを作成し、パーツウィンドウにドラッグ&ドロップで追加します。

パーツ追加

パーツの追加ができたら、自由に配置していきます。

配置が完了したら、
[ファイル] > [書き出し] から、dataフォルダ内にCSVファイルとして保存します。
保存先のフォルダを選んだあとに、[CSV Export Options] とウィンドウが表示されますが、ここではType1を選んでください。

保存したCSVファイルを開くと、先ほど配置したマップチップとCSV内の番号が一致していることが確認できるはずです。


6-4 外部エディタで作ったマップを反映する

先ほど作成したCSVファイルをプログラム上で読み込めるようにしていきます。
読み込み方については、以下のサイトを参考にしてください。


マップの読み込みは、Bgクラスで行いたいと思います。
マップチップの配列は、m_chipData変数で管理します。
そのため、kChipDataからm_chipDataに書き換える必要があります。
チップの数も書き換えておきます。
あと、配列が[y][x]といった形になっていたので、わかりやすく[x][y]に書き換えます。
・Bg.h

class Bg
{
private:
	/// <summary>
	/// マップデータを読み込む
	/// </summary>
	void LoadMapData();

private:
	int m_mapData;	 // マップデータ
};

・Bg.cpp

#include <fstream>
#include <sstream>

namespace
{
	// チップを置く数
	constexpr int kChipNumX = 46;
	constexpr int kChipNumY = 17;
}

Bg::Bg(Player* pPlayer):
	m_chipData()
{
	// マップデータを読み込む
	LoadMapData();
}

bool Bg::IsCollision(Rect rect, Rect& chipRect)
{
	for (int y = 0; y < kChipNumY; y++)
	{
		for (int x = 0; x < kChipNumX; x++)
		{
			// マップチップ0番は当たり判定がないため飛ばす
			if (m_chipData[x][y] == 0) continue;
		}
	}
}

void Bg::LoadMapData()
{
	std::ifstream file("data/mapChip.csv");
	std::string line;

	// getline関数で1行ずつ読み込む
	int y = 0;
	while (std::getline(file, line) && y < kChipNumY)
	{
		std::istringstream stream(line);
		std::string field;

		// 「,」区切りごとにデータを読み込む
		int x = 0;
		while (getline(stream, field, ',') && x < kChipNumX)
		{
			// 文字列をint型に変換してm_chipDataに追加する
			m_chipData[x][y] = std::stoi(field);
			x++;
		}
		y++;
	}
}

void Bg::DrawMapChip()
{
	// マップチップの描画
	for (int y = 0; y < kChipNumY; y++)
	{
		for (int x = 0; x < kChipNumX; x++)
		{
			// 設置するチップ
            int chipNo = m_chipData[x][y];
		}
	}
}

これで実行します。
CSVファイルで作成したマップがゲーム上に正しく反映されるようになりました。

実行画面

まとめ

これで一通りの実装が完了しました。お疲れ様でした!
最後まで読んでいただきありがとうございます。
記事は3本分のボリュームになってしまいましたが、ここまでの内容を実装できれば、基本的な2Dゲームの土台は作れるはずです。
今回、アニメーションや演出部分など紹介しきれなかった技術もたくさんあるので、ぜひ自分でもいろいろと試して実装してみてください。

全体のソースコードはGitHubにアップしています。


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

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