6
9

More than 3 years have passed since last update.

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

Posted at

以前の記事
【Python】Pyxelでゲームを作るための最初の一歩
【Python】Pyxelでゲームを作る - エディターを使う -

はじめに

Pyxelのエディタを使ってドット絵が描けるようになったので、その絵を使って簡単がゲームを作ってみることにします。

準備

イメージエディタとタイルマップエディタを使って、以下のような絵を用意します。


左端のしかめっ面のキャラクターを、「G」と書かれたゴール地点まで動かすゲームを作ることにします。
最初のコードは以下のとおりです。

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クラスを作成、変数として表示位置を示す変数を定義します。
その変数を使って位置を指定し描画を行っています。

表示位置を(16,16)にしたので、左端から右下にそれぞれ1マスずつ移動した場所に表示されました。

キャラクターを移動させる

キー入力に応じて、playerの変数x,yを変化させ、キャラクターを移動させましょう。
状況の変化に関する内容はupdate関数に書き込んでいきます。

    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クラスを以下のようにします。

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クラスに以下の関数を追加します。

    # 移動に関する情報を更新する関数
    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関数の中に、先程追加した関数を書き込めば終了です。

    def update(self):
        self.update_move()

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

キャラクターが移動中か?
→移動していない→キー入力があるか?→あり/なし
→移動中→移動は終了したか?→終了した/していない

壁をすり抜けないようにする

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

    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関数を以下のようにします。

    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、文字列、色です。

おわりに

コード全文です。

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でパックマンぽいゲームを作る 前編
Pyxelで倉庫番ゲームを作る(前編)

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9