Pygame チュートリアル

Chimp 徹底解説

(Line by Line Chimp)

by Pete Shinners
pete@shinners.org

Revision 2.1, January 9, 2001


はじめに

ここでは pygame用の "chimp (チンパンジー)" なるゲームの例を紹介しましょう。 このサンプルは、せまいスクリーンの中を動きまわる猿をパンチすれば 富と繁栄が約束される、というものです。例自体はいたってシンプルで、 エラーチェックなどは貧弱です。サンプルプログラムでは、pygame で 可能なこと -- グラフィックウインドウを作ったり、画像やサウンドを 読み込んだり、TTF (TrueTypeフォント) テキストを描画したり、基本的な イベント処理やマウス処理をしたり -- を紹介します。

ここで使っているプログラムと画像は、 pygame の標準ソースパッケージに含まれています。 pygame バージョン 1.3 ではこのサンプルは完全に書き直されており、 2つの機能が追加され、エラーチェックも修正されました。 もともとの例に比べてサイズは倍近くになっていますが、 前よりもずっと見るべきところが多くなっていますし、 他のプロジェクトでも再利用してもらえそうなコードもあります。

チュートリアルはコードのブロックごとに進め、 おもにそのコードがどのように動くかを解説していきます。 また、どうすればそれを改良することができるかといったことや、 エラーチェックがあるとどのように助かるかといったことも説明します。

これは pygame のコードを初めて見る人にとってはいいサンプルとなるでしょう。 pygame をインストールしてあれば、examples ディレクトリ中にある chimp のデモを自分で走らせてみることもできます。
(これはバナー広告じゃありませんよ、スクリーンショットです)
Chimp のスクリーンショット
全ソースコード

モジュールを import する

これはプログラムに必要なすべてのモジュールを import するコードです。 ここではまた、いくつかの pygame モジュールが使用可能かどうかもチェックしています。
import os, sys
import pygame
from pygame.locals import *

if not pygame.font: print '警告: フォントが使えません'
if not pygame.mixer: print '警告: サウンドが使えません'

さいしょに、標準的な python モジュールの "os" と "sys" を import します。 これらはプラットフォームに依存しないパス名を処理するときなどに役立ちます。

次の行で pygame パッケージを import しています。 pygame を import すると、これは pygame に属するモジュールすべてを import します。いくつかの pygame モジュールはオプションで、 もしこれらが見つからないときには、それらの変数名には "None" が入ります (訳注: python ではモジュール名も変数のひとつ)。

さて、pygame モジュールには "locals" という特別なモジュールが 存在します。このモジュールは pygame のサブセットを含んでおり、 それらのメンバはよく使われる定数や関数があります。これらの名前は お使いのプログラムのグローバル名前空間に入れておくと便利だということが わかっています。locals モジュールには矩形オブジェクトを作成するための "Rect" モジュールや、pygame の残りの部分とやりとりするのに 必要な "QUIT" "HWSURFACE" などの定数が含まれています。 locals モジュールをグローバル名前空間に import するのは完全にあなた次第です。 もし import しないことにしても、locals のすべてのメンバ名は pygame モジュール中ですでに使用可能になっています。

さいごに、pygame にフォントやサウンドモジュールがない場合に ちょっとした警告メッセージを出すようにしておきます。

素材を読み込む

ここでは画像とサウンドを読み込む、2つの関数を紹介しましょう。 それぞれの関数を個々にみていくことにします。
def load_image(name, colorkey=None):
    fullname = os.path.join('data', name)
    try:
        image = pygame.image.load(fullname)
    except pygame.error, message:
        print '画像を読み込めません:', name
        raise SystemExit, message
    image = image.convert()
    if colorkey is not None:
        if colorkey is -1:
            colorkey = image.get_at((0,0))
        image.set_colorkey(colorkey, RLEACCEL)
    return image, image.get_rect()

この関数は、読み込む画像の名前を引数にとります。 またオプションとして画像にカラーキー (colorkey) を指定することもできます。 カラーキーは、その画像の透明色を表現するのに使われます。

この関数では最初にファイルのフルパス名を作成します。 今回は、すべての素材は "data" サブディレクトリに 格納されていることになっています。os.path.join 関数を使えば、 ゲームが動いているプラットフォームに関係なく正しいパス名を生成できます。

つぎに pygame.image.load 関数をつかって画像を読み込みましょう。 ここではこの関数を try/except ブロックで囲むことにします。 これで、もし画像の読み込み中に問題が起こってもきれいに抜け出すことができます。 画像が読み込まれたら重要な関数呼び出し convert() をおこないます。 これは Surface (サーフェイス) のコピーを新規に作成し、それを現在のディスプレイの深度に あうように変換します。つまり画像を画面にはりつける (訳注: blitする) のを できるだけ速くするわけです。

最後に画像のカラーキーを指定しましょう。 もしユーザが引数 colorkey に値を指定していたら、その値を 画像のカラーキーとして使います。この色はふつう (255, 255, 255) のような RGB で指定されます。colorkey には -1 を指定することもできます。 この場合、関数は画像のいちばん左上にあるピクセルを見て、 その色をカラーキーとして使います。
def load_sound(name):
    class NoneSound:
        def play(self): pass
    if not pygame.mixer:
        return NoneSound()
    fullname = os.path.join('data', name)
    try:
        sound = pygame.mixer.Sound(fullname)
    except pygame.error, message:
        print 'サウンドを読み込めません:', wav
        raise SystemExit, message
    return sound

おつぎはサウンドを読み込む関数です。 この関数が最初にやることは、pygame.mixer モジュールが正しく import されているか どうかをチェックすることです。もしされていなければ、関数は ダミーの play メソッドをもつ小さいクラスインスタンスを返します。 このゲームにとっては、これはふつうの Sound オブジェクトとほとんど 同様にふるまい、余計なエラー処理をつけずに走らせることができます。

この関数は画像読み込みの関数と似ていますが、 いくつか異なる問題を扱います。最初にサウンドイメージのフルパス名を 生成したあと、try/except ブロックの中でサウンドファイルを読み込みます。 そのあと読み込んだ Sound オブジェクトを単に返すのです。

ゲームオブジェクトのクラス

ここで、私たちのゲームで使うオブジェクトを表現する 2つのクラスを つくりましょう。このゲームで使われるロジックのほとんどが これら 2つのクラスに押し込められています。 ここでは両方いっぺんに見ることにしましょう。
class Fist(pygame.sprite.Sprite):
    """マウスに従って、画面上の固いこぶしを移動させる"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) # Sprite の初期化を呼びだす
        self.image, self.rect = load_image('fist.bmp', -1)
        self.punching = 0

    def update(self):
        "こぶしをマウス座標に従って移動させる"
        pos = pygame.mouse.get_pos()
        self.rect.midtop = pos
        if self.punching:
            self.rect.move_ip(5, 10)

    def punch(self, target):
        "こぶしが target と重なっていたら真を返す"
        if not self.punching:
            self.punching = 1
            hitbox = self.rect.inflate(-5, -5)
            return hitbox.colliderect(target.rect)

    def unpunch(self):
        "こぶしを元に戻すときに呼ばれる"
        self.punching = 0

ここではプレイヤーのこぶし (fist) を表現するクラスをつくります。 このクラスは pygame.sprite モジュールの Sprite クラスから 派生させます。クラスの新しいインスタンスが作成されると、 __init__ 関数が呼ばれます。まず最初にすることは、かならず基底クラスの __init__ 関数を呼びだすようにすることです。Sprite の __init__ 関数は そのオブジェクトをスプライトとして扱えるように準備するものです。 このゲームでは、Sprite を描画する Group クラスのひとつを使っています。 これらのクラスは、"image" および "rect" 属性をもつ sprite を 描画できます。これらの属性を変更するだけで、レンダラは現在の画像を 現在位置に描画しなおします。

すべての sprite は update() メソッドを持っています。 この関数はふつう 1フレームごとに一回呼ばれ、ここには その sprite を動かしたり、変数を更新したりするためのコードが 入ります。こぶし用の update() メソッドは、こぶしをマウスポインタの 位置に動かします。またこれは、「パンチしている」状態のときに こぶしの位置を若干ずらす、といったこともやっています。

そのあとの 2つの関数 punch() と unpunch() は こぶしのパンチ状態を変えます。また、punch() メソッドは もし与えられた sprite とこぶしが重なっていた場合 true を返します。

class Chimp(pygame.sprite.Sprite):
    """画面じゅうを猿ちゃんを動かす。猿がパンチされたらグルグルさせる。"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) # Sprite の初期化を呼び出す
        self.image, self.rect = load_image('chimp.bmp', -1)
        screen = pygame.display.get_surface()
        self.area = screen.get_rect()
        self.rect.bottomright = self.area.bottomright
        self.move = 9
        self.dizzy = 0

    def update(self):
        "猿の「ぐるぐる状態(dizzy)」によって、移動させるかグルグルするかを変える"
        if self.dizzy:
            self._spin()
        else:
            self._walk()

    def _walk(self):
        "猿を画面の端から端まで移動させる、端まで来たら向きをかえる"
        newpos = self.rect.move((self.move, 0))
        if not self.area.contains(newpos):
            self.move = -self.move
            newpos = self.rect.move((self.move, 0))
            self.image = pygame.transform.flip(self.image, 1, 0)
        self.rect = newpos

    def _spin(self):
        "猿の画像をグルグルさせる"
        center = self.rect.center
        self.dizzy += 12
        if self.dizzy >= 360:
            self.dizzy = 0
            self.image = self.original
        else:
            rotate = pygame.transform.rotate
            self.image = rotate(self.original, self.dizzy)
        self.rect = self.image.get_rect()
        self.rect.center = center

    def punched(self):
        "これが呼ばれると猿はグルグルしだす"
        if not self.dizzy:
            self.dizzy = 1
            self.original = self.image

Chimp (チンパンジー) クラスは Fist (こぶし) クラスに比べると すこし長いですが、複雑さは大して変わりません。このクラスは 画面にそって猿を左右に動かします。猿がパンチされると、彼は エキサイティングな効果を出すためにぐるぐる回ります。 このクラスも同様に Sprite クラスを基底としており、 Fist クラスと同じやり方で初期化されます。 また初期化するさいに、"area" 属性をディスプレイと同じサイズに設定しています。

Chimp の update 関数は、ただ単に現在「ぐるぐる状態 (dizzy) かどうか」を 見ているだけです。この状態は猿がパンチされてぐるぐる回っているときに真になります。 この関数は _spin あるいは _walk メソッドのどちらかを呼び出します。 関数がアンダースコアで始まっているのは、これらが Chimp クラスの中だけで 使われるべきであるということを示す Python の標準的な書式です。さらに進んで アンダースコアを 2個つけることもできます。こうすれば Python はそれらを本当の private メソッドにしたでしょうが、まあここではそこまでの保護は必要ないでしょう。:)

ここでの walk メソッドは現在の rect を与えられたオフセットだけ 動かすことによって、新しい猿の位置を生成します。もしこの位置が 画面からはみ出してしまうものだった場合、これは移動オフセットを反転させます。 また、このとき pygame.transform.flip 関数を使ってイメージも反転させます。 これは猿が進む向きを変えているように見せかける、荒けずりのやり方といえるでしょう。

Spin メソッドは猿が現在「ぐるぐる状態 (dizzy)」であるときに 呼び出されます。dizzy 属性は現在の回転量を格納するのに使われます。 猿がまるごと一回転 (360度) したあと、この関数は猿を回転していない 元の状態に戻します。transform.rotate を呼ぶ前に、この関数を ただの "rotate" というローカル変数に束縛しているのがわかるでしょう。 これはべつに必要というわけではないのですが、ただ単に次の行を 短くするために使われています。rotate 関数を呼ぶときは 次のことに注意してください。ここではつねにオリジナルの猿の画像を 回転させています。画像は回転させるとわずかに質がおちます。 同一の画像をくりかえし回転させると、画質はそのたびにだんだん 悪くなっていきます。また、画像を回転させると、その画像サイズは 変わります。これは画像のカドが横へはみ出てしまうためで、 その結果、画像が元より大きくなってしまうというものです。 新しい画像の中心が、かならず古い画像の中心と一致するようにしなくてはいけません。 動かず、回転だけをするようにね。

最後のメソッド、punched() は、その Sprite に「ぐるぐる状態」に 突入するよう指示するものです。これはその Sprite の画像の回転を 開始させます。またこれは現在のイメージを "original" にコピーしておく ということもやっています。

いっさいがっさいを初期化する

pygame でいろいろやるまえに、まずは必ずそのモジュールを初期化しておく必要があります。 ではプログラムの main() 関数を見てみましょう。実際にはこれがすべてを実行します。
pygame.init()
screen = pygame.display.set_mode((468, 60))
pygame.display.set_caption('Monkey Fever')
pygame.mouse.set_visible(0)

最初の pygame を初期化する行は、ちょっとした仕事をしてくれます。 これは import されている pygame モジュールをチェックし、 その各々を初期化しようと試みます。ここで戻ってモジュールが初期化に 失敗したかどうかをチェックするということもありうるのですが、 ここではそこまで深く考えないことにしましょう。 また、特定のモジュールをそれぞれ手動で初期化することにより、 より多くのことがコントロールできるようになります。これらは 多くの場合必要のないものですが、もしお望みならばそうすることも可能です。

つぎに display グラフィックスモードを設定します。ここで、 pygame.display は display の設定に関するすべてを扱うということに注意してください。 今回の例では簡単な細長いウインドウを使います。グラフィックスモードの 設定については完全に別のチュートリアルが存在しますが、もし気にしないのであれば ふつう pygame は何かうまくいくものを提供してくれます。 ここでは色数 (色深度) を指定していないので、Pygame は最適な色数を選択してくれるでしょう。

最後に display ウインドウのタイトルを設定し、そのウインドウ内では マウスカーソルをオフにします。じつに簡単なものですが、とりあえずこれで 命令を受けつけてくれる小さな黒いウインドウができたわけです。 ふつうマウスカーソルはデフォルトで見えているため、それを不可視にするときでない 限り、わざわざカーソルの状態を設定する必要はありません。

背景を作成

このプログラムでは背景に文字を入れることにします。 これは背景を表示する単一の画像をつくっておき、それをくり返し使えばいいでしょう。 最初のステップは、そのような Surface をつくることです。
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((250, 250, 250))

これは新しい display ウインドウと同じサイズの Surface を作成します。 Surface を作成したあと convert() を呼んでいることに気づかれるでしょう。 convert を引数なしで使うと、この背景は display のウインドウと同じ形式に変換されます。 これはもっとも高速な描画をもたらします。

また、ここでは背景全体を白っぽいベタぬりにしています。 fill は RGB の 3つの値からなるタプルを color 引数としてとります。

背景に文字を書き、中央によせる

さて、これで背景の Surface ができました。ではこれの上に文字を書きましょう。 これをやるのは、pygame.font モジュールが正しく import できたときだけです。 import できないときは、この部分はとばすことにしています。
if pygame.font:
    font = pygame.font.Font(None, 36)
    text = font.render("Pummel The Chimp, And Win $$$", 1, (10, 10, 10))
    textpos = text.get_rect()
    textpos.centerx = background.get_rect().centerx
    background.blit(text, textpos)

ごらんのように、これをやるにはいくつかのステップがあります。 まず font オブジェクトをつくり、それをべつの新しい Surface に描画します。 つぎに新しい Surface の中心座標を得て、それを背景 Surface のうえに はりつけます (訳注: blitします)。

フォントは font モジュールの Font() コンストラクタによって作られます。 ふつうこの関数には TrueType フォントの名前を渡すのですが、None を 渡すこともできます。この場合はデフォルトのフォントが使われます。 また、Font コンストラクタには作成したいフォントのサイズを渡す必要があります。

そしてできたフォントを新しい Surface に描画します。 render 関数はこの文字列に適したサイズの新しい Surface を作成します。 またここでは render に、暗い灰色を使って、 アンチエイリアスされた (なめらかに見える) 文字列を描画するよう指示しています。

つぎにすべきことは、この display における文字列の中心座標をみつけることです。 この文字列の大きさから、"Rect" オブジェクトを作成します。 これで display の中央に文字列を簡単に配置できるようになります。

さいごに、この文字列を背景画像にはりつけます (訳注: blitします)。

設定のあいだディスプレイに背景を表示する

まだ、ディスプレイ上にあるのは黒いウインドウだけです。 他の資源を読み込まれるのを待っている間に、ここで今つくった 背景を表示してやりましょう。
screen.blit(background, (0, 0))
pygame.display.flip()

これは背景全体を display ウインドウに表示します。 blit はもうわかりますよね。でもこの flip という関数は何でしょうか?

pygame では、display 上の Surface に対する変更はすぐ目に見えるようには反映されません。 ふつう、ダブルバッファをもつ (double bufferd) display では、 変更を実際のディスプレイ上に反映するために display をひっくり返す (flip) 必要があります。 flip() 関数はたんにウインドウ領域全体をひっくり返すもので、 シングルバッファ用の Surface にもダブルバッファ用の Surface にも使えるため、 ここではこれでいいでしょう。

ゲーム用オブジェクトを準備する

ここで、ゲーム中で必要なすべてのオブジェクトを作成しておきましょう。
whiff_sound = load_sound('whiff.wav')
punch_sound = load_sound('punch.wav')
chimp = Chimp()
fist = Fist()
allsprites = pygame.sprite.RenderPlain((fist, chimp))
clock = pygame.time.Clock()

まず上で定義した load_sound 関数をつかって音響効果を読み込みます。 つぎにそれぞれの Sprite クラスに対するインスタンスを作成します。 最後にそれらすべての Sprite をふくむ、Sprite の Group を作成します。

実際には、ここでは RenderPlain と名づけられた特別な Sprite Group を 使っています。これを使うと、その Sprite Group がふくむすべての Sprite を 画面に表示することができます。RenderPlain と呼ばれるのは、これがただの Render よりももうすこし高度な機能をもっているからですが、 このゲームでは単純な描画しか使いません。ここではその Group に所属する すべての Sprite のリストを渡して "allsprites" と呼ばれる Group をつくっています。 あとから Group に Sprite を足したり引いたりすることもできますが、 このゲームでは使いません。

clock オブジェクトはフレームレートの調整に使います。 これはメインループの中で、ゲームが速くなりすぎないよう調節するのに使いましょう。

メインループ

大したことはありません。ただの無限ループです。
while 1:
    clock.tick(60)

すべてのゲームはある種のループで走ります。 通常、これは計算機の状態とユーザからの入力をチェックし、 全オブジェクトの状態を更新し、それらを画面に描画する、 といったことからなります。 この例もまたご多分にもれずそうなっているのがわかるでしょう。

また、ここでは clock オブジェクトを呼んでいます。 これによって、このゲームが毎秒 60 フレーム以上の 速さにならないようにしています。

すべての入力イベントを処理する

イベントキューに対する処理では、これは極端に単純な例です。
for event in pygame.event.get():
    if event.type is QUIT:
        return
    elif event.type is KEYDOWN and event.key is K_ESCAPE:
        return
    elif event.type is MOUSEBUTTONDOWN:
        if fist.punch(chimp):
            punch_sound.play() #ボカ
            chimp.punched()
        else:
            whiff_sound.play() #スカ
    elif event.type is MOUSEBUTTONUP:
        fist.unpunch()

まず pygame から、すべての利用可能な Event を取得し、 おのおの対してひとつずつ処理を行います。 最初の 2つの条件判定はユーザがゲームを終了したかどうか、 あるいは Esc キーを押したかどうかをチェックしています。 こうした場合、プログラムは main() 関数から抜けて正常に終了します。

次にチェックしているのは、マウスボタンが押されたか放されたかです。 もしボタンが押された場合、fist オブジェクトに対して それが chimp に当たったかどうかをたずねます。 その後適切な音響効果を流し、もし猿に当たっていれば、 (猿の punched() メソッドを呼び出すことによって) その猿に対してぐるぐる回るよう 指示します。

Sprite の状態を更新する

allsprites.update()

Sprite Group には update() メソッドがあり、 これはその Group が含んでいるスプライトの各 update() メソッドを 単に呼び出すだけです。各オブジェクトはその状態に応じて動くことでしょう。 chimp が 1ステップ横に動いたり、パンチされた時にぐるぐる回ったりするのは ここです。

画面全体を描画する

これですべてのオブジェクトが正しい位置にきました。描画するなら今です。
screen.blit(background, (0, 0))
allsprites.draw(screen)
pygame.display.flip()

最初の blit で背景を画面全体に描画します。 これは前のフレームにあったものをすべて消してしまうので (すこし非効率ですが、まあこのゲームならこれでいいでしょう)、 つぎに draw() メソッドを使います。この Sprite Group はまさに "DrawPlain" の Sprite Group インスタンスなので、これは各スプライトを どのように描画するか知っているはずです。最後に、 pygame のソフトウエア ダブルバッファを flip() して一丁あがりです。 これですべてが一度に描画されました。

ゲームオーバー

ユーザは終了しました。片付けのお時間です。

pygame で走っているゲームを片付けるのはものすごく簡単です。 すべての変数は自動的にデストラクタが呼び出されるので、 実のところやることはまったくありません。


訳: Yusuke Shinyama