step06 pygameライブラリの使い方:洞窟探検ゲーム

この『山之口洋の極・楽 python 講座【応用編】』も、折り返し点を回った。ここから先はゴールに向かう復路だ。これまでの各stepでは、GUIアプリケーションの書き方を、tkinterライブラリを利用しつつ学んできた。実用系アプリであれば、およそどんなものでもtkinterで不自由しない。あとは各自、こんなことをパソコンに(あるいはpythonに)やらせたいという好奇心と発想さえあれば、いくらでも書けるだろう。
しかし、なにもGUIアプリは、実用系アプリに限ったものではない。ゲームもまた、アプリの一大領域を占めている※1。しかも、プログラミングを学習するのに最適な性格を持っている。

1 他にも、アート系アプリや、制御系アプリなどの領域がある。前者については、「【番外編】アート系アプリ」で簡単に扱う。後者は、ロボットやドローンや家電製品など、パソコン以外のハードウェアを制御するアプリを指す。これはpythonの得意分野で、たとえば、Googleが主導する自動運転車の制御アプリには、python言語/OpenCVライブラリを用いて書かれたものが多い。この講座では、1人1人にハードウェア(たとえばロボット)を準備できないので、題材として扱えないのだが、python言語を習得したら、ぜひチャレンジしてほしい領域の1つである。たとえば、自分の部屋の照明やエアコンを、身振りや音声でコントロールするなど、比較的手頃なテーマだろう。

ゲームは実用アプリとどこが違うのだろうか。ゲームには、実用アプリと同様、一定の目標がある。ユーザとのインタラクションもある。だが、実用ツールはユーザからの操作抜きには動かないのに対し、一般的なゲームはユーザが操作しないでも勝手に動く。そこに偶然性と、ゲームとしての面白さが生まれるのである。
言い換えれば、始めからうまくいくようにデザインされているのが実用アプリであるのに対し、うまくいかない対象をコントロールすること自体を楽しむのがゲームなのだ。
このstepと次のstepでは、pythonを使ったゲームプログラミングの基礎を学ぶ。

pygameライブラリ

ゲームも実用アプリと同じGUIアプリだから、開発にはGUIツールキットが必要だ。だが、これまで使ってきたtkinterライブラリは、ゲーム作成に便利な機能をまったく備えていないため、相当な困難が予想される。
そこで、pygameという新たなGUIツールキットを使うことにする。pygameには、一般的なゲーム作成に必要・便利な次のような機能が組み込まれているので、短いプログラムでそれらしい動作をするゲームを作れる。

tkinterのキャンバスと類似のものをサーフェス(SURFACE)と呼んだり、細かい用語や用法の違いはあるが、このstepのサンプルを学ぶだけで慣れるだろう。大ざっぱにいえば、pygameはtkinterよりもずっと画像(とサウンド)に特化しており、単純なフレームワークである。
とはいえ、tkinterと同様、pygameライブラリも、全体としてはかなり大きな仕様を有しており、この講義資料で詳細に説明するのは無理である。プログラミングにあたっては、以下のドキュメントを参考にしてほしい。プログラミングでは、英語の説明を読みこなす能力も必要である。ただし、まずはサンプルプログラムを読んで、役割のわからない部分についてリファレンス情報に当たるのが、効率的な方法だ。 tkinterを利用してゲームプログラムを書くのは難しいといったが、逆に、pygameを使って簡単な実用アプリは容易に書ける。実例として、図6-1に、pygame版のデジタル時計の実行画面を示す。

図6-1: pygame版のデジタル時計

リスト6-1のプログラムを、tkinter版のデジタル時計(リスト1-2)と比較してみよう。
変数SURFACEに代入されているオブジェクトはゲーム画面全体であり、tkinterのキャンバスウィジェットに相当する。pygameでは、ウィンドウ全体がグラフィックス表示を行うためのいわばキャンバスであり、実用アプリで使われていたウィジェットは、ボタンやエントリといった単純なもの以外は、あまり登場しない※2

2 その代わり、画面の中で動く多種多様なモノ(スプライト)が、プログラムの主役となる。


リスト6-1: dwatch_pg.pyw

リスト6-1のプログラムについて、簡単に解説する。
メインルーチン中には、表示更新ループという無限ループが必ずあり、画面に対して必要な各種描画命令を実行した後、


pygame.display.update()
という画面更新命令で、画面全体が更新される。このupdate()メソッドこそpygameプログラムの中心であり、この簡単な例では分かりにくいが、画面上のすべての描画要素(後述のスプライトなど)に自動的に伝播する仕組みになっている。
ループの最後にある、イベント処理の書き方も定石である。1回のループ実行中にイベントキューに蓄えられたイベントを取り出し、タイプ別に分類して、それぞれの処理を行う。この場合は、画面右上の「×」を押すと、プログラムを終了する処理が記述されている。

洞窟探検ゲーム

さて、デジタル時計のプログラムは、tkinterと比較するために挙げただけで、このような実用アプリなら、なにもpygameライブラリを使うまでもない。そこで今度は、多少ゲームらしいプログラムを書いてみよう。
この洞窟探検ゲームは、パソコンが登場した1980年代初期からある、非常に古いゲームの1つだが、ゲームプログラムの最小限の要素をバランスよく含んでいるので、教材としては最適である。図6-2に実行画面を示す。
ゲームなのだから、まずは遊んでみよう。使うのはスペースバーだけであり、ルールは説明するまでもあるまい(緑の壁にぶつからないように飛ぶだけ)。

図6-2: 洞窟探検ゲーム

リスト6-2に、このゲームのプログラム全体を示す。百数十行とやや長いが、クラスを利用してできる限り構造化しているため、読みにくくはないだろう。


リスト6-2: cave.pyw

リスト6-2のプログラムについて、簡単に解説する。
ここでは、画面上に表示されるモノ、すなわち宇宙船、洞窟、得点をそれぞれクラス化し、それぞれのモノがどのようにふるまうかを考えて、それらを各クラスのメソッドとして定義している。こうした考え方をオブジェクト指向プログラミングといい、複雑な事象を整理するための強力な手段を提供する。ゲームでは、画面上に登場するモノが主役であるから、オブジェクト指向の考え方がしっくりくるのである。さまざまな処理が、各クラスのメソッドとして整理された結果、メイン関数中の表示更新ループは、極めてコンパクトになり、ゲームのアルゴリズムをわかりやすく伝えてくれる※3

3 クラスを使わずに、つまりモジュールと関数だけで、同じプログラムを書くことを想像してみるとよい。

インスタンス変数について。たとえば宇宙船クラスの__init__()メソッド中に、

self.x = 0              # 位置(左上隅のx)
self.y = 250            # 位置(左上隅のy)
self.vy = 0             # 速度(y方向)
などの代入文があるが、これらがそれである。インスタンス変数は、生成されたオブジェクト(ここでは宇宙船)1つにつき1セットずつ生成され、その初期値がここで与えられる。pythonには明示的に変数を宣言する構文がないので、インスタンス変数の存在が目立ちにくく、注意が必要だ。
クラスにはインスタンス変数の他に、クラス変数もある。この講座では1度も使っていないが、たとえば宇宙船が何台も出てくるゲームでは、それらの台数を管理したいに違いない。だがそのデータは、個々の宇宙船の属性ではない。そうしたときに、

class 宇宙船():
    機数 = 0            # 登場している宇宙船の総数
    # 生成・初期化
    def __init__(self):
        self.機数 += 1
のように、クラスで共有されるデータを格納するのに使う。アクセスの記法は、

宇宙船.機数 = 0
self.機数 = 0
のどちらでもよいが、インスタンス変数との違いを明示できる、上の書き方を奨める。
インスタンス変数とクラス変数を総称してメンバ変数ということもあるが、pythonの正式な用語ではないようだ。一般にはインスタンス変数とメンバ変数を同じ意味で用いても混乱はない。
ゲームの速度を決めるために、変数FPSCLOCKに代入したオブジェクト(pygame.time.Clock)がタイムキーパーとしての役割を果たしている。
画像ファイルをゲーム中の物体と結びつけて表示する手順についても、確認しておこう。

self.image = pygame.image.load("ship.png")        # 90 x 60
self.bangimage = pygame.image.load("bang.png")    # 120 x 120
以下の項目がどのように実現されているか、読んで把握しよう。

演習:リプレイ機能の追加

このゲームでは、宇宙船が壁に衝突してゲームオーバーになると、再起動しないとリプレイできず、不便である。そこで、ゲーム終了時にメッセージを表示し、Enterキーで再度ゲームを開始できる機能(リプレイ機能)を追加してみよう。