Skip to content
第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;
    };
}