【Python】Pyxelで簡単な迷路ゲームを作る - 敵を出現させる -

以前の記事
[【Python】Pyxelでゲームを作るための最初の一歩](https://qiita.com/hiro_underclass/items/c187757987e37b9ba7da)
[【Python】Pyxelでゲームを作る - エディターを使う -](https://qiita.com/hiro_underclass/items/2cf51f4edfbda7864929)
[【Python】Pyxelで簡単な迷路ゲームを作る](https://qiita.com/hiro_underclass/items/81464fb52e6d7504d14c)
#はじめに
迷路をたどってゴールを目指すゲームに、敵キャラクターを出現させてみます。
ぶつかるとゲームオーバーになるようにして、少しゲームっぽくしてみたいと思います。
#準備
怖い顔の敵キャラクターを追加。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/7263feab-9e9c-6a59-8702-ffa4da245202.png" width=70%>
壁を増やして迷路っぽくします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/2eec01af-716b-687f-0509-6fa8746c7b2a.png" width=70%>
ベースとなるコード。まだ敵は現れません。

```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("assets/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()
```

#敵を表示させる

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

        pyxel.load("assets/sample.pyxres")

        self.player = Player(16, 16)

        # 敵キャラクターのインスタンスを生成
        self.enemy = Player(16, 96)
# 中略
    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)

        # 敵キャラクターの描画
        pyxel.blt(self.enemy.x, self.enemy.y, 0, 0, 16, 16, 16, 0)
```

敵キャラクターの座標を管理するために、`Player`クラスのインスタンスを生成します。
初期位置のy座標は、画面の一番下が128なので、そこから2マス上がった96にします。
そして描画関数の中に、敵を描画する機能を追記します。
引数`(x,y,img,u,v,w,h,colkey)`のうち、`x,y`は`enemy`の持つ変数を、`u,v`は`16,16`を指定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/fba4524c-eb9c-76b7-32a2-c10af58af425.png" width=70%>
#敵を動かす
敵はランダムに動くことにします。キャラクターの移動は`update_move`関数で管理しているので、これを修正していきます。
よく見ると、最初のブロックの「押されたキーによって動く方向を決める」部分以外はそのまま使えそうです。
したがって、「動く方向を決める」部分を別の関数として切り出し、自分のプレイヤーの移動か敵キャラクターの移動かによって、別々の関数を呼び出すようにします。
「動く方向を決める」関数は以下のようにします。
(なお、`random`モジュールを使うので、先頭に`import random`と書いておきます。)

```python
    # 自分のプレイヤーはキーによって動く方向を決める
    def move_manual(self, player):
        if pyxel.btnp(pyxel.KEY_LEFT):
            player.dx = -1
        elif pyxel.btnp(pyxel.KEY_RIGHT):
            player.dx = 1
        elif pyxel.btnp(pyxel.KEY_UP):
            player.dy = -1
        elif pyxel.btnp(pyxel.KEY_DOWN):
            player.dy = 1

    # 敵のキャラクターはランダムに動く方向を決める
    def move_auto(self, player):
        rand = random.randint(1, 4)
        if rand == 1:
            player.dx = -1
        elif rand == 2:
            player.dx = 1
        elif rand == 3:
            player.dy = -1
        elif rand == 4:
            player.dy = 1
```

次に、`update_move`関数を修正します。
今までは`Player`クラスのオブジェクトが1個しかなかったので、`self.player`を直接操作していました。
今回は自分と敵の2個のオブジェクトがあるので、どのオブジェクトを動かすか選択できるようにします。
ということで、移動させる`Player`クラスのオブジェクトと、マニュアル移動かオート移動かを示すブール値を引数にしました。

```python
    def update_move(self, player, is_manual):
        # マニュアル移動かオート移動かによって呼び出す関数を分岐
        if player.move_count == 0:
            if is_manual:
                self.move_manual(player)
            else:
                self.move_auto(player)

            # ここから下は変更なし(ただしself.playerをplayerに変更している)
            if player.dx != 0 or player.dy != 0:
                player.move_count = 16

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

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

最後に、`update`関数の中を修正します。

```python
    def update(self):
        self.update_move(self.player, True)
        self.update_move(self.enemy, False)
```

これで敵キャラクターが動くようになりました。
#敵とぶつかったらゲームオーバー
敵とぶつかったらゲームオーバーになるようにしてみます。
ぶつかったかどうかは、自分と敵の位置座標の関係によって判定します。いわゆる「当たり判定」というやつですね。

```python
    # 敵とぶつかったか判定する
    def check_hit(self, player, enemy):
        dx = player.x - enemy.x
        dy = player.y - enemy.y
        if -16 < dx < 16 and -16 < dy < 16:
            return True
        else:
            return False
```

x,y座標ともに、-16〜16の間であれば、ぶつかったと判定されます。
(`-16 < dx < 16`のように比較演算子をつなげて書けることを初めて知りました。便利ですね。)
ちなみに引数をなくして`self.player`と`self.enemy`としても良いのですが、敵が複数になったりした場合に融通が効かなくなるので、判定するオブジェクトを引数に指定するようにしています。

```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)

        pyxel.blt(self.enemy.x, self.enemy.y, 0, 0, 16, 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)

        # 敵とぶつかったらゲームオーバー
        if self.check_hit(self.player, self.enemy):
            pyxel.rect(0, 0, 128, 16, 8)
            pyxel.text(40, 5, "GAME OVER...", 7)
```

`draw`の最後に、ゲームオーバーか判定する箇所を追加しました。
これで敵にぶつかるとゲームオーバーになります。
#仕上げ
ゲームをクリアしたときもそうですが、ゲームオーバーになってもゲームが続行されてしまいます。
ゲームクリアorゲームオーバーになったらゲームが終了し、動かなくなるようにします。

```python
    def __init__(self):
        pyxel.init(128, 128)

        pyxel.load("assets/sample.pyxres")

        self.player = Player(16, 16)

        self.enemy = Player(16,96)

        # ゲーム終了を示すフラグ
        self.game_end = False

# 中略
    def update(self):
        # ゲーム終了でない場合のみ更新する
        if self.game_end == False:
            self.update_move(self.player, True)
            self.update_move(self.enemy, False)

    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)

        # 敵キャラクターの描画
        pyxel.blt(self.enemy.x, self.enemy.y, 0, 0, 16, 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)
            self.game_end = True

        # 敵とぶつかったらゲームオーバー
        if self.check_hit(self.player, self.enemy):
            pyxel.rect(0, 0, 128, 16, 8)
            pyxel.text(40, 5, "GAME OVER...", 7)
            self.game_end = True
```

ゲームオーバーになると更新がストップし、ゲームが続行できなくなりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/912003/9ba0a34a-423d-a583-9efc-a994d533a0f1.png" width=70%>
結構難易度が高くなりました。
以下、コード全文です。関数名が適当なのでちょっと読みにくいかもしれません。

```python
import pyxel
import random

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("assets/sample.pyxres")

        self.player = Player(16, 16)
        self.enemy = Player(16, 96)

        self.game_end = False
        
    def run(self):
        pyxel.run(self.update, self.draw)

    def update_move(self, player, is_manual):
        if player.move_count == 0:
            if is_manual:
                self.move_manual(player)
            else:
                self.move_auto(player)

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

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

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

    def move_auto(self, player):
        rand = random.randint(1, 4)
        if rand == 1:
            player.dx = -1
        elif rand == 2:
            player.dx = 1
        elif rand == 3:
            player.dy = -1
        elif rand == 4:
            player.dy = 1

    def check_hit(self, player, enemy):
        dx = player.x - enemy.x
        dy = player.y - enemy.y
        if -16 < dx < 16 and -16 < dy < 16:
            return True
        else:
            return False

    def update(self):
        if self.game_end == False:
            self.update_move(self.player, True)
            self.update_move(self.enemy, False)

    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)
        pyxel.blt(self.enemy.x, self.enemy.y, 0, 0, 16, 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)
            self.game_end = True

        if self.check_hit(self.player, self.enemy):
            pyxel.rect(0, 0, 128, 16, 8)
            pyxel.text(40, 5, "GAME OVER...", 7)
            self.game_end = True

App().run()
```

(2021/1/12追記)
コメントにてコードの改善が提案されています。ご覧ください。
#参考
https://github.com/kitao/pyxel/blob/master/README.ja.md
[Pyxelでパックマンぽいゲームを作る　前編](https://qiita.com/stmn/items/4048d4af2a9613594b60)
[Pyxelで倉庫番ゲームを作る(前編)](https://qiita.com/2448/items/dea71fc35024b9a242a3)