Skip to content
第03回 敵の出現と移動

4 / 4 敵のゲームループ組み込みと動作確認 ​

このステップの目的

EnemyManagerをゲーム全体の更新・描画ループへ接続し、自機、弾、敵が同時に正しく動作することを確認します。

Gameクラスへの接続と描画順序 ​

完成したEnemyManagerをGameクラスのメンバとして組み込み、ゲームループの更新と描画へ追加します。

Game.cppcpp
void Game::Update(float deltaTime)
{
    player.Update(input, deltaTime);
    playerShots.Update(deltaTime);
    enemies.Update(deltaTime);

    player.TryShoot(input, playerShots);
}

void Game::Draw() const
{
    // 描画順はプレイヤー、プレイヤーの弾、敵、敵の弾の順(存在するものだけ描画)
    player.Draw(images);
    playerShots.Draw(images);
    enemies.Draw(images);
}

重ね合わせの描画順序 ​

オブジェクトの描画順序は、画面の手前と奥の見た目を左右します。
本教材では「プレイヤー機体 → プレイヤー弾 → 敵」の順に描画を行っています。これにより、弾が自機の前から発射され、敵の手前を弾が通過する、自然な前後関係の画面が構成されます。

動作確認の手順 ​

ビルドして実行し、以下の項目を順に確認します。

  1. 敵の自動出現: 画面上端から一定間隔(0.6秒ごと)で敵が次々と現れることを確認します。
  2. 出現レーンの分散: 敵が常に同じ場所から出るのではなく、横8つのレーンから均等に散らばって出現することを確認します。
  3. 2種類の挙動の共存: 直進する敵に混ざって、4体に1体の割合で左右に波打ちながら進む敵が出現することを確認します。
  4. 自機と弾の非干渉: 敵が出現している間も、プレイヤーの移動操作や弾の連射が問題なく動作することを確認します。
  5. 画面外での安全な消滅: 画面最下部まで到達した敵が正常に消去され、24体の上限に達して出現が止まってしまわないことを確認します。

この段階では、プレイヤーの弾が敵をすり抜けます。当たり判定が未実装のため、正常な動作です。

チェックリスト
  • 一定間隔で画面上端から敵が出現することを確認できる
  • 敵が各レーンから順に出現し、画面下部へ進むことを確認できる
  • 直進する敵と左右に揺れる敵が混ざって出現することを確認できる
  • 敵が出現しても、プレイヤーの移動や弾の発射が正常に動作することを確認できる
  • GameクラスがEnemyManagerを介して敵全体の更新と描画を委譲していることを説明できる

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

第03回では、敵1体の動作を扱うEnemyと、出現や配列を管理するEnemyManagerを実装しました。これで、ゲーム画面に動く標的が現れます。

第04回では、プレイヤーの弾と敵の命中判定を実装します。弾と敵の衝突を調べ、敵を撃破してスコアを得るという、シューティングの中心となる流れを完成させます。

参考実装 ​

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

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

    };
}
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;
        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 (position.y > tuning::ScreenHeight)
            Deactivate();
    }

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

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

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

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

    Rect Enemy::GetDrawRect() const
    {
        return tuning::enemy::DrawRect.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;

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

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

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

    private:
        Vector2 position = {};
        float baseX = 0.0f;
        float moveSpeed = 0.0f;
        float age = 0.0f;
        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,
                0.0f
            };

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

        // 停止中の敵を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/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();

        // 小数分の待ち時間を次フレームへ持ち越す
        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);

        player.TryShoot(input, playerShots);
    }

    void Game::Draw() const
    {
        // 描画順はプレイヤー、プレイヤーの弾、敵、敵の弾の順(存在するものだけ描画)
        player.Draw(images);
        playerShots.Draw(images);
        enemies.Draw(images);
    }
}
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;

        ImageManager images;
        Input input;
        Player player;
        PlayerShotManager playerShots;
        EnemyManager enemies;
    };
}
Source/Shooting/ImageManager.cpp
Source/Shooting/ImageManager.cppcpp
#include <array>
#include <cstddef>

#include <DxLib.h>

#include "Shooting/ImageManager.h"

namespace dxgame::shooting
{
    namespace
    {
        using ImageId = ImageManager::Id;

        struct ImageEntry
        {
            ImageId id;
            const wchar_t* path;
        };

        constexpr std::array<ImageEntry, static_cast<std::size_t>(ImageId::Count)> ImageTable =
        {
            ImageEntry { ImageId::Player, L"Assets/Images/Player.png" },
            ImageEntry { ImageId::PlayerShot, L"Assets/Images/Bullet.png" },
            ImageEntry { ImageId::Enemy, L"Assets/Images/Enemy.png" }
        };

        std::size_t ToIndex(ImageId id)
        {
            return static_cast<std::size_t>(id);
        }
    }

    ImageManager::ImageManager()
    {
        handles.fill(-1);
    }

    ImageManager::~ImageManager()
    {
        for (int handle : handles)
        {
            if (handle != -1)
                DeleteGraph(handle);
        }
    }

    ImageManager::LoadResult ImageManager::LoadAll()
    {
        for (const ImageEntry& entry : ImageTable)
        {
            std::size_t index = ToIndex(entry.id);
            handles[index] = LoadGraph(entry.path);

            if (handles[index] == -1)
                return { false, entry.path };
        }

        return { true, {} };
    }

    void ImageManager::Draw(Id id, const Rect& rect) const
    {
        int handle = handles[ToIndex(id)];
        DrawExtendGraph(
            static_cast<int>(rect.x),
            static_cast<int>(rect.y),
            static_cast<int>(rect.x + rect.width),
            static_cast<int>(rect.y + rect.height),
            handle,
            TRUE);
    }
}
Source/Shooting/ImageManager.h
Source/Shooting/ImageManager.hcpp
#pragma once

#include <array>
#include <cstddef>
#include <string>

#include "DxGame/Rect.h"

namespace dxgame::shooting
{
    // 画像の読み込み、ハンドルの所有、拡大縮小描画、解放を管理する
    class ImageManager
    {
    public:
        // ゲーム内で使う画像を種類ごとに識別する
        enum class Id
        {
            Player,
            PlayerShot,
            Enemy,
            Count
        };

        // 全ハンドルを未取得の状態で初期化する
        ImageManager();

        // 取得済みの画像ハンドルをすべて解放する
        ~ImageManager();

        // ハンドルを二重に解放しないようコピー構築を禁止する
        ImageManager(const ImageManager&) = delete;

        // ハンドルを二重に解放しないようコピー代入を禁止する
        ImageManager& operator=(const ImageManager&) = delete;

        // 画像の読み込み結果と、失敗した場合の相対パスを表す
        struct LoadResult
        {
            bool succeeded = true;
            std::wstring failedPath;
        };

        // 対応表にある画像をすべて読み込む
        // 1つでも読み込めない場合は、その画像の相対パスとともに失敗を返す
        LoadResult LoadAll();

        // idの画像をrectの大きさへ拡大縮小して描く
        void Draw(Id id, const Rect& rect) const;

    private:
        std::array<int, static_cast<std::size_t>(Id::Count)> handles = {};
    };
}
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;

    private:
        // 弾の中心座標
        Vector2 position = {};
        bool isActive = false;
    };
}
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 float Speed = 480.0f;
    }

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