見出し画像

【Pyxel】Pyxelで学ぶゲームプログラム 〜マップデータの読み込み2

前回に引き続いて、読み込んだマップデータを元に、プレイヤーの移動処理とゲームクリアの判定を実装します。

なお、前回使用したデータは、以下からダウンロード可能です。
http://syun777.sakura.ne.jp/tmp/pyxel/tilemap.zip

■プレイヤーの移動

一筆書きでは、マスをワープして移動していましたが、今回は滑らかにマス目を移動できるようにします。

まずは状態管理用に、Stateの定数を作成します。

import pyxel
import math # math.floorを使うので必要
from enum import Enum # Enumクラスを使う

# 状態
class State(Enum):
    Standby = 1 # 待機中
    Moving  = 2 # 移動中 

次に、変数初期化用のためにAppクラス に init() を追加します。__init__() から「マップデータ読み込み」と「プレイヤー位置取得」と「マップデータからの削除」の部分を移動させて、新しく self.xnext / self.ynext / self.move_timer / self.state 変数を追加します。

    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.move_timer = 0 # 移動中タイマー
        self.state = State.Standby # 状態

この初期化関数を __init__() で呼び出します。

class App:
    MOVE_SPEED = 10 # 移動速度
    def __init__(self):        
        pyxel.init(160, 120, fps=60)
        self.init() # 初期化
        pyxel.image(0).load(0, 0, "tileset.png")
        pyxel.run(self.update, self.draw)

移動速度の定数 "MOVE_SPEED" も追加しました。

キー入力処理として、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
        
        # 移動する
        self.xnext = xnext
        self.ynext = ynext

        return True

移動できたら True を返すようにしています。
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
                self.state = State.Standby

プレイヤーの描画 draw_player() を修正します。

    # プレイヤーの描画
    def draw_player(self):
        px = self.x
        py = self.y
        if self.state == State.Moving:
            # 移動中だけ特殊処理
            dx = self.xnext - self.x
            dy = self.ynext - self.y
            # 移動先との差を線形補間する
            px += dx * self.move_timer / self.MOVE_SPEED
            py += dy * self.move_timer / self.MOVE_SPEED
        Map.draw_chip(px, py, 8)    

移動中 (State.Moving) の場合のみ、移動先と現在値の差分をとって線形補間で動かすようにしています。
では、実行してニャンコが自由に動き回るのを確認してください。

ひとまず動き回れるようになったのですが、画面外や壁との当たり判定ができていません。

■障害物との当たり判定を実装する

壁と当たり判定を行うようにします。

マップチップ番号「5, 6, 7, 10, 11, 12, 15, 16, 17」 とマップ外に出たら壁とみなして移動できないようにします。
まずは壁との当たり判定をチェックする関数 check_wall() を作成します。

    def check_wall(self, i, j):
        # 指定の座標が壁かどうかチェックする
        if i < 0 or 6 <= i:
            return True # マップ外
        if j < 0 or 6 <= j:
            return True # マップ外

        v = self.map[j][i]
        if v in [5, 6, 7, 10, 11, 12, 15, 16, 17]:
            # 壁なので移動できない
            return True

        # 移動可能
        return False

まずはマップ外かどうかを判定して、その後チップが壁かどうかをチェックします。
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

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

        return True

では実行して、壁との判定が正しくできていることを確認します。

■アイテムの配置。回収できるようにする

最後に、アイテムを配置して、アイテムを全て回収したらゲームクリアとします。
"map.txt" を開いて、以下のように編集します。

0,  0,  0, 0,  0,  8
0,  0,  0, 0,  1,  0
1,  5,  7, 0,  0,  0
0, 15, 17, 1,  0,  0
0,  0,  0, 0,  5,  7
0,  0,  0, 0, 15, 17

実行してりんごアイテムが配置されることを確認します。

ただし、まだ回収することはできません。

プレイヤーの移動完了後、アイテムを回収するようにします。
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
                # アイテム回収
                self.set_map(self.x, self.y, 0)
                self.state = State.Standby

実行するとアイテムが回収できるようになりました。

最後にゲームクリア判定を実装します。まずは State に GameClear (ゲームクリア) 定数を追加します。

# 状態
class State(Enum):
    Standby = 1 # 待機中
    Moving  = 2 # 移動中 
    GameClear = 3 # ゲームクリア

続いてupdate() にクリア判定処理を追加します。
self.search_map(1) でりんごが存在しているかどうかをチェックすることができます。

    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
                # アイテム回収
                self.set_map(self.x, self.y, 0)
                # りんごを検索
                px, py = self.search_map(1)
                if px == -1:
                    # 見つからなかったのでゲームクリア
                    self.state = State.GameClear
                else:
                    self.state = State.Standby

draw() でクリア状態であれば "GAME CLEAR" を表示するようにします。

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

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

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

実行して "GAME CLEAR" が表示されることを確認します。

■完成データについて

もしうまく動作しない場合は、完成データがこちらからダウンロード可能です。
http://syun777.sakura.ne.jp/tmp/pyxel/tilemap2.zip

■マップデータについて

前回と今回で実装したマップの処理ですが、実はPyxelにはマップエディタが内蔵されていて、それを使うとマップの作成が少し楽できます。それについては今後紹介しようと思っています。

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