Skip to content
第04回 プレイヤーの弾と敵の命中判定

4 / 4 命中判定の動作確認 ​

このステップの目的

弾、敵、得点の変化を順に観察し、シューティングの基本的な流れが正しく動くことを確認します。

得点を画面へ表示する ​

得点を増やす処理とは別に、Game::Drawの末尾へ次の描画を追加します。DrawFormatStringは、書式内の%dに整数を埋め込んで表示する関数です。Lは、このプロジェクトの文字列形式に合わせるための印です。

Game.cpp(Drawの末尾へ追加)cpp
enemies.Draw(images);
DrawFormatString(16, 16, GetColor(255, 255, 255), L"SCORE %02d", score);

画面左上の得点が、敵を倒すたびに1増えることを確認します。弾が当たっても敵が生きている場合は増えません。

動作確認の手順 ​

実装できたら、Visual Studioでビルドして実行し、次の項目を順に確認してください。

  1. 射撃と敵の接近: 画面上端から敵が出現して下降し、自機の弾が敵へ向かって前進することを確認します。
  2. 命中と消滅: 弾が敵の命中範囲に入った瞬間、弾と敵の双方が即座に画面から消滅することを確認します。
  3. 得点の加算: 敵を1体撃破するたびに、画面上のスコアが1ずつ正しく増えることを確認します。
  4. 貫通の防止: 敵が密集している場所へ弾を撃ち込んだ際、1発の通常弾で倒せる敵は1体のみであり、背後の敵へ貫通しないことを確認します。
  5. 画面外での挙動: 命中しなかった弾や、撃破されなかった敵が画面端を越えた際、正常に自動消滅(Deactivate())することを確認します。

もし弾が敵をすり抜けてしまう場合は、Rect::Overlaps()の判定式や、GetHitbox()で現在位置positionが加算されているかを確認してください。

チェックリスト
  • プレイヤーの弾が敵に命中した瞬間に双方が消滅することを確認できる
  • 敵を撃破したときのみ得点が1加算されることを確認できる
  • 1発の弾が複数の敵へ貫通せず、1体のみに命中することを確認できる
  • Gameクラスが仲介役として弾と敵の命中判定を調停していることを説明できる
  • 判定に表示矩形ではなく命中矩形(GetHitbox())が使われていることを説明できる
  • 第02・03回までの表示、移動、Managerの責務を変更していない

まとめと次回への引き継ぎ ​

第04回では、弾と敵の命中判定を実装しました。敵を撃破してスコアを得るという、ゲームの中心となる遊びができました。

第05回では、敵がプレイヤーを狙って撃つ敵弾の発射と移動を実装します。今回の管理方法と命中判定の仕組みを使い、敵弾用のクラスと管理クラスを追加して、ゲームの緊張感を高めます。

参考実装 ​

今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。

Source/DxGame/Rect.h
Source/DxGame/Rect.hcpp
#pragma once

#include "DxGame/Vector2.h"

namespace dxgame
{
    // 左上座標と縦横の幅で矩形を表す
    // 表示サイズや命中判定サイズを指定するために使用する
    struct Rect
    {
        float x = 0.0f;
        float y = 0.0f;
        float width = 0.0f;
        float height = 0.0f;

        // 原点が矩形の中央になるRectを返す
        static constexpr Rect Centered(float width, float height)
        {
            return { -width * 0.5f, -height * 0.5f, width, height };
        }

        // offsetだけ移動した矩形を返す 大きさは変わらない
        // offsetだけ移動した矩形を返す(大きさは不変)
        constexpr Rect MovedBy(const Vector2& offset) const
        {
            return { x + offset.x, y + offset.y, width, height };
        }

        // 矩形の中心座標を返す
        constexpr Vector2 GetCenter() const
        {
            return { x + width * 0.5f, y + height * 0.5f };
        }

        // otherと面積を持って重なっていればtrueを返す
        // 辺が触れているだけの状態は重なりとして扱わない
        constexpr bool Overlaps(const Rect& other) const
        {
            return
                x < other.x + other.width && other.x < x + width &&
                y < other.y + other.height && other.y < y + height;
        }
    };
}
Source/Shooting/Enemy.cpp
Source/Shooting/Enemy.cppcpp
#include <cmath>

#include "Shooting/ImageManager.h"
#include "Shooting/Tuning/GameTuning.h"

#include "Shooting/Enemy.h"

namespace dxgame::shooting
{
    void Enemy::Spawn(const Vector2& startPosition, float speed, Behaviour spawnBehaviour)
    {
        position = startPosition;
        baseX = startPosition.x;
        moveSpeed = speed;
        age = 0.0f;
        hp = tuning::enemy::MaxHp;
        behaviour = spawnBehaviour;
        isActive = true;
    }

    void Enemy::Update(float deltaTime)
    {
        if (!isActive)
            return;

        age += deltaTime;
        position.y += moveSpeed * deltaTime;

        if (behaviour == Behaviour::Wave)
        {
            float waveOffset = std::sin(age * tuning::enemy::WaveSpeed)
                * tuning::enemy::WaveAmplitude;
            position.x = baseX + waveOffset;
        }

        if (GetHitbox().y > tuning::ScreenHeight)
            Deactivate();
    }

    void Enemy::Draw(const ImageManager& images) const
    {
        if (!isActive)
            return;

        images.Draw(ImageManager::Id::Enemy, GetDrawRect());
    }

    void Enemy::TakeDamage(int damage)
    {
        hp -= damage;

        if (hp <= 0)
            Deactivate();
    }

    bool Enemy::IsActive() const
    {
        return isActive;
    }

    void Enemy::Deactivate()
    {
        isActive = false;
    }

    Rect Enemy::GetDrawRect() const
    {
        return tuning::enemy::DrawRect.MovedBy(position);
    }

    Rect Enemy::GetHitbox() const
    {
        return tuning::enemy::Hitbox.MovedBy(position);
    }
}
Source/Shooting/Enemy.h
Source/Shooting/Enemy.hcpp
#pragma once

#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"

namespace dxgame::shooting
{
    class ImageManager;

    // 敵一体の位置、移動、表示、実行状態を管理する
    // 敵1体の位置、移動、表示、実行状態を管理する
    class Enemy
    {
    public:
        // 下方向へ直進するか左右へ波打ちながら進むかを表す
        enum class Behaviour
        {
            Straight,
            Wave
        };

        // 中心座標、速度、移動方法を設定して敵を出現させる
        void Spawn(const Vector2& startPosition, float speed, Behaviour spawnBehaviour);

        // 敵を移動させ、画面の下へ出たら停止する
        void Update(float deltaTime);

        // 実行中の敵を画像で描く
        void Draw(const ImageManager& images) const;

        // HPからdamageを減らし0以下になったら停止する
        void TakeDamage(int damage);

        // 更新と描画の対象ならtrueを返す
        bool IsActive() const;

        // 敵を更新と描画の対象外にする
        void Deactivate();

        // 表示用Rectを現在位置へ移動して返す
        Rect GetDrawRect() const;

        // 現在位置から求めた命中判定用Rectを返す
        Rect GetHitbox() const;

    private:
        Vector2 position = {};
        float baseX = 0.0f;
        float moveSpeed = 0.0f;
        float age = 0.0f;
        int hp = 0;
        Behaviour behaviour = Behaviour::Straight;
        bool isActive = false;
    };
}
Source/Shooting/EnemyManager.cpp
Source/Shooting/EnemyManager.cppcpp
#include "Shooting/ImageManager.h"
#include "Shooting/Tuning/GameTuning.h"

#include "Shooting/EnemyManager.h"

namespace dxgame::shooting
{
    void EnemyManager::Reset()
    {
        for (Enemy& enemy : enemies)
            enemy.Deactivate();

        spawnTimer = tuning::enemy::SpawnDelay;
        spawnIndex = 0;
    }

    void EnemyManager::Update(float deltaTime)
    {
        for (Enemy& enemy : enemies)
            enemy.Update(deltaTime);

        spawnTimer -= deltaTime;

        if (spawnTimer <= 0.0f)
        {
            SpawnOne();
            spawnTimer = tuning::enemy::SpawnInterval;
        }
    }

    void EnemyManager::Draw(const ImageManager& images) const
    {
        for (const Enemy& enemy : enemies)
            enemy.Draw(images);
    }

    void EnemyManager::SpawnOne()
    {
        // spawnIndexから出現レーン、速度、移動方法を順番に決める
        for (Enemy& enemy : enemies)
        {
            if (enemy.IsActive())
                continue;

            int speedLevel = spawnIndex % tuning::enemy::SpeedLevels;
            float speed = tuning::enemy::BaseSpeed + speedLevel * tuning::enemy::SpeedStep;
            int lane = spawnIndex % tuning::enemy::LaneCount;
            Vector2 startPosition =
            {
                tuning::enemy::LaneStart + lane * tuning::enemy::LaneSpacing,
                -tuning::enemy::Hitbox.height * 0.5f
            };

            Enemy::Behaviour pattern =
                (spawnIndex + 1) % tuning::enemy::WaveInterval == 0
                    ? Enemy::Behaviour::Wave
                    : Enemy::Behaviour::Straight;

            enemy.Spawn(startPosition, speed, pattern);
            ++spawnIndex;

            return;
        }
    }
}
Source/Shooting/EnemyManager.h
Source/Shooting/EnemyManager.hcpp
#pragma once

#include <array>
#include <cstddef>

#include "Shooting/Enemy.h"

namespace dxgame::shooting
{
    // 敵の管理クラス
    class EnemyManager
    {
    public:
        // すべての敵を停止し、出現順と最初の出現待ち時間を初期化
        void Reset();

        // すべての敵を更新する 出現タイマーを進め、必要なら敵を生成する
        // すべての敵を更新する(出現タイマーを進め、必要に応じて敵を生成)
        void Update(float deltaTime);

        // アクティブなすべての敵を描画する
        void Draw(const class ImageManager& images) const;

    private:
        // Gameだけが命中判定とデバッグ表示のために内部配列を参照できる
        friend class Game;

        // 停止中の要素を再利用しゲーム中にはメモリを確保しない
        static constexpr std::size_t Capacity = 24;

        // Gameが命中判定で状態を変更できるようすべての敵への参照を返す
        std::array<Enemy, Capacity>& GetAll() { return enemies; }

        // Gameが状態を変更せず参照できるようすべての敵へのconst参照を返す
        const std::array<Enemy, Capacity>& GetAll() const { return enemies; }

        // 停止中の敵を1体選び出現順に応じた設定で生成する
        // 空きがない場合は何もしない
        void SpawnOne();

        std::array<Enemy, Capacity> enemies = {};
        float spawnTimer = 0.0f;
        int spawnIndex = 0;
    };
}
Source/Shooting/Game.cpp
Source/Shooting/Game.cppcpp
#include <DxLib.h>

#include <string>

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

        player.Reset();
        playerShots.Reset();
        enemies.Reset();
        score = 0;

        // 小数分の待ち時間を次フレームへ持ち越す
        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)
    {
        player.Update(input, deltaTime);
        playerShots.Update(deltaTime);
        enemies.Update(deltaTime);

        ResolveCollisions();

        player.TryShoot(input, playerShots);
    }

    void Game::Draw() const
    {
        // 描画順はプレイヤー、プレイヤーの弾、敵、敵の弾の順(存在するものだけ描画)
        player.Draw(images);
        playerShots.Draw(images);
        enemies.Draw(images);
        DrawFormatString(16, 16, GetColor(255, 255, 255), L"SCORE %02d", score);
    }

    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;
            }
        }
    }
}
Source/Shooting/Game.h
Source/Shooting/Game.hcpp
#pragma once

#include "Shooting/ImageManager.h"
#include "Shooting/Input.h"
#include "Shooting/EnemyManager.h"
#include "Shooting/Player.h"
#include "Shooting/PlayerShotManager.h"

namespace dxgame::shooting
{
    // ゲームに必要なオブジェクトを所有し1フレームの処理を取りまとめる
    class Game
    {
    public:
        // 必須画像を読み込んでゲームループを開始する
        // 画像を読み込めない場合はループを開始せずfalseを返す
        bool Run();

    private:
        // プレイヤーを1フレーム分更新する
        // deltaTimeは移動計算で1フレーム分として使う秒数
        void Update(float deltaTime);

        // プレイヤーを描く
        void Draw() const;

        // プレイヤー弾と敵の命中を判定する
        void ResolveCollisions();

        ImageManager images;
        Input input;
        Player player;
        PlayerShotManager playerShots;
        EnemyManager enemies;
        int score = 0;
    };
}
Source/Shooting/PlayerShot.cpp
Source/Shooting/PlayerShot.cppcpp
#include "Shooting/ImageManager.h"
#include "Shooting/Tuning/GameTuning.h"

#include "Shooting/PlayerShot.h"

namespace dxgame::shooting
{
    void PlayerShot::Spawn(const Vector2& startPosition)
    {
        position = startPosition;
        isActive = true;
    }

    void PlayerShot::Update(float deltaTime)
    {
        if (!isActive)
            return;

        position.y -= tuning::playerShot::Speed * deltaTime;

        // DrawRect全体が画面の上へ出た弾を再利用できる状態に戻す
        Rect drawRect = GetDrawRect();
        if (drawRect.y + drawRect.height < 0.0f)
           Deactivate();
    }

    void PlayerShot::Draw(const ImageManager& images) const
    {
        if (!isActive)
            return;

        Rect drawRect = GetDrawRect();
        images.Draw(ImageManager::Id::PlayerShot, drawRect);
    }

    bool PlayerShot::IsActive() const
    {
        return isActive;
    }

    void PlayerShot::Deactivate()
    {
        isActive = false;
    }

    Rect PlayerShot::GetDrawRect() const
    {
        return tuning::playerShot::DrawRect.MovedBy(position);
    }

    Rect PlayerShot::GetHitbox() const
    {
        return tuning::playerShot::Hitbox.MovedBy(position);
    }
}
Source/Shooting/PlayerShot.h
Source/Shooting/PlayerShot.hcpp
#pragma once

#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"

namespace dxgame::shooting
{
    class ImageManager;

    // プレイヤーが発射する1発の弾の位置と実行状態を管理する
    class PlayerShot
    {
    public:
        // 中心をstartPositionへ移動して弾を実行状態にする
        void Spawn(const Vector2& startPosition);

        // 弾を上方向へ移動し画面外へ出たら停止する
        // deltaTimeは1フレームの経過秒数
        void Update(float deltaTime);

        // 実行中の弾を画像で描く
        void Draw(const ImageManager& images) const;

        // 更新、描画、命中判定の対象ならtrueを返す
        bool IsActive() const;

        // 弾を更新、描画、命中判定の対象外にする
        void Deactivate();

        // 表示用Rectを現在位置へ移動して返す
        Rect GetDrawRect() const;

        // 現在位置から求めた命中判定用Rectを返す
        Rect GetHitbox() const;

    private:
        // 弾の中心座標
        Vector2 position = {};
        bool isActive = false;
    };
}
Source/Shooting/PlayerShotManager.h
Source/Shooting/PlayerShotManager.hcpp
#pragma once

#include <array>
#include <cstddef>

#include "Shooting/PlayerShot.h"

namespace dxgame::shooting
{
    class ImageManager;

    // プレイヤー弾を固定数所有し生成、更新、描画、再利用を管理する
    class PlayerShotManager
    {
    public:
        // 停止中の弾の中心をstartPositionに合わせて生成する
        // 空きがない場合は生成せずfalseを返す
        bool Spawn(const Vector2& startPosition);

        // 所有するすべてのプレイヤー弾を更新する
        // deltaTimeは1フレームの経過秒数
        void Update(float deltaTime);

        // 所有するすべてのプレイヤー弾を描く
        void Draw(const ImageManager& images) const;

        // 所有するすべてのプレイヤー弾を停止して空の状態へ戻す
        void Reset();

    private:
        // Gameだけが命中判定とデバッグ表示のために内部配列を参照できる
        friend class Game;

        // 停止中の要素を再利用しゲーム中にはメモリを確保しない
        static constexpr std::size_t Capacity = 48;

        // Gameが命中判定で状態を変更できるようすべての弾への参照を返す
        std::array<PlayerShot, Capacity>& GetAll() { return shots; }

        // Gameが状態を変更せず参照できるようすべての弾へのconst参照を返す
        const std::array<PlayerShot, Capacity>& GetAll() const { return shots; }

        std::array<PlayerShot, Capacity> shots = {};
    };
}
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;

    // 座標、距離、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 float MoveSpeed = 240.0f;

        // 弾を発射してから次に発射できるまでの時間 ここでは8フレーム間隔の意味
        // 弾を発射してから次に発射できるまでの時間(ここでは8フレーム間隔)
        inline constexpr float ShotInterval = 8.0f * config::DeltaTime;
    }

    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 WaveAmplitude = 30.0f;
        inline constexpr float WaveSpeed = 3.0f;
        inline constexpr int WaveInterval = 3;
        inline constexpr int MaxHp = 1;
    }
}