見出し画像

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



1.導入

1-1 はじめに

この記事は、C++の教科書レベルがある程度理解できた方が2Dゲームを作れるようになることを目指した内容です。
ゲーム業界を目指している方向けに書いているため、ところどころで就活に関する話題も交えていきます。

今回はロックマン風の2D横スクロールアクションを作ろうと思います。
実装は以下の順番で進めていきます。

1.導入
2.キャラクター実装
3.弾うち実装
4.当たり判定実装
5.マップスクロール
6.マップチップ追加

本記事①では、主にキャラクターの実装を行います。


1-2 DXライブラリ導入

そして、今回はDXライブラリを使用して制作を行います。
DXライブラリを初めて使う方は以下の公式サイトを参考にダウンロードとセットアップを行ってください。


セットアップが完了したら新しくプロジェクトを作成し、main.cppを追加します。
まずはこのファイルに、ウィンドウの初期化など、ゲームを起動するための基本的な処理を書いていきます。

main.cpp作成

実行すると、ウィンドウが表示されます。
Escキーで終了できます。

これで導入部分はできたので、次にキャラクターの実装に移っていきます。

1-3 補足

命名規則は、以下のようにキャメルケースを使用し、統一します。
・メンバ変数 : 先頭にm_をつける(例 : m_pos)
・定数 : 先頭にkをつける(例 : kSpeed)


2.キャラクター実装

2-1 Characterクラス作成

まずは、キャラクターを作っていきます。
今回はプレイヤーと敵の2種類がいるため、継承を使ってキャラクターを共通で管理できるようにします。
継承についての詳しい説明は以下のサイトを参考にしてください。


Character.hCharacter.cppを追加してCharacterクラスを作ります。
このクラスにはプレイヤーと敵の両方に共通する処理(初期化・更新・描画など)を記述します。
まずは以下の関数を定義しておきます。
 ・コンストラクタ
 ・デストラクタ
 ・初期化
 ・更新
 ・描画

Charaterクラス作成

余談ですが、関数の上に///を書くと、
/// <summary>
///
/// </summary>
というコメントができます。
これは、ドキュメントコメントというもので、関数の内容を簡単に説明したいときに使えます。詳細は以下のサイトを確認してください。

就活でゲームを提出する際はコメントが適切に書かれているかも見られています。
そのため、関数を作成したらドキュメントコメントを書くことを意識するとよいでしょう。


2-2 プレイヤー、敵を表示する

2-2-1 画像を準備する

まずは、プレイヤーと敵の画像を準備します。
.sln(ソリューションファイル)があるフォルダーを開き、dataフォルダを作成してください。
その中に、player.pngenemy.pngを追加します。

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

player.png
enemy.png

次に、プレイヤーと敵がそれぞれ画面上に表示されるようにしていきます。プレイヤーと敵のクラスをCharacterクラスから派生させて作成します。

Playerクラス作成
Enemyクラス作成

それぞれのキャラで画像を読み込む必要があるため、Characterクラスには画像ハンドルを保持するメンバ変数(m_handle)を追加します。

画像の読み込みはDXライブラリのLoadGraph関数を使います。
DXライブラリの使い方については下記のサイトを確認してください。

Player.cppとEnemy.cppの1行目に #include "DxLib.h" と記述します。
これでDXライブラリの関数を使えるようになります。

コンストラクタで画像を読み込み、デストラクタで解放します。
今回はパス名を "data/player.png" , "data/enemy.png" としています。
・Player.cpp

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

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

・Enemy.cpp

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

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

※画像のパス名は、絶対に日本語を使わないでください。
日本語を使うとエラーが発生する可能性があります。パス名やフォルダ名には半角英数字を使用しましょう。
日本語パスを使っていると就活だったら9割落とされると思います。

続いて、Draw() 関数を使って画像を描画してみます。
DrawGraph()関数で読み込んだ画像の描画ができます。
座標はとりあえずプレイヤーがx=0, y=0、敵がx=150, y=0で設定しておきます。
・Player.cpp

void Player::Draw()
{
    DrawGraph(0, 0, m_handle, true); 
}

・Enemy.cpp

void Enemy::Draw()
{
    DrawGraph(150, 0, m_handle, true); 
}

これで一度実行してみましょう。
プレイヤーと敵の画像が表示されま…せんね。

画像が表示されない理由は、プレイヤーや敵のインスタンスがまだ生成されていないからです。
PlayerとEnemy のインスタンスを生成し、描画関数を呼び出す処理を追加していきます。

2-2-2 メモリを確保する

C++でのメモリ管理には以下の2つの方法があります・
・new / delete を使った手動管理
・std::shared_ptr などのスマートポインタを使った自動管理

就活ではスマートポインタの方が良いですが、今回は初心者向けということでnew/deleteを使います。
詳しい解説は以下のサイトを参考にしてください。


キャラクターの生成・更新・描画処理を管理するためのSceneMainクラスを作成します。
SceneMain.hSceneMain.cppを作成しました。

SceneMainクラス作成

まずはSceneMainクラスが使えるようにしていきます。
main.cppの上部に#includeを追加します。

#include "SceneMain.h"

ゲームループの前にSceneMainのインスタンスを生成し、初期化します。

SceneManager* m_pScene = new SceneManager;
pScene->Init();

次に、ゲームループ内で、Update()とDraw()を呼び出します。

m_pScene->Update();
m_pScene->Draw();

SceneMain.h に、プレイヤーと敵のメンバ変数をポインタとして追加します。

class SceneMain
{
private:
	Player* m_pPlayer;
	Enemy* m_pEnemy;
};

次に、SceneMain.cppのコンストラクタでメモリの確保を行います。

SceneMain::SceneMain()
{
	m_pPlayer = new Player;
	m_pEnemy = new Enemy;
}

これだけだと「型指定子が必要です」といったエラーが出るかと思います。 なので、SceneMain.hとSceneMain.cppに以下を追加します。
・SceneMain.h

class Player;
class Enemy; 

・SceneMain.cpp

#include "Player.h" 
#include "Enemy.h" 

これでメモリの確保ができるようになりました。
メモリを使用してキャラクターの表示を行っていきます。
SceneMain.cppのDraw()関数で呼び出しを行います。

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

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

アロー演算子(->)を使ってポインタからメンバ関数を呼び出しています。アロー演算子の詳しい使い方は以下を参考にしてください。

これで実行するとプレイヤーと敵がどちらも描画されるようになりました。

実行画面

これで問題はないのですが、プレイヤーや敵の Draw() 関数では、
DrawGraphF(0, 0, m_handle, true); と画像を描画する処理が全く同じ形で書かれています。
このような共通処理は、基底クラスであるCharacterクラスにまとめることで、コードの重複を防げて管理もしやすくなります。

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

#include "DxLib.h"
#include "SceneMain.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	// 画面サイズの変更
	SetGraphMode(1920, 1080, 32);

	if (DxLib_Init() == -1)	// DXライブラリ初期化処理
	{
		return -1;			// エラーが起きたら直ちに終了
	}

	// ダブルバッファモード
	SetDrawScreen(DX_SCREEN_BACK);

	SceneMain* m_pScene = new SceneMain;
	m_pScene->Init();

	while (ProcessMessage() == 0)
	{
		LONGLONG  time = GetNowHiPerformanceCount();
		// 画面のクリア
		ClearDrawScreen();

		m_pScene->Update();
		m_pScene->Draw();

		//裏画面を表画面を入れ替える
		ScreenFlip();

		// fpsを60に固定
		while (GetNowHiPerformanceCount() - time < 16667)
		{
		}

		if (CheckHitKey(KEY_INPUT_ESCAPE))
		{
			break;
		}

	}
	
	DxLib_End(); // DXライブラリ使用の終了処理

	return 0;	 // ソフトの終了 
}

・Character.h

#pragma once
#include "Vec2.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();

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

protected:
	int m_handle;	// キャラクターの画像
};

・Character.cpp

#include "Character.h"

Character::Character():
	m_handle(-1),
{
}

Character::~Character()
{
}

void Character::Init()
{
}

void Character::Update()
{
}

void Character::Draw()
{
    DrawGraphF(0, 0, m_handle, true);
}

・Player.h

#pragma once
#include "Character.h"

/// <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;
}

・Player.cpp

#include "DxLib.h"
#include "Player.h"

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

Player::~Player()
{
}

void Player::Init()
{
}

void Player::Update()
{
}

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

・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()
{
}

void Enemy::Init()
{
}

void Enemy::Update()
{
}

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

2-3 プレイヤーを移動させる

次は、プレイヤーを動かせるようにしていきます。

2-3-1 パッド入力

今回は、ゲームパッドでキャラクターを操作できるようにしますが、キーボード操作にも対応しているので、パッドがなくても大丈夫です。

まずはゲームパッドの入力状態を管理するためのPadクラスを作成します。
このクラスを使うことで、以下のことができるようになります。
・ボタンを押し続けている間反応する
・ボタンを押した瞬間だけ反応する
・ボタンを離した瞬間だけ反応する

Pad.hとPad.cppを作成しました。
今回は、複数のクラスで使えるようにしたいのでクラス化はせず、namespaceを使って実装していきます。
namespeceについては以下のサイトの「名前空間スコープ」を確認してください。

それでは処理を書いていきます。
・Pad.h

namespace Pad
{
  // 更新処理
  void Update();

  // 押されているか
  bool IsPress(int key);

  // 押された瞬間を取得
  bool IsTrigger(int key);

  // 離した瞬間を取得
  bool IsRelease(int key);
}

・Pad.cpp

#include "DxLib.h"
#include "Pad.h"

namespace
{
	// 前のフレームのパッド押し下げ状態
	int lastPad = 0;
	// このフレームのパッド押し下げ状態
	int nowPad = 0;
}

namespace Pad
{
	void Update()
	{
		// 前のフレームに取得したパッド情報を一つ古い情報にする
		lastPad = nowPad;

		// 現在のパッドの情報を取得する
		nowPad = GetJoypadInputState(DX_INPUT_KEY_PAD1);
	}

	bool Pad::IsPress(int key)
	{
		// 現在のフレームで押されている
		return (nowPad & key);
	}

	bool Pad::IsTrigger(int key)
	{
		bool isNow = (nowPad & key);	// 現在フレーム
		bool isLast = (lastPad & key);	// 前のフレーム

		// 現在のフレームに押されていて前回のフレームに押されていない
		if (isNow && !isLast)
		{
			return true;
		}
		return false;
	}

	bool Pad::IsRelease(int key)
	{
		bool isNow = (nowPad & key);	// 現在のフレーム
		bool isLast = (lastPad & key);	// 前のフレーム

		// 現在フレームに押されていなくて前回のフレームは押されていた
		if (!isNow && isLast)
		{
			return true;
		}
		return false;
	}
}

DXライブラリの機能である GetJoypadInputState(int InputType) ;
1つ前のフレームと現在のフレームの入力状態を取得します。
押されていない場合は0、押されている場合は1が返ってきます。

このままだと入力状態が更新できないので、
main.cppの更新部分に以下のように記述してください。

Pad::Update();

これで入力状態を取得と更新ができるようになりました。
下記にPad.hとPad.cpp、main.cppのソースコードを張り付けておきます。
・Pad.h

#pragma once

/// <summary>
/// ゲームパッドの入力を管理するクラス
/// </summary>
namespace Pad
{
	/// <summary>
	/// パッドの更新処理
	/// </summary>
	void Update();

	/// <summary>
	/// 押されているか
	/// </summary>
	/// <param name="key">入力キー</param>
	/// <returns>押されているかどうか</returns>
	bool IsPress(int key);

	/// <summary>
	/// 押された瞬間を取得
	/// </summary>
	/// <param name="key">入力キー</param>
	/// <returns>押されたかどうか</returns>
	bool IsTrigger(int key);

	/// <summary>
	/// 離した瞬間を取得
	/// </summary>
	/// <param name="key">入力キー</param>
	/// <returns>離されたかどうか</returns>
	bool IsRelease(int key);
}

・Pad.cpp

#include "DxLib.h"
#include "Pad.h"

namespace
{
	// 前のフレームのパッド押し下げ状態
	int lastPad = 0;
	// このフレームのパッド押し下げ状態
	int nowPad = 0;
}

namespace Pad
{
	void Update()
	{
		// 前のフレームに取得したパッド情報を一つ古い情報にする
		lastPad = nowPad;

		// 現在のパッドの情報を取得する
		nowPad = GetJoypadInputState(DX_INPUT_KEY_PAD1);
	}

	bool Pad::IsPress(int key)
	{
		// 現在のフレームで押されている
		return (nowPad & key);
	}

	bool Pad::IsTrigger(int key)
	{
		bool isNow = (nowPad & key);	// 現在フレーム
		bool isLast = (lastPad & key);	// 前のフレーム

		// 現在のフレームに押されていて前回のフレームに押されていない
		if (isNow && !isLast)
		{
			return true;
		}
		return false;
	}

	bool Pad::IsRelease(int key)
	{
		bool isNow = (nowPad & key);	// 現在のフレーム
		bool isLast = (lastPad & key);	// 前のフレーム

		// 現在フレームに押されていなくて前回のフレームは押されていた
		if (!isNow && isLast)
		{
			return true;
		}
		return false;
	}
}

・main.cpp

#include "DxLib.h"
#include "Pad.h"
#include "SceneMain.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	// 画面サイズの変更
	SetGraphMode(1920, 1080, 32);

	if (DxLib_Init() == -1)	// DXライブラリ初期化処理
	{
		return -1;			// エラーが起きたら直ちに終了
	}

	// ダブルバッファモード
	SetDrawScreen(DX_SCREEN_BACK);

	SceneMain* m_pScene = new SceneMain;
	m_pScene->Init();

	while (ProcessMessage() == 0)
	{
		LONGLONG  time = GetNowHiPerformanceCount();
		// 画面のクリア
		ClearDrawScreen();

		Pad::Update();
		m_pScene->Update();
		m_pScene->Draw();

		//裏画面を表画面を入れ替える
		ScreenFlip();

		// fpsを60に固定
		while (GetNowHiPerformanceCount() - time < 16667)
		{
		}

		if (CheckHitKey(KEY_INPUT_ESCAPE))
		{
			break;
		}

	}
	
	DxLib_End(); // DXライブラリ使用の終了処理

	return 0;	 // ソフトの終了 
}

それでは、実際にプレイヤーを動かしてみましょう。
Playerクラスに移動します。
今回は、Playerクラスに移動用の関数を作成しました。
・Player.h

private:
 void Move();

・Player.cpp

#include "Pad.h"

void Player::Move()
{
  // ここに処理を追加していく
}

とりあえず、プレイヤーが左右に動くように処理を追加していきます。
その前に、X軸とY軸それぞれの変数で管理するのが面倒なので、
2次元のベクトルを管理するVector2のクラスを作ります。

2-3-2 Vector2クラス作成

このクラスでベクトルの加減算・乗算・正規化などの基本操作ができるようにしていきます。
まずは"Vec2.h"を作成します。
Vector2の処理についてはすべてヘッダー側で記述します。

#pragma once
#include <cmath>

class Vec2
{
public:
	float x;
	float y;

public:
	Vec2() :
		x(0.0f),
		y(0.0f)
	{
	}

	Vec2(float posX, float posY) :
		x(posX),
		y(posY)
	{
	}

	// 単項演算子+	Vec2 = +Vec2
	Vec2 operator+() const
	{
		return *this;
	}

	// 単項演算子-	Vec2 = -Vec2
	Vec2 operator-() const
	{
		return Vec2{ -x, -y };
	}

	// 足し算 c = a + b, c = (a += b)
	Vec2 operator+(Vec2 vec) const
	{
		return Vec2{ x + vec.x, y + vec.y };
	}
	// Vec2 += Vec2
	Vec2 operator+=(Vec2 vec)
	{
		x += vec.x;
		y += vec.y;
		return *this;
	}

	// 引き算 
	Vec2 operator-(Vec2 vec) const
	{
		return Vec2{ x - vec.x, y - vec.y };
	}
	// Vec2 -= Vec2
	Vec2 operator-=(Vec2 vec)
	{
		x -= vec.x;
		y -= vec.y;
		return *this;
	}

	// 掛け算
	Vec2 operator*(float scale) const
	{
		return Vec2{ x * scale, y * scale };
	}
	// Vec2 *= float
	Vec2 operator*=(float scale)
	{
		x *= scale;
		y *= scale;
		return *this;
	}

	// 割り算
	Vec2 operator/(float scale) const
	{
		return Vec2{ x / scale, y / scale };
	}
	// Vec2 /= float
	Vec2 operator/=(float scale)
	{
		x /= scale;
		y /= scale;
		return *this;
	}

	// ベクトルの長さの2乗を求める
	float sqLength() const
	{
		return x * x + y * y;
	}

	// ベクトルの長さを求める
	float length() const
	{
		return sqrtf(sqLength());
	}

	// 自身の正規化を行う
	void normalize()
	{
		float len = length();
		if (len > 0.0f)
		{
			x /= len;
			y /= len;
		}
		// 長さ0の場合は何もしない
	}

	// 自身を正規化したベクトルを取得する
	Vec2 getNormalize() const
	{
		float len = length();
		if (len > 0.0f)
		{
			return Vec2{ x / len, y / len };
		}
		// 長さが0の場合
		return Vec2{ 0.0f, 0.0f };
	}
};

演算子オーバーロードについては、以下のサイトを確認してください。

Vector2クラスができたのでいよいよプレイヤーを動かしていきます。

2-3-3 ボタンを押したら移動する

移動の処理は、2-3-1で作成したPlayerクラスのMove()関数の中に追加していきます。
まずは、キャラクターの座標と移動量を保存する変数をCharacterクラスに追加します。
・Character.h

#include "Vec2.h"

protected:
 Vec2 m_pos;  // 座標
 Vec2 m_move; // 移動量

次に、プレイヤーの移動に必要な処理を実装していきます。
今回は長押ししている間移動するようにしたいので、Pad::IsPress(InputType)を使用します。
InputTypeの部分を以下にすることで左右キーの入力ができます。
 PAD_INPUT_LEFT
 PAD_INPUT_RIGHT

ここでは、移動速度を定数として定義します。
コード内に直接数値(マジックナンバー)を書くと、あとから値を変更がしづらくなってしまいます。
特に就職活動においては、マジックナンバーがあるだけで評価が下がり、落不合格になる確率が上がってしまいます。

それから、描画位置が(0, 0)になっているのでキャラクターの位置を(m_pos.x, m_pos.y)に変更して正しい位置に描画されるように修正します。

・Player.cpp

namespace
{
	constexpr float kSpeed = 20.0f; // 移動速度
}

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

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

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


これで実行してみてください。
下の動画のように左右キーを押すとプレイヤーが動くはずです。

実行画面

現在の状態では、左に移動してもプレイヤーの向きが変わりません。
そこで、移動方向に応じてプレイヤーの向きが変わるようにしていきます。

Characterクラスにキャラクターの向きを管理する変数を追加します。
・Character.h

private:
 bool m_isRight; // キャラクターが右を向いているか

・Character.cpp

Character::Character():
	m_handle(-1),
	m_isRight(true)
{
}

次に、プレイヤーの移動時にm_isRightを更新するようにします。
そして描画についてですが、DXライブラリには画像を左右反転して描画する機能があります。
m_isRightがtrueの場合はDrawGraph()、falseの場合はDrawTurnGraph()を使用します。

・Player.cpp

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::Draw()
{
	if (m_isRight)
	{
		DrawGraphF(m_pos.x, m_pos.y, m_handle, true);
	}
	else
	{
		DrawTurnGraphF(m_pos.x, m_pos.y, m_handle, true);
	}
}

これで移動方向に応じてプレイヤーの向きが反転するようになりました。

実行画面

2-4 重力を反映する

ほとんどのアクションゲームには「重力」があります。
このゲームでも重力処理を実装していきます。

まずは、キャラクターに重力を与えるための関数をCharacterクラス内に作成します。
・Character.h

class Character
{
protected:
  void Gravity();
}

・Character.cpp

namespace
{
  constexpr float kGravity = 1.5f; // 重力
}

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

void Player::Gravity()
{
	m_move.y += kGravity;
}

次に、プレイヤーと敵キャラクターに重力が働くよう、それぞれのUpdate() 関数からCharacter::Update()を呼び出します。
・Player.cpp

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

・Enemy.cpp

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

これでプレイヤーと敵が落下するようになりました。

実行画面

ただし、このままだとキャラクターが落下し続けてしまいます。
そこで、着地判定を追加し、地面に到達したら止まるようにします。
・Character.cpp

namespace
{
  constexpr float kGravity = 1.5f;  // 重力
  constexpr float kGround = 900.0f; // 地面位置
}

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

  // 着地処理
  if (m_pos.y >= kGround)
  {
	  m_pos.y = kGround;
	  m_move.y = 0.0f;
  }
}

これで画面外に落ちることはなくなりました。

実行画面

2-5 ジャンプを実装する

続いて、ジャンプを実装していきます。
今回はプレイヤーのみジャンプできるようにしたいので、Playerクラスに処理を追加します。

ボタンを押したらY方向の移動量をマイナスにすることで、プレイヤーが上方向に飛ぶようにします。

ボタンを押した瞬間にジャンプするようにしたいので、2-3-1で作成したPad::IsTrigger(PAD_INPUT_1)を使用します。
コントローラーではXボタン、キーボードではZキーでジャンプできるようになります。
・Player.h

class Player : public Character
{
private:
  void Jump();
}

・Player.cpp

namespace
{
	constexpr float kJumpPower = 20.0f; // ジャンプ力
}

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

void Player::Jump()
{
	if (Pad::IsTrigger(PAD_INPUT_1))
	{
		m_move.y = -kJumpPower;
	}
}

実行してみます。

実行画面

きれいなジャンプができましたね!
ジャンプ力(kJumpPower)と重力(kGravity)の値を変えることでジャンプの高さや滞空時間を調整できます。
しかし、今のままだと空中で何度もジャンプができてしまいます。

無限ジャンプ編

カービィのホバリングだったらこれで問題ないのですが、今回の仕様では「地面に着地している間だけジャンプができる」ようにしたいです。
そこで、「キャラクターが地面についているか」を判定するm_isGroundという変数を追加します。

Characterクラスにm_isGroundを追加します。
地面にいる間はm_isGroundをtrueに、ジャンプした瞬間にm_isGroundをfalseにします。
・Character.h

class Character
{
protected:
  bool m_isGround; // 地面に着地しているか
}

・Character.cpp

Character::Character():
	m_isGround(false)
{
}

void Character::Update()
{
	// 着地処理
	if (m_pos.y >= kGround)
	{
		m_pos.y = kGround;
		m_move.y = 0.0f;
		m_isGround = true;
	}
}

・Player.cpp

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

	if (Pad::IsTrigger(PAD_INPUT_1))
	{
		m_move.y = -kJumpPower;
		m_isGround = false;
	}
}

ここでは早期リターンを使い、空中にいる場合は処理をスキップしています。
こうすることでネストが深くならず、コードがシンプルで読みやすくなります。

これで実行すると、ボタンを連打しても無限にジャンプしなくなりました。


2-6 ジャンプの仕組み

ここでジャンプの仕組みについて簡単に解説します。
より詳しく知りたい方は、桜井さんの動画がとても分かりやすので、ぜひそちらも参考にしてみてください。

ジャンプの動きは、「上向きの力を一瞬だけ加えてその後は重力で落ちていく」という流れになっています。

ジャンプボタンを押した瞬間に、次のような式で上方向の速度を設定します。
m_move.y = -kJumpPower;
そして、毎フレーム重力を足していきます。
m_move.y += kGravity
フレームが進むにつれてm_move.yは0に近づき、やがて下向きの速度になります。
これにより、放物線のようなふわっとしたジャンプが実現できます。


2-7 応用編:ジャンプの高さを変える

マリオやロックマンのようなアクションゲームでは、ボタンを押す長さによってジャンプの高さが変わる仕組みがありますよね。
短く押すと小ジャンプ、長めに押すと大ジャンプになるやつです。
ここでは、その仕組みの実装していきます。

ボタンを押してからボタンを離すまでのフレーム数を計測します。
このフレーム数が
10フレーム以内の場合は小ジャンプ
30フレーム以内の場合は中ジャンプ
それ以上は大ジャンプ
といった感じで3段階のジャンプを実装します。
・Character.h

protected:
	int m_jumpFrame;        // ジャンプフレーム数
	bool m_isJumpPreparing; // ジャンプ準備中か

・Character.cpp

Character::Character():
	m_jumpFrame(0),
	m_isJumpPreparing(false),
{
}

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

	// 着地処理
	if (m_pos.y >= kGround)
	{
		m_pos.y = kGround;
		m_move.y = 0.0f;
		m_isGround = true;
        
        // ジャンプ準備中は飛ばす
		if (m_isJumpPreparing) return;
		m_jumpFrame = 0;
	}
}

・Player.cpp

namespace
{
	constexpr int kSmallJumpFrame = 8;		// 小ジャンプのフレーム数
	constexpr int kMediumJumpFrame = 13;	// 中ジャンプのフレーム数
	constexpr float kSmallJumpHeight = 0.5f;	// 小ジャンプの高さ
	constexpr float kMediumJumpHeight = 0.7f;	// 中ジャンプの高さ
	constexpr float kBigJumpHeight = 1.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;
}

ジャンプのボタンを押したらフレームを計測し始めて、ボタンを離したらフレームに応じてジャンプの高さを変えています。
定数の値を変えることでジャンプの高さを調整できます。

実行画面

まとめ

これでアクションゲームの基本的な挙動(移動、ジャンプ)が実装できました!
次の記事では3.弾うちと4.当たり判定を実装していきます。


キャラクター実装までのソースコードを下に張り付けておきます。
・main.cpp

#include "DxLib.h"
#include "SceneMain.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	// 画面サイズの変更
	SetGraphMode(1920, 1080, 32);

	if (DxLib_Init() == -1)	// DXライブラリ初期化処理
	{
		return -1;			// エラーが起きたら直ちに終了
	}

	// ダブルバッファモード
	SetDrawScreen(DX_SCREEN_BACK);

	SceneMain* m_pScene = new SceneMain;
	m_pScene->Init();

	while (ProcessMessage() == 0)
	{
		LONGLONG  time = GetNowHiPerformanceCount();
		// 画面のクリア
		ClearDrawScreen();

		m_pScene->Update();
		m_pScene->Draw();

		//裏画面を表画面を入れ替える
		ScreenFlip();

		// fpsを60に固定
		while (GetNowHiPerformanceCount() - time < 16667)
		{
		}

		if (CheckHitKey(KEY_INPUT_ESCAPE))
		{
			break;
		}

	}
	
	DxLib_End(); // DXライブラリ使用の終了処理

	return 0;	 // ソフトの終了 
}

・Character.h

#pragma once
#include "Vec2.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();

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

protected:
	int m_handle;			// キャラクターの画像
	int m_jumpFrame;		// ジャンプ中のフレーム数
	bool m_isJumpPreparing; // ジャンプ準備中か
	bool m_isRight;			// キャラクターが右を向いているか
	bool m_isGround;		// 地面についているか
	Vec2 m_pos;				// 座標
	Vec2 m_move;			// 移動量
};

・Character.cpp

#include "Character.h"

namespace
{
	constexpr float kGravity = 1.5f;  // 重力
	constexpr float kGround = 900.0f; // 地面位置
}

Character::Character():
	m_handle(-1),
	m_jumpFrame(0),
	m_isJumpPreparing(false),
	m_isRight(true),
	m_isGround(false)
{
}

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

void Character::Init()
{
}

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

	// 着地処理
	if (m_pos.y >= kGround)
	{
		m_pos.y = kGround;
		m_move.y = 0.0f;
		m_isGround = true;

		// ジャンプ準備中は飛ばす
		if (m_isJumpPreparing) return;
		m_jumpFrame = 0;
	}
}

void Character::Draw()
{
    if (m_isRight)
	{
		DrawGraphF(m_pos.x, m_pos.y, m_handle, true);
	}
	else
	{
		DrawTurnGraphF(m_pos.x, m_pos.y, m_handle, true);
	}
}

void Character::Gravity()
{
	m_move.y += kGravity;
}

・Player.h

#pragma once
#include "Character.h"

/// <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;
}

・Player.cpp

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

namespace
{
	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();
}

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;
}

・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();
}


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

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