Pygame は SDL のための Python 用ラッパで、 Pete Shinners によって書かれたんだ。これが意味するところはだよ、 pygame を使えば、Python で書かれたゲームやら 他のマルチメディアなアプリケーションやらが、なんの修正もなしで 他の SDL をサポートしてるプラットフォーム (Windows, Unix, Mac, beOS やその他いろいろ) で走るってことだ。
Pygame を覚えるのはやさしいかもしれない。でもグラフィックス プログラミングの世界ってのは初心者にとってはえらくややこしい。 ぼくがこれを書いたのは、これまで何年か pygame と その先行者であるところの pySDL と格闘してきて、 そのなかでモノにした役に立つ知識を広めるためであーる。 ぼくはこれらのアドバイスを重要な順に並べるようにしたつもりだけれど、 あるヒントがどれくらい役に立つものかはこのドキュメントを 読んでいるあなたのプロジェクトの詳細と、あなたの背景知識とに かかっている。
いちばん大切なことは、Python をしっかり使えるようになることだ。 グラフィックスプログラミングのように複雑になるであろうものを 学ぶとき、あなたが自分の言語に不慣れだと、これは 本当に骨の折れる仕事になる。それなりのグラフィカルでない プログラムをまず Python で書いてみよう - テキストファイルを読み取るプログラムとか、 数当てゲームをするプログラムとか、家計簿とか、そんなものを。 文字列とリスト操作を快適にできるようになろう - 文字列やリストを split し、切り取り、そして連結するやりかたを学ぼう。 import の仕組みを学ぼう - 複数のソースにまたがるプログラムを 書こう。自分独自の関数を書いて、数値と文字を操作するやりかたを 練習しよう。どうやってこの 2つを変換するか。Python の構文と、 リストや辞書の使いかたが第2の母国語になるくらいまで上達しよう - リストを切り取ったりキーを sort したりするのに、わざわざドキュメントを あさるようなことはしたくないでしょ? トラブルがあっても、 メイリングリストや、comp.lang.python や IRC に駆け込むのは 我慢しよう。かわりに、インタプリタを立ち上げてその問題を 数時間いじくりまわしてみるんだ。 Python Quick Reference を印刷して、コンピュータのとなりにいつもおいておこう。
これらはとてつもなく退屈な仕事に思えるかもしれない。 けれど、Python に慣れることで得た確信は、いざゲームを 書く段になったときに信じられないくらいの奇跡をうみ出す。 実際のコードを書くときに節約できる時間に比べたら、 それまで Python を鍛えてきた時間なんて屁みたいなもんだ。
Pygame ドキュメント一覧で、ごちゃごちゃしたクラスを 見ると混乱するかもしれない。大切なのは、かなりのことをやるのに 必要な関数はこれらのうちのごく一部でいいということなんだ。 多くのクラスはたぶん使いもしないだろう -- ぼくだって最初の一年間は Channel, Joystick, cursors, Userrect, surfarray や version といった 関数には手をつけさえしなかった。
Pygame のもっとも重要な部分は “surface (サーフェイス)” だ。 surface というのはちょうど白紙の紙切れだと思えばいい。 surface に対してはかなりいろんなことができる -- 線を引いたり、 その一部分に色をつけたり、イメージをコピーしたりされたり、 そして各ピクセルごとの色を設定・取得できたりする。 surface は (それなりの大きさまでなら) どんなサイズにもできるし、 (それなりの数までなら) いくらでも好きなだけ多くの surface を持てる。 ある surface は特別だ -- これは pygame.display.set_mode() で 作成する surface で、“display surface (ディスプレイ サーフェイス)” と 呼ばれている。これは画面全体を表す surface で、これに対して したことはみんなユーザの画面に反映される。この surface は ひとつしか持てない。これは SDL の制限で、pygame のじゃない。
じゃあどうやって surface を作ったらいいんだろうか? 上でふれたように、特別な “display surface” は pygame.display.set_mode() でつくれる。 image.load() を使えば画像をもった surface がつくれるし、 font.render()を使えばテキスト文字列をもった surface もつくれる。 Surface() を使えば、まったく何もない surface さえつくれるんだよ。
ほとんどの surface 関数はそんなに重要じゃない。 blit(), fill(), set_at() そして get_at() を覚えれば十分だ。
最初に surface.convert() の説明を読んだとき、 ぼくはそれがそんなに気にかけるほどのものだとは思わなかった。 「自分は png しか使わないから、ぼくが扱うものはすべて 同一の形式になるはずだ、だから convert() なんか必要ない」 -- でもこれは激しく間違っていることがわかった。
convert() が言っている “format (形式)”というのは ファイル形式 (png とか jpeg とか gif とか) のことじゃなかったんだ -- ここでの形式というのは “pixel format (ピクセル形式)” のことだった。 これは surface がピクセル中の個々の色をどうやって記録しているかという 方式をさす。もしその surface の形式が display の形式と違っていると、 SDL はそれを毎回 blit するごとに「現場で」変換する -- これはとても時間がかかるものだ。でもこのことでそんなに心配しなくてもいい。 ただ blit するときにある程度の速度が必要なら、convert() は 必要なんだということだけ知っていればいいんだ。
じゃあどうやって convert を使うんだろう? image.load() 関数で surface を作ったすぐあとにこれを呼べばいい。 つまり今までこうしていたのを:
surface = pygame.image.load('foo.png')
かわりに、こうすればいい:
surface = pygame.image.load('foo.png').convert()
簡単でしょ? これを呼ぶ必要があるのは各 surface で一回だけ、 画像をディスクから読み込むときだけだ。そしてその結果にはうれしくなるよ。 ぼくのところでは convert() を呼ぶことで、blit の スピードは約 6倍になった。
convert() を使うべきでない唯一の場合というのは、 その画像の内部形式をほんとうにいじりたいときだけだ -- 画像変換プログラムとか、そのたぐいのものを作ろうとしているなら、 出力ファイルを入力ファイルと同じピクセル形式にしたいと思うだろう。 でもゲームを書いているときなら、スピードが必要だ。 convert() を使おう。
Pygame プログラムで十分なフレームレートがとれない 原因の最たるものは、pygame.display.update() 関数が 誤解されて使われていることだ。Pygame では、display surface に ただ何かを描画するだけではそれは画面に現れない -- pygame.display.update() を呼ぶ必要がある。 そしてこの関数を呼ぶやり方は 3つある。
グラフィックスのプログラミングをはじめてまだ間もない ほとんどの人々は最初の関数を使う -- フレームごとに 画面全体を更新するのだ。問題は、ほとんどの人にとって これは話にならないくらい遅いということだ。ぼくのマシンでは、 update() を呼ぶと 35ミリ秒を消費する。大したことないように 思えるかもしれないが、1秒間に 1000 / 35 = 28フレームが 最高速度 だと考えるとどうだろうか。しかもここにはゲームの ロジックは含まれていない。blit もなし、入力もなし、 AI もなしだ。ただじっと座って、画面が更新されてるのを見るだけで、 28 フレーム/秒がぼくのマシンでは最高のフレームレートなんだ。うあ。
これに対する解決策は “汚れた rect のアニメーション” と呼ばれる。 フレームごとに画面全体を更新するのではなく、前のフレームから変更された (訳注: 汚れた) 部分だけを更新しよう。ぼくはこれらの長方形 (rect) をリストに保持しておいて、 各フレームの最後で update(the_dirty_rectangles) を呼ぶことにしている。 スプライトを動かすやりかたを詳しく書くと、ぼくは:
こいつがうみだす速度のちがいには、ぶったまげる。 Solarwolf を思い出してよ、 これは何十というスプライトがスムーズに動いて update していて、 そのうえまだ背景に交差する星々を流していて、これまた update する時間が じゅうぶんにあるんだ。
このテクニックがうまくいかない場合が 2つある。ひとつは、 ウインドウあるいはスクリーンがフレームごとに、本当にすべて update されなければならない場合 -- 上から見下ろす形の地図を使う戦略ゲームなどの スムーススクロールするエンジンやサイドスクロールを考えてみてよ。 じゃあこういう場合はどうすればいいのか? えーと、簡単な答えはだね -- この手のゲームを pygame で書くべきではないということだ。長めの答えとしては、 いちどに数ピクセルをスクロールさせればいい。 完璧なスムーススクロールを実現させようとしないこと。 プレイヤーはスクロールが速いゲームを評価するんで、 背景が少々ぎこちなくても気にしないものだ。
最後の注意。すべてのゲームが高いフレームレートを必要とするわけではない。 戦略的ウォーゲームなら毎秒数フレームの update でじゅうぶんやっていけるだろう -- この場合、汚れた rect アニメーションなどの複雑な技は必要ないかもしれない。
pygame.display.set_mode() で使ういろんなフラグを見てみると、 こんなふうに思うかもしれない。「HWSURFACE だって? んー、よさそうだな… ハードウエアアクセラレーションを嫌う人なんかいないし… お、DOUBLEBUF? なんか速そうだな。これもいれちゃえ!」 これはきみの責任じゃない。何年にもわたって 3D ゲームを見てきているぼくらは、 ハードウエアアクセラレーションはよいもので、ソフトウエアによる描画は 遅いと信じこまされているからね。
でも残念なことに、ハードウエアによる描画には多くの欠点がつきまとう:
ハードウエア描画に適した場所がある。これは Windows 上では 非常に安定して動くので、もし他のプラットフォームでのパフォーマンスを 気にしないのであれば、絶大なスピード増加がのぞめるだろう。 でも、これはコストのかかるものだ -- 複雑さも頭痛のタネも増加する。 自分がやっていることにきちんと確信がもてるまでは、 古き良き SWSURFACE を使いつづけるほうがいい。
ときに新人ゲームプログラマーは、そのゲームにとって本当に 重要ではないことで長い間頭を悩ませてしまう。副次的な問題を 「きちんと」させたいという気持ちはわかる。でも、 ゲームを作るプロセスの早い段階では、どの解を選ぶべきかは もちろんのこと、何が重要な質問かさえもわかっていないものだ。 その結果は不必要な言い訳に終わることがある。
たとえば画像ファイルをどうやって管理するか考えてみよう。 フレームごとに画像ファイルを用意すべきだろうか、あるいはスプライトごとに? もしかしたら、すべての画像はひとつの zip アーカイブに収めるべきじゃないだろうか? メイリングリストに質問したり、答えについてディベートしたり、統計をとってみたり、 などなど、多くのプロジェクトで莫大な時間がむだになっている。 でもこれらは本質的な問題じゃないんだ -- そんなことを議論している時間があるなら、 その時間を実際のゲームをコーディングすることに使うべきなんだ。
ここでの教訓は、実際に動く「そこそこの」解決策のほうが、 決してたどりつかない完璧な解決策よりはるかにいいということ。
Pete Shinners のラッパは、格好いいアルファ透過効果や 高速な blit を含んでいる。でもぼくが pygame で一番すきなのは、 低レベルな Rect クラスだと認めなければならないだろう。Rect は ただの長方形だ -- その左上端の位置と、幅と、高さのみによって定義される。 pygame 関数の多くは、rect 値を引数としてとる。それらはまた 「rectスタイル」 -- Rect と同じ値をもつシーケンスのことだ -- も 引数として受けつけている。だからもしぼくが 10,20 から 40,50 の範囲を あらわす rect を定義したければ、以下のどの方法でも同じように定義できる:
rect = pygame.Rect(10, 20, 30, 30) rect = pygame.Rect((10, 20, 30, 30)) rect = pygame.Rect((10, 20), (30, 30)) rect = (10, 20, 30, 30) rect = ((10, 20, 30, 30))
でも、もし最初の 3つの方法を使っているなら、rect のユーティリティ関数を 使うことができる。これらの関数は移動や収縮伸張、2つの rect の和をとったり、 いろんな衝突判定用の機能をふくんでいる。
たとえば点 (x, y) をふんでいるすべてのスプライトのリストが ほしいとしよう -- プレイヤーはそこをクリックしたり、もしかすると 現在の弾がその位置にあったりするのかもしれない。それぞれのスプライトが .rect メンバをもっていたとすると、これはじつに簡単だ -- ただこうするだけ:
sprites_clicked = [sprite for sprite in all_my_sprites_list if sprite.rect.collidepoint(x, y)] (訳注: sprites_clicked = filter(lambda sprite: sprite.rect.collidepoint(x, y), all_my_sprites_list) というやり方もあります)
Rect クラスは、それを surface やその他のグラフィック関数の 引数としてとれるということ以外、これらにはなんの関係もない。 グラフィックスとはなんの関係もないが、それでも長方形を定義したい ところで使うことだってできる。Rect を使うなんて思いもしなかったような プロジェクトでも、rect を使える場所がいくつかみつかっている。
というわけで、めでたくスプライトを動かすことができたら、 次に必要なのはそいつらが互いにぶつかっているかどうかを知ることだ。 次のようなものを書くのは、心をそそられることだろう:
ほかにもスプライトどうしのマスクの AND をとったり、 いろいろな方法はあるが、いずれにせよこれを Pygame でやると たぶんあまりにも遅すぎて、どうしようもなくなるだろう。 ほとんどのゲームでは、たぶん「rectの部分的な衝突判定」をやるだけのほうがいい -- 実際の画像サイズよりやや小さめの rect をつくり、それを 衝突判定に使うようにする。このほうがずっと高速だし、 ほとんどの場合プレイヤーは多少いいかげんな判定でも気がつかない。
Pygame のイベントシステムにはやや注意が必要だ。 入力デバイス (キーボード、マウスあるいはジョイスティック) が どうなっているかを知るには、実際には 2つのやり方がある。
最初の方法は、入力デバイスの状態を直接チェックすることだ。 これは、たとえば pygame.mouse.get_pos() や pygame.key.get_pressed() なんかを呼ぶことで実現できる。 これは その関数を呼んだ時点での 入力デバイスの状態を 教えてくれるだろう。
2番目の方法は SDL のイベントキューを使うことだ。 このキューはイベントのリスト -- イベントが 検出されると、リストに追加される -- になっている。 そしてこれらのイベントは取り出されるとリストから消える。
どちらのやり方にも長所と短所がある。 自分で状態をチェックする方法 (最初の方法) は正確さが売り物だ -- 入力が与えられた時刻を正確に知ることができる。もし mouse.get_pressed([0]) が 1 ならば、これはマウスの左ボタンが まさにこの瞬間に 押されていることを 意味している。イベントキューの場合は、ただマウスボタンが過去のある時点で 押されたということを教えてくれるだけだ。だからもしキューを 頻繁にチェックしているなら OK だけど、コードの他の部分を実行しているせいで チェックがおくれると、入力のずれは大きくなる。状態をたえずチェックする 方法のもうひとつの利点は、“和音の検出” が簡単になることだ。 つまり、いちどに複数の状態をチェックできる。もし T キーと F キーが 同時に押されているかどうかを知りたいなら、ただこうすればいい:
if (key.get_pressed[K_t] and key.get_pressed[K_f]): print "いよっ!"
しかしキューを使っている場合、それぞれのキー入力は 完全に独立したイベントとしてキューにたまる。だからまず T キーが 押されたことを覚えておいても、F キーがくるまではわからない。 すこし複雑だ。
けれどもこの状態チェック法にはひとつの大きな欠点がある。 その関数が呼ばれた瞬間の入力デバイスの状態しか報告されないのだ。 だから、もし mouse.get_pressed() が呼ばれる直前に ユーザがマウスボタンを押して、離したとしたら、マウスボタンは 0 を返す -- つまり get_pressed() はボタンが押されたのを完全にとり逃してしまうのだ。 でもイベントキューならこれら 2つのイベント、MOUSEBUTTONDOWN と MOUSEBUTTONUP は確実にキューに残り、取り出されて処理されるのを待ってくれる。
ここでの教訓はこうだ: 自分の要求に合った方法を選ぼう。 もし自分のループでやることがそんなにない -- たとえば ただ 'while 1' ループの中なんかにすわって、入力を 待つだけなら get_pressed() やら何やらの状態チェック関数を 使えばいい。入力の遅れは大きくはならない。いっぽう、 すべてのキー入力がとても重要な意味をもつが、入力の遅れは重要では ないとき -- ユーザがテキストボックスに何かタイプしたりするときなんかは、 イベントキューを使おう。いくつかのキー入力はやや遅れるかもしれないが、 とにかく全部受けとることはできる。
ついでに event.poll() と wait() の違いについて。 poll() はプログラムをブロックしないので、こちらのほうが 入力を待っているあいだプログラムを中断させてしまう wait() よりもよさそうに見えるかもしれない。けれども poll() が 走っているあいだは使用可能な CPU 時間の 100% を消費してしまう。 そしてこれはイベントキューを NOEVENTS で埋めつくすことになるだろう。 set_blocked() を使って、必要なイベントだけを取るようにしよう -- こうすればイベントキューはずっと管理しやすくなる。
これら 2つのテクニックについては大きな混乱があるみたいだ。 たぶんそれは使われている用語からきていると思う。
“カラーキー (Colorkey) blit” というのは、ある画像中で特定の色をもつピクセルは それがなんであれすべて透明とみなせ、と pygame に教えるものだ。 これら透明ピクセルはその画像の他の部分が blit されるときには blit されない。 だから背景を塗りつぶさないですむ。長方形以外の形をしたスプライトは こうやって作っている。これには、ただ surface.set_colorkey(color) を 呼ぶだけだ。ここで color は RGB 値のタプル -- たとえば (0,0,0) とか -- になる。これでもとの画像中の黒いピクセルはすべて、黒のかわりに透明になるだろう。
“アルファ (Alpha)” はこれとは別のものだ。これには 2つの味つけが存在する。 ひとつは「画像ごとのアルファ」で、これは画像のすべてに適用されるアルファ属性だ。 たぶんこちらが望むほうだろう。アルファ属性は正しくは「半透明」として知られ、 描画に使う画像を やや 不透明にする。たとえば、ある surface の アルファ値を 192 にしてこれを背景に blit すると、各ピクセル色の 3/4 は描画した元絵の色が混ざり、のこりの 1/4 は背景の色が混ざったものになる。 カラーキーとアルファは一緒に使えることを覚えておこう -- これはある画像を、部分的には完全に透明にし、部分的に半透明にできる。
アルファのもうひとつの味つけである「ピクセルごとのアルファ」は もっとこみ入ってる。基本的には、描画に使う画像の各ピクセルが それぞれのアルファ値を 0〜255 の範囲でもつようになる。 だからこれを背景に blit したとき、各ピクセルが異なる不透明さをもつように できるのだ。このタイプのアルファ属性はカラーキー blit と一緒に 使うことはできないし、画像ごとのアルファを上書きしてしまう。 ピクセルごとのアルファがゲームに使われることはほとんどなく、 これを使うためには使う画像を、 特別な「アルファチャンネル」をもつグラフィックエディタで 保存しておかなければいけない。これはとにかく複雑だ -- まだ使うのはよそう。
最後の注意 (でもこれがどうでもいいわけじゃないよ、 ただ最後に来ただけ)。Pygame は SDL のとっても軽いラッパで、 これはまた、お使いの OS のグラフィックス用のとっても軽いラッパでもある。 いままでに書いてきたようなことをやって、それでもまだコードが遅いとしたら、 問題は Python でデータを扱うそのやり方にあるということが多い。 ある種の書き方は、Python では何をやるにも遅くなってしまうことがある。 でもラッキーなことに、Python はじつにクリアーな言語なんだ -- もしコードの一部が不自然に見えたり、重そうに見えたりしたら、 まだスピードアップがのぞめるということ。 Python Performance Tips を読んで、どうやって自分のコードのスピードアップをはかるかの参考にしよう。 この文書では、よく練られていない最適化は諸悪の根源だ、と述べられている。 もし、ただ十分には速くないという程度なら、さらに速くしようとして コードをひねりまわすのはやめよう。なるべくしてはならないこともあるんだよ :)
さあこれで、もうきみはぼくが pygame について知っている 実用的な知識はすべて身につけたわけだ。あとはゲームを書くだけだ!
David Clark は熱心な pygame ユーザで、コミュニティによって作成された Python ゲームの展示サイト Pygame Code Repository の編集者でもある。 また彼は標準的な pygame アーケードゲーム Twitch の作者でもある。
訳: Yusuke Shinyama