Skip to content

第06回 敵と衝突判定

前回: 第05回 弾を撃つ / 次回: 第07回 クラス分割入門

今回の授業内容

  • 敵を表す構造体を追加する
  • 固定配列で複数の敵を生成する
  • 弾と敵の矩形で衝突判定を行う
  • 敵撃破でスコアを増やす
  • プレイヤーと敵の衝突でHPを減らし、無敵時間で連続ダメージを防ぐ

今回の開始状態

第05回終了時点では、プレイヤーが弾を撃てます
今回の終了時点では、敵が一定間隔で出現し、弾で倒せるようにします

まだEnemyクラスは作りません
弾と同じように、まずはGame内の構造体と配列で実装します

変更するファイル

ファイル変更内容
Game.hEnemy構造体、敵配列、スコア、HP、無敵タイマー、フレーム数を追加する
Game.cpp敵生成、敵更新、衝突判定、無敵時間、UI表示を追加する
Collision.h第04回のIsOverlapping()を利用する

敵のデータを追加する

Game.hに敵を表す構造体と配列を追加します

cpp
struct Enemy
{
    Vec2 position;
    float speed;
    bool active;
};

static constexpr int maxEnemies = 24;
Enemy enemies[maxEnemies] = {};
int frame = 0;
int score = 0;
int playerHp = 3;
int playerInvincibleTimer = 0;

Enemyの形は第05回のBulletとほぼ同じです
「位置」「速度」「有効フラグ」という組み合わせは、画面を動き回るオブジェクトに共通するパターンだと考えると理解しやすくなります
frameは、敵を一定間隔で出現させるために使います

敵を出現させる

Update()の先頭でframeを増やし、36フレームごとに敵を出します

cpp
++frame;

if (frame % 36 == 0)
    SpawnEnemy();

SpawnEnemy()では、出現するx座標をランダムに決め、空いている敵を探して初期位置を入れます

cpp
void Game::SpawnEnemy()
{
    const int lane = GetRand(7);
    const float x = 64.0f + lane * 72.0f;

    for (Enemy& enemy : enemies)
    {
        if (!enemy.active)
        {
            enemy.position = { x, -24.0f };
            enemy.speed = 2.0f;
            enemy.active = true;
            return;
        }
    }
}

GetRand(7)は0から7までの整数をランダムに返すDXライブラリの関数です
laneを7段階の枠として扱い、64.0f + lane * 72.0fでx座標へ変換することで、敵が画面内の決まった位置(レーン)のどれかに出現するようにしています
出現位置をランダムにする一方で、速度は2.0fで固定します
速度を敵ごとに変える調整は、第12回の独自要素として扱います

敵の更新では、下方向へ移動し、画面外へ出たら無効化します
この処理は、第05回の弾の更新と同じ「有効なものだけ動かし、範囲外に出たら無効化する」という形です

cpp
for (Enemy& enemy : enemies)
{
    if (!enemy.active)
        continue;

    enemy.position.y += enemy.speed;

    if (enemy.position.y > screenHeight + 40.0f)
        enemy.active = false;
}

弾と敵の衝突を調べる

弾と敵の矩形を作り、IsOverlapping()で重なりを調べます
この時点ではクラスの関数ではなく、ループ内で矩形を作ります

cpp
Rect enemyRect = { enemy.position.x - 16.0f, enemy.position.y - 16.0f, 32.0f, 32.0f };
Rect bulletRect = { bullet.position.x - 4.0f, bullet.position.y - 10.0f, 8.0f, 20.0f };

衝突したら、敵と弾を無効化し、スコアを増やします

cpp
if (IsOverlapping(enemyRect, bulletRect))
{
    enemy.active = false;
    bullet.active = false;
    ++score;
    break;
}

この判定は「敵1体につき、弾の配列を先頭から調べる」という二重ループになります
衝突が見つかったらbreakで弾のループだけを抜けるのは、1体の敵が同じフレーム内で複数の弾に同時に当たり、スコアが2回以上増えてしまうことを防ぐためです
breakが抜けている場合と、enemy.active = falseにした直後もループを続けた場合とで、スコアの増え方がどう変わるかを試して確認しておくと、このbreakの役割が実感しやすくなります

プレイヤーと敵の衝突とHP

プレイヤーと敵の衝突では、敵を消してHPを減らします

cpp
Rect playerRect = { playerPosition.x - 14.0f, playerPosition.y - 14.0f, 28.0f, 28.0f };

if (enemy.active && IsOverlapping(enemyRect, playerRect))
{
    enemy.active = false;
    --playerHp;
}

この形のまま実装すると、プレイヤーと敵が重なり続けている間、毎フレームHPが減り続けます
敵1体との接触なら1フレームで敵が消えるため大きな問題にはなりませんが、複数の敵が同時に重なった場合や、後の回で敵の当たり判定を大きくした場合に、HPが一瞬で0になる恐れがあります
これを防ぐため、ダメージを受けた直後は一定時間ダメージを受けない「無敵時間」を導入します

cpp
if (enemy.active && IsOverlapping(enemyRect, playerRect) && playerInvincibleTimer <= 0)
{
    enemy.active = false;
    --playerHp;
    playerInvincibleTimer = 90;
}

if (playerInvincibleTimer > 0)
    --playerInvincibleTimer;

playerInvincibleTimerは「あと何フレーム無敵か」を表すカウンターです
90フレーム(60フレーム換算で1.5秒)は、プレイヤーが被弾に気づいて距離を取るのに十分な長さとして設定しています
無敵時間の間はダメージを受けませんが、見た目が変わらないと受講者はダメージを受けたことに気づきにくくなります
そこで、無敵時間中は数フレームごとに表示・非表示を切り替える点滅処理をDraw()の先頭に追加します

cpp
const bool blinking = playerInvincibleTimer > 0 && (playerInvincibleTimer / 4) % 2 == 0;

Draw()では、これまでプレイヤーの三角形を無条件に描いていた箇所を、blinkingfalseのときだけ描くように変更します

cpp
if (!blinking)
{
    DrawTriangle(
        static_cast<int>(playerPosition.x),
        static_cast<int>(playerPosition.y - 18.0f),
        static_cast<int>(playerPosition.x - 16.0f),
        static_cast<int>(playerPosition.y + 14.0f),
        static_cast<int>(playerPosition.x + 16.0f),
        static_cast<int>(playerPosition.y + 14.0f),
        cyan,
        TRUE
    );
}

playerInvincibleTimer / 4で4フレームごとに値を切り替え、% 2 == 0で偶数区間だけ描画をスキップすることで、点滅させています
弾と敵の描画処理は変更しません
HPが0になった後のゲームオーバー処理は、第09回で状態として整理します

動作確認

  1. 敵が上から出現して下へ移動することを確認する
  2. 弾が敵に当たると敵と弾が消えることを確認する
  3. スコアが1増えることを確認する
  4. プレイヤーと敵が当たるとHPが減り、プレイヤーが一定時間点滅することを確認する
  5. 点滅している間は敵に触れてもHPが減らないことを確認する
  6. frame % 36の値を変えて、出現間隔の違いを比較する

よくある問題

  • 敵が出ない場合は、frameを毎フレーム増やしているか確認する
  • スコアが一度に複数増える場合は、衝突後にbreakしているか確認する
  • 消えた敵にさらに当たる場合は、activeの確認位置を見直す
  • 無敵時間中もHPが減り続ける場合は、衝突判定の条件にplayerInvincibleTimer <= 0を含めているか確認する
  • 点滅がずっと止まらない場合は、playerInvincibleTimerを毎フレーム減らしているか確認する

今回の完成条件

  • 敵を固定配列で複数管理できる
  • 一定間隔で敵を出現させられる
  • 弾と敵の衝突でスコアを増やせる
  • プレイヤーと敵の衝突でHPを減らせる
  • 無敵時間が必要な理由と、点滅で状態を伝える意図を説明できる

まとめ

今回は、敵、衝突、スコア、HP、無敵時間を追加しました
次回は、長くなってきた処理をPlayerBulletEnemyのクラスへ分けます