【Python】Pyxelで簡単な迷路ゲームを作る

以前の記事
[【Python】Pyxelでゲームを作るための最初の一歩](https://qiita.com/hiro_underclass/items/c187757987e37b9ba7da)
[【Python】Pyxelでゲームを作る - エディターを使う -](https://qiita.com/hiro_underclass/items/2cf51f4edfbda7864929)
#はじめに
Pyxelのエディタを使ってドット絵が描けるようになったので、その絵を使って簡単がゲームを作ってみることにします。
#準備
イメージエディタとタイルマップエディタを使って、以下のような絵を用意します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/ccdf72ba-76a9-3f86-ad2c-731bd514f1ce.png">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/44e31b7f-3505-5719-c6b3-ad7e4d9ea053.png">
左端のしかめっ面のキャラクターを、「G」と書かれたゴール地点まで動かすゲームを作ることにします。
最初のコードは以下のとおりです。

```python
import pyxel

# キャラクターのクラスを作成
class Player:
    def __init__(self, x, y):
        # 表示位置の座標を示す変数
        self.x = x
        self.y = y

class App:
    def __init__(self):
        pyxel.init(128, 128)

        pyxel.load("sample.pyxres")

        # Playerクラスのインスタンスを生成、初期位置の座標を引数に渡す
        self.player = Player(16, 16)
        
    def run(self):
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        pyxel.cls(0)

        pyxel.bltm(0, 0, 0, 0, 0, 16, 16, 0)

        # playerの変数x,yを使用して描画
        pyxel.blt(self.player.x, self.player.y, 0, 0, 0, 16, 16, 0)

App().run()
```

キャラクターを意味する`Player`クラスを作成、変数として表示位置を示す変数を定義します。
その変数を使って位置を指定し描画を行っています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/48a1f3c8-cc10-046d-ea6a-aa83254d2b54.png" width=60%>
表示位置を(16,16)にしたので、左端から右下にそれぞれ1マスずつ移動した場所に表示されました。
#キャラクターを移動させる
キー入力に応じて、`player`の変数`x,y`を変化させ、キャラクターを移動させましょう。
状況の変化に関する内容は`update`関数に書き込んでいきます。

```python
    def update(self):
        # キー入力に応じて表示位置を変化させる
        if pyxel.btnp(pyxel.KEY_LEFT):
            self.player.x -= 16
        elif pyxel.btnp(pyxel.KEY_RIGHT):
            self.player.x += 16
        elif pyxel.btnp(pyxel.KEY_UP):
            self.player.y -= 16
        elif pyxel.btnp(pyxel.KEY_DOWN):
            self.player.y += 16
```

上下左右のキー入力に応じて`x,y`を1マスにあたる16ずつ増減させることで、マス目にそって移動できるようになりました。
しかしここで2つの問題が発生します。

1. 移動が一瞬で終わってしまう。もう少しゆっくり移動させたい。
2. 外側の茶色のマスは壁のつもりですが、キャラクターは壁をすり抜け、画面の外まで移動できてしまう。

#ゆっくり移動させる
まず1については、移動量を示す変数を新たに作ることで対応します。
キーが押されたら移動量を16と設定し、ドット1個分移動するごとに移動量を減らしていきます。移動量が0になったら移動終了です。
まずは`Player`クラスを以下のようにします。

```python
class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y

        # x,yの変化量と、全体の移動量
        self.dx = 0
        self.dy = 0
        self.move_count = 0

    # x,yの変化量分x,yを増減させる関数
    def move(self):
        self.x += self.dx
        self.y += self.dy
```

次に、`App`クラスに以下の関数を追加します。

```python
    # 移動に関する情報を更新する関数
    def update_move(self):
        # 移動量が0であれば、キー入力があるか判定する
        if self.player.move_count == 0:
            if pyxel.btnp(pyxel.KEY_LEFT):
                self.player.dx = -1
            elif pyxel.btnp(pyxel.KEY_RIGHT):
                self.player.dx = 1
            elif pyxel.btnp(pyxel.KEY_UP):
                self.player.dy = -1
            elif pyxel.btnp(pyxel.KEY_DOWN):
                self.player.dy = 1

            # キー入力がありx,yの変化量がセットされれば、移動量16をセット
            if self.player.dx != 0 or self.player.dy != 0:
                self.player.move_count = 16

        # 移動量が0でなければ、移動中の状態
        else:
            # playerを移動させ、移動量を1減らす
            self.player.move()
            self.player.move_count -= 1

            # 移動量が0になったら移動終了、x,yの変化量をリセットする
            if self.player.move_count == 0:
                self.player.dx = 0
                self.player.dy = 0
```

最後に、`update`関数の中に、先程追加した関数を書き込めば終了です。

```python
    def update(self):
        self.update_move()
```

これで、キー入力に応じてゆっくり移動させることができるようになりました。
ポイントは、キャラクターの状況を以下のように判定し、それに応じて各変数を変化させているところです。
>キャラクターが移動中か？
→移動していない→キー入力があるか？→あり／なし
→移動中→移動は終了したか？→終了した／していない

#壁をすり抜けないようにする
続いて、2については、キャラクターの移動先の座標を判定することで対応します。
先程作った`update_move`関数を以下のようにします。

```python
    def update_move(self):
        if self.player.move_count == 0:
            if pyxel.btnp(pyxel.KEY_LEFT):
                self.player.dx = -1
            elif pyxel.btnp(pyxel.KEY_RIGHT):
                self.player.dx = 1
            elif pyxel.btnp(pyxel.KEY_UP):
                self.player.dy = -1
            elif pyxel.btnp(pyxel.KEY_DOWN):
                self.player.dy = 1
            if self.player.dx != 0 or self.player.dy != 0:
                self.player.move_count = 16

            # 移動先が壁であれば、各変数を0にして移動をキャンセル
            if pyxel.tilemap(0).get(
                    self.player.x / 8 + self.player.dx * 2,
                    self.player.y / 8 + self.player.dy * 2) == 6:
                self.player.dx = 0
                self.player.dy = 0
                self.player.move_count = 0

        else:
            self.player.move()
            self.player.move_count -= 1
            if self.player.move_count == 0:
                self.player.dx = 0
                self.player.dy = 0
```

`tilemap`クラスの`get`関数を使っているのですが、これがなかなか混乱します。
まず、1枚目のタイルマップを使用しているので、`tilemap`のインデックスは0になります。
次に、`get`の引数には、タイルマップ上の座標`x,y`を指定します。イメージの座標はドット単位、タイルマップの座標は8×8ドットで1単位(タイル1枚)と、座標の単位が異なることに注意が必要です。
ドット単位の座標をタイル単位の座標に変換する方法は以下のとおりです。

`player`のイメージとしての移動先座標(ドット単位)は以下のようになります。

```
(self.player.x + self.player.dx * 16,
self.player.y + self.player.dy * 16)
```

現在の座標 ＋ x,yそれぞれの変化量 × 16ドット分、ということですね。
タイルマップの座標は8ドットで1マスなので、上の座標のx,yをそれぞれ8で割ることになります。

```
(self.player.x / 8 + self.player.dx * 2,
self.player.y / 8 + self.player.dy * 2)
```

これでタイル単位の座標がわかりました。

こうして座標を入力すると、その座標に存在するタイルマップの「値」が帰ってきます。
どのような値が返ってくるかというと、タイルマップエディタ右下枠における、タイルのインデックスです。
タイルは8×8ドットで1枚なので、左上が0で、右に8ドットずれるごとに1,2,・・・と増えていきます。1行あたり32個のタイルがあるので、インデックスは31までとなります。
2行目は32,33,・・・、3行目は64,65,・・・といったようにインデックスがついています。
今回壁になっているタイルは4番目のタイルですが、8ドットを1単位として考えると、インデックスは6になります。
このため、`get`の戻り値が6の場合に、各変数をリセットして、移動をキャンセルしています。
以上で、キャラクターの移動が実装できました。
#ゴールしたときにメッセージを出す
ゴールにたどり着いたときにメッセージを出すため、`draw`関数を以下のようにします。

```python
    def draw(self):
        pyxel.cls(0)

        pyxel.bltm(0, 0, 0, 0, 0, 16, 16, 0)

        pyxel.blt(self.player.x, self.player.y, 0, 0, 0, 16, 16, 0)

        # ゴールしたらメッセージを出す
        if pyxel.tilemap(0).get(self.player.x / 8,self.player.y / 8) == 2:
            pyxel.rect(0, 0, 128, 16, 8)
            pyxel.text(40, 5, "GAME CLEAR!!", 7)
```

先程と同様、ゴールしたかどうかの判定には、`get`関数を使っています。ゴール地点のタイルのインデックスは2となります。
`text`で文字を表示できます。引数は、表示位置座標x,y、文字列、色です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/c8b37e6d-febe-4c32-df89-caf7e24c57a6.png" width=60%>
#おわりに
コード全文です。

```python
import pyxel

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y

        self.dx = 0
        self.dy = 0
        self.move_count = 0

    def move(self):
        self.x += self.dx
        self.y += self.dy

class App:
    def __init__(self):
        pyxel.init(128, 128)

        pyxel.load("sample.pyxres")

        self.player = Player(16, 16)
        
    def run(self):
        pyxel.run(self.update, self.draw)

    def update_move(self):
        if self.player.move_count == 0:
            if pyxel.btnp(pyxel.KEY_LEFT):
                self.player.dx = -1
            elif pyxel.btnp(pyxel.KEY_RIGHT):
                self.player.dx = 1
            elif pyxel.btnp(pyxel.KEY_UP):
                self.player.dy = -1
            elif pyxel.btnp(pyxel.KEY_DOWN):
                self.player.dy = 1

            if self.player.dx != 0 or self.player.dy != 0:
                self.player.move_count = 16

            if pyxel.tilemap(0).get(
                    self.player.x / 8 + self.player.dx * 2,
                    self.player.y / 8 + self.player.dy * 2) == 6:
                self.player.dx = 0
                self.player.dy = 0
                self.player.move_count = 0
        else:
            self.player.move()
            self.player.move_count -= 1

            if self.player.move_count == 0:
                self.player.dx = 0
                self.player.dy = 0

    def update(self):
        self.update_move()

    def draw(self):
        pyxel.cls(0)

        pyxel.bltm(0, 0, 0, 0, 0, 16, 16, 0)

        pyxel.blt(self.player.x, self.player.y, 0, 0, 0, 16, 16, 0)

        if pyxel.tilemap(0).get(self.player.x / 8,self.player.y / 8) == 2:
            pyxel.rect(0, 0, 128, 16, 8)
            pyxel.text(40, 5, "GAME CLEAR!!", 7)

App().run()
```

ちょっとごちゃごちゃしているので、もう少しシンプルにまとめる方法があるかもしれません。
敵も落とし穴も道もないので、迷路ゲームといえるのかわかりませんが、ゲームの原型らしきものができました。
#参考
https://github.com/kitao/pyxel/blob/master/README.ja.md
[Pyxelでパックマンぽいゲームを作る　前編](https://qiita.com/stmn/items/4048d4af2a9613594b60)
[Pyxelで倉庫番ゲームを作る(前編)](https://qiita.com/2448/items/dea71fc35024b9a242a3)