見出し画像

Step105 【Pythonで縦スクロールシューティングを作る#4】:当たり判定の実装

これまでの記事では、

  • 32×32サイズの自機キャラクター

  • 縦方向スクロール型のシューティング

  • 自機の移動とショット

  • 簡単な敵キャラクターの出現と移動

までを実装してきました。

現在の状態では、敵は出現して動くものの、当たっても何も起こらない という段階です。

今回はさらに進めて、シューティングゲームに欠かせない「当たり判定」 を実装していきましょう!


1. 今回のゴール

今回の記事では、以下の2つの当たり判定を扱います。

  • 自機弾 vs 敵

  • 敵 vs 自機

これが実装できると、

  • 弾が敵に当たると敵が消える

  • 敵に触れるとゲームオーバーになる

という感じになります(*^^*)


2. 当たり判定の考え方

今回使用する考え方として、Rect(長方形)同士の当たり判定 を考えてみます。

Pygameでは、画像の位置や大きさをRect(レクト)という四角形で管理できます。この際、

rect1.colliderect(rect2)

たったこの1行で、

「2つの四角形が重なっているかどうか」

を True / False で判定できるので、これを利用しましょう!
思ったより簡単そうでしょ?(*‘ω‘ *)

複雑な計算は不要なので、初心者の方でも安心して扱える方法 となります。


3. 完成コード

まずは、次のコードをそのまま書いて実行してください。
前回のソースコードがある場合には、違っているところの修正だけでもOKです(*^^*)

import pygame
import sys
import os
import random

# ----------------------------
# 初期設定
# ----------------------------
pygame.init()

SCREEN_WIDTH = 480
SCREEN_HEIGHT = 640
FPS = 60

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Shooting Game")

clock = pygame.time.Clock()

# フォント(ゲームオーバー表示用)
font = pygame.font.SysFont(None, 64)
small_font = pygame.font.SysFont(None, 28)

# ----------------------------
# パス設定
# ----------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
IMAGE_DIR = os.path.join(BASE_DIR, "assets", "images")

# ----------------------------
# 画像読み込み
# ----------------------------
player_image = pygame.image.load(
    os.path.join(IMAGE_DIR, "player.png")
).convert_alpha()

bullet_image = pygame.image.load(
    os.path.join(IMAGE_DIR, "bullet.png")
).convert_alpha()

enemy_image = pygame.image.load(
    os.path.join(IMAGE_DIR, "enemy.png")
).convert_alpha()

# 記事構成に合わせて自機は 32x32 を想定
PLAYER_SIZE = 64
BULLET_SIZE = 24
ENEMY_SIZE = 64

player_image = pygame.transform.scale(player_image, (PLAYER_SIZE, PLAYER_SIZE))
bullet_image = pygame.transform.scale(bullet_image, (BULLET_SIZE, BULLET_SIZE))
enemy_image = pygame.transform.scale(enemy_image, (ENEMY_SIZE, ENEMY_SIZE))

# ----------------------------
# 自機設定
# ----------------------------
PLAYER_SPEED = 5

def reset_game():
    """ゲームを初期状態に戻す(リスタート用)"""
    player_rect = pygame.Rect(
        SCREEN_WIDTH // 2 - PLAYER_SIZE // 2,
        SCREEN_HEIGHT - PLAYER_SIZE - 20,
        PLAYER_SIZE,
        PLAYER_SIZE
    )

    bullets = []
    enemies = []

    last_shot_time = 0
    last_enemy_spawn_time = 0

    game_over = False
    return player_rect, bullets, enemies, last_shot_time, last_enemy_spawn_time, game_over


player_rect, bullets, enemies, last_shot_time, last_enemy_spawn_time, game_over = reset_game()

# ----------------------------
# 弾設定
# ----------------------------
BULLET_SPEED = 8
SHOT_INTERVAL = 200  # ミリ秒

# ----------------------------
# 敵設定
# ----------------------------
ENEMY_SPEED = 2
ENEMY_SPAWN_INTERVAL = 1000  # ミリ秒

# ----------------------------
# メインループ
# ----------------------------
while True:
    current_time = pygame.time.get_ticks()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        # ゲームオーバー時のリスタート
        if event.type == pygame.KEYDOWN:
            if game_over and event.key == pygame.K_r:
                player_rect, bullets, enemies, last_shot_time, last_enemy_spawn_time, game_over = reset_game()

    keys = pygame.key.get_pressed()

    # ----------------------------
    # 更新処理(ゲームオーバー中は止める)
    # ----------------------------
    if not game_over:
        # 自機移動
        if keys[pygame.K_LEFT]:
            player_rect.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT]:
            player_rect.x += PLAYER_SPEED
        if keys[pygame.K_UP]:
            player_rect.y -= PLAYER_SPEED
        if keys[pygame.K_DOWN]:
            player_rect.y += PLAYER_SPEED

        player_rect.left = max(player_rect.left, 0)
        player_rect.right = min(player_rect.right, SCREEN_WIDTH)
        player_rect.top = max(player_rect.top, 0)
        player_rect.bottom = min(player_rect.bottom, SCREEN_HEIGHT)

        # ショット処理
        if keys[pygame.K_SPACE]:
            if current_time - last_shot_time >= SHOT_INTERVAL:
                bullet_rect = pygame.Rect(
                    player_rect.centerx - BULLET_SIZE // 2,
                    player_rect.top,
                    BULLET_SIZE,
                    BULLET_SIZE
                )
                bullets.append(bullet_rect)
                last_shot_time = current_time

        for bullet in bullets[:]:
            bullet.y -= BULLET_SPEED
            if bullet.bottom < 0:
                bullets.remove(bullet)

        # 敵の出現
        if current_time - last_enemy_spawn_time >= ENEMY_SPAWN_INTERVAL:
            enemy_x = random.randint(0, SCREEN_WIDTH - ENEMY_SIZE)
            enemy_rect = pygame.Rect(enemy_x, -ENEMY_SIZE, ENEMY_SIZE, ENEMY_SIZE)
            enemies.append(enemy_rect)
            last_enemy_spawn_time = current_time

        # 敵の移動
        for enemy in enemies[:]:
            enemy.y += ENEMY_SPEED
            if enemy.top > SCREEN_HEIGHT:
                enemies.remove(enemy)

        # 当たり判定①:自機弾 vs 敵
        for bullet in bullets[:]:
            for enemy in enemies[:]:
                if bullet.colliderect(enemy):
                    bullets.remove(bullet)
                    enemies.remove(enemy)
                    break

        # 当たり判定②:敵 vs 自機
        for enemy in enemies:
            if enemy.colliderect(player_rect):
                game_over = True
                break

    # 描画
    screen.fill((0, 0, 0))
    screen.blit(player_image, player_rect)

    for bullet in bullets:
        screen.blit(bullet_image, bullet)

    for enemy in enemies:
        screen.blit(enemy_image, enemy)

    if game_over:
        text = font.render("GAME OVER", True, (255, 255, 255))
        sub = small_font.render("Press R to Restart", True, (200, 200, 200))
        screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, SCREEN_HEIGHT // 2 - 60))
        screen.blit(sub, (SCREEN_WIDTH // 2 - sub.get_width() // 2, SCREEN_HEIGHT // 2 + 10))

    pygame.display.update()
    clock.tick(FPS)

4. コード解説① ➤Rect を使った当たり判定

弾・敵・自機はすべて Rect で管理しています。

if bullet.colliderect(enemy):

この1行で、

  • 弾と敵が重なっているか?

を判定できます。


5. コード解説② ➤自機弾 vs 敵

bullets.remove(bullet)
enemies.remove(enemy)

当たった瞬間に、弾と敵をそれぞれリストから削除しています。

[:] を使って コピーしたリストをループ している点も、
初心者の方には重要なポイントです。


6. コード解説③ ➤敵 vs 自機

if enemy.colliderect(player_rect):
    game_over = True

敵が自機に触れた瞬間、ゲームオーバー状態に切り替えています。


7. 今回のまとめ

今回の記事では、

  • Rect を使った当たり判定の基本

  • 自機弾と敵の衝突処理

  • 敵と自機の衝突処理

  • ゲームオーバー状態の導入

を学習しました。


8. 次回予告

次回は、

  • スコア表示

  • 敵を倒した数を数える

  • 数字が増えていく楽しさ

を実装していきます。
少しずつ完成に近づけていきましょう。

次の記事


いいなと思ったら応援しよう!

DOI@日々学びの本質を考え続ける探求者 よろしければ応援お願いします! いただいたチップは引き続きプログラミングや学びについて、皆さんの利益になるようなよい記事を書くことで恩返しをさせていただきます!