Python

【Python】シューティングゲームを作ってみた(初心者プログラミング)

takahide

おはようございます。タカヒデです。

本日はPythonで「シューティングゲーム」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • まずは簡単なゲームを作って興味を持ってみたい

ぜひ実際にコードを打ちながら作成してみてください。

Contents
  1. 「シューティングゲーム」完成イメージ
  2. STEP1:Pygameの準備とウィンドウ表示
  3. STEP2:プレイヤー機体を表示して左右に動かす
  4. STEP3:弾を撃てるようにする
  5. STEP4:敵キャラとスコアを追加して「ゲーム」にする
  6. STEP5:コード全体を整理して読みやすくする(リファクタリング)
  7. 完成

「シューティングゲーム」完成イメージ

まずは、「シューティングゲーム」完成後の最終的なコードと完成イメージです。

import pygame
import random

# ===== 基本設定 =====
WIDTH, HEIGHT = 800, 600
FPS = 60
PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()
font = pygame.font.SysFont("meiryo", 28)

# ===== プレイヤー =====
class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self, keys):
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.x += PLAYER_SPEED
        self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))
        if self.cooldown > 0:
            self.cooldown -= 1

    def can_shoot(self):
        return self.cooldown == 0 and self.alive

    def shoot(self, bullets):
        bullets.append(Bullet(self.x, self.rect().top))
        self.cooldown = SHOOT_COOLDOWN_FRAMES

    def draw(self, surface):
        top = (self.x, self.y - self.h//2)
        left = (self.x - self.w//2, self.y + self.h//2)
        right = (self.x + self.w//2, self.y + self.h//2)
        pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
        pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

# ===== 弾 =====
class Bullet:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.r = 4
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.r), int(self.y - self.r), self.r*2, self.r*2)

    def update(self):
        self.y += BULLET_SPEED
        if self.y < -10:
            self.alive = False

    def draw(self, surface):
        pygame.draw.circle(surface, (255, 255, 120), (int(self.x), int(self.y)), self.r)

# ===== 敵 =====
class Enemy:
    def __init__(self):
        self.w, self.h = 28, 20
        self.x = random.randint(40, WIDTH - 40)
        self.y = -30
        self.speed = random.uniform(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX)
        self.alive = True
        base = random.randint(130, 220)
        self.color = (base, 60, 80)

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self):
        self.y += self.speed
        if self.y > HEIGHT + 40:
            self.alive = False

    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect(), border_radius=4)
        pygame.draw.rect(surface, (240, 160, 170), self.rect(), 2, border_radius=4)

# ===== メインゲーム =====
def main():
    player = Player()
    bullets, enemies = [], []
    score, frame = 0, 0
    game_over = False
    running = True

    while running:
        clock.tick(FPS)
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False
            if e.type == pygame.KEYDOWN and game_over and e.key == pygame.K_RETURN:
                # リスタート(最小限の初期化)
                player = Player()
                bullets.clear()
                enemies.clear()
                score, frame, game_over = 0, 0, False

        keys = pygame.key.get_pressed()

        if not game_over:
            player.update(keys)
            if keys[pygame.K_SPACE] and player.can_shoot():
                player.shoot(bullets)

            for b in bullets:
                b.update()
            bullets = [b for b in bullets if b.alive]

            frame += 1
            if frame % SPAWN_EVERY_FRAMES == 0:
                enemies.append(Enemy())

            for en in enemies:
                en.update()
            enemies = [en for en in enemies if en.alive]

            # 衝突判定(弾→敵)
            for b in bullets:
                for en in enemies:
                    if b.alive and en.alive and b.rect().colliderect(en.rect()):
                        b.alive = False
                        en.alive = False
                        score += 10

            bullets = [b for b in bullets if b.alive]
            enemies = [en for en in enemies if en.alive]

            # 衝突判定(敵→自機)
            for en in enemies:
                if en.rect().colliderect(player.rect()):
                    player.alive = False
                    game_over = True
                    break

        # 描画(背景は単色)
        screen.fill((15, 18, 25))
        for b in bullets:
            b.draw(screen)
        for en in enemies:
            en.draw(screen)
        if player.alive:
            player.draw(screen)

        # スコアと案内
        screen.blit(font.render(f"Score: {score}", True, (230, 230, 240)), (12, 10))
        if game_over:
            msg1 = pygame.font.SysFont(None, 64).render("GAME OVER", True, (255, 220, 220))
            msg2 = font.render("Press Enter to Restart", True, (210, 210, 220))
            screen.blit(msg1, msg1.get_rect(center=(WIDTH//2, HEIGHT//2 - 16)))
            screen.blit(msg2, msg2.get_rect(center=(WIDTH//2, HEIGHT//2 + 28)))

        pygame.display.flip()

    pygame.quit()

if __name__ == "__main__":
    main()

こんなゲームが作れます。

STEP1:Pygameの準備とウィンドウ表示

まずはゲームの土台となる「ウィンドウ」と「メインループ」を用意します。ここができれば、以降はこの枠の中にゲーム要素を足していくだけです。

#Pygameを使うための準備
import pygame

#画面サイズとフレームレート
WIDTH, HEIGHT = 800, 600  # 幅と高さ
FPS = 60                  # 1秒あたりの更新回数

#Pygameの初期化とウィンドウ作成
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()

#テキスト描画用のフォント
font = pygame.font.SysFont("meiryo", 28)

#メインループ
def main():
    running = True
    while running:
        clock.tick(FPS)  # 更新速度を一定に保つ

        # イベント処理:ウィンドウの×ボタンなど
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False

        # 背景を塗りつぶす
        screen.fill((15, 18, 25))

        # 画面中央付近に案内テキストを描く
        msg = font.render("ウィンドウが表示されれば準備完了です", True, (220, 220, 230))
        rect = msg.get_rect(center=(WIDTH // 2, HEIGHT // 2))
        screen.blit(msg, rect)

        # 画面を更新
        pygame.display.flip()

    pygame.quit()

#このファイルを実行したときだけmain()を起動
if __name__ == "__main__":
    main()

手順①:Pygameの初期化とウィンドウ作成

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()

「pygame.init()」はPygame全体の準備を一括で行う関数です。
「display.set_mode」はウィンドウを作る関数で、幅と高さを指定します。
「set_caption」はタイトルバーに表示される文字列を設定します。
「Clock」は「1秒に何回画面を更新するか」を調整します。これにより、動作が速すぎたり遅すぎたりするのを防ぎます。

手順②:定数で基本設定をまとめる

WIDTH, HEIGHT = 800, 600
FPS = 60

「定数」とは、途中で変えない前提の値をまとめておく書き方です。
画面サイズやフレームレートをここに置くことで、後で調整が必要になったときに見つけやすくしています。

手順③:メインループで「入力→更新→描画」を回す

while running:
    clock.tick(FPS)
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            running = False
    screen.fill((15, 18, 25))
    ...
    pygame.display.flip()

ゲームは毎フレーム「入力を読む→状態を更新する→画面に描く」を繰り返して動きます。
ここではまず入力として「ウィンドウを閉じる」操作だけを処理し、背景を塗って、最後に「flip」で画面を切り替えています。
「clock.tick(FPS)」でフレームレートを固定し、PC性能による速度差を抑えます。

手順④:テスト用の文字を描く

font = pygame.font.SysFont(None, 28)
msg = font.render("ウィンドウが表示されれば準備完了です", True, (220, 220, 230))
rect = msg.get_rect(center=(WIDTH // 2, HEIGHT // 2))
screen.blit(msg, rect)

「font」は文字の見た目を作る仕組みです。「render」で文字列から画像を作り、「blit」で画面に貼り付けます。
「get_rect(center=…)」を使うと、画像の位置合わせが簡単になります。ここでは中央に表示しています。

STEP1が完了した時点では以下のアプリができています。

STEP2:プレイヤー機体を表示して左右に動かす

次は、画面下に自分の機体(プレイヤー)を表示し、左右キーで動かせるようにします。
まだ弾は撃ちませんが、「動くものがある画面」になることで一気にゲームらしくなります。

import pygame  # 2Dゲームや描画を簡単に行えるライブラリ
import random  # ★STEP2追加:ランダムな数値を作る標準モジュール

#画面サイズとフレームレート
WIDTH, HEIGHT = 800, 600  # 幅と高さ(ピクセル)
FPS = 60                  # 1秒あたりの更新回数

# ★STEP2追加:ゲームの基本パラメータ
PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

#Pygameの初期化とウィンドウ作成
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()

#テキスト描画用のフォント(案内表示に使用)
font = pygame.font.SysFont("meiryo", 28)

# ★STEP2追加:プレイヤー(自機)を表すクラス
class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self, keys):
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.x += PLAYER_SPEED
        # 画面外に出ないように左右の位置を制限
        self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))
        if self.cooldown > 0:
            self.cooldown -= 1

    def can_shoot(self):
        return self.cooldown == 0 and self.alive

    def shoot(self, bullets):
        # Bulletクラスは後のSTEPで定義します
        bullets.append(Bullet(self.x, self.rect().top))
        self.cooldown = SHOOT_COOLDOWN_FRAMES

    def draw(self, surface):
        top = (self.x, self.y - self.h//2)
        left = (self.x - self.w//2, self.y + self.h//2)
        right = (self.x + self.w//2, self.y + self.h//2)
        # 本体
        pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
        # 外枠
        pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

# ★STEP2変更:プレイヤーを動かすメインループ
def main():
    player = Player()  # ★STEP2追加:プレイヤーのインスタンスを作成
    running = True
    while running:
        clock.tick(FPS)  # 更新速度を一定に保つ

        # イベント処理:ウィンドウの×ボタンなど
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False

        # ★STEP2追加:キーボード入力の状態をまとめて取得
        keys = pygame.key.get_pressed()

        # ★STEP2追加:プレイヤーの位置を更新
        player.update(keys)

        # 背景を塗りつぶす
        screen.fill((15, 18, 25))

        # ★STEP2追加:プレイヤーを描画
        player.draw(screen)

        # ★STEP2変更:画面下部に案内テキストを描く
        msg = font.render("←→ または A/D で移動できます", True, (220, 220, 230))
        screen.blit(msg, (20, HEIGHT - 40))

        # 画面を更新
        pygame.display.flip()

    pygame.quit()

#このファイルを実行したときだけmain()を起動
if __name__ == "__main__":
    main()

手順①:ゲーム用のパラメータをまとめて定義する

PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

ここではゲーム全体で使う「数値の設定」をひとまとめにしています。
このような値を「パラメータ」や「定数」と呼びます。途中で何度も出てくる数字を上のほうに集めておくと、難易度調整やスピード調整をしたいときに探しやすくなります。

このSTEPでは実際に使っているのは「PLAYER_SPEED」だけですが、後のSTEPで弾や敵を追加するときに、ここで定義した値をそのまま使います。

手順②:Playerクラスで自機の情報をまとめる

class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

「クラス」は、そのゲーム内の「もの」の情報と動きをまとめる仕組みです。ここではプレイヤーを表す「Player」というクラスを作っています。
「init」は「コンストラクタ」と呼ばれ、Playerが作られた瞬間に一度だけ呼ばれる初期設定用の関数です。ここで大きさや初期位置などを決めています。

プレイヤーの位置は「x」と「y」で表します。xが左右方向、yが上下方向です。画面の中央下あたりに自機が配置されるようにしています。

手順③:左右キーでプレイヤーを動かす

def update(self, keys):
    if keys[pygame.K_LEFT] or keys[pygame.K_a]:
        self.x -= PLAYER_SPEED
    if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
        self.x += PLAYER_SPEED
    self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))

「update」は毎フレーム呼び出される「状態を更新する」ための関数です。ここではキー入力に応じて左右に移動しています。
「keys」は「pygame.key.get_pressed()」で取得した、現在押されているキーです。
左キーまたはAキーが押されていれば左へ、右キーまたはDキーが押されていれば右へ少しずつ移動します。

最後の1行では「max」と「min」を使って、プレイヤーが画面の外へ出ないように制限しています。

  • 「min(WIDTH – self.w//2, self.x)」で右端の限界
  • 「max(self.w//2, …)」で左端の限界

手順④:自機の当たり判定の作成

def rect(self):
    return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

「rect」は、プレイヤーを囲む四角形(当たり判定用の領域)を作るための関数です。
Pygameの「Rect」は「左上の座標+幅と高さ」で四角形を表現します。
この四角形は後のSTEPで「弾や敵とぶつかったかどうか」を判断するときに使います。

手順⑤:自機を画面に描画する

def draw(self, surface):
    top = (self.x, self.y - self.h//2)
    left = (self.x - self.w//2, self.y + self.h//2)
    right = (self.x + self.w//2, self.y + self.h//2)
    pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
    pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

「draw」は自機を画面に描くための関数です。ここでは三角形を組み合わせて、シンプルな機体を表現しています。
「polygon」は多角形を描く関数で、3点の座標を渡して三角形を描いています。その上に少し明るい色で枠線を描くことで、見やすくしています。

手順⑥:メインループでプレイヤーを動かして描く

def main():
    player = Player()
    running = True
    while running:
        clock.tick(FPS)
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False

        keys = pygame.key.get_pressed()
        player.update(keys)

        screen.fill((15, 18, 25))
        player.draw(screen)

        msg = font.render("←→ または A/D で移動できます", True, (220, 220, 230))
        screen.blit(msg, (20, HEIGHT - 40))

        pygame.display.flip()

ここではSTEP1で作ったメインループに、プレイヤーの生成・更新・描画を追加しています。
流れとしては、

  • イベント処理
  • キー入力の取得
  • プレイヤーの更新
  • 背景の描画
  • プレイヤーの描画
  • 画面更新

という順番になっています。

このSTEP2までで、「自機が画面下に表示され、左右キーで動かせる」状態になっています。

STEP3:弾を撃てるようにする

次は、スペースキーを押すと弾が発射されるようにします。画面下の自機から上方向に弾が飛んでいくことで、一気に「シューティングゲーム」らしくなります。

import pygame  # 2Dゲームや描画を簡単に行えるライブラリ
import random  # ★STEP2追加:ランダムな数値を作る標準モジュール

#画面サイズとフレームレート
WIDTH, HEIGHT = 800, 600  # 幅と高さ(ピクセル)
FPS = 60                  # 1秒あたりの更新回数

#ゲームの基本パラメータ
PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

#Pygameの初期化とウィンドウ作成
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()

#テキスト描画用のフォント(案内表示に使用)
font = pygame.font.SysFont("meiryo", 28)

#プレイヤー(自機)を表すクラス
class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self, keys):
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.x += PLAYER_SPEED
        # 画面外に出ないように左右の位置を制限
        self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))
        if self.cooldown > 0:
            self.cooldown -= 1

    def can_shoot(self):
        return self.cooldown == 0 and self.alive

    def shoot(self, bullets):
        bullets.append(Bullet(self.x, self.rect().top))
        self.cooldown = SHOOT_COOLDOWN_FRAMES

    def draw(self, surface):
        top = (self.x, self.y - self.h//2)
        left = (self.x - self.w//2, self.y + self.h//2)
        right = (self.x + self.w//2, self.y + self.h//2)
        # 本体
        pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
        # 外枠
        pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

# ★STEP3追加:弾(プレイヤーのショット)を表すクラス
class Bullet:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.r = 4
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.r), int(self.y - self.r), self.r*2, self.r*2)

    def update(self):
        self.y += BULLET_SPEED
        if self.y < -10:
            self.alive = False

    def draw(self, surface):
        pygame.draw.circle(surface, (255, 255, 120), (int(self.x), int(self.y)), self.r)

# ★STEP3変更:プレイヤーと弾を動かすメインループ
def main():
    player = Player()
    bullets = []  # ★STEP3追加:発射された弾を入れておくリスト
    running = True
    while running:
        clock.tick(FPS)  # 更新速度を一定に保つ

        # イベント処理:ウィンドウの×ボタンなど
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False

        # キーボード入力の状態をまとめて取得
        keys = pygame.key.get_pressed()

        # プレイヤーの位置を更新
        player.update(keys)

        # ★STEP3追加:スペースキーでショット
        if keys[pygame.K_SPACE] and player.can_shoot():
            player.shoot(bullets)

        # ★STEP3追加:弾の位置を更新
        for b in bullets:
            b.update()
        bullets = [b for b in bullets if b.alive]  # 画面外に出た弾を片付ける

        # 背景を塗りつぶす
        screen.fill((15, 18, 25))

        # ★STEP3追加:弾を描画
        for b in bullets:
            b.draw(screen)

        # プレイヤーを描画
        player.draw(screen)

        # 画面下部に案内テキストを描く
        msg = font.render("←→ / A・D で移動、Space で弾を発射", True, (220, 220, 230))
        screen.blit(msg, (20, HEIGHT - 40))

        # 画面を更新
        pygame.display.flip()

    pygame.quit()

#このファイルを実行したときだけmain()を起動
if __name__ == "__main__":
    main()

手順①:Bulletクラスで「弾1発」の情報をまとめる

class Bullet:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.r = 4
        self.alive = True

「Bulletクラス」は、画面上に存在する弾1発の情報を持つためのクラスです。
弾の位置は「x」「y」、大きさは「r」(半径)、まだ画面内に生きているかどうかを「alive」で管理します。

「弾1発」のクラスを作成しておくことで、リストでたくさんの弾を扱うときに管理しやすくなります。

手順②:弾の当たり判定用の四角形を作る

def rect(self):
    return pygame.Rect(int(self.x - self.r), int(self.y - self.r), self.r*2, self.r*2)

Pygameの「Rect」は四角形を表すクラスで、後のSTEPで「敵に当たったか」を判定するときに使います。

手順③:弾を上方向へ移動させる

def update(self):
    self.y += BULLET_SPEED
    if self.y < -10:
        self.alive = False

「update」は1フレームごとに弾の状態を更新する関数です。
「BULLET_SPEED」はマイナスの値(-10)なので、y座標は毎フレーム上方向へ移動します。画面の上端より少し外まで行ったら「alive」をFalseにして、後でリストから消せるようにしています。

こうしておくことで、画面外に弾がたまり続けることを防止しています。

手順④:弾を画面に描く

def draw(self, surface):
    pygame.draw.circle(surface, (255, 255, 120), (int(self.x), int(self.y)), self.r)

「draw」は画面に弾を描く関数です。「circle」を使って小さな丸として描画しています。

このように、見た目の処理は「draw」、動きの処理は「update」と役割を分けています。

手順⑤:メインループで弾を管理する準備

player = Player()
bullets = []  # ★STEP3追加:発射された弾を入れておくリスト

メインループの冒頭で、弾を入れておくためのリストを用意しています。

弾は1発だけでなく、連続で発射される可能性があります。そのため、「弾のリスト」を持っておき、画面に存在する全ての弾をまとめて管理します。

手順⑥:スペースキーで弾を発射する

if keys[pygame.K_SPACE] and player.can_shoot():
    player.shoot(bullets)

「get_pressed」で取得したキー情報から、「スペースキーが押されているかどうか」を調べています。
「can_shoot」は「弾を撃てる状態かどうか」を判定する関数で、クールダウンを設定することで連射しすぎないようにしています。
条件を満たしていれば、「player.shoot(bullets)」で弾を1つ生成し、「bullets」リストに追加します。

手順⑦:弾を更新して、不要な弾を片付ける

for b in bullets:
    b.update()
bullets = [b for b in bullets if b.alive]

まず全ての弾に対して「update」を呼び出し、位置を進めます。
その後、「alive」がFalseになった弾(画面外に出た弾)をリストから取り除きます。

手順⑧:弾を描画する

for b in bullets:
    b.draw(screen)
player.draw(screen)

背景を塗ったあと、弾を先に描画し、その上にプレイヤーを描画しています。
描画の順番は「奥にあるもの → 手前にあるもの」の順に行うと分かりやすくなります。

STEP3までで、「自機を左右に動かしながらスペースキーで弾を撃つ」ことができるようになります。

STEP4:敵キャラとスコアを追加して「ゲーム」にする

ここでは、上から落ちてくる敵キャラを追加し、弾が当たったら消えてスコアが加算されるようにします。画面に目的が生まれ、シューティングとして遊べる形になります。

import pygame  # 2Dゲームや描画を簡単に行えるライブラリ
import random  # ★STEP2追加:ランダムな数値を作る標準モジュール

#画面サイズとフレームレート
WIDTH, HEIGHT = 800, 600  # 幅と高さ
FPS = 60                  # 1秒あたりの更新回数

#ゲームの基本パラメータ
PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

#Pygameの初期化とウィンドウ作成
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()

#テキスト描画用のフォント
font = pygame.font.SysFont("meiryo", 28)

#プレイヤー(自機)を表すクラス
class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self, keys):
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.x += PLAYER_SPEED
        # 画面外に出ないように左右の位置を制限
        self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))
        if self.cooldown > 0:
            self.cooldown -= 1

    def can_shoot(self):
        return self.cooldown == 0 and self.alive

    def shoot(self, bullets):
        bullets.append(Bullet(self.x, self.rect().top))
        self.cooldown = SHOOT_COOLDOWN_FRAMES

    def draw(self, surface):
        top = (self.x, self.y - self.h//2)
        left = (self.x - self.w//2, self.y + self.h//2)
        right = (self.x + self.w//2, self.y + self.h//2)
        # 本体
        pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
        # 外枠
        pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

#弾(プレイヤーのショット)を表すクラス
class Bullet:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.r = 4
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.r), int(self.y - self.r), self.r*2, self.r*2)

    def update(self):
        self.y += BULLET_SPEED
        if self.y < -10:
            self.alive = False

    def draw(self, surface):
        pygame.draw.circle(surface, (255, 255, 120), (int(self.x), int(self.y)), self.r)

# ★STEP4追加:敵キャラを表すクラス
class Enemy:
    def __init__(self):
        self.w, self.h = 28, 20
        self.x = random.randint(40, WIDTH - 40)  # 画面上部のランダムな位置に出現
        self.y = -30
        self.speed = random.uniform(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX)
        self.alive = True
        base = random.randint(130, 220)
        self.color = (base, 60, 80)

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self):
        self.y += self.speed
        if self.y > HEIGHT + 40:
            self.alive = False  # 画面外まで落ちたら削除対象

    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect(), border_radius=4)
        pygame.draw.rect(surface, (240, 160, 170), self.rect(), 2, border_radius=4)

# ★STEP4変更:プレイヤー・弾・敵・スコアを管理するメインループ
def main():
    player = Player()
    bullets, enemies = [], []        # ★STEP4変更:敵リストを追加
    score, frame = 0, 0              # ★STEP4追加:スコアとフレームカウンタ
    game_over = False                # ★STEP4追加:ゲームオーバー中かどうか
    running = True

    while running:
        clock.tick(FPS)

        # イベント処理:ウィンドウの×ボタンなど
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False
            # ★STEP4追加:ゲームオーバー中にEnterキーでリスタート
            if e.type == pygame.KEYDOWN and game_over and e.key == pygame.K_RETURN:
                player = Player()
                bullets.clear()
                enemies.clear()
                score, frame, game_over = 0, 0, False

        # キーボード入力の状態をまとめて取得
        keys = pygame.key.get_pressed()

        # ★STEP4追加:ゲームオーバーでない時の処理のインデント
        if not game_over:
            # プレイヤーの更新
            player.update(keys)

            # スペースキーでショット
            if keys[pygame.K_SPACE] and player.can_shoot():
                player.shoot(bullets)

            # 弾の更新
            for b in bullets:
                b.update()
            bullets = [b for b in bullets if b.alive]

            # ★STEP4追加:一定間隔で敵を出現させる
            frame += 1
            if frame % SPAWN_EVERY_FRAMES == 0:
                enemies.append(Enemy())

            # ★STEP4追加:敵の更新
            for en in enemies:
                en.update()
            enemies = [en for en in enemies if en.alive]

            # ★STEP4追加:衝突判定(弾→敵)
            for b in bullets:
                for en in enemies:
                    if b.alive and en.alive and b.rect().colliderect(en.rect()):
                        b.alive = False
                        en.alive = False
                        score += 10  # 敵を倒したらスコア加算

            bullets = [b for b in bullets if b.alive]
            enemies = [en for en in enemies if en.alive]

            # ★STEP4追加:衝突判定(敵→自機)
            for en in enemies:
                if en.rect().colliderect(player.rect()):
                    player.alive = False
                    game_over = True
                    break

        # 背景を塗りつぶす
        screen.fill((15, 18, 25))

        # 弾を描画
        for b in bullets:
            b.draw(screen)

        # ★STEP4追加:敵を描画
        for en in enemies:
            en.draw(screen)

        # プレイヤーを描画
        if player.alive:
            player.draw(screen)

        # ★STEP4追加:スコア表示
        screen.blit(font.render(f"Score: {score}", True, (230, 230, 240)), (12, 10))

        # ★STEP4追加:ゲームオーバー表示
        if game_over:
            msg1 = pygame.font.SysFont(None, 64).render("GAME OVER", True, (255, 220, 220))
            msg2 = font.render("Press Enter to Restart", True, (210, 210, 220))
            screen.blit(msg1, msg1.get_rect(center=(WIDTH//2, HEIGHT//2 - 16)))
            screen.blit(msg2, msg2.get_rect(center=(WIDTH//2, HEIGHT//2 + 28)))

        pygame.display.flip()

    pygame.quit()

#このファイルを実行したときだけmain()を起動
if __name__ == "__main__":
    main()

手順①:Enemyクラスで「敵1体」を定義する

class Enemy:
    def __init__(self):
        self.w, self.h = 28, 20
        self.x = random.randint(40, WIDTH - 40)
        self.y = -30
        self.speed = random.uniform(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX)
        self.alive = True
        base = random.randint(130, 220)
        self.color = (base, 60, 80)

敵キャラ1体分の情報を「Enemyクラス」としてまとめています。

  • 「w, h」…敵の幅と高さ
  • 「x, y」…位置(最初は画面の上の見えないところからスタート)
  • 「speed」…下方向に落ちる速さ
  • 「alive」…まだ画面に存在しているかどうか
  • 「color」…少しランダムに変化する色

「random.randint」や「random.uniform」はランダムな数を作る関数です。これによって、敵が毎回違う位置・スピードで出現し、単調にならないようにしています。

手順②:敵の動きと削除条件

def update(self):
    self.y += self.speed
    if self.y > HEIGHT + 40:
        self.alive = False

「update」は毎フレーム呼び出され、敵の位置を下方向へ進めます。
画面の下(HEIGHT)よりさらに少し下まで落ちたら、「alive」をFalseにして、あとでリストから消せるようにしています。
こうすることで、見えなくなった敵を放置せず、メモリや処理の無駄を減らせます。

手順③:敵を画面に描画する

def draw(self, surface):
    pygame.draw.rect(surface, self.color, self.rect(), border_radius=4)
    pygame.draw.rect(surface, (240, 160, 170), self.rect(), 2, border_radius=4)

敵は四角形(rect)として描いています。

手順④:メインループで敵を出現させる

bullets, enemies = [], []
score, frame = 0, 0
...
if not game_over:
    ...
    frame += 1
    if frame % SPAWN_EVERY_FRAMES == 0:
        enemies.append(Enemy())
  • 「enemies」リスト…画面上に存在する敵をまとめて管理
  • 「frame」…フレーム数(whileループが何回回ったか)

を使って、一定間隔で新しい敵を追加しています。
「SPAWN_EVERY_FRAMES」が小さいほど、敵の出現頻度が高くなり、ゲームが難しくなります。

手順⑤:弾と敵の衝突判定でスコアを加算する

for b in bullets:
    for en in enemies:
        if b.alive and en.alive and b.rect().colliderect(en.rect()):
            b.alive = False
            en.alive = False
            score += 10

2重ループで「すべての弾」と「すべての敵」の組み合わせをチェックしています。
「colliderect」はPygameの四角形同士の衝突判定関数で、「ぶつかったかどうか」を簡単に調べることができます。
当たっていたら、その弾と敵を両方「alive = False」にして、スコアを10点増やします。

手順⑥:敵と自機がぶつかったらゲームオーバーにする

for en in enemies:
    if en.rect().colliderect(player.rect()):
        player.alive = False
        game_over = True
        break

「全ての敵」について、自機の四角形と重なっているかを調べています。
1体でも当たればゲームオーバーなので、「game_over」をTrueにしてループを抜けています。
ゲームオーバー後は「if not game_over:」の中の更新処理が止まるため、プレイヤーや敵の動きが停止します。

手順⑦:スコアとゲームオーバー表示

screen.blit(font.render(f"Score: {score}", True, (230, 230, 240)), (12, 10))

if game_over:
    msg1 = pygame.font.SysFont(None, 64).render("GAME OVER", True, (255, 220, 220))
    msg2 = font.render("Press Enter to Restart", True, (210, 210, 220))
    screen.blit(msg1, msg1.get_rect(center=(WIDTH//2, HEIGHT//2 - 16)))
    screen.blit(msg2, msg2.get_rect(center=(WIDTH//2, HEIGHT//2 + 28)))

スコアは左上に表示しています。
ゲームオーバー時には、中央に「GAME OVER」と「Press Enter to Restart」という2行のメッセージを表示します。

手順⑧:Enterキーでゲームをリスタートする

if e.type == pygame.KEYDOWN and game_over and e.key == pygame.K_RETURN:
    player = Player()
    bullets.clear()
    enemies.clear()
    score, frame, game_over = 0, 0, False

ゲームオーバー中にEnterキーが押されたとき、プレイヤー・弾・敵・スコア・フレーム数を初期化して最初の状態に戻します。

このSTEP4までで、「敵を撃ってスコアを稼ぎ、当たるとゲームオーバー、Enterで再挑戦」という、一通り遊べる形のシューティングゲームが完成しています。

STEP5:コード全体を整理して読みやすくする(リファクタリング)

ここでは、ゲームの動きを変えずに「構造」と「見通し」を良くするためのリファクタリングを行います。

import pygame
import random

# ===== 基本設定 =====
WIDTH, HEIGHT = 800, 600
FPS = 60
PLAYER_SPEED = 6
BULLET_SPEED = -10
ENEMY_SPEED_MIN, ENEMY_SPEED_MAX = 2, 4
SPAWN_EVERY_FRAMES = 30
SHOOT_COOLDOWN_FRAMES = 8

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Simple Shooting - Minimal")
clock = pygame.time.Clock()
font = pygame.font.SysFont("meiryo", 28)

# ===== プレイヤー =====
class Player:
    def __init__(self):
        self.w, self.h = 36, 28
        self.x, self.y = WIDTH // 2, HEIGHT - 70
        self.cooldown = 0
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self, keys):
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.x -= PLAYER_SPEED
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.x += PLAYER_SPEED
        self.x = max(self.w//2, min(WIDTH - self.w//2, self.x))
        if self.cooldown > 0:
            self.cooldown -= 1

    def can_shoot(self):
        return self.cooldown == 0 and self.alive

    def shoot(self, bullets):
        bullets.append(Bullet(self.x, self.rect().top))
        self.cooldown = SHOOT_COOLDOWN_FRAMES

    def draw(self, surface):
        top = (self.x, self.y - self.h//2)
        left = (self.x - self.w//2, self.y + self.h//2)
        right = (self.x + self.w//2, self.y + self.h//2)
        pygame.draw.polygon(surface, (80, 180, 255), [left, right, top])
        pygame.draw.polygon(surface, (200, 230, 255), [left, right, top], 2)

# ===== 弾 =====
class Bullet:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.r = 4
        self.alive = True

    def rect(self):
        return pygame.Rect(int(self.x - self.r), int(self.y - self.r), self.r*2, self.r*2)

    def update(self):
        self.y += BULLET_SPEED
        if self.y < -10:
            self.alive = False

    def draw(self, surface):
        pygame.draw.circle(surface, (255, 255, 120), (int(self.x), int(self.y)), self.r)

# ===== 敵 =====
class Enemy:
    def __init__(self):
        self.w, self.h = 28, 20
        self.x = random.randint(40, WIDTH - 40)
        self.y = -30
        self.speed = random.uniform(ENEMY_SPEED_MIN, ENEMY_SPEED_MAX)
        self.alive = True
        base = random.randint(130, 220)
        self.color = (base, 60, 80)

    def rect(self):
        return pygame.Rect(int(self.x - self.w//2), int(self.y - self.h//2), self.w, self.h)

    def update(self):
        self.y += self.speed
        if self.y > HEIGHT + 40:
            self.alive = False

    def draw(self, surface):
        pygame.draw.rect(surface, self.color, self.rect(), border_radius=4)
        pygame.draw.rect(surface, (240, 160, 170), self.rect(), 2, border_radius=4)

# ===== メインゲーム =====
def main():
    player = Player()
    bullets, enemies = [], []
    score, frame = 0, 0
    game_over = False
    running = True

    while running:
        clock.tick(FPS)
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False
            if e.type == pygame.KEYDOWN and game_over and e.key == pygame.K_RETURN:
                player = Player()
                bullets.clear()
                enemies.clear()
                score, frame, game_over = 0, 0, False

        keys = pygame.key.get_pressed()

        if not game_over:
            player.update(keys)
            if keys[pygame.K_SPACE] and player.can_shoot():
                player.shoot(bullets)

            for b in bullets:
                b.update()
            bullets = [b for b in bullets if b.alive]

            frame += 1
            if frame % SPAWN_EVERY_FRAMES == 0:
                enemies.append(Enemy())

            for en in enemies:
                en.update()
            enemies = [en for en in enemies if en.alive]

            # 衝突判定(弾→敵)
            for b in bullets:
                for en in enemies:
                    if b.alive and en.alive and b.rect().colliderect(en.rect()):
                        b.alive = False
                        en.alive = False
                        score += 10

            bullets = [b for b in bullets if b.alive]
            enemies = [en for en in enemies if en.alive]

            # 衝突判定(敵→自機)
            for en in enemies:
                if en.rect().colliderect(player.rect()):
                    player.alive = False
                    game_over = True
                    break

        # 描画
        screen.fill((15, 18, 25))
        for b in bullets:
            b.draw(screen)
        for en in enemies:
            en.draw(screen)
        if player.alive:
            player.draw(screen)

        # スコア表示
        screen.blit(font.render(f"Score: {score}", True, (230, 230, 240)), (12, 10))

        # ゲームオーバー表示
        if game_over:
            msg1 = pygame.font.SysFont(None, 64).render("GAME OVER", True, (255, 220, 220))
            msg2 = font.render("Press Enter to Restart", True, (210, 210, 220))
            screen.blit(msg1, msg1.get_rect(center=(WIDTH//2, HEIGHT//2 - 16)))
            screen.blit(msg2, msg2.get_rect(center=(WIDTH//2, HEIGHT//2 + 28)))

        pygame.display.flip()

    pygame.quit()

if __name__ == "__main__":
    main()

手順①:大きな役割単位でコメントを修正

# ===== 基本設定 =====
# ...
# ===== プレイヤー =====
# ...
# ===== 弾 =====
# ...
# ===== 敵 =====
# ...
# ===== メインゲーム =====

役割ごとに、

  • 「基本設定」
  • 「プレイヤー」
  • 「弾」
  • 「敵」
  • 「メインゲーム」

という単位で、コメントの見出しをそろえています。
加えて不要なコメントを削除しています。
これにより、初めてコードを読む人でも「どこを見ればプレイヤーの処理なのか」「どこがメインループなのか」をすぐに探せるようにしていますなります。

今回のリファクタリングはコメントのみのため、これでコードが完成しました。

完成

以上でPythonで作る「シューティングゲーム」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
タカヒデ
タカヒデ
ITを楽しく勉強中
通信会社の企画職で働く30代 専門性がないことに悩みながら半分趣味でITエンジニアリングやプログラミングを学習中 IT初心者が楽しいと思えるように発信することを目指しています
記事URLをコピーしました