Skip to content
第06回 プレイヤーの被弾と無敵時間

4 / 4 被弾処理の動作確認 ​

このステップの目的

HPの減少、被弾直後の点滅、無敵時間による連続ダメージ防止を観察し、次回のゲーム状態遷移へつなげます。

動作確認の手順 ​

プロジェクトをビルドして実行し、以下の項目を順に確認します。

  1. 敵弾による被弾と点滅: 飛来する敵弾にわざと機体を接触させ、敵弾が消滅すると同時に機体が高速で点滅を始めることを確認します。
  2. 無敵時間中の保護: 点滅している間(1.5秒間)に別の敵弾や敵本体に触れても、追加のダメージを受けないことを確認します。また、触れた敵弾はその場で正しく消滅することを確認します。
  3. 無敵時間の解除: 点滅が終了した後は通常の状態に戻り、再び敵弾に当たると正しくダメージを受けることを確認します。
  4. 敵本体との激突: 敵キャラクターそのものに機体を衝突させた際も、敵が消滅し、機体が点滅してダメージを受けることを確認します。
  5. HPが0になった時の挙動: 3回被弾してHPが0になった後も、現時点ではゲームオーバー画面への遷移を実装していないため、ゲームはそのまま継続します。これは次回のテーマである状態遷移で完成させます。
チェックリスト
  • 敵弾や敵本体に接触した際にHPが正しく減少することを確認できる
  • 被弾直後に機体が点滅し、無敵状態になることを確認できる
  • 無敵時間中に連続してダメージを受けないことを確認できる
  • Player::TryTakeDamage()が被弾成立時のみtrueを返す仕組みを説明できる
  • 接触した敵弾が即座にDeactivate()され、多重判定が防がれていることを説明できる

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

第06回では、HP管理、無敵時間、敵弾と敵本体の接触判定を実装しました。敵の攻撃を避け、被弾に耐えるという駆け引きができました。

第07回では、ゲームの状態管理と終了条件を実装して、一つの作品に仕上げます。タイトルからプレイ画面への切り替え、HPが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();
        enemyShots.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);
        enemyShots.Update(deltaTime);

        ResolveCollisions();

        Vector2 playerPosition = player.GetHitbox().GetCenter();
        for (Enemy& enemy : enemies.GetAll())
        {
            enemy.TryShoot(enemyShots, playerPosition);
        }

        player.TryShoot(input, playerShots);
    }

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

        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();
        }
    }
}
Source/Shooting/Player.cpp
Source/Shooting/Player.cppcpp
#include <algorithm>
#include <cmath>

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

#include "Shooting/Player.h"

namespace dxgame::shooting
{
    using Action = Input::Action;

    void Player::Reset()
    {
        position = tuning::player::StartPosition;
        shotTimer = 0.0f;
        invincibleTimer = 0.0f;
        hp = tuning::player::MaxHp;
    }

    void Player::Update(const Input& input, float deltaTime)
    {
        Vector2 direction;

        if (input.IsDown(Action::MoveLeft))
            direction.x -= 1.0f;
        if (input.IsDown(Action::MoveRight))
            direction.x += 1.0f;
        if (input.IsDown(Action::MoveUp))
            direction.y -= 1.0f;
        if (input.IsDown(Action::MoveDown))
            direction.y += 1.0f;

        // 斜め入力を長さ1に収める
        // 方向と長さの計算は第05回で詳しく扱う
        float length = std::sqrt(direction.x * direction.x + direction.y * direction.y);

        if (length > 1.0f)
        {
            direction.x /= length;
            direction.y /= length;
        }

        position.x += direction.x * tuning::player::MoveSpeed * deltaTime;
        position.y += direction.y * tuning::player::MoveSpeed * deltaTime;

        // 指定の範囲全体が画面内に残るよう位置を制限する
        const Rect& bounds = tuning::player::Bounds;

        float minX = -bounds.x;
        float minY = -bounds.y;
        float maxX = tuning::ScreenWidth - bounds.x - bounds.width;
        float maxY = tuning::ScreenHeight - bounds.y - bounds.height;
        position.x = std::clamp(position.x, minX, maxX);
        position.y = std::clamp(position.y, minY, maxY);

        shotTimer = std::max(0.0f, shotTimer - deltaTime);
        invincibleTimer = std::max(0.0f, invincibleTimer - deltaTime);
    }

    void Player::Draw(const ImageManager& images) const
    {
        constexpr float BlinkRate = 20.0f;
        bool blinkOff =
            invincibleTimer > 0.0f &&
            static_cast<int>(invincibleTimer * BlinkRate) % 2 == 0;

        if (blinkOff)
            return;

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

    bool Player::TryShoot(const Input& input, PlayerShotManager& shots)
    {
        if (!input.IsDown(Action::Shoot) || shotTimer > 0.0f)
            return false;

        if (!shots.Spawn(GetMuzzlePosition()))
            return false;

        shotTimer = tuning::player::ShotInterval;
        return true;
    }

Vector2 Player::GetMuzzlePosition() const
{
    return
    {
        position.x,
        position.y - 32.0f
    };
}

    Rect Player::GetDrawRect() const
    {
        return tuning::player::DrawRect.MovedBy(position);
    }

    Rect Player::GetHitbox() const
    {
        return tuning::player::Hitbox.MovedBy(position);
    }

    bool Player::TryTakeDamage()
    {
        if (invincibleTimer > 0.0f)
            return false;

        --hp;
        invincibleTimer = tuning::player::InvincibleTime;
        return true;
    }

    bool Player::IsAlive() const
    {
        return hp > 0;
    }

    int Player::GetHp() const
    {
        return hp;
    }
}
Source/Shooting/Player.h
Source/Shooting/Player.hcpp
#pragma once

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

namespace dxgame::shooting
{
    class Input;
    class ImageManager;
    class PlayerShotManager;

    // プレイヤーの中心座標、移動、発射間隔を管理する
    class Player
    {
    public:
        // プレイヤーを開始位置へ戻す
        void Reset();

        // 入力から移動方向を求めプレイヤーを画面内で移動させる
        // deltaTimeは移動計算で1フレーム分として使う秒数
        void Update(const Input& input, float deltaTime);

        // プレイヤー画像を表示用Rectに合わせて描く
        void Draw(const ImageManager& images) const;

        // 入力と発射間隔を確認し、弾を生成できたらtrueを返す
        bool TryShoot(const Input& input, PlayerShotManager& shots);

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

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

        // 被弾可能ならHPを1減らして無敵時間を始める
        // 無敵時間中は状態を変更せずfalseを返す
        bool TryTakeDamage();

        // HPが1以上ならtrueを返す
        bool IsAlive() const;

        // 現在のHPを返す
        int GetHp() const;

    private:
        // 発射する弾の中心座標を返す
        Vector2 GetMuzzlePosition() const;
        // プレイヤーの中心座標
        Vector2 position = {};
        float shotTimer = 0.0f;
        float invincibleTimer = 0.0f;
        int hp = 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;

    // 座標、距離、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;
    }
}