pygame2で1からゲームUIを作る！

# 概要

この記事は[フューチャー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/future) 14日目の記事です。
昨日の記事は@RuyPKGさんによる[新人でも、楽がしたい！ ~議事録の準備~](https://qiita.com/RuyPKG/items/abf72dde739e52364bcf)となってます。

先日社内勉強会にて、@shibukawaさんがゲームエンジンの紹介をしていたのがこの記事を書いたきっかけになります。
Pythonで実装したボードゲームのUIが欲しく、HTMLによる表現だと動きを表現するのが難しかったのでPythonのゲームエンジンであるpygameを触ってみることにしました。

# pygameとは
Pythonで動かすことの出来る、クロスプラットフォームのゲームエンジンです。
ゲームのグラフィック部分をPythonで書くことができるならば、Pythonで書かれたゲームロジックをそのまま埋め込むことが出来るのでとても便利です。

このpygameですが、2009年に一旦開発が停止してしばらく動いていなかったのでpygameをご存知の方は乗り換えした人も多いのではないでしょうか。
しかし、pygame2リリースに向けた開発が2019年になって活発になり、来年中にpygame2の本バージョンがリリースされそうです。
2.0.0.dev6からはpython3.8もサポートされていて先が楽しみです。
今後注目を浴びてくる気がします。

# 環境

- pygame: 2.0.0.dev6
- python: 3.8.0

# 環境構築

pygame公式の[Getting Started](https://www.pygame.org/wiki/GettingStarted)を参考に環境構築を進めます。

pip経由で、pygameをインストールします。
この際、バージョンを指定しないと最新安定版である1.9.6が入ってしまうので注意しましょう。(1.9.6はpython3.6までしか対応していません)

```python
pip install pygame==2.0.0.dev6
```

## サンプルを動かしてみる

多数のサンプルゲームがpygameには含まれています。
試しに一つ実行してみましょう。

```bash
python -m pygame.examples.aliens
```
<img width="400" alt="Screen Shot 2019-12-14 at 19.40.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/63b8a3d3-8bd2-1883-5036-079d4fd5ce7f.png">

インベーダーゲームのようなものが実行されました！
サンプルの一覧とそれぞれどんなゲームかは[gitのexampleフォルダのREADME](https://github.com/pygame/pygame/tree/master/examples)にあるので色々実行してみると面白いです。

## 1からサンプルを作る

サンプルを解読しながら、自分でコードを描き上げていきます。
今回は、背景に文字を書くだけのサンプルを目指します。

### 処理の流れ

静的な画面とは違い、ゲームエンジンを用いて描画する場合は高速で画面を更新する必要があります。
そのため、以下のような流れで処理を行います。

1. 画像読み込み等の初期化
1. 画面をクリアし、背景画像を描画
1. 画面に表示するオブジェクトの位置や値を更新して描画
1. 画面の更新の反映
1. 2に戻る

ゲームエンジンでは、画面に表示するオブジェクトはスプライトを使って描画されているので、スプライトクラスを継承したクラスを使って実装時は描画します。
スプライトが何か気になった方は、調べてみてください。
描画を高速化するためのハードウェア実装の話等、歴史的経緯があって面白いですが知らなくても特に問題はないと思います。

```python3
import os
import pygame as pg

# game constants
SCREENRECT = pg.Rect(0, 0, 640, 480)
SCORE = 0

main_dir = os.path.split(os.path.abspath(__file__))[0]


def load_image(file):
    """ loads an image, prepares it for play
    """
    file = os.path.join(main_dir, "data", file)
    try:
        surface = pg.image.load(file)
    except pg.error:
        raise SystemExit('Could not load image "%s" %s' % (file, pg.get_error()))
    return surface.convert()

class Score(pg.sprite.Sprite):
    """ to keep track of the score.
    """

    def __init__(self):
        pg.sprite.Sprite.__init__(self)
        self.font = pg.font.Font(None, 40)
        self.font.set_italic(1)
        self.color = pg.Color("white")
        self.lastscore = -1
        self.update()
        self.rect = self.image.get_rect().move(10, 450)

    def update(self):
        """ We only update the score in update() when it has changed.
        """
        if SCORE != self.lastscore:
            self.lastscore = SCORE
            msg = "Score: %d" % SCORE
            self.image = self.font.render(msg, 0, self.color)

def main(winstyle=0):
    pg.init()
    # Set the display mode
    winstyle = 0  # |FULLSCREEN
    bestdepth = pg.display.mode_ok(SCREENRECT.size, winstyle, 32)
    screen = pg.display.set_mode(SCREENRECT.size, winstyle, bestdepth)

    # create the background, tile the bgd image
    bgdtile = load_image("background.jpg")
    background = pg.Surface(SCREENRECT.size)
    background.blit(bgdtile, (0, 0))
    screen.blit(bgdtile, (0, 0))
    pg.display.flip()

    # Initialize Game Groups
    all = pg.sprite.RenderUpdates()

    # Create Some Starting Values
    clock = pg.time.Clock()
    global SCORE
    if pg.font:
        all.add(Score())

    # Run our main loop whilst the player is alive.
    while True:
        all.clear(screen, background)
        SCORE += 123456789
        all.update()

        # draw the scene
        dirty = all.draw(screen)
        pg.display.update(dirty)

        # cap the framerate at 40fps. Also called 40HZ or 40 times per second.
        clock.tick(40)


if __name__ == "__main__":
    main()
```

data以下に背景画像(background.jpg)を準備しておく必要があります。
ちょっと長いですが、上記サンプルを実行すると、以下のように背景の描画と文字の表示ができます。

<img width="400" alt="Screen Shot 2019-12-14 at 21.30.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/e81f21d1-8031-7aa2-c93c-1f7e66cfe07a.png">

以下ゲームエンジン独特の部分を説明します。

### Spriteのグループ化

```python
all = pg.sprite.RenderUpdates() # Spriteグループの作成
all.add(Score()) # Spriteグループへの追加
all.update() # Spriteグループの一括更新
all.draw(screen) # Spriteグループの描画
```

pygameにはスプライトをグループ化できる機能が備わっていて、画面の全ての要素や、特定のグループ（例えば敵キャラクターのみ等）の要素を一斉に更新したり描画するのが簡単にできるようなっています。
Spriteのグループには順序付きグループや単体スプライト用のグループや様々な種類があり、用途に応じて使い分けると良いでしょう。
[公式ドキュメント](http://westplain.sakuraweb.com/translate/pygame/Sprite.cgi)で一覧は確認できます。


### 画面のリフレッシュレートの指定

```python
clock = pg.time.Clock() # Clockの生成
clock.tick(40) # Clockを用いて1/40秒経過するまで処理を待つ
```

オブジェクトを動かす際には、処理の重さに関わらず同じ速さで動かすために処理が早く終わりすぎた場合は待つ必要があります。
これを実現するため、pygameではClockオブジェクトを使用します。
Clockオブジェクトを作ってclock.tick()を呼ぶだけで、前回の呼び出しからフレームレートに応じた時間sleepしてくれるのでとても便利です。

# 最後に

今回紹介した描画の機能以外にも、pygameには

* 入力の受けつけ
* 音の再生
* Android, iOS対応

等様々な機能があります。
リアルタイムに更新されるUIをPythonで書けるのは非常に便利なので、pygame2の正式リリースが待ちきれないですね！