第07回 ゲームの状態と終了条件
4 / 4 ゲーム進行の動作確認
このステップの目的
タイトルからゲームを始め、勝敗に応じて結果画面へ移り、リトライするまでの流れを確認します。
動作確認の手順
プロジェクトをビルドして実行し、以下のシナリオに沿って画面遷移とゲーム進行を確認します。
- タイトル画面からの開始
起動直後にタイトル画面が表示されることを確認します。決定キー(SpaceまたはEnter)を押してゲームが開始され、自機や敵が動き始めることを確認します。 - ゲームオーバーへの遷移とリトライ
敵や敵弾にわざと3回接触し、HPが0になった瞬間に「GAME OVER」のテキストが表示されてゲームが停止することを確認します。
結果画面でRキーを押し、HPが3に戻り、画面上の敵や弾が消去されて最初からリトライできることを確認します。 - ゲームクリアへの遷移
敵を次々に倒してスコアが30点に達した瞬間(または60秒間生き残った瞬間)に、「GAME CLEAR!」のテキストが表示されることを確認します。 - タイトル画面への復帰
結果画面でEscapeキーを押し、正しくタイトル画面へ戻れることを確認します。
チェックリスト
- タイトル画面からゲームプレイを開始できることを確認できる
- HPが0になった瞬間にゲームオーバー画面へ正しく遷移することを確認できる
- スコア目標達成または時間切れでゲームクリア画面へ遷移することを確認できる
- 結果画面からリトライした際、自機、敵、弾、スコア、時間が初期状態へ正しくリセットされることを確認できる
- 状態遷移がすべて
ChangeState()を経由して安全に行われていることを説明できる
まとめと次回への引き継ぎ
第07回では、タイトル、プレイ、結果、リトライまでの進行をステートマシンで実装しました。これで、最初から最後まで遊べる基本完成状態になりました。
第08回からは、プロの基準を目指してコードを整理し、演出を加えます。まず、画像の読み込みと解放を行うImageManagerの仕組みを詳しく見ます。C++のリソース管理で最も重要なRAIIと所有権を学びます。
参考実装
今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。
Source/Shooting/Game.cpp
Source/Shooting/Game.cppcpp
#include <cmath>
#include <string>
#include <DxLib.h>
#include <Windows.h>
#include "Config/ApplicationConfig.h"
#include "Shooting/Tuning/GameTuning.h"
#include "Shooting/Game.h"
namespace
{
void ShowResourceLoadError(
const wchar_t* resourceType,
const std::wstring& failedPath)
{
std::wstring message = resourceType;
message += L"の読み込みに失敗しました。\n\n対象: ";
message += failedPath;
message += L"\n\nAssetsフォルダーとファイル名を確認してください。";
MessageBoxW(
nullptr,
message.c_str(),
L"リソース読み込みエラー",
MB_OK | MB_ICONERROR);
}
}
namespace dxgame::shooting
{
namespace config = dxgame::config;
bool Game::Run()
{
const ImageManager::LoadResult imageResult = images.LoadAll();
if (!imageResult.succeeded)
{
ShowResourceLoadError(L"画像", imageResult.failedPath);
return false;
}
ChangeState(State::Title);
// 小数分の待ち時間を次フレームへ持ち越す
double nextFrame = GetNowCount();
while (ProcessMessage() == 0)
{
input.Update();
if (input.IsDown(Input::Action::Exit))
break;
Update(config::DeltaTime);
ClearDrawScreen();
Draw();
ScreenFlip();
nextFrame += config::FrameMilliseconds;
int waitMilliseconds = static_cast<int>(nextFrame - GetNowCount());
if (waitMilliseconds > 0)
WaitTimer(waitMilliseconds);
else
nextFrame = GetNowCount();
}
return true;
}
void Game::Update(float deltaTime)
{
switch (state)
{
case State::Title:
UpdateTitle();
break;
case State::Playing:
UpdatePlaying(deltaTime);
break;
case State::GameClear:
case State::GameOver:
UpdateResult();
break;
}
}
void Game::Draw() const
{
switch (state)
{
case State::Title:
DrawTitle();
break;
case State::Playing:
DrawPlaying();
break;
case State::GameClear:
case State::GameOver:
DrawPlaying();
DrawResult();
break;
}
}
void Game::StartGame()
{
// 前回の結果を残さず、全オブジェクトを開始状態へ戻す
player.Reset();
playerShots.Reset();
enemies.Reset();
enemyShots.Reset();
timeLeft = tuning::game::TimeLimit;
score = 0;
ChangeState(State::Playing);
}
void Game::ChangeState(State nextState)
{
state = nextState;
}
void Game::UpdateTitle()
{
if (input.IsPressed(Input::Action::Confirm))
StartGame();
}
void Game::UpdatePlaying(float deltaTime)
{
timeLeft -= deltaTime;
if (timeLeft < 0.0f)
timeLeft = 0.0f;
player.Update(input, deltaTime);
playerShots.Update(deltaTime);
enemies.Update(deltaTime);
enemyShots.Update(deltaTime);
player.TryShoot(input, playerShots);
ResolveCollisions();
if (!player.IsAlive())
{
ChangeState(State::GameOver);
return;
}
if (score >= tuning::game::ClearScore || timeLeft <= 0.0f)
{
ChangeState(State::GameClear);
return;
}
// 発射時点のプレイヤー位置を、すべての敵が狙う位置として共有する
Vector2 playerPosition = player.GetHitbox().GetCenter();
for (Enemy& enemy : enemies.GetAll())
{
enemy.TryShoot(enemyShots, playerPosition);
}
}
void Game::UpdateResult()
{
if (input.IsPressed(Input::Action::Confirm))
StartGame();
else
if (input.IsPressed(Input::Action::Back))
ChangeState(State::Title);
}
void Game::ResolveCollisions()
{
// プレイヤーの弾は最初に重なった敵だけに命中させる
for (PlayerShot& playerShot : playerShots.GetAll())
{
if (!playerShot.IsActive())
continue;
for (Enemy& enemy : enemies.GetAll())
{
if (!enemy.IsActive())
continue;
if (!playerShot.GetHitbox().Overlaps(enemy.GetHitbox()))
continue;
enemy.TakeDamage(tuning::playerShot::Damage);
playerShot.Deactivate();
if (!enemy.IsActive())
++score;
break;
}
}
for (EnemyShot& enemyShot : enemyShots.GetAll())
{
if (!enemyShot.IsActive())
continue;
if (!enemyShot.GetHitbox().Overlaps(player.GetHitbox()))
continue;
enemyShot.Deactivate();
player.TryTakeDamage();
}
for (Enemy& enemy : enemies.GetAll())
{
if (!enemy.IsActive())
continue;
if (!enemy.GetHitbox().Overlaps(player.GetHitbox()))
continue;
enemy.Deactivate();
player.TryTakeDamage();
}
}
void Game::DrawTitle() const
{
DrawString(194, 128, L"VERTICAL SHOOTING", GetColor(80, 220, 255));
DrawString(248, 210, L"PRESS ENTER", GetColor(255, 230, 80));
DrawString(208, 276, L"ARROW / WASD : MOVE", GetColor(210, 218, 232));
DrawString(238, 306, L"Z / SPACE : SHOT", GetColor(210, 218, 232));
}
void Game::DrawPlaying() const
{
// 描画順はプレイヤー、プレイヤーの弾、敵、敵の弾の順
player.Draw(images);
playerShots.Draw(images);
enemies.Draw(images);
enemyShots.Draw(images);
DrawHud();
}
void Game::DrawResult() const
{
SetDrawBlendMode(DX_BLENDMODE_ALPHA, 168);
DrawBox(0, 0, tuning::ScreenWidth, tuning::ScreenHeight, GetColor(0, 0, 0), TRUE);
SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
if (state == State::GameClear)
DrawString(254, 164, L"GAME CLEAR", GetColor(100, 255, 160));
else
DrawString(254, 164, L"GAME OVER", GetColor(255, 96, 112));
DrawFormatString(276, 216, GetColor(255, 255, 255), L"SCORE %02d", score);
DrawString(170, 282, L"ENTER : RETRY T : TITLE", GetColor(255, 230, 80));
}
void Game::DrawHud() const
{
int secondsLeft = static_cast<int>(std::ceil(timeLeft));
DrawFormatString(16, 16, GetColor(255, 255, 255), L"SCORE %02d / %02d", score, tuning::game::ClearScore);
DrawFormatString(16, 42, GetColor(255, 255, 255), L"HP %d", player.GetHp());
DrawFormatString(532, 16, GetColor(255, 255, 255), L"TIME %02d", secondsLeft);
}
}Source/Shooting/Game.h
Source/Shooting/Game.hcpp
#pragma once
#include "Shooting/ImageManager.h"
#include "Shooting/Input.h"
#include "Shooting/EnemyManager.h"
#include "Shooting/EnemyShotManager.h"
#include "Shooting/Player.h"
#include "Shooting/PlayerShotManager.h"
namespace dxgame::shooting
{
// ゲームに必要なオブジェクトを所有しStateごとの進行を取りまとめる
class Game
{
public:
// 必須画像を読み込んでゲームループを開始する
// 画像を読み込めない場合はループを開始せずfalseを返す
bool Run();
private:
// 今どの画面を表示しているかを表す
enum class State
{
Title,
Playing,
GameClear,
GameOver
};
// 現在のStateに応じて更新処理を分ける
void Update(float deltaTime);
// 現在のStateに応じて描画処理を分ける
void Draw() const;
// オブジェクトとスコアを初期化してプレイを開始する
void StartGame();
// 現在の画面をnextStateへ変更する
void ChangeState(State nextState);
// タイトルで決定入力を待つ
void UpdateTitle();
// プレイ中のオブジェクトと命中判定を更新する
void UpdatePlaying(float deltaTime);
// 結果画面で再開またはタイトルへの戻りを受け付ける
void UpdateResult();
// プレイヤー弾、敵弾、敵本体の重なりを判定する
void ResolveCollisions();
// タイトル画面を描く
void DrawTitle() const;
// プレイ画面とHUDを描く
void DrawPlaying() const;
// 結果画面を重ねて描く
void DrawResult() const;
// スコア、HP、残り時間を描く
void DrawHud() const;
ImageManager images;
Input input;
Player player;
PlayerShotManager playerShots;
EnemyManager enemies;
EnemyShotManager enemyShots;
State state = State::Title;
float timeLeft = 0.0f;
int score = 0;
};
}Source/Shooting/Input.cpp
Source/Shooting/Input.cppcpp
#include <DxLib.h>
#include "Shooting/Input.h"
namespace dxgame::shooting
{
namespace
{
using Action = Input::Action;
constexpr unsigned int ToBit(Action action)
{
return 1u << static_cast<unsigned int>(action);
}
struct Binding
{
Action action;
int key;
};
constexpr Binding Bindings[] =
{
{ Action::MoveLeft, KEY_INPUT_LEFT },
{ Action::MoveLeft, KEY_INPUT_A },
{ Action::MoveRight, KEY_INPUT_RIGHT },
{ Action::MoveRight, KEY_INPUT_D },
{ Action::MoveUp, KEY_INPUT_UP },
{ Action::MoveUp, KEY_INPUT_W },
{ Action::MoveDown, KEY_INPUT_DOWN },
{ Action::MoveDown, KEY_INPUT_S },
{ Action::Shoot, KEY_INPUT_Z },
{ Action::Shoot, KEY_INPUT_SPACE },
{ Action::Confirm, KEY_INPUT_RETURN },
{ Action::Back, KEY_INPUT_T },
{ Action::Exit, KEY_INPUT_ESCAPE }
};
}
void Input::Update()
{
previous = current;
current = 0;
for (const Binding& binding : Bindings)
{
if (CheckHitKey(binding.key))
current |= ToBit(binding.action);
}
}
bool Input::IsDown(Action action) const
{
return (current & ToBit(action)) != 0;
}
bool Input::IsPressed(Action action) const
{
unsigned int bit = ToBit(action);
return (current & bit) != 0 && (previous & bit) == 0;
}
}Source/Shooting/Input.h
Source/Shooting/Input.hcpp
#pragma once
namespace dxgame::shooting
{
// シューティングゲームに必要な入力の状況を返す
class Input
{
public:
// ゲーム内で扱う入力別のアクション
enum class Action
{
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
Shoot,
Confirm,
Back,
Exit
};
// 物理キーを調べて現在と前フレームの入力状態を更新する
void Update();
// 押されている間はtrueを返す
bool IsDown(Action action) const;
// 押されたフレームだけtrueを返す
bool IsPressed(Action action) const;
private:
unsigned int current = 0;
unsigned int previous = 0;
};
}Source/Shooting/Tuning/GameTuning.h
Source/Shooting/Tuning/GameTuning.hcpp
#pragma once
#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"
#include "Config/ApplicationConfig.h"
namespace dxgame::shooting::tuning
{
// ゲーム画面の横幅 単位はピクセル
// ゲーム画面の横幅(単位:ピクセル)
inline constexpr int ScreenWidth = 640;
// ゲーム画面の縦幅 単位はピクセル
// ゲーム画面の縦幅(単位:ピクセル)
inline constexpr int ScreenHeight = 480;
namespace game
{
inline constexpr int ClearScore = 30;
inline constexpr float TimeLimit = 60.0f;
}
// 座標、距離、Rectの各値はピクセル単位で指定する
// 時間は秒、速度は1秒あたりの移動量で指定する
// プレイヤーのpositionは中心座標を表す
// 各Rectはpositionからの相対範囲として指定する
namespace player
{
// ゲーム開始時のプレイヤー中心座標
inline constexpr Vector2 StartPosition = { 320.0f, 424.0f };
// プレイヤー中心を基準にした画像の表示範囲
inline constexpr Rect DrawRect = Rect::Centered(48.0f, 58.0f).MovedBy({ 0.0f, 3.0f });
inline constexpr Rect Bounds = Rect::Centered(28.0f, 28.0f);
// プレイヤー中心を基準にした命中範囲
inline constexpr Rect Hitbox = Rect::Centered(28.0f, 28.0f);
// プレイヤーの速度 単位はピクセル/秒
// プレイヤーの速度(単位:ピクセル/秒)
inline constexpr float MoveSpeed = 240.0f;
// 弾を発射してから次に発射できるまでの時間 ここでは8フレーム間隔の意味
// 弾を発射してから次に発射できるまでの時間(ここでは8フレーム間隔)
inline constexpr float ShotInterval = 8.0f * config::DeltaTime;
// 被弾した際の無敵時間 単位は秒
// 被弾した際の無敵時間(単位:秒)
inline constexpr float InvincibleTime = 1.5f;
// ゲーム開始時のHP
inline constexpr int MaxHp = 3;
}
namespace playerShot
{
// 弾の中心を基準にした画像の表示範囲
inline constexpr Rect DrawRect = Rect::Centered(10.0f, 28.0f);
// 弾の中心を基準にした命中範囲
inline constexpr Rect Hitbox = Rect::Centered(8.0f, 20.0f);
// 弾の速度 単位はピクセル/秒
// 弾の速度(単位:ピクセル/秒)
inline constexpr float Speed = 480.0f;
// 敵へ与えるダメージ
inline constexpr int Damage = 1;
}
namespace enemy
{
inline constexpr int LaneCount = 8;
inline constexpr float LaneStart = 64.0f;
inline constexpr float LaneSpacing = 72.0f;
inline constexpr Rect DrawRect = Rect::Centered(42.0f, 42.0f);
inline constexpr Rect Hitbox = Rect::Centered(32.0f, 32.0f);
inline constexpr float BaseSpeed = 120.0f;
inline constexpr float SpeedStep = 12.0f;
inline constexpr int SpeedLevels = 4;
inline constexpr float SpawnInterval = 0.60f;
inline constexpr float SpawnDelay = 0.40f;
inline constexpr float ShotDelay = 0.80f;
inline constexpr float ShotInterval = 1.80f;
inline constexpr float WaveAmplitude = 30.0f;
inline constexpr float WaveSpeed = 3.0f;
inline constexpr int WaveInterval = 3;
inline constexpr int MaxHp = 1;
}
namespace enemyShot
{
inline constexpr Rect DrawRect = Rect::Centered(14.0f, 14.0f);
inline constexpr Rect Hitbox = Rect::Centered(10.0f, 10.0f);
inline constexpr float Speed = 180.0f;
}
}