第11回 成果物の作成と検証
4 / 4 配布状態で起動確認する
このステップの目的
制作プロジェクトとは別の場所で、開始から結果まで確認します。
ZIPを別の場所へ展開する
配布フォルダーをZIPに圧縮し、別の新しいフォルダーへ展開してください。展開先のShootingGame2D.exeを起動します。読み込みに失敗したら、エラーに表示されたパスとAssetsの内容を比べます。
一連の動作を試す
| 確認場面 | 操作と期待する結果 |
|---|---|
| 起動 | タイトルが表示され、タイトルBGMが鳴る |
| 開始 | Enterでプレイが始まり、方向キーで移動できる |
| 射撃・撃破 | Zで弾と音が出る。敵を倒すと得点が増える |
| 被弾 | HPが減り、点滅中は続けてダメージを受けない |
| ゲームオーバー | HPが0になると結果が表示される |
| クリア | 目標得点の達成、または時間切れで結果が表示される |
| 再挑戦 | 結果画面でEnterを押すとHP・敵・弾・得点・時間が初期化される |
| タイトル・終了 | 結果画面のTでタイトルへ戻り、Escapeで終了する |
不具合があれば、再現する操作を記録してください。制作プロジェクトで修正し、再ビルドして配布候補を作り直します。次回は最終調整を行います。
チェックリスト
- 別の場所へ展開したZIPから起動できた。
- 画像・音声と、クリア・ゲームオーバーの両方を確認した。
- 再挑戦で前回の状態が残らないことを確認した。
参考実装
今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。
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 DrawOutline(const dxgame::Rect& rect, int color)
{
DrawBox(
static_cast<int>(rect.x),
static_cast<int>(rect.y),
static_cast<int>(rect.x + rect.width),
static_cast<int>(rect.y + rect.height),
color,
FALSE);
}
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;
namespace
{
using SoundId = SoundManager::Id;
using ImageId = ImageManager::Id;
}
bool Game::Run()
{
const ImageManager::LoadResult imageResult = images.LoadAll();
if (!imageResult.succeeded)
{
ShowResourceLoadError(L"画像", imageResult.failedPath);
return false;
}
const SoundManager::LoadResult soundResult = sounds.LoadAll();
if (!soundResult.succeeded)
{
ShowResourceLoadError(L"音声", soundResult.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)
{
if (input.IsPressed(Input::Action::Debug))
showDebug = !showDebug;
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
{
DrawBackground();
switch (state)
{
case State::Title:
DrawTitle();
break;
case State::Playing:
DrawPlaying();
break;
case State::GameClear:
case State::GameOver:
DrawPlaying();
DrawResult();
break;
}
if (showDebug)
DrawDebug();
}
void Game::StartGame()
{
// 前回の結果を残さず、全オブジェクトを開始状態へ戻す
player.Reset();
playerShots.Reset();
enemies.Reset();
enemyShots.Reset();
effects.Reset();
playTime = 0.0f;
timeLeft = tuning::game::TimeLimit;
score = 0;
ChangeState(State::Playing);
}
void Game::ChangeState(State nextState)
{
state = nextState;
// Stateに入ったときのBGMをここで切り替える
switch (state)
{
case State::Title:
sounds.PlayBgm(SoundId::TitleBgm);
break;
case State::Playing:
sounds.PlayBgm(SoundId::PlayingBgm);
break;
case State::GameClear:
sounds.StopBgm();
sounds.PlaySe(SoundId::Clear);
break;
case State::GameOver:
sounds.StopBgm();
sounds.PlaySe(SoundId::GameOver);
break;
}
}
void Game::UpdateTitle()
{
if (input.IsPressed(Input::Action::Confirm))
StartGame();
}
void Game::UpdatePlaying(float deltaTime)
{
playTime += deltaTime;
timeLeft -= deltaTime;
if (timeLeft < 0.0f)
timeLeft = 0.0f;
player.Update(input, deltaTime);
playerShots.Update(deltaTime);
enemies.Update(deltaTime);
enemyShots.Update(deltaTime);
effects.Update(deltaTime);
if (player.TryShoot(input, playerShots))
{
sounds.PlaySe(SoundId::Shot);
}
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();
effects.Spawn(enemy.GetHitbox().GetCenter());
if (!enemy.IsActive())
++score;
sounds.PlaySe(SoundId::Hit);
break;
}
}
for (EnemyShot& enemyShot : enemyShots.GetAll())
{
if (!enemyShot.IsActive())
continue;
if (!enemyShot.GetHitbox().Overlaps(player.GetHitbox()))
continue;
enemyShot.Deactivate();
effects.Spawn(enemyShot.GetHitbox().GetCenter());
if (player.TryTakeDamage())
sounds.PlaySe(SoundId::Damage);
}
for (Enemy& enemy : enemies.GetAll())
{
if (!enemy.IsActive())
continue;
if (!enemy.GetHitbox().Overlaps(player.GetHitbox()))
continue;
enemy.Deactivate();
effects.Spawn(enemy.GetHitbox().GetCenter());
if (player.TryTakeDamage())
sounds.PlaySe(SoundId::Damage);
}
}
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));
DrawString(264, 336, L"F1 : DEBUG", GetColor(210, 218, 232));
}
void Game::DrawPlaying() const
{
// 描画順はプレイヤー、プレイヤーの弾、敵、敵の弾とし、エフェクトを最後に重ねる
player.Draw(images);
playerShots.Draw(images);
enemies.Draw(images);
enemyShots.Draw(images);
effects.Draw();
DrawHud();
}
void Game::DrawBackground() const
{
// 背景を上下に2枚並べて切れ目なく繰り返す
int scroll = static_cast<int>(playTime * tuning::background::ScrollSpeed);
int scrollOffset = scroll % tuning::ScreenHeight;
Rect first =
{
0.0f,
static_cast<float>(scrollOffset - tuning::ScreenHeight),
static_cast<float>(tuning::ScreenWidth),
static_cast<float>(tuning::ScreenHeight)
};
Rect second =
{
0.0f,
static_cast<float>(scrollOffset),
static_cast<float>(tuning::ScreenWidth),
static_cast<float>(tuning::ScreenHeight)
};
images.Draw(ImageId::Background, first);
images.Draw(ImageId::Background, second);
}
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);
}
void Game::DrawDebug() const
{
if (state == State::Title)
return;
int drawColor = GetColor(80, 220, 255);
int hitColor = GetColor(255, 80, 100);
DrawOutline(player.GetDrawRect(), drawColor);
DrawOutline(player.GetHitbox(), hitColor);
for (const PlayerShot& playerShot : playerShots.GetAll())
{
if (!playerShot.IsActive())
continue;
DrawOutline(playerShot.GetDrawRect(), drawColor);
DrawOutline(playerShot.GetHitbox(), hitColor);
}
for (const EnemyShot& enemyShot : enemyShots.GetAll())
{
if (!enemyShot.IsActive())
continue;
DrawOutline(enemyShot.GetDrawRect(), drawColor);
DrawOutline(enemyShot.GetHitbox(), hitColor);
}
for (const Enemy& enemy : enemies.GetAll())
{
if (!enemy.IsActive())
continue;
DrawOutline(enemy.GetDrawRect(), drawColor);
DrawOutline(enemy.GetHitbox(), hitColor);
}
}
}Source/Shooting/Game.h
Source/Shooting/Game.hcpp
#pragma once
#include "Shooting/EffectManager.h"
#include "Shooting/SoundManager.h"
#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;
// 背景画像を上下に2枚並べてスクロール表示する
void DrawBackground() const;
// 結果画面を重ねて描く
void DrawResult() const;
// スコア、HP、残り時間を描く
void DrawHud() const;
// 表示範囲と命中範囲を重ねて描く
void DrawDebug() const;
ImageManager images;
SoundManager sounds;
Input input;
Player player;
PlayerShotManager playerShots;
EnemyManager enemies;
EnemyShotManager enemyShots;
EffectManager effects;
State state = State::Title;
float playTime = 0.0f;
float timeLeft = 0.0f;
int score = 0;
bool showDebug = false;
};
}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::Debug, KEY_INPUT_F1 },
{ 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,
Debug,
Exit
};
// 物理キーを調べて現在と前フレームの入力状態を更新する
void Update();
// 押されている間はtrueを返す
bool IsDown(Action action) const;
// 押されたフレームだけtrueを返す
bool IsPressed(Action action) const;
private:
unsigned int current = 0;
unsigned int previous = 0;
};
}