もどる

この記事は「うずら」とAIが協力して作成しました。
なるべく正確さを心がけていますが、最新の公式ドキュメントなどもあわせて確認してみてね!

python2026.3.9

3-6. 命中!当たり判定で敵を撃ち落とせ

実践:命中!
敵を撃破

こんにちは!世界最高に親切な技術講師うずらです。

前回の授業では、宇宙をさまよう敵キャラクターを出現させることに成功しましたね!

辞書魔法敵の襲来

でも、せっかく敵が出現しても、こちらの攻撃が当たらないとゲームになりません。
今回は、発射した弾が敵に「命中!」する瞬間をプログラミングでとらえる魔法を学びます。

Pythonのリスト操作と条件分岐を駆使して、宇宙の平和を取り戻しましょう!

1. 3秒でわかるまとめ

今回のゴールは、プレイヤーの弾が敵に当たったときに、その敵を画面から消す(撃破する)仕組みを作ることです。

これは「当たり判定」という、ゲーム開発において最も基本的なテクニックを使って実現します。

    ┏━━━━━┓
    ┃ 弾  ┃ <─ 当たり判定!
    ┃     ┃
    ┗━▲━━━┛
      └─ 「弾の座標」と「敵の範囲」を比較して、重なっているか確認する。

2. 当たり判定の基本:四角形の重なりを見つけよう

「当たり判定」とは、ゲームの中の2つの物体が接触したかどうかを判断する処理のことです。
今回は、シンプルに四角形(矩形)の当たり判定を考えます。

弾も敵も、画面上では四角い図形として描画されています。この2つの四角形が「重なっているか?」を調べることで、命中したかどうかを判定できるのです。

Pyxelでは、画面の左上が (0, 0) 座標で、右に行くほど x 座標が、下に行くほど y 座標が増えることを思い出してください。

魔法の絵筆描画関数

2つの四角形が重なっているかどうかは、それぞれの x 座標と y 座標、そして幅(w)と高さ(h)を使って判断できます。

たとえば、物体A (x1, y1, w1, h1) と物体B (x2, y2, w2, h2) があるとします。

この2つが重なっている条件は、次のように表せます。

x1 < x2 + w2  AND  x1 + w1 > x2  AND
y1 < y2 + h2  AND  y1 + h1 > y2

言葉で表現すると、少し複雑に感じるかもしれませんね。

補足だよ

この条件は、より簡単に言うと「どちらかがもう一方の左側、右側、上側、下側に完全に存在しない」ことを確認しているのと同じ意味です。完全に存在しない、つまり重なっていないパターンを除外していくと、残るのは「重なっている」パターンだけ、という考え方です。

check_collision 関数を覗いてみよう(今回は使いません)

今回のコードには、この四角形同士の当たり判定を行う check_collision 関数が追加されています。

# 矩形同士の当たり判定
def check_collision(x1, y1, w1, h1, x2, y2, w2, h2):
    return (x1 < x2 + w2 and x1 + w1 > x2 and
            y1 < y2 + h2 and y1 + h1 > y2)

この関数は、2つの矩形の座標とサイズを受け取り、重なっていれば True、重なっていなければ False を返します。

豆知識

多くのゲームでは、キャラクターやオブジェクトに「当たり判定用の四角形(ヒットボックス)」を設定し、この check_collision のような関数を使って当たり判定を行います。

今回は、弾が小さく、敵も四角形であるため、弾の「中心座標(もしくは左上座標)」が敵の四角形の中に含まれているかを判定する、よりシンプルな方法で当たり判定を行います。

3. 敵に弾が当たった!コードで実現する命中判定

それでは、いよいよ弾が敵に当たったときの処理を実装していきましょう。

前回のコードから、update 関数の中にある敵の更新処理 (enemies リストを処理する部分) に変更を加えます。

update 関数は毎フレーム、ゲームの状態を更新する場所でしたね。

もしもの魔法自機移動

変更点その1: 当たり判定の定数と spawn_enemy の微調整

まず、敵のサイズに関する定数 ENEMY_WENEMY_H を追加します。
今回のコードでは敵の幅と高さをそれぞれ 8 に設定しています。

# --- 定数設定 ---
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
GAME_TITLE = "ASTRO SURVIVOR"
STAR_COUNT = 40

# キャラクターのサイズ
PLAYER_W = 8
PLAYER_H = 8
PLAYER_SPEED = 3
ENEMY_W = 8 # ★追加★
ENEMY_H = 8 # ★追加★

変更点その2: update 関数内の当たり判定ロジック

update 関数の中の for e in enemies: のループに注目してください。
ここが今回追加する、当たり判定のメインの部分です。

    # 敵の更新
    surviving_enemies = []
    for e in enemies:
        e['y'] += e['vy']
            
        if e['y'] > SCREEN_HEIGHT:
            continue

        # ここから追加・変更される部分
        hit = False # 弾が当たったかどうかを記録するフラグ
        # 弾のリストを逆順に見ていく
        for b_idx in reversed(range(len(bullets))):
            bx, by = bullets[b_idx][0], bullets[b_idx][1] # 弾の座標を取得

            # 弾の座標が敵の四角形の中にあるか判定
            if (e['x'] < bx < e['x'] + ENEMY_W and
                e['y'] < by < e['y'] + ENEMY_H):
                
                # 当たっていたら、その弾をリストから削除
                del bullets[b_idx]
                hit = True # フラグをTrueにする
                break      # この敵に当たった弾が見つかったので、次の弾のチェックは不要
        
        # 弾が当たっていたら、この敵は削除対象なのでsurviving_enemiesには追加しない
        if hit:
            continue
        
        surviving_enemies.append(e) # 当たらなかった敵は生き残る
    enemies = surviving_enemies # 生き残った敵だけでリストを更新

新しいコードは、敵のループ (for e in enemies:) の中に、さらに弾のループ (for b_idx in reversed(range(len(bullets))):) を入れ子にしています。
これは「全ての敵に対して、全ての弾が当たったかどうかをチェックする」という処理を行っているためです。

当たり判定の条件式を詳しく見よう

特に重要なのは、この部分です。

if (e['x'] < bx < e['x'] + ENEMY_W and
    e['y'] < by < e['y'] + ENEMY_H):

これは、「弾の x 座標 (bx) が、敵の x 座標 (e['x']) より大きく、かつ、敵の右端 (e['x'] + ENEMY_W) より小さい」という条件と、「弾の y 座標 (by) が、敵の y 座標 (e['y']) より大きく、かつ、敵の下端 (e['y'] + ENEMY_H) より小さい」という条件を同時に満たすかどうかをチェックしています。

イメージとしては、敵の四角形の左上座標を (e['x'], e['y'])、右下座標を (e['x'] + ENEMY_W, e['y'] + ENEMY_H) としたときに、弾の左上座標 (bx, by) がその範囲内に完全に収まっているかを判定しています。

補足だよ

弾の描画サイズは幅2、高さ4ですが、ここでは弾の左上座標(1点)が敵の領域内に入ったかで判定しています。より厳密な四角形同士の当たり判定は check_collision のような関数を使うことが多いですが、まずはシンプルな点判定で動作を確認しましょう。

reversed()del の使い方

弾のリストを処理する部分で、for b_idx in reversed(range(len(bullets))):del bullets[b_idx] という記述が出てきました。

reversed() でリストを逆順に処理する理由は何ですか?

リストから要素を削除する際、前から順番に削除すると、リストの要素数が減り、インデックスがずれてしまいます。例えば、bullets の0番目を削除すると、元の1番目が新しい0番目になります。そのままループを続けると、処理がスキップされてしまったり、範囲外エラーが発生したりする可能性があります。
しかし、後ろから順番に削除していけば、それより前のインデックスがずれる心配がないため、安全に要素を削除できるのです。

del bullets[b_idx] は、指定したインデックス b_idx の要素をリスト bullets から削除するPythonの命令です。
弾が敵に当たったら、その弾は消えるべきなので、この命令を使ってリストから取り除きます。

breakcontinue でループを制御する

  • break:この文に出会うと、現在実行中の for ループ(または while ループ)を直ちに終了し、ループの次の行に処理を進めます。
    今回のコードでは、1つの敵に弾が当たったら、それ以上この敵に対して他の弾が当たるかチェックする必要はないので、break で弾のループを抜けています。

  • continue:この文に出会うと、ループの残りの処理をスキップし、次のループの繰り返しへと進みます。
    もし弾が当たった敵 (if hit:) があれば、その敵は撃破されるので、surviving_enemies リストには追加せず、次の敵の処理へと移っています。

豆知識

breakcontinue は、ループ処理を効率的に、かつ意図通りに制御するために非常に役立つ命令です。Pythonの基本的な制御フローなので、ぜひ使いこなしてください。

4. 完成!敵を撃ち落とすゲームを体験しよう

これで、弾が敵に当たると敵が消える(撃破される)仕組みが完成しました!

実際にコードを動かして、敵を撃ち落とす爽快感を味わってみましょう!

ファイル名を 3-6_collision.py として保存し、実行してみてください。

実際に動かしてみよう!

矢印キーで宇宙船を操作し、スペースキーで弾を発射できます。敵に弾を当てて撃破しましょう!

いかがでしたか? 弾が敵に命中し、敵が画面から消えるのを確認できたでしょうか。
目に見える形でPythonのコードがゲームを動かしているのを見ると、とてもワクワクしますね!

補足だよ

今回は敵が弾に当たるとすぐに消えてしまいますが、次回以降の章で爆発エフェクトを追加したり、敵のHP(体力)を実装したりして、よりリッチなゲーム体験にしていきます。

次回は、今度はプレイヤーが敵に当たってしまった場合の「被弾判定」と「ゲームオーバー」の処理を実装します。いよいよゲームらしくなってきますね!お楽しみに!

実践:激突!ゲームオーバー

最後まで読んでくれてありがとう!🌱
ノートみたいに、いつでも見返してね。