見出し画像

【Pyxel】倉庫番の作り方

今回は倉庫番の作り方を紹介します。倉庫番とは、荷物を指定の位置まで移動させるとゲームクリアとなるゲームです。

倉庫番はゲームプログラムを理解するための重要なエッセンスが詰まったゲームで、「ゲームプログラマになる前に覚えておきたい技術」でも最初の題材として取り上げられています。

なお、今回の内容は【Pyxel】一筆書きゲームの作り方【Pyxel】Pyxelで学ぶゲームプログラム 〜マップデータの読み込みで学んだ知識を前提としていますので、プログラムに自信がなければ、一度こちらを読むことをオススメします。

■素材のダウンロード

今回はマップデータの読み込み方法の記事で作成したデータをもとに作ります。

こちらの "sokoban_practice.zip" をダウンロードしておきます。

■まず実行して動作を確認する

「python man.py」などで実行して動作を確認します。りんごを全て回収するとゲームクリアになるゲームが始まります。

■タイル画像の説明

"tileset.png" の番号は以下のように扱います。

今回、使うのは以下の番号のものです。

* 5, 6, 7, 10, 11, 12, 15, 16, 17: 壁(移動不可)
* 8: プレイヤーのスタート地点
* 20: 荷物の初期座標
* 21: 荷物を目的となる場所に置いた時に表示する画像
* 22: 荷物を置くべき場所の目印

■荷物を配置する

まずは荷物を配置します。"map.txt" を開いて以下のように数値を指定します。

22, 0,  0, 0,  0,  8
0,  0,  0, 0, 20,  0
20, 5,  7, 0,  0,  0
0, 15, 17,20,  0, 22
0,  0,  0, 0,  5,  7
0,  0,  0,22, 15, 17

実行すると、1歩進んだ時点でゲームクリアとなってしまいます。

これは、前に作ったりんごを全て回収するとゲームクリアとなる処理が残っているためとなります。
なので、Appクラスの update() を修正します。

    def update(self):
        # 更新
        if self.state == State.Standby:
            # キー入力待ち
            if self.input_key():
                # 移動開始する
                self.move_timer = 0
                self.state = State.Moving
        elif self.state == State.Moving:
            # 移動中
            self.move_timer += 1
            if self.move_timer == self.MOVE_SPEED:
                # 移動完了
                self.x = self.xnext
                self.y = self.ynext
                if self.check_clear():
                    # ゲームクリア条件を満たした
                    self.state = State.GameClear
                else:
                    self.state = State.Standby

アイテム回収とクリア判定を修正しました。新たに check_clear() を追加しています。これはゲームクリアしたかどうかを判定する関数となります。

check_clear() はひとまず False を返すだけのものにしておき、あとで実装します。

    def check_clear(self):
        # クリア判定
        return False

では、実行して自由に動き回れることを確認します。

■ブロック(荷物)の実装

当たり判定ができておらずすり抜けてしまうので、ブロック(荷物)を実装します。

class Block:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.xnext = x # 移動先X座標
        self.ynext = y # 移動先Y座標
        self.move_timer = 0 # 移動タイマー
        self.is_moving = False # 移動中かどうか
    def move_next(self, xnext, ynext):
        # 指定の座標に移動する
        self.is_moving = True
        self.move_timer = 0
        self.xnext = xnext
        self.ynext = ynext
    def update(self):
        # 更新
        if self.is_moving:
            # 移動中
            self.move_timer += 1
            if self.move_timer >= App.MOVE_SPEED:
                # 移動完了
                self.x = self.xnext
                self.y = self.ynext
                self.is_moving = False
    def draw(self):
        # 描画
        x, y = self.x, self.y
        if self.is_moving:
            # 移動中
            dx = self.xnext - self.x
            dy = self.ynext - self.y
            rate = 1.0 * self.move_timer / App.MOVE_SPEED
            x += dx * rate
            y += dy * rate
        
        Map.draw_chip(x, y, 20)

移動中かどうかのフラグ is_moving が Trueであれば移動処理を行う、というものです。移動処理そのものはプレイヤーの移動方法とほとんど同じで、move_next() が呼び出されると、xnext / ynext の位置に線形補間で移動するものとなっています。

次に、ブロックの生成を実装します。Appクラスの init() を修正します。

    def init(self):
        # 初期化
        # マップデータ読み込み
        self.map = self.load_map("map.txt")
        # プレイヤーの位置を取得
        self.x, self.y = self.search_map(8)
        # マップデータからプレイヤーを削除
        self.set_map(self.x, self.y, 0)
        self.xnext = self.x # 移動先X座標
        self.ynext = self.y # 移動先Y座標
        # ブロック生成
        self.blocks = []
        while True:
            # ブロックが見つからなくなるまで生成する
            x, y = self.search_map(20)
            if x == -1:
                # 見つからないので生成終了
                break
            block = Block(x, y)
            self.blocks.append(block)
            self.set_map(x, y, 0) # マップデータからブロックを削除
        self.move_timer = 0 # 移動中タイマー
        self.state = State.Standby # 状態

"ブロック生成" のところが生成処理となります。search_map() で "20" 番のチップを探し、見つかったらその位置に Block を生成します。

これでブロックが生成されたので当たり判定です。check_block() を追加します。

    def check_block(self, x, y):
        # 移動先にブロックがあるかどうかをチェックする
        for block in self.blocks:
            if block.x == x and block.y == y:
                # 移動先にブロックがある
                return True
        return False

input_key() で check_block() を呼び出すようにします。

    def input_key(self):
        # キー入力判定
        xnext = self.x
        ynext = self.y
        if pyxel.btn(pyxel.KEY_LEFT):
            xnext -= 1
        elif pyxel.btn(pyxel.KEY_RIGHT):
            xnext += 1
        if pyxel.btn(pyxel.KEY_UP):
            ynext -= 1
        elif pyxel.btn(pyxel.KEY_DOWN):
            ynext += 1
        
        if self.x == xnext and self.y == ynext:
            # 異動先が同じなので移動しない
            return False
        
        if self.check_wall(xnext, ynext):
            # 壁なので移動できない
            return False
        
        if self.check_block(xnext, ynext):
            # 荷物があるので移動できない
            return False

        # 移動する
        self.xnext = xnext
        self.ynext = ynext

        return True

最後に荷物を描画します。draw() を修正します。

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

        # マップの描画
        self.draw_map()

        # 荷物の描画
        for block in self.blocks:
            block.draw()

        # プレイヤーの描画
        self.draw_player()

        if self.state == State.GameClear:
            pyxel.text(4, 52, "GAME CLEAR", 9)

実行して荷物にめり込まないことを確認します。

ひとまずここまでのデータとプログラムです。

■ブロックを押せるようにする

ブロック移動処理 move_block() を実装します。

    def move_block(self, x, y, dx, dy):
        # 指定の座標にあるブロックを移動させる
        for block in self.blocks:
            if block.x == x and block.y == y:
                xnext = x + dx
                ynext = y + dy
                # 異動先チェック
                if self.check_wall(xnext, ynext) or self.check_block(xnext, ynext):
                    # 異動先に壁またはブロックがあるので移動できない
                    return False
                
                # 移動できる
                block.move_next(xnext, ynext)
        
        # 存在しない場合は移動できる
        return True

引数の x, y はブロックが存在する座標です。dx, dy がブロックの移動方向となります。check_wall() またh check_block() のどちらかが有効であれば移動できないことになるのでブロックはそのままとなります。

では、この関数を input_key() で呼び出します。

    def input_key(self):
        # キー入力判定
        xnext = self.x
        ynext = self.y
        if pyxel.btn(pyxel.KEY_LEFT):
            xnext -= 1
        elif pyxel.btn(pyxel.KEY_RIGHT):
            xnext += 1
        if pyxel.btn(pyxel.KEY_UP):
            ynext -= 1
        elif pyxel.btn(pyxel.KEY_DOWN):
            ynext += 1
        
        if self.x == xnext and self.y == ynext:
            # 移動先が同じなので移動しない
            return False
        
        if self.check_wall(xnext, ynext):
            # 壁なので移動できない
            return False

        if self.check_block(xnext, ynext):
            # ブロック移動処理
            dx = xnext - self.x
            dy = ynext - self.y
            if self.move_block(xnext, ynext, dx, dy) == False:
                # ブロックを動かせない
                return False
            
        # 移動する
        self.xnext = xnext
        self.ynext = ynext

        return True

self.check_block() でブロックが存在していた(True)場合、ブロック移動処理をmove_block()を呼び出すようにします。

さらに移動のための更新処理が必要なので、update() を修正します。

    def update(self):
        # 更新
        if self.state == State.Standby:
            # キー入力待ち
            if self.input_key():
                # 移動開始する
                self.move_timer = 0
                self.state = State.Moving
        elif self.state == State.Moving:
            # 移動中
            for block in self.blocks:
                block.update()
            self.move_timer += 1
            if self.move_timer == self.MOVE_SPEED:
                # 移動完了
                self.x = self.xnext
                self.y = self.ynext
                if self.check_clear():
                    # ゲームクリア条件を満たした
                    self.state = State.GameClear
                else:
                    self.state = State.Standby

移動中の場合、プレイヤーと一緒にブロックも動かすようにしました。では、実行してブロックが押せることを確認してみてください。

ブロックの先に壁やブロックがあった場合にもちゃんと押せないことを確認しておくと良いでしょう。

■ゲームクリア判定を実装する

最後にゲームクリア判定を実装します。考え方としては、ブロックの存在する座標が「ブロックの目標となるチップ」だった場合に、そのブロックを正しい位置に置かれた、と判定します。

まず、マップチップ情報を取得するための関数 get_map() を追加します。

    def get_map(self, i, j):
        # 指定の位置の値を取得する
        return self.map[j][i]

次に、Blockクラスを修正します。正しい位置かどうかを判定するためのフラグ completeBlockクラスの__init__() に追加します。

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.xnext = x # 移動先X座標
        self.ynext = y # 移動先Y座標
        self.move_timer = 0 # 移動タイマー
        self.is_moving = False # 移動中かどうか
        self.complete = False # 正しい位置かどうか

次に、Blockクラスの update() を修正します。少しズルをして引数に Appクラスを渡すようにしました。

    def update(self, app):
        # 更新
        if self.is_moving:
            # 移動中
            self.move_timer += 1
            if self.move_timer >= App.MOVE_SPEED:
                # 移動完了
                self.x = self.xnext
                self.y = self.ynext
                self.is_moving = False

        if app.get_map(self.x, self.y) == 22:
            # 目印の場所に移動した
            self.complete = True
        else:
            self.complete = False

Appクラスの get_map() を呼び出してマップ情報を取得して、"22" (目標のチップ番号) なら移動完了とみなします。

Blockクラスの draw() を修正します。

    def draw(self):
        # 描画
        x, y = self.x, self.y
        if self.is_moving:
            # 移動中
            dx = self.xnext - self.x
            dy = self.ynext - self.y
            rate = 1.0 * self.move_timer / App.MOVE_SPEED
            x += dx * rate
            y += dy * rate
        
        chip = 20
        if self.complete:
            chip = 21 # 正しい位置にある
        Map.draw_chip(x, y, chip)

正しい位置に移動したら描画するものを変えるという変更です。

Blockクラスの update() の引数を変えてしまったので、Appクラスの update() で呼び出している部分も修正します。

    def update(self):
        # 更新
        if self.state == State.Standby:
            # キー入力待ち
            if self.input_key():
                # 移動開始する
                self.move_timer = 0
                self.state = State.Moving
        elif self.state == State.Moving:
            # 移動中
            for block in self.blocks:
                block.update(self)
            self.move_timer += 1
            if self.move_timer == self.MOVE_SPEED:
                # 移動完了
                self.x = self.xnext
                self.y = self.ynext
                if self.check_clear():
                    # ゲームクリア条件を満たした
                    self.state = State.GameClear
                else:
                    self.state = State.Standby

「移動中」のところの block.update(self) という部分が修正箇所です。

最後に、Appクラスの check_clear() を修正します。

    def check_clear(self):
        # クリア判定
        for block in self.blocks:
            if block.complete == False:
                # まだ正しくない場所にあるものがある
                return False
        
        # 全て配置済み
        return True

全てのブロックの complete フラグが True であればゲームクリアとします。

では実行して、ブロックを所定の位置に移動させるとゲームクリアになることを確認します。

■完成データ

もしうまくいかない場合は、こちらのデータで確認をしてみてください。


この記事が気に入ったらサポートをしてみませんか?