Pygame チュートリアル

たすけて! 絵を動かすにはどうやるの?

(Help! How Do I Move An Image?)

by Pete Shinners
pete@shinners.org

Revision 1.2, August 20, 2002

プログラミングやグラフィックスに慣れていない多くの人々にとって、 絵 (image) を画面上で動かす方法を理解するのはえらく大変です。 ぜんぶの概念を学ぶまでは、これは非常にややこしく見えるでしょう。 でも、ここでつまづくのはあなただけではありません。 ここではそれをどうやって実現するかについて、なるべく順を追って説明していきます。 最終的には、自分のアニメーションをどうやって効率化するか、 というところにまでもっていきたいと思います。

この文書は、python のプログラミングを説明しているわけではありません。 ただ pygame でのいくつかの基本的な要素を説明しているだけなので注意してください。

画面はただのピクセル

Pygame には display surface (ディスプレイ サーフェイス) があります。 これは基本的には画面上にうつっている絵で、複数のピクセルからできています。 これらのピクセルを塗り変える方法は、おもに blit() 関数を呼ぶことです。 これはある絵からべつの絵にピクセルを複写します。 (訳注: Jargon File によれば、 blit とは 画像処理などで大量のデータをいちどに転送する操作をさす。 日本語では「貼りつける」ぐらいの意味か)

最初に理解すべきことは、ここです。あなたが画面に絵を blit するとき、 あなたがやっていることは、単なる画面上のピクセルの色を変更することです。 ピクセルそのものは、新しく加えることも動かすこともできません。 私たちはすでに画面上にあるピクセルを塗り変えているだけなのです。 Pygame では画面上に blit するこれらの画像もまた surface なのですが、 この surface を display surface と結びつけるすべはありません。 「これらを画面上に blit する」というのは、その内容を画面に複写するということなのです。 それでも、もとのオリジナルな画像はそのまま持っていることができます。

たぶんこの簡単な説明で、絵を「動かす」には何が必要か わかっていただけるでしょう。実際には、私たちは何も動かしたりはしません。 私たちはただ単に画像を新しい位置に blit しているだけなのです。 でも、新しい位置に絵を貼る前に、まず古い位置の絵を「消す」必要があります。 さもないと画面上には同じ絵が 2つの場所に現れてしまいます。 古い絵を消して、新しい位置にすばやく描くことで、私たちは 絵が動いたように「見せかけている」のです。

以後このチュートリアルでは、この過程をより簡単なステップに分解していきます。 画面上で複数の画像を動かすにはどうやるのが一番いいか、といったことまで扱います。 ここまで読んだ方は、おそらくすでにいくつかの疑問をお持ちになっていることでしょう。 たとえば、どうやってある画像を描画する前に、いまの画像を「消す」のか? というような。 それとも、まだサッパリな状態でしょうか? だいじょうぶ。残りの部分を読めば きっとわかるようになります。

ちょっとひと戻りして

ピクセルと画像の概念は、もしかすると、まだあまりなじみのないものかもしれません。 よろしい、ここにいいニュースがあります。なぜならこれから数節のあいだで使うコードは、 私たちのやりたいことには十分ですが、にもかかわらずピクセルを使っていないからです。 かわりに 6つの数字が入っているちょっとした python のリストをつくることにしましょう。 これが画面に現れるはずの何やらファンタスティックな画像だと思ってください。 あとで実際のグラフィックスを扱いますが、これがいかに実際のグラフィックスと 似ているかを知ったらちょっとびっくりするかもしれません。

では画面のリストをつくって、それを 1 と 2 からなる 美しい背景で埋めつくしましょう。

>>> screen = [1, 1, 2, 2, 2, 1]
>>> print screen
[1, 1, 2, 2, 2, 1]

これで背景はできあがりました。が、この画面にプレイヤーがいなければ 楽しいことは始まりそうにありません。ですからここでわれらが偉大なるヒーローを 登場させましょう。その外見は数字の 8 のような形をしています。エイトマンですな。 こいつを画面の中央ちかくにくっつけてやります。何が起こるでしょうか。

>>> screen[3] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

いまここですぐに pygame のグラフィックスプログラミングの世界に 飛びこんだとしたら、ここまでで得たことはそれとかなり似ています。 画面になにか面白いものを表示できるようにはなったわけです。 しかし、このままでは何も動いてはくれません。でも今ならまだ ただの数字をスクリーンとして使っているので、 もしかすると主人公を動かすのがより簡単かもしれません、よね?

主人公を動かす

このキャラクタを動かす前に、まず彼の位置のようなものを知っている必要があります。 前の節では、主人公を描くのに適当な位置を選んだだけでした。 今度はもうすこし形式的なことをやってみましょう。

>>> playerpos = 3
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

これで、彼を新しい位置に動かすのはずっと簡単になります。 playerpos の値を変えて、単に彼をもういちど画面上に描画してやればいいんです。

>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 8, 8, 2, 1]

おーっと。ヒーローが 2人になっちゃいました。 ひとりは前の位置に、そしてもうひとりは新しい位置にいます。 「古い」位置にいるヒーローを消去しなければならないというのは、まさにこういう 理由からなのです。彼を消しさるには、このリスト中の値をそれまであった数字に 戻さなくてはいけません。つまりこれは、画面上にヒーローを乗せる前の数字も 覚えておかなくてはならないということです。これをやるには何通りかの方法が ありますが、ふつうもっとも簡単なのは背景画面のコピーを別にとっておくことです。 となると、このちっぽけなゲームにはいくつかの改造をほどこす必要がありそうですね。

地図をつくる

ここでやりたいのは、私たちが背景と呼ぶものを簡単なリストとして別にとっておくことです。 もとの画面と同じに見える別の背景を 1 と 2 を使ってつくっておきましょう。 つぎに、この背景の各要素をもとの画面上にコピーします。 そのあと、最終的に我がヒーローをその画面上に描画すればよいのです。

>>> background = [1, 1, 2, 2, 2, 1]
>>> screen = [0]*6                     # 新しい空白の画面
>>> for i in range(6):
...    screen[i] = background[i]
>>> print screen
[1, 1, 2, 2, 2, 1]
>>> playerpos = 3
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 2, 8, 2, 1]

これはかなり余計な作業に見えるかもしれません。 主人公を動かしていた前の段階と比べると、これではちっとも進んでいないようにも見えるでしょう。 がしかし、これで主人公を正しく動かすための情報がそろったことになるのです。

主人公を動かす (第2弾)

ここまでくると、ヒーローを左右に動かすのは簡単です。 まずヒーローのいた元の位置を消去してやります。 これは背景から正しい値を画面上にコピーしてくることによって実現できます。 しかるのちに、私たちの主人公を画面上の新しい位置に描けばよいのです。
>>> print screen
[1, 1, 2, 8, 2, 1]
>>> screen[playerpos] = background[playerpos]
>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 1, 8, 2, 2, 1]

こうなりました。主人公は 1マス左に動いています。 同じコードをもう一回くり返せば、さらにもう 1マス左に動かすことができます。

>>> screen[playerpos] = background[playerpos]
>>> playerpos = playerpos - 1
>>> screen[playerpos] = 8
>>> print screen
[1, 8, 2, 2, 2, 1]

すばらしい! これはいわゆるスムースアニメーションと呼べるようなものではありません。 が、いくつかの小さな変更をおこなうことで、この作業を直接 画面上のグラフィックスに対してもおこなうことができます。

"blit" の定義

次の節では、リストから現実の画面を使うようプログラムに 変更をくわえます。これからグラフィックスを表示するさいには、 blit という用語がひんぱんに使われることになりますが、 グラフィックスを使うのがはじめてなら、この用語にもおそらく なじみがないかもしれません。

BLIT: 「あるデータを blit する」とは、基本的には ある画像イメージから別のイメージへと図形をコピーすることです。 より厳密な定義をいえば、これはあるデータの配列を、 ビットマップ配列の転送先へとコピーする、ということになります。 単に blit をピクセルの 「代入」 と考えてもかまいません。 ちょうど上で画面リストに値を代入していたように、 blit は画像のピクセルの色を別の画像に代入します。

他の画像処理ライブラリでは bitblt あるいはたんに blt という 用語が使われている場合もありますが、同じ意味です。 これも基本的にはメモリのある場所から別の場所に内容をコピーすることをさしています。 実際には、これは単なるメモリのコピーよりもいくらか高度なこと -- たとえばピクセル形式を変換したり、切り取ったり、 スキャンラインの勾配を計算したり -- を含んでいます。 より高度な blit では透過やその他の特殊効果といったことも処理されます。

リストから画面へ

上の例でみたコードをとってきて、それを pygame で動くようにするのは じつにストレートなやり方でできます。ここで私たちはすでにいくつかの すてきなグラフィックスを読み込んでおり、それに "terrain1"(地形1)、 "terrain2"(地形2)、 および "hero"(ヒーロー!) と名前をつけたものとしましょう。さっきリストに番号を 代入していた部分で、こんどは画面にグラフィックスを blit するのです。 もうひとつの大きな変更は、単一の配列インデックス (0 から 5) によって 位置を表現するかわりに、こんどは 2次元座標を使うことです。 ここでは私たちのゲームで使うグラフィックスがおのおの 10ピクセルの 大きさを持っているとします。
>>> background = [terrain1, terrain1, terrain2, terrain2, terrain2, terrain1]
>>> screen = create_graphics_screen()
>>> for i in range(6):
...    screen.blit(background[i], (i*10, 0))
>>> playerpos = 3
>>> screen.blit(playerimage, (playerpos*10, 0))

どうですか? このコードはなにやら非常になじみ深いものに見えるでしょう。 そして (願わくば) より重要なことですが、上にあげたコードは意味がとれそうに思えませんか? さっきリストに値をセットする例で説明したのが、画面に (blitをつかって) ピクセルを セットするのと同じように見えるとよいのですが。ここでほんとに増えている作業は プレイヤーの位置を画面上の座標に変換する部分です。いまのところ、ここでは "(playerpos*10, 0)" という荒っぽいやり方を使っていますが、もっと スマートにやる方法ももちろんあります。ではつぎにプレイヤーの絵を この画面上で動かしてみましょう。次にあげるコードにはもはや何の驚きもないはずです:

>>> screen.blit(background[playerpos], (playerpos*10, 0))
>>> playerpos = playerpos - 1
>>> screen.blit(playerimage, (playerpos*10, 0))

さてさて、以上のコードで私たちは簡単な背景上にどうやってヒーローの絵を表示するかが わかりました。そのヒーローをどうやって 1キャラクタ分左へ適切に動かすかも わかりました。では次に何をしましょうか? ええと、このコードはまだちょっとばかし ぶかっこうですので、まずは背景と、プレイヤーの座標を表現する もうすこしきれいなやり方を追求しましょう。そしたらつぎに、ちょっぴりスムーズで、 よりリアルなアニメーションへと進むことにします。

画面の座標

ある物体を画面上に配置するには、blit() 関数にたいして その画像を画面上のどこに置くかを指定する必要があります。 Pygame では、私たちはつねに位置を (X,Y) という座標で渡しています。 これは順に画面の右に向かって何ピクセル分かということと、画面の下に向かって 何ピクセル分かということを示しています。ある surface の左上隅は (0, 0) という 座標で表され、すこし右に行けば (10, 0) になり、そこからさらにすこし下に行くと (10, 10) になるわけです。blit するときは、座標の引数はつねに その転送元イメージの左上隅が転送先のどの部分に貼りつけられるべきであるかを 指定しています。

Pygame には Rect という、これらの座標を扱うための便利な“容れ物”があります。 Rect はこれらの座標にかこまれた四角い領域を表現するもので、 これは左上隅の座標と大きさをもちます。Rect にはたくさんの便利な メソッドがあって、これらは Rect を移動したり配置したりするのを補助できます。 つぎの例では、物体の位置を表すのに Rect を使ってみることにしましょう。

Pygame の多くの関数が Rect を引数としてとることは覚えておいて損はありません。 また、これらの関数に対して、簡単な 4要素のタプル (左, 上, 幅, 高さ) を渡すこともできます。 いつもいつも Rect オブジェクトを使えというわけではありませんが、 ふつうおそらくそうしたいと思うことでしょう。また、blit() 関数もその位置指定に Rect を使うことができます。この場合、Rect はその左上隅の座標が実際の位置として使われます。

背景を変える

いままでのセクションでは、背景をいろいろなタイプの地形としてリストに格納してきました。 これはキャラクタベースのゲームを作るにはよい方法ですが、 私たちはスムースなアニメーションをやりたいのです。これをすこし簡単にするため、 ここでは背景を、画面全体をおおう単一の画像にすることにしましょう。 こうしておけば物体を (再描画するまえに) 「消去」したいとき、 私たちはたんに消したい部分の背景を画面上に blit するだけですみます。

blit の 3番目の引数はオプション引数ですが、これに Rect を渡してやると blit に転送元イメージのある特定の部分だけを転送させるようにできます。 以下の例をみればこれを使ってどのようにプレイヤー画像を消去しているかが わかるでしょう。

それから、つぎのことも覚えておいてください。 今後画面上になにかを描き終わったときには、pygame.display.update() を 呼ぶようにします。こうすることで、今までに画面上におこなったいっさいの 描画が目に見える形で現れます。

なめらかな動き

なにかをなめらかに動いているように見せるには、それを一度に 2、3ピクセルずつ動かす必要があります。以下のコードはある物体が スクリーンを横切ってなめらかに動くようにするものです。 今までに覚えたことを元にしていますから、このコードはじつに簡単に見えることでしょう。

>>> screen = create_screen()
>>> player = load_player_image()
>>> background = load_background_image()
>>> screen.blit(background, (0, 0))       # 背景を描く
>>> position = player.get_rect()
>>> screen.blit(player, position)         # プレイヤーを描く
>>> pygame.display.update()               # いっさいがっさいを表示
>>> for x in range(100):                  # 100フレームぶんをアニメーション
...    screen.blit(background, position, position) # 消す
...    position = position.move(2, 0)     # プレイヤー動かす
...    screen.blit(player, position)      # 新しいプレイヤー描画
...    pygame.display.update()            # いっさいがっさいを表示
...    pygame.time.delay(100)             # 1/10秒だけ待つ

さあこれでできました。これが画面上で物体をスムースにアニメーションさせるために 必要なコードのすべてです。すてきな背景を使うことすらできるでしょう。 背景をこのように扱うもうひとつの利点は、プレイヤーの画像を透明にしたり 一部を切り抜いたりしても、依然として正しく背景の上に表示できることなんです (ボーナスとして)。

また、上のコードではループの最後で pygame.time.delay() に制御を投げています。 これはプログラムを若干スローダウンさせます。これがないと物体はあまりに 速く走り去りすぎてよく見えないかもしれません。

で、おつぎは?

ここまでたどりつきました。 願わくばこれでこの記事が約束していたすべてのことができているといいのですが。 でも現時点では、まだこのコードが次のベストセラー・ゲームとなるようには ちょっと見えませんね。いったいどうやって複数の物体を動かせばいいのでしょうか? プレイヤー画像を読み込むための load_player_image() みたいな謎の関数って いったい何なんでしょう? それから、ユーザの入力を受けつけるような 簡単な方法も必要です。ループも 100フレームぶんどころじゃききませんね。 ではこれからこの例をつかって、おやじやおふくろにも喜んでもらえるような オブジェクト指向的創造、とでもいうものをやってみることにしましょう。

はじめに謎な関数ありき

ここで述べる関数についての完全な情報は、ほかのチュートリアルや リファレンスマニュアルを参照してください。pygame.image モジュールは load() という関数をもっていて、これが私たちのやりたいことをやってくれます。 画像を読み込む行は次のように置きかえます。

>>> player = pygame.image.load('player.bmp').convert()
>>> background = pygame.image.load('liquid.bmp').convert()

簡単でしょう。load 関数はただファイル名をとって、読み込まれた画像をもつ 新しい surface を返すだけです。読み込んだあとは、その surface のメソッド convert() を呼び出しています。これはその画像の surface を返すのですが、 そのピクセルフォーマットは画面と同じものに変換されています。 画像イメージを画面と同じピクセルフォーマットにしておくと、それらはより高速に blit することができるのです。もし convert を実行しないと、blit() 関数は 遅くなるでしょう。なぜならこれは実行するたびにある型のピクセルを別の型に 変換する必要が生じるからです。

また、load() と convert() がどちらも新しい surface を返すのに気がついた人も いるかもしれません。どちらの行も、おのおのが本当に 2つの surface を作っているのです。 他のプログラミング言語では、これはメモリリークを起こすことがありますが (それはよいことではありません)、ラッキーなことに Python はこれをうまく処理してくれますから、 pygame はたとえ私たちがある surface を使わなくてもちゃんと後片付けをしてくれます。

上の例にあったもうひとつの謎な関数は create_screen() でした。 Pygame では、新しいグラフィックス用のウインドウを作るのはいとも簡単です。 640x480 の surface をつくるコードは以下のようなものです。 これ以外の引数を渡さなければ、pygame は私たちにとって最適な色深度と ピクセルフォーマットを勝手に選んでくれます。

>>> screen = pygame.display.set_mode((640, 480))

入力を処理する

さて、私たちはメインループに否が応でも変更を加えなければなりません。 なんらかのユーザ入力 (ユーザがウインドウを閉じたりなど) を受けつけるためです。 そのためには「イベント処理 (event handling)」を私たちのプログラムに加える必要があります。 グラフィカルなプログラムは、すべてこのイベントを基にした設計 (Event Based design) を 使用しています。プログラムは「キーが押された」とか「マウスが動いた」などのイベントを コンピュータから受けとり、そしてそれらの異なるイベントに反応します。 以下はそういったコードの外観です。100 フレームぶんのループをするかわりに、 私たちはユーザが停止を命ずるまで待ちつづけることにするのです。

>>> while 1:
...    for event in pygame.event.get():
...        if event.type in (QUIT, KEYDOWN):
...            sys.exit()
...    move_and_draw_all_game_objects()

このコードがやっていることは、まず最初に無限ループがあって、 そこでユーザから何らかのイベントがあったかどうかをチェックしています。 ユーザがキーを押すか、そのウインドウの「閉じる」ボタンを押すと プログラムは終了します。イベントをすべてチェックしたら、 ゲームの物体を動かしたり表示したりします。 (動かす前に消去もしなければなりませんが。)

複数の絵を動かす

ここからが実際にコードを変更していくところです。 ここでは、10個の異なる絵を画面上で動かすとしましょう。 これを処理するのによい方法は、Python のクラスを使うことです。 ゲーム用の物体をあらわすクラスをつくってやることにしましょう。 このオブジェクトはそれ自身を動かす関数をもっており、 私たちはこれを好きなだけ作ることができます。 オブジェクトを描画し移動させるための関数は、いちどに 1フレーム (あるいは 1ステップ) ぶんだけ動かすように作らなくてはなりません。 このクラスを作成する Python のコードは以下のようになります。

>>> class GameObject:
...    def __init__(self, image, height, speed):
...        self.speed = speed
...        self.image = image
...        self.pos = image.get_rect().move(0, height)
...    def move(self):
...        self.pos = self.pos.move(0, self.speed)
...        if self.pos.right > 600:
...            self.pos.left = 0

このクラスには 2つの関数があります。init 関数はオブジェクトを 構築するときに使うコンストラクタであり、これは物体の位置と速度を設定します。 move メソッドは物体を 1ステップだけ動かします。もしこれが行き過ぎた場合、 この関数は物体を左端の位置に戻します。

いっさいがっさいをまとめる

さあ、これで新しい物体クラスができました。これをゲーム内に組み込んでやりましょう。 私たちのプログラムにおける main 関数は次のようなものになります。

>>> screen = pygame.display.set_mode((640, 480))
>>> player = pygame.image.load('player.bmp').convert()
>>> background = pygame.image.load('background.bmp').convert()
>>> screen.blit(background, (0, 0))
>>> objects = []
>>> for x in range(10): 		# 10個つくる
...    o = GameObject(player, x*40, x)
...    objects.append(o)
>>> while 1:
...    for event in pygame.event.get():
...        if event.type in (QUIT, KEYDOWN):
...            sys.exit()
...    for o in objects:
...        screen.blit(background, o.pos, o.pos)
...    for o in objects:
...        o.move()
...        screen.draw(o.image, o.pos)
...    pygame.display.update()
...    pygame.time.delay(100)

これで終わりです。このコードでは、10個のオブジェクトを画面上でアニメーションさせています。 唯一説明が必要な箇所は、すべてのオブジェクトを消して、すべてのオブジェクトを 描画する 2つのループでしょう。これを正しくやるためには、描画する前に すべてのオブジェクトを消しておく必要があるのです。上のサンプルでは これはどちらでも関係ないように見えるかもしれまんが、物体が互いに重なりあっているような場合、 2つのループをこのようにすることは重要になります。

これ以後はあなた自身で

さて、これからやるべきことはいったい何でしょうか? まずは、この例でしばらく遊んでみることをおすすめします。 ここであげた例の完全に動くバージョンは、 pygame の examples ディレクトリにあります。 これは "moveit.py" と呼ばれるファイルで、ぜひともこれに目を通して、 いじって走らせてみてください。

このあとやりたくなるのは、たぶん 1つ以上の異なった種類の オブジェクトを使ってみることかもしれません。 オブジェクトをもう表示したくないときに、 それらをきれいに「抹消する」方法をみつけたくなるかもしれません。 または、画面上で変化した領域のリストを display.update() に渡すことによって 画面を更新するようなことかもしれません。

Pygame には他にもこれらの問題をあつかっているチュートリアルやサンプルがあります。 なので、学べるときには学びつづけましょう :-)

最後に。いつでも pygame のメイリングリストかチャットルームに来て 質問してください。この手のことで力になってくれる人々がかならずいますから。

それと最後の最後に。どうか楽しんでください。ゲームってそのためにあるんですから。


訳: Yusuke Shinyama