Pyxelでパックマンぽいゲームを作る　前編

#はじめに
みなさんどうもこんにちは。[高知工科大 Advent Calendar 2018](https://adventar.org/calendars/2959#list-2018-12-09)の10日目を担当するstmnです。よろしくお願いします。今回はPyxelを使って、パックマンぽいゲームを作っていこうと思います。
前編では敵判定はなしで、クッキーとパワークッキーを取った際にスコアが増えるようになる所まで実装します。実装すると下記のgifのようなものができます。
![pyxel-181210-223432.gif](https://qiita-image-store.s3.amazonaws.com/0/113331/2c3379bd-7932-6e14-39a0-7f556925c4e0.gif)


#事の始まり(茶番)
~~本題読みたい人は飛ばしてください~~
今年のAdvent Calendar、なにかこっかな〜
ん？
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">日本向けのPyxel紹介記事を書きました。<a href="https://twitter.com/hashtag/python?src=hash&amp;ref_src=twsrc%5Etfw">#python</a> <a href="https://twitter.com/hashtag/gamedev?src=hash&amp;ref_src=twsrc%5Etfw">#gamedev</a> <a href="https://twitter.com/hashtag/IndieGameDev?src=hash&amp;ref_src=twsrc%5Etfw">#IndieGameDev</a> <a href="https://twitter.com/hashtag/pico8?src=hash&amp;ref_src=twsrc%5Etfw">#pico8</a> <a href="https://twitter.com/hashtag/tic80?src=hash&amp;ref_src=twsrc%5Etfw">#tic80</a><br><br>レトロゲームエンジン Pyxel でプログラミングを始めよう！ - kitao&#39;s blog <a href="https://t.co/n1GRRxe0W8">https://t.co/n1GRRxe0W8</a></p>&mdash; Takashi Kitao (@kitao) <a href="https://twitter.com/kitao/status/1066272009317888000?ref_src=twsrc%5Etfw">2018年11月24日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

面白そう！ゲーム系のプログラム書いた事ないけど、触ってみたいしやってみるか！
ってノリで~~担当日の2日前から~~書いてみました。

#Pyxel(ピクセル)って？

[Pyxel(ピクセル)](https://github.com/kitao/pyxel/blob/master/README.ja.md)は和製のレトロゲームエンジンで以下のような特徴があります。

* ドットゲーム絵ゲームが作れる
* 記述言語はPython
* マルチプラットフォーム
* 機能がシンプルで覚える量が少ない
* 日本語ドキュメントがある(作者が日本人)
* etc...

詳細な特徴については、

- [作者様のブログ](http://tkitao.hatenablog.com/entry/2018/11/24/185346)
- [Pyxelのマニュアル](https://github.com/kitao/pyxel/blob/master/README.ja.md)

を見ていただけたらいいと思います。

#作成目標
パックマンのようなゲームを作ることを目標にします。
ただし、BGMやSEに関しては、知識が全くない為触れません。作品で使うドット絵は全て、ライブラリに付属する**Pyxel Editor**を使うようにします。
また、ゲームプログラムとして設計に甘いところが多々あると思いますが、優しい目で見守っていただいて、気づいたことがあれば、アドバイス頂ければ幸いです。

# 動作環境
今回の実装は以下のような環境とディレクトリ構成で行なっていきます。

```:動作環境
OS      : macOS Mojave 10.14.1
Python  : 3.6.3
pyxel   : 0.9.4
glfw    : 3.2.1
```

``` :ディレクトリ構成
.
├── app.py        // mainプロジェクト
└── pacman.pyxel　 // pyxeleditorで作成した情報を持つファイル
```

#ドット絵作成
はじめに、ゲームに使うドット絵を記述します。
コンソールで以下のコマンドを打って**Pyxel Editor** を起動させます。

```:コンソール

$ pyxeleditor pacman.pyxel
```

その後、ドット絵の記述とタイルマップの作成を行なっていきます。
## イメージエディタ
まずは、イメージエディタでゲーム内で出現させるドット絵を描いていきます。ちなみに私はこのように描いてみました。
<img width="475" alt="スクリーンショット 2018-12-09 23.06.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/113331/3a5236c4-c424-95a7-038c-1f1458ba1135.png">
このエディタで描く事のできる最小の単位は１ドットで、８ドット毎に薄く藍色の線が引かれています。また、１つのイメージには256×256ドットまでドット絵を描くことができ、右側の白い四角の位置をずらすことで書きたい場所に描くことができます。
今回、ゲーム内で使うドット絵は全て8×８ドットで描いてます。
<img width="691" alt="スクリーンショット 2018-12-10 20.07.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/113331/e75006ff-2794-56bb-0093-322964ed5dba.png">


##タイルマップエディタ
次にゲームの背景となるタイルマップを作成します。タイルマップは先ほど作成したドット絵をペタペタと貼りながら作成していきます。
今回はこのような形で作成しました。
<img width="473" alt="スクリーンショット 2018-12-09 23.07.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/113331/c81b6d4f-efe7-3299-ded6-aea9314657d9.png">
画像だけ貼られてもイメージが湧きにくいと思うので、説明付きの画像を下に貼っておきます。
ちなみに、タイルマップの全体図が真っ黄色なのは、一番最初にイメージの左上を全てのタイルに設定する仕様になっているので、自分の描いた丸い黄色い球体が全体に配置されているからです。
<img width="824" alt="スクリーンショット 2018-12-10 20.27.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/113331/6c011460-ae9b-fd0f-fc8e-5460c5440bd8.png">

#プログラム作成
では、ここからコードを書いていきます。
##タイルマップの表示
まずは作成したタイルマップを表示してみましょう。

```app.py
import pyxel

class App:
    def __init__(self):
        # ゲームの設定。widthとheightの単位はドット
        # init(width, height, [caption], [fps])
        pyxel.init(128,128,caption="パックマンぽいゲーム", fps=25)

        # 作成したドット絵やタイルマップの情報を読み込む
        pyxel.load('pacman.pyxel')
        # 毎フレーム毎にupdateとdrawを呼び出す
        pyxel.run(self.update, self.draw)

    # ゲーム内で扱う情報を更新したり、キー入力の処理などを行う
    def update(self):
        # qキーが押されたらゲームを終了する。
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

    # ゲーム内で描画されるドット絵の処理をする
    def draw(self):
        # 真っ黒に背景をする
        pyxel.cls(0)
        # タイルマップを描画する
        self.tilemap_draw()

    # タイルマップの描画処理
    def tilemap_draw(self):
        base_x = 0
        base_y = 0
        tm = 0
        u = 0
        v = 0
        w = 16
        h = 16
        # 指定したtm(template)番号の(u,v)座標から
        # サイズ(w,h)の大きさを(base_x,base_y)座標に描画する
        pyxel.bltm(base_x,base_y,tm,u,v,w,h)
App()
```

##実行
では実際に実行してみたいと思います。

```shell
$ python app.py
```
すると、下記のようなゲームウィンドが表示されると思います。
<img width="633" alt="スクリーンショット 2018-12-10 16.47.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/113331/8be85bf7-c891-5155-c7cf-be16902a0d64.png">
これで、タイルマップをゲームウインドウに表示することができました。

##パックマンの実装
次は、パックマンを実装していきたいと思います。
### パックマンクラスの作成
パックマンには以下の情報を保持させます。

- パックマンの描画される座標
- パックマンの進む方向
- パックマンの向いてる向き
- パックマンのいる座標
- 描画するかどうかのフラグ
- 獲得スコア
- パワークッキーの効果中か(次回実装)

最初に話したように、前編では敵に対しての処理を扱わないので、最後の項目以外を表現できるようにクラス変数を定義していきたいと思います。

```app.py
import pyxel

# 追加
class Pacman:
    def __init__(self):
        """
        pacman自体を生成するクラス
        """
        # 描画されるドットの座標
        self.dot_x = 8
        self.dot_y = 8

        # パックマンの進む方向の情報
        self.x_change_quantity = 0
        self.y_change_quantity = 0

        # 0方向なし, 1 上, 2を下, 3を右, 4を左
        self.vectol = 0

        # 今いるタイルを座標化する
        self.tile_x = 1
        self.tile_y = 1

        # どのパックマンをプロットするかの情報
        self.plot_pacman_x_coordinate = 0

        # スコア
        self.score = 0

class App:
    #・・・・・・・・・

App()
```

###パックマンの動作の実装
それではパックマンの動作ロジックと描画機能を実装していきたいと思います。

```app.py
import pyxel

class Pacman:
    #・・・・・・・・・

class App:
    def __init__(self):
        #・・・・・・・・・

    def update(self):
        #・・・・・・・・・
        # 追加
        self.update_pacman_state()

    def draw(self):
        #・・・・・・・・・
        # 追加
        self.draw_pacman()

    # 追加
    def update_pacman_state(self):
        # 描画が終わったら、次の移動判定をする
        if self.pacman.dot_x % 8 == 0 and self.pacman.dot_y % 8 == 0:
            # 進行方向を確認
            # 壁に行こうとしてたら
            if self.tilemap_state.get(self.pacman.tile_x + self.pacman.x_change_quantity, self.pacman.tile_y + self.pacman.y_change_quantity) == 33:
                # その場所から動かないように値に変える
                self.pacman.x_change_quantity = 0
                self.pacman.y_change_quantity = 0
            # Wキーが押されていた場合に
            elif pyxel.btn(pyxel.KEY_W):
                # 次に移動する先が、背景、クッキー、パワークッキーなら
                if self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y - 1) == 5 or self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y - 1) == 64 or self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y - 1) == 65:
                    # 上に行くように設定
                    self.pacman.x_change_quantity =  0
                    self.pacman.y_change_quantity = -1
                    # 描画する向きを上に設定
                    self.pacman.vectol = 1
            # Sキーが押されていた場合に
            elif pyxel.btn(pyxel.KEY_S):
                # 次に移動する先が、背景、クッキー、パワークッキーなら
                if self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y + 1) == 5 or self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y + 1) == 64 or self.tilemap_state.get(self.pacman.tile_x, self.pacman.tile_y + 1) == 65:
                    # 下に行くように設定
                    self.pacman.x_change_quantity =  0
                    self.pacman.y_change_quantity =  1
                    # 描画する向きを下に設定
                    self.pacman.vectol = 2
            # Dキーが押されていた場合に
            elif pyxel.btn(pyxel.KEY_D):
                # 次に移動する先が、背景、クッキー、パワークッキーなら
                if self.tilemap_state.get(self.pacman.tile_x + 1, self.pacman.tile_y) == 5 or self.tilemap_state.get(self.pacman.tile_x + 1, self.pacman.tile_y) == 64 or self.tilemap_state.get(self.pacman.tile_x + 1, self.pacman.tile_y) == 65:
                    # 右に行くように設定
                    self.pacman.x_change_quantity =  1
                    self.pacman.y_change_quantity =  0
                    # 描画する向きを右に設定
                    self.pacman.vectol = 3
            # Aキーが押されていた場合に
            elif pyxel.btn(pyxel.KEY_A):
                # 次に移動する先が、背景、クッキー、パワークッキーなら
                if self.tilemap_state.get(self.pacman.tile_x - 1, self.pacman.tile_y) == 5 or self.tilemap_state.get(self.pacman.tile_x - 1, self.pacman.tile_y) == 64 or self.tilemap_state.get(self.pacman.tile_x - 1, self.pacman.tile_y) == 65:
                    # 左にいくように設定
                    self.pacman.x_change_quantity = -1
                    self.pacman.y_change_quantity =  0
                    # 描画する向きを左に設定
                    self.pacman.vectol = 4

            # タイルの座標を進行方向に合わせて変える
            self.pacman.tile_x += self.pacman.x_change_quantity
            self.pacman.tile_y += self.pacman.y_change_quantity

        # 毎フレーム毎に描画する座標を変更していく
        self.pacman.dot_x += self.pacman.x_change_quantity
        self.pacman.dot_y += self.pacman.y_change_quantity

    # 追加
    def draw_pacman(self):
        # 移動しない場合は描画したパックマンの状態をキープ
        if self.pacman.x_change_quantity == 0 and self.pacman.y_change_quantity == 0:
            pass
        else:
            # 口の動きを表現するために、2フレームに１回
            # 丸の状態を表示する
            if pyxel.frame_count % 2 == 0:
                self.pacman.plot_pacman_x_coordinate = 0
            elif self.pacman.vectol == 1:
                # 上向きに開いた口のイメージの座標を設定
                self.pacman.plot_pacman_x_coordinate = 8
            elif self.pacman.vectol == 2:
                # 下向きに開いた口のイメージの座標を設定
                self.pacman.plot_pacman_x_coordinate = 16
            elif self.pacman.vectol == 3:
                # 右向きに開いた口のイメージの座標を設定
                self.pacman.plot_pacman_x_coordinate = 24
            elif self.pacman.vectol == 4:
                # 左向きに開いた口のイメージの座標を設定
                self.pacman.plot_pacman_x_coordinate = 32
        # イメージ０に登録されている(self.pacman.plot_pacman_x_coordinate,0)の座標から
        # 8×8サイズを参照にして、(self.pacman.dot_x,self.pacman.dot_y)の座標に描画する
        pyxel.blt(self.pacman.dot_x,self.pacman.dot_y,0,self.pacman.plot_pacman_x_coordinate,0,8,8)

App()
```
##実行
ここまで実装すればクッキーは消えませんが、パックマンが動くようになります。

```shell
$ python app.py
```
![pyxel-181210-215351.gif](https://qiita-image-store.s3.amazonaws.com/0/113331/531821ad-7c69-a3dd-b8e5-928235c96bb6.gif)
次はクッキーに触れた時の処理を実装していきます。

##クッキーの処理
パックマンが移動する際に、その座標にクッキーかパワークッキーがある場合はスコアと特定の値追加して、読み込んだタイルマップの情報を変化させてあげる必要があります。
また、今の実装ではスコアを表示することができないので表示するように変更を行い、スコアを表示できるだけのスペースを用意します。

```app.py
import pyxel

class Pacman:
    #・・・・・・・・・

class App:
    def __init__(self):
        #・・・・・・・・・
        # 変更
        # 変更前：pyxel.init(128,128,caption="パックマンぽいゲーム", fps=25)
        pyxel.init(128,140,caption="パックマンぽいゲーム", fps=25)

    def update(self):
        #・・・・・・・・・
        # 追加
        self.update_tilemap()

    def draw(self):
        #・・・・・・・・・

    # 追加
    def update_tilemap(self):
        # ノーマルクッキーはスコア30点追加
        # パワークッキーはスコア100点追加

        # ノーマルクッキーに触れたら
        if self.tilemap_state.get(self.pacman.tile_x,self.pacman.tile_y) == 65:
            self.pacman.score += 30

        # パワークッキーに触れたら
        elif self.tilemap_state.get(self.pacman.tile_x,self.pacman.tile_y) == 64:
            self.pacman.score += 100

        # パックマンがノーマルクッキーとパワークッキーを踏んだら、何もないブロックに変える
        pyxel.tilemap(0).set(self.pacman.tile_x,self.pacman.tile_y,5,refimg=0)

App()
```

##実行
では実行してみしょう。

```shell
$ python app.py
```
![pyxel-181210-223432.gif](https://qiita-image-store.s3.amazonaws.com/0/113331/b921f81f-7c39-a7dc-deaf-a6de6b13c3b1.gif)
ちゃんとクッキーとパワークッキーは消えて、スコアも追加されています。

#終わりに
以上で前編は終わりです。どうだったでしょうか？
実際にコードを書いてみて、ゲームを作った事のない人でも短時間で理解できて、かなりいい感じに作れる良さげなゲームエンジンだと思いました。ゲームプログラムを書いてる人なら、もっと使いこなせるんじゃないかと思います。

#後編について
後編では敵の移動と当たり判定などを扱っていきたいと思います。