【Pygame】ブロック崩しの作り方とサンプルコード(効果音付き)

Pygameを用いてブロック崩しを作る方法とソースコードを解説します。

Pygameでブロック崩しゲームの作成

前回の記事「スプライトの使い方とグループ化」と「スプライト同士の衝突判定と跳ね返り」の活用例として、以下のブロック崩しゲームを作成する方法を解説します。

【今回使用する画像】
Windowsのペイントソフトで作りました。リンク先からダウンロードしてご自由にお使いください。

【今回使用する音声】
「無料 BGM・効果音のフリー音源素材(Springin’ Sound Stock)様」からお借りしました。

  • paddle_sound.mp3・・・リンク先の音声ファイル「8bit下降9.mp3」を使用しました
  • block_sound.mp3・・・リンク先の音声ファイル「破壊音_たくさん破壊.mp3」を使用しました
  • gameover_sound.mp3・・・リンク先の音声ファイル「8bit選択8.mp3」を使用しました

動画解説版

本ページの内容は以下動画でも解説しています。

サンプルコード


コード解説

上記サンプルコードの各部分を解説します。

1. インポートと定数の定義

import math
import sys
import pygame
from pygame.locals import *
  • mathsysはPythonの標準ライブラリでインポートします。
  • pygameはゲーム開発用のライブラリでインポートします。
  • pygame.localsから特定の定数をインポートします。

2. 画面サイズとファイルパスの定義

# 画面サイズ
SCREEN = Rect(0, 0, 400, 400)

# 画像ファイルのパス
PADDLE_IMG_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/img/paddle.png"
BLOCK_IMG_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/img/block.png"
BALL_IMG_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/img/ball.png"

# 音声ファイルのパス
PADDLE_SOUND_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/mp3/paddle_sound.mp3"
BLOCK_SOUND_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/mp3/block_sound.mp3"
GAMEOVER_SOUND_PATH = "/Users/github/sample/python/pygame/breakout/sample01/assets/mp3/gameover_sound.mp3"
  • 画面サイズを定義。
  • 画像や音声ファイルのパスを定義。

3. パドルのスプライトクラス

# バドルのスプライトクラス
class Paddle(pygame.sprite.Sprite):
    # コンストラクタ(初期化メソッド)
    def __init__(self, filename):
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image = pygame.image.load(filename).convert()
        self.rect = self.image.get_rect()
        self.rect.bottom = SCREEN.bottom - 20          # パドルのy座標

    def update(self):
        self.rect.centerx = pygame.mouse.get_pos()[0]  # マウスのx座標をパドルのx座標に
        self.rect.clamp_ip(SCREEN)                     # ゲーム画面内のみで移動
  • パドルの画像を読み込み、位置を設定。
  • マウスの位置に応じてパドルを移動。

4. ボールのスプライトクラス

このクラスは、ボールの動きを管理し、ゲームの進行を制御する重要な部分です。
特に、衝突処理や反射角度の計算が肝となります。

# ボールのスプライトクラス
class Ball(pygame.sprite.Sprite):
    # コンストラクタ(初期化メソッド)
    def __init__(self, filename, paddle, blocks, score, speed, angle_left, angle_right):
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image = pygame.image.load(filename).convert()
        self.rect = self.image.get_rect()
        self.dx = self.dy = 0  # ボールの速度
        self.paddle = paddle  # パドルへの参照
        self.blocks = blocks  # ブロックグループへの参照
        self.update = self.start # ゲーム開始状態に更新
        self.score = score
        self.hit = 0  # 連続でブロックを壊した回数
        self.speed = speed # ボールの初期速度
        self.angle_left = angle_left # パドルの反射方向(左端:135度)
        self.angle_right = angle_right # パドルの反射方向(右端:45度)
  • initメソッド: ボールの初期設定を行います。
  • 画像の読み込み、位置の設定、速度の初期化、スプライトグループ「blocks」への追加、パドルやブロックへの参照を行っています。
  • パドルやブロックとの衝突を管理。
    # ゲーム開始状態(マウスを左クリック時するとボール射出)
    def start(self):
        # ボールの初期位置(パドルの上)
        self.rect.centerx = self.paddle.rect.centerx
        self.rect.bottom = self.paddle.rect.top

        # 左クリックでボール射出
        if pygame.mouse.get_pressed()[0] == 1:
            self.dx = 0
            self.dy = -self.speed
            self.update = self.move
  • startメソッド: ゲーム開始時のボールの位置をパドルの上に設定し、左クリックでボールを射出します。
    # ボールの挙動
    def move(self):
        self.rect.centerx += self.dx
        self.rect.centery += self.dy

        # 壁との反射
        if self.rect.left < SCREEN.left:    # 左側
            self.rect.left = SCREEN.left
            self.dx = -self.dx              # 速度を反転
        if self.rect.right > SCREEN.right:  # 右側
            self.rect.right = SCREEN.right
            self.dx = -self.dx
        if self.rect.top < SCREEN.top:      # 上側
            self.rect.top = SCREEN.top
            self.dy = -self.dy

        # パドルとの反射(左端:135度方向, 右端:45度方向, それ以外:線形補間)
        # 2つのspriteが接触しているかどうかの判定
        if self.rect.colliderect(self.paddle.rect) and self.dy > 0:
            self.hit = 0                                # 連続ヒットを0に戻す
            (x1, y1) = (self.paddle.rect.left - self.rect.width, self.angle_left)
            (x2, y2) = (self.paddle.rect.right, self.angle_right)
            x = self.rect.left                          # ボールが当たった位置
            y = (float(y2-y1)/(x2-x1)) * (x - x1) + y1  # 線形補間
            angle = math.radians(y)                     # 反射角度
            self.dx = self.speed * math.cos(angle)
            self.dy = -self.speed * math.sin(angle)
            self.paddle_sound.play()                    # 反射音

        # ボールを落とした場合
        if self.rect.top > SCREEN.bottom:
            self.update = self.start                    # ボールを初期状態に
            self.gameover_sound.play()
            self.hit = 0
            self.score.set_score(0)                               # スコアを0点にする
            #self.score.add_score(-100)                  # スコア減点-100点
  • moveメソッド: ボールの移動と衝突処理を行います。壁やパドル、ブロックとの衝突を検出し、反射やスコアの更新を行います。

ブロックとボールの衝突判定とブロックの削除

blocks_collided = pygame.sprite.spritecollide(self, self.blocks, True)
if blocks_collided:  # 衝突ブロックがある場合
    oldrect = self.rect
    for block in blocks_collided:
        # ボールが左からブロックへ衝突した場合
        if oldrect.left < block.rect.left and oldrect.right < block.rect.right:
            self.rect.right = block.rect.left
            self.dx = -self.dx

        # ボールが右からブロックへ衝突した場合
        if block.rect.left < oldrect.left and block.rect.right < oldrect.right:
            self.rect.left = block.rect.right
            self.dx = -self.dx

        # ボールが上からブロックへ衝突した場合
        if oldrect.top < block.rect.top and oldrect.bottom < block.rect.bottom:
            self.rect.bottom = block.rect.top
            self.dy = -self.dy

        # ボールが下からブロックへ衝突した場合
        if block.rect.top < oldrect.top and block.rect.bottom < oldrect.bottom:
            self.rect.top = block.rect.bottom
            self.dy = -self.dy
        self.block_sound.play()     # 効果音を鳴らす
        self.hit += 1               # 衝突回数
        self.score.add_score(self.hit * 10)   # 衝突回数に応じてスコア加算
  • pygame.sprite.spritecollide(self, self.blocks, True): ボール (self) とブロック (self.blocks) の衝突をチェックします。第三引数の True は、ボールと衝突したブロックをグループから削除することを意味します。これにより、ブロックが消えます。
  • if blocks_collided:: 衝突したブロックがある場合に処理を行います。
  • for block in blocks_collided:: 衝突したすべてのブロックに対して処理を行います。
  • 各方向からの衝突判定: ボールがブロックにどの方向から衝突したかを判定し、ボールの反射方向を変更します。
  • self.block_sound.play(): 衝突音を再生します。
  • self.hit += 1: 衝突回数をカウントします。
  • self.score.add_score(self.hit * 10): 衝突回数に応じてスコアを加算します。

5. ブロッククラス

class Block(pygame.sprite.Sprite):
    def __init__(self, filename, x, y):
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image = pygame.image.load(filename).convert()
        self.rect = self.image.get_rect()
        # ブロックの左上座標
        self.rect.left = SCREEN.left + x * self.rect.width
        self.rect.top = SCREEN.top + y * self.rect.height
  • __init__メソッド: ブロックの初期化を行います。
    • pygame.sprite.Sprite.__init__(self, self.containers): スプライトの初期化を行い、ブロックをスプライトグループに追加します。
    • self.image = pygame.image.load(filename).convert(): ブロックの画像を読み込み、self.imageに設定します。
    • self.rect = self.image.get_rect(): 画像の矩形(位置とサイズ)を取得し、self.rectに設定します。
    • self.rect.leftself.rect.top: ブロックの左上座標を設定します。xyの値に基づいて、ブロックの位置を計算します。

6. スコアクラス

class Score():
    def __init__(self, x, y):
        self.sysfont = pygame.font.SysFont(None, 20)
        self.score = 0
        (self.x, self.y) = (x, y)
    def draw(self, screen):
        img = self.sysfont.render("SCORE:" + str(self.score), True, (255,255,250))
        screen.blit(img, (self.x, self.y))
    def add_score(self, x):
        self.score += x
    def set_score(self, score):
        self.score = score
  • __init__メソッド: スコアの初期化を行います。
    • self.sysfont = pygame.font.SysFont(None, 20): フォントを設定します。サイズは20ピクセルです。
    • self.score = 0: スコアを0に初期化します。
    • (self.x, self.y) = (x, y): スコアの表示位置を設定します。
  • drawメソッド: スコアを画面に描画します。
    • img = self.sysfont.render("SCORE:" + str(self.score), True, (255,255,250)): スコアをテキストとしてレンダリングし、画像として生成します。
    • screen.blit(img, (self.x, self.y)): 生成したスコア画像を指定した位置に描画します。
  • add_scoreメソッド: スコアを加算します。
    • self.score += x: 現在のスコアに指定された値を加算します。
  • set_scoreメソッド: スコアを設定します。
    • self.score = score: スコアを指定された値に設定します。

7. ゲームのメインループ

ゲームの主要なスプライトを初期化し、適切なグループに追加することで、描画や衝突判定を効率的に行えるようにしています。

描画用のスプライトグループ

group = pygame.sprite.RenderUpdates()
  • group: すべてのスプライトを管理するグループです。このグループは、スプライトの描画と更新を行います。

衝突判定用のスプライトグループ

blocks = pygame.sprite.Group()
  • blocks: ブロックのスプライトを管理するグループです。このグループは、ボールとの衝突判定に使用されます。

スプライトグループに追加

Paddle.containers = group
Ball.containers = group
Block.containers = group, blocks
  • Paddle.containers: パドルのスプライトをgroupに追加します。
  • Ball.containers: ボールのスプライトをgroupに追加します。
  • Block.containers: ブロックのスプライトをgroupblocksの両方に追加します。

Block.containersgroupblocksの両方を追加する理由は、ブロックのスプライトが描画と衝突判定の両方に関係するためです。

  • 描画のためのグループ (group)
    • 描画: groupは、すべてのスプライトを描画するためのグループです。これにより、ブロックが画面に表示されます。
    • 更新: groupは、スプライトの位置や状態を更新するためにも使用されます。これにより、ゲームの進行に合わせてスプライトが適切に動作します。
  • 衝突判定のためのグループ (blocks)
    • 衝突判定: blocksは、ボールとの衝突判定を行うためのグループです。これにより、ボールがブロックに当たったときに適切な反応(ブロックの消失やスコアの更新)が行われます。

このように、ブロックのスプライトを両方のグループに追加することで、ゲームの描画と衝突判定の両方を効率的に管理することができます。

【補足】
`pygame.sprite.spritecollide(self, self.blocks, True)`は、指定されたスプライト(ボール)とグループ(`blocks`)内のスプライトとの衝突をチェックします。

もし、`Block.containers = group, blocks`を`Block.containers = group`に変更すると、ブロックのスプライトが`blocks`グループに追加されなくなります。
つまり、`pygame.sprite.spritecollide(self, self.blocks, True)`でブロックとの衝突が検出されなくなります。その結果、ボールがブロックに当たってもブロックが消えず、スコアも更新されません。

ボール自体は他のスプライト(パドルやブロック)との衝突判定を行いますが、ボールが他のスプライトと衝突する対象とはならないため、`blocks`グループに含まれる必要はありません。

パドルの作成

paddle = Paddle(PADDLE_IMG_PATH)
  • paddle: パドルのスプライトを作成し、画像を読み込みます。

ブロックの作成

for x in range(1, 15):
    for y in range(1, 11):
        Block(BLOCK_IMG_PATH, x, y)
  • ブロックの配置: 14列×10行のブロックを作成し、各ブロックの位置を設定します。

スコアの表示

score = Score(10, 10)
  • score: スコア表示用のオブジェクトを作成し、画面の座標(10, 10)に配置します。

ボールの作成

Ball(BALL_IMG_PATH, paddle, blocks, score, 5, 135, 45)
  • Ball: ボールのスプライトを作成し、初期設定を行います。パドル、ブロック、スコア、速度、反射角度などを設定します。

ループ処理の実行を継続するフラグ

running = True  # ループ処理の実行を継続するフラグ
  • running: ゲームループを継続するかどうかを制御するフラグです。Trueの間はループが続きます。

8. メインループ

while running:
    clock.tick(60)      # フレームレート(60fps)
    screen.fill((0,20,0))
    # 全てのスプライトグループを更新
    group.update()
    # 全てのスプライトグループを描画       
    group.draw(screen)
    # スコアを描画  
    score.draw(screen) 
    # 画面更新 
    pygame.display.update()
  • clock.tick(60): フレームレートを60FPSに設定します。これにより、ゲームの動作が一定の速度で進行します。
  • screen.fill((0,20,0)): 画面を緑色で塗りつぶします。これにより、前のフレームの残像が消えます。
  • group.update(): すべてのスプライトの状態を更新します。各スプライトのupdateメソッドが呼び出されます。
  • group.draw(screen): すべてのスプライトを画面に描画します。
  • score.draw(screen): スコアを画面に描画します。
  • pygame.display.update(): 画面を更新し、描画内容を表示します。

イベント処理

for event in pygame.event.get():
    # 閉じるボタンが押されたら終了
    if event.type == QUIT: 
        running = False
    # キーイベント
    if event.type == KEYDOWN:
        # Escキーが押されたら終了
        if event.key == K_ESCAPE:   
            running = False
  • イベントループ: ユーザーの入力やシステムのイベントを処理します。
  • QUITイベント: ウィンドウの閉じるボタンが押された場合に発生し、runningFalseにしてループを終了します。
  • KEYDOWNイベント: キーが押されたときに発生します。
  • K_ESCAPEキー: Escキーが押された場合、runningFalseにしてループを終了します。

9. 終了処理

pygame.quit()
sys.exit()
  • pygame.quit(): Pygameを終了し、リソースを解放します。
  • sys.exit(): プログラムを終了します。

関連ページ

Pygameの使い方については以下ページで解説しています。

【Pygame超入門】使い方とサンプルゲームを解説
Pygameで2Dゲームを簡単に制作する方法を入門者向けに解説します。

Python全般については以下ページで解説しています。

【Python超入門】使い方とサンプル集
Pythonの使い方について、基礎文法から応用例まで入門者向けに解説します。

コメント

  1. たすけてーー より:

    # 壁との反射
    if self.rect.left SCREEN.right: # 右側

    ここの部分にエラーが出てしまいます原因は何でしょうか?

  2. あああ より:

    # 壁との反射
    if self.rect.left SCREEN.right: # 右側

    ここの部分にエラーが出てしまいます。
    まだまだ初心者でとりあえずコピー&ペーストで実行しているだけなのですが、困っています。
    = がないのが原因でしょうか?

    よろしくお願いします。

    • 管理人 より:

      コメントありがとうございます。
      ご指摘のとおり等号、不等号がないのが原因です。
      記事のソースコードを修正しましたのでご確認くさだい。

  3. Tetsu より:

    使用しているpngはどこで入手できますか?
    URLとかあれば教えてください。

    • 管理人 より:

      画像ファイル上で右クリックし、[画像ファイルを保存する]などを選択すると元画像を保存できます。

  4. わからない太郎 より:

    参考にさせていただいております。

    どうしても、ソースコードを入れ込んでみたのですが、importの箇所で読み込みません。

    完成品と比較をしたいのでファイルをいただければ幸いです。
    どこができていないのか比較をしたいです。
    何卒よろしくお願いいたします。

    • 管理人 より:

      掲載しているプログラムが完成版です。
      ゲーム内でロードする画像ファイルは既に掲載しておりますが、音声ファイルは再配布禁止のフリー音源を使用しているため
      入手先を紹介するまでとなっております。
      そのため、全ファイル一式をご提供というのはできませんので、ご理解いただけますと幸いです。
      importでエラーが出ているということはpygameモジュールをインストールできていない可能性がありますので
      再度、「pip install pygame」でインストールされてみてはいかがでしょうか。

  5. 匿名 より:

    LinuxでPythonを勉強し始めた素人です。
    ファイルの部分で若干修正を要しましたが、無事動きました。

    ありがとうございます。