step07 ブロック崩しゲーム

前stepで解説した、洞窟探検ゲームでは、単純すぎてpygameライブラリの威力が十分に伝わらなかったかもしれない。実際、あの程度ならば、無理をすればtkinterライブラリでも書けないことはない。その場合、キャンバスウィジェットに並べたモノをフレーム毎に動かすわけだから、ノロノロした、かったるいゲームになるのは必定だが。
そこでこのstepでは、より本格的な「ブロック崩しゲーム」を用いて、pygameの、ゲームに特化した機能を重点的に紹介する。
なお、このプログラムの作成に当たっては、Web上にいくつかあるpygameプログラミングの紹介サイトを参考にした。特に、ゲーム制作速報氏のサイトから多くの知識をいただいている。感謝とともにお断りしておく。

ブロック崩しゲーム

図7-1に、ブロック崩しゲームの実行画面を示す。
このゲームも起源は古く、なんとパソコン以前に遡る。最初は、アナログ電子回路を用いたハードウェア玩具だったのだ。現在、さまざまなパソコンに移植されているバージョンは、それをいわばデジタル化したものだ。
とにかく、まず遊んでみよう。ゲームプログラムでは、実際にプレイすることで、動作とプログラムの対応が自然に頭に入ってくるので、遊びも学びなのだ。サウンドが加わると、ゲームの楽しさが盛り上がるのはなぜだろう。

図7-1: ブロック崩しゲーム blocks.pyw

プログラムの解説

リスト7-1に、ブロック崩しゲームのプログラムを示す。やや長いが、クラスによって整理されているので、注意深く読めば分かるはずだ。ゲームでは、画面上の目に見えるモノ、動くモノをそのままクラスで表現することが多く、オブジェクト指向プログラミングの教材としては最適だといえる。


リスト7-1: blocks.pyw

以下、簡単にプログラムを解説する。

クラス定義

まず、画面には、つぎの4種類のモノが登場する。それぞれに専用のクラスを定義するのが、ゲームプログラミングの定石だ。

クラスの数は洞窟探検ゲームより1つ多いだけだが、それぞれの動作は格段に複雑である。特にボール(Ball)は、壁やパドルに反射して動き回るほか、ブロックにぶつかって「崩す」など、多岐にわたる動作をする。Ballクラスの長大なmoveメソッドをよく読み、動作の詳細がどのようなプログラムで実現されているか理解しよう。
また、ブロック(Block)は、クラスは1つだが、画面上には初期状態で140個(10段×14列)ものインスタンス(オブジェクト)が配置されている。

スプライトとスプライトグループ

このように、ゲームではしばしば、画面上に多数のモノがあり、それらが複雑な相互作用(たとえば衝突)をする。それらの動作を効率よく定義し、すばやく表示するために、pygameをはじめとするゲーム用のプログラミング・フレームワークでは、次のようなゲーム専用の仕組みを用意している。

スプライト(Sprite)

軽量に動作する画像クラス。このプログラムでは、パドル、ボール、ブロックの3種類のクラスが、pygame.sprite.Spriteクラスの子クラスとして定義されている。このようにすることで、親クラスで定義されている多数のメソッドや属性(メンバ変数)が、子クラスでも使え、効率よいプログラミングができる。たとえば、初期化の際、self.imageメンバに画像ファイルをロード(および変換)しておくだけで、明示的に描画コマンドを呼ぶことなく、それぞれのモノが表示されていることに注意しよう。

スプライトグループ(sprite.Group)

多くのスプライトをまとめて扱うことで、さらに効率よいプログラミングができる。ここでは、main()関数中で、描画用のスプライトグループ(group)と、衝突判定用のスプライトグループ(blocks)が定義されている。


group = pygame.sprite.RenderUpdates()  # 描画用のスプライトグループ
blocks = pygame.sprite.Group()         # 衝突判定用のスプライトグループ
前者にはパドル、ボール、ブロック(つまりすべてのスプライト)が、後者にはブロック(140個)が属する。

Paddle.containers = group
Ball.containers = group
Block.containers = group, blocks

衝突判定

pygameでは、モノ同士の衝突判定を丸投げできる仕組みがある。
Ballクラスの初期化メソッドに、引数としてblocksグループが渡されていることに注意しよう。これはボールとブロックとの衝突判定で使うからである。ボールクラスのmove()メソッド中で、


blocks_collided = pygame.sprite.spritecollide(self, self.blocks, True)
のように、ボールに衝突したブロックのリストを一挙に取得している。この処理を自分でプログラムするのは、かなりの手間だろう。

表示更新(update)の伝播

もう1つ、というか、pygameライブラリを使う最大の理由が、この機能である。
main()関数の表示更新ループ中で、描画用のスプライトグループを利用した、更新と描画の処理が行われている。個々のスプライトではなく、グループを指定することで、一挙に更新と描画ができるのである。


group.update()        # 全てのスプライトグループを更新
group.draw(screen)    # 全てのスプライトグループを描画
特に、更新(update)メソッドは実に巧妙な仕組みで、グループに対して呼び出すだけで、そのグループに属するすべてのスプライトに、いわば「お触れ」が回るようにして、updateメソッドの呼び出しがかかる。パドルのupdate()メソッドでは、マウス座標を取得して自分の位置を決めている。パドルが画面外に出てしまうのを防ぐrect.clamp_ip()メソッドは興味深い(可動域の制限というのは、ゲームでは非常によくある処理であり、だからこそpygameにこうしたメソッドが備わっている)。

def update(self):
    self.rect.centerx = pygame.mouse.get_pos()[0]  # マウスのx座標をパドルのx座標に
    self.rect.clamp_ip(SCREEN)                     # ゲーム画面内のみで移動
ボールのupdate()メソッドは、一見未定義のように見えるが、ちゃんと定義されている。ゲーム開始までは、updateはstart()メソッドを指しており、ボールはパドルに貼り付いているように見える。

self.update = self.start
そして、ゲーム開始時にmove()メソッドに切り替えられている。更新ループ毎に、move()メソッドが呼ばれ、ボールが動き回るようになるのはここからだ。

self.update = self.move
このように、update()メソッドは各スプライトクラスのメソッドを呼び分ける便利なフック関数の役割を担っている。


ゲーム画面の準備やイベントの取得については、前stepの洞窟探検ゲームとほぼ同じである。
また、このゲームでは効果音が使用されているが、それらはpygame.mixerという仕組みでサポートされている。使用法は見たとおり簡単だ。


Ball.paddle_sound = pygame.mixer.Sound("flashing.wav")    # パドルにボールが衝突した時の効果音取得

self.paddle_sound.play() # 反射音
最終課題でゲーム系アプリを選ぶグループは、ぜひ効果的なサウンドでゲームを盛り上げてほしいものだ。

演習:ブロックの再描画

最後までプレイすると分かるが、ブロックをすべて消しても、このアプリではそれ以上何も起こらない。すべてのブロックが消された時に、ブロックの壁を再表示してゲームを続行できるように改良してみよう。ポイントは、全ブロックが消されたことをどのやって判定するかだろう。