読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習・Deep Learningに関する記事が多いです。



ブロックを壊す

Pygame

ブロックを配置してボールが当たると壊れるようにします。ブロックをブロックグループに追加し、ボールスプライトとブロックグループの衝突判定を行います。Spriteの衝突判定機能を使うと簡単にできます。ここまでくるとゲームっぽくなってきます。

breakout03.zip
f:id:aidiary:20100807231438p:plain

ブロックの実装と配置

まずブロックを表すBrickクラスから作ります(Brickはつづりミスではなくレンガや積み木という意味です。別にBlockでもいいです)。ブロッククラスもパドルやボールと同じく、Spriteを継承するのでスプライトになります。しかし、非常に単純でコンストラクタしかありません。

class Brick(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self, self.containers)
        self.image, self.rect = load_image("brick.png")
        # ブロックの位置を更新
        self.rect.left = SCR_RECT.left + x * self.rect.width
        self.rect.top = SCR_RECT.top + y * self.rect.height

コンストラクタで指定した引数の位置にブロックを配置します。ここで注意なんですが、(x, y) はピクセル単位の座標ではなく、ブロックのサイズを1マスとしたときのマス単位の座標です。フィールド上にブロックをぎっしりはめこんだとき一番左上が(0,0)になります。その右となりのブロックは(1,0)です。ピクセル単位よりマス単位で座標を指定できた方がブロックの配置が簡単になります。実際にブロックをフィールド上に配置するのはmain()です。

    # ブロックを作成
    # 自動的にbricksグループに追加される
    for x in range(1, 11):  # 1列から10列まで
        for y in range(1, 6):  # 1行から5行まで
            Brick(x, y)

for文で10列5行のブロックを作成しています。ここでBrick(x,y)とオブジェクトを作っただけでどこにも格納していません。実は、後で見るようにブロックにもデフォルトスプライトグループ(self.containers)を指定しています。そのため、ブロックを作成したと同時にスプライトグループに追加されます。ブロックの描画や更新はスプライトグループが行うので作成したブロックはちゃんと画面に描画されます。

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

次にボールがブロックに当たったときにブロックが消える処理を実装します。スプライトとグループが衝突したかを調べるための方法がpygameには用意されています。そのために新しいスプライトグループを追加します。

    # スプライトグループを作成して登録
    all = pygame.sprite.RenderUpdates()  # 描画用グループ
    bricks = pygame.sprite.Group()       # 衝突判定用グループ
    Paddle.containers = all
    Ball.containers = all
    Brick.containers = all, bricks

パドルやボールは今までallというRenderUpdatesグループに追加していました。RenderUpdatesは、描画や更新を行うスプライトグループです。ブロックも描画や更新はしなくてはいけないのでBrick.containersにallを追加しています。新しく追加されたのはbricksというpygame.sprite.Groupです。こちらは主に衝突判定に使うスプライトグループです。今回は、ボールとブロックグループがぶつかったか判定したいのでブロックを追加するbricksグループを作成しています。bricksには先ほど作成したように10x5=50個のBrickが追加されます。デフォルトスプライトグループに2つ以上のグループを追加するには、all, brickのようにカンマで区切って並べます。実際は、タプルなので (all, brick) としても同じです。ボールとブロックの衝突は、ボールスプライトとブロックスプライトグループの衝突があるか調べます。pygameでは、スプライトとスプライトグループの衝突を調べるpygame.sprite.spritecollide()という関数があります。

 pygame.sprite.spritecollide(sprite, group, dokill)

という形式の関数でspriteとgroup内のいずれかのスプライトが衝突したか調べ、group内の衝突したスプライトをリストで返します。dokillをTrueにしておくとspriteと衝突したgroup内のスプライトをすべてgroupから消します。groupから消えたスプライトはdraw()されなくなるので画面からも消えます。実際にボールとブロックが衝突したときの処理を見てみます。Ballのmove()です。

        # ブロックを壊す
        # ボールと衝突したブロックリストを取得
        bricks_collided = pygame.sprite.spritecollide(self, self.bricks, True)
        if bricks_collided:  # 衝突ブロックがある場合
            oldrect = self.rect
            for brick in bricks_collided:  # 各衝突ブロックに対して
                # ボールが左から衝突
                if oldrect.left < brick.rect.left < oldrect.right < brick.rect.right:
                    self.rect.right = brick.rect.left
                    self.dx = -self.dx
                # ボールが右から衝突
                if brick.rect.left < oldrect.left < brick.rect.right < oldrect.right:
                    self.rect.left = brick.rect.right
                    self.dx = -self.dx
                # ボールが上から衝突
                if oldrect.top < brick.rect.top < oldrect.bottom < brick.rect.bottom:
                    self.rect.bottom = brick.rect.top
                    self.dy = -self.dy
                # ボールが下から衝突
                if brick.rect.top < oldrect.top < brick.rect.bottom < oldrect.bottom:
                    self.rect.top = brick.rect.bottom
                    self.dy = -self.dy

上の例では、

       bricks_collided = pygame.sprite.spritecollide(self, self.bricks, True)

となっています。selfはボールスプライト、self.bricksはブロックスプライトグループです。つまり、ボールスプライトとブロックスプライトグループが衝突しているか調べ、衝突したブロックのリストをbricks_collidedに格納します。dokillをTrueとしているので衝突したブロックはbricksグループから取り除かれ消えます。次に衝突ブロックがある場合、どの方向からボールがブロックに当たったかを調べてボールの反射方向を計算しています。ここで、oldrectはボールの矩形を表します。if文内でself.rectを更新しているので元のボールの矩形を保持しておく必要があります。ボールがどの方角からブロックに当たったかを求める条件式は下の図を見てください。ボールの矩形とブロックの矩形の位置関係から判断できます。

f:id:aidiary:20100807231439p:plain

self.rect.right = brick.rect.leftのような式はボールが上図のようにブロックに食い込んだとき食い込まないように位置を戻してやる式です。ifを並べる形で elifを使わなかったのはボールがちょうどブロックの角にぶつかる場合もあるからです。たとえば、ブロックの左下に当たった場合、ボールが左から衝突とボールが下から衝突の2つの条件を満たします。

Pygameの衝突判定関数

今回はスプライトとグループの衝突判定を調べましたが、pygameには他にもいろいろ衝突判定関数が用意されています。

  1. スプライトとグループの衝突判定(pygame.sprite.spritecollide())
  2. グループとグループの衝突判定(pygame.sprite.groupcollide())

今回使ったのは1番目のスプライトとグループの衝突判定を行う関数です。2番目のグループとグループの衝突判定はたとえばボールがたくさんある場合にボールグループとブロックグループで衝突判定を行うときに使います。たぶんあとで紹介します。では、スプライトとスプライトの衝突判定はどうすればいいでしょう?これは、ボールの反射でパドルとボールの衝突判定を調べたようにRect.colliderect()を使えばいいですね。

pygame1.8.0からpygame.sprite.collide_rect()というスプライトとスプライトの衝突判定関数が追加されています。でも引数がSpriteではなく、(left, right) を取るようになっていて意味がよくわかりません。中身を調べてみないと。