【Pyxel入門】パックマン風ゲームを作る #4 マップの作成

Python

前回の記事ではパックマンの向きが変わるようにしました。

【Pyxel入門】パックマン風ゲームを作る #3 キャラクターの向き

今回は、マップを作成していきます。

マップチップ作成

マップチップを以下のように作成しました。

水平、垂直反転で使い回せるものは省略しましたが、反転を使わずに全部書いた方が楽だったと、作ってみて思いました。

stage
室井

昔のゲームは容量を削減するためにこういったところで頑張ったらしいですね。

マップデータの定義

2次元リストに各マス目の情報を以下のように持たせることにしました。

1文字目:水平反転の有無(0 か 1)
2文字目:垂直反転の有無(0 か 1)
3文字目:マップチップの番号(左上から16×16ごとに番号を割り当ててある)

self.map = [
    [ "000", "000", "000", "000", "000", "000", "000", "002", "000", "000", "000", "000", "000", "000", "000"],
    [ "000", "005", "105", "000", "005", "105", "000", "014", "000", "005", "105", "000", "005", "105", "000"],
    [ "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000"],
    [ "000", "005", "105", "000", "004", "000", "005", "006", "105", "000", "004", "000", "005", "105", "000"],
    [ "000", "000", "000", "000", "002", "000", "000", "002", "000", "000", "002", "000", "000", "000", "000"],
    [ "008", "008", "101", "000", "007", "105", "000", "014", "000", "005", "107", "000", "001", "008", "008"],
    [ "018", "018", "111", "000", "014", "000", "000", "000", "000", "000", "014", "000", "011", "018", "018"],
    [ "000", "000", "000", "000", "000", "000", "005", "003", "105", "000", "000", "000", "000", "000", "000"],
    [ "101", "000", "004", "000", "004", "000", "000", "000", "000", "000", "004", "000", "004", "000", "001"],
    [ "111", "000", "014", "000", "014", "000", "005", "006", "105", "000", "014", "000", "014", "000", "011"],
    [ "000", "000", "000", "000", "000", "000", "000", "002", "000", "000", "000", "000", "000", "000", "000"],
    [ "000", "005", "10a", "000", "005", "105", "000", "014", "000", "005", "105", "000", "00a", "105", "000"],
    [ "000", "000", "002", "000", "000", "000", "000", "000", "000", "000", "000", "000", "002", "000", "000"],
    [ "101", "000", "014", "000", "004", "000", "005", "003", "105", "000", "004", "000", "014", "000", "001"],
    [ "109", "000", "000", "000", "002", "000", "000", "000", "000", "000", "002", "000", "000", "000", "009"],
]

自分でも書いていて意味がわからなくなりそうだったので、反転を使わずにマップチップにゴリゴリ書いたほうが楽ですね。

pyxelには標準でタイルマップエディタが入っているので、それを使っても良いでしょう。

Mapクラスの作成・呼び出し

Mapクラスを以下のように作成しました。
インスタンスは生成しないので、defの上に@classmethodと付けておきます。

class Map:
    SIZE = 16 # チップサイズ
    CHIP_WIDTH = 16
    CHIP_HEIGHT = 1
    
    # マップチップ座標をスクリーン座標に変換
    @classmethod
    def to_screen(cls, i, j):
        return (i * cls.SIZE, j * cls.SIZE)
    
    # マップチップの描画
    @classmethod
    def draw_chip(cls, i, j, val):
        # スクリーン座標に変換
        x, y = cls.to_screen(i, j)
        # マップチップの番号を取得
        no = str(val[2])
        no = 10 if no == "a" else int(no)
        
        # チップ画像の座標を計算
        u = (no % cls.CHIP_WIDTH) * cls.SIZE
        v = (math.floor(no / cls.CHIP_WIDTH)) * cls.SIZE
       
        # チップ反転
        w_reverse = -1 if val[0] == "1" else 1
        h_reverse = -1 if val[1] == "1" else 1

        pyxel.blt(x, y, 1, u, v, cls.SIZE * w_reverse, cls.SIZE * h_reverse, 0)

マップチップの枚数が二桁になったときの処理を考えておらず、作っている途中で10枚目が必要になってしまいました。

そこで、マップデータには10の代わりにaを持たせて、

no = 10 if no == "a" else int(no)

という命令で誤魔化しました。

Appクラスからは以下の関数で呼び出します。

def draw_map(self):
    # 各チップの描画
    for j, arr in enumerate(self.map):
        for i, d in enumerate(arr):
            Map.draw_chip(i, j, d)

ソースコード

前回からの主な変更点

  • Mapクラスの追加
  • Appクラスにマップデータを2次元リストで定義
import pyxel
import math

WINDOW_H = 240
WINDOW_W = 240
char_H = 16
char_W = 16

# キャラクターのクラスを作成
class Player:
    def __init__(self, x, y, h):
        
        # 表示位置の座標を示す変数
        self.x = x
        self.y = y
        
        # 画像の座標を示す変数
        self.u = 0
        self.h = h # プレイヤーの判別にも使用

        # 方向を示す変数
        self.up_down = 1
        self.left_right = 1
        self.rotation = 0

        # 画像切り替えの折返し判断
        self.sw = 0
        
        # 移動速度
        self.speed = 4
        
        # キーバインド
        if h == 0: #プレイヤー1の設定
            self.up = pyxel.KEY_UP
            self.down = pyxel.KEY_DOWN
            self.left = pyxel.KEY_LEFT
            self.right = pyxel.KEY_RIGHT
        if h == 1: #プレイヤー2の設定
            self.up = pyxel.KEY_W
            self.down = pyxel.KEY_S
            self.left = pyxel.KEY_A
            self.right = pyxel.KEY_D
        if h == 2: #プレイヤー3の設定
            pass
        if h == 3: #プレイヤー4の設定
            pass

    # キャラグラフィック変更
    def update(self):
        if self.sw == 0:
            self.u += 1
            if self.u == 2:
                self.sw = 1
        else:
            self.u -= 1
            if self.u == 0:
                self.sw = 0
    
    # キャラクター移動
    def move(self):
        if pyxel.btn(self.up):
            self.y -= self.speed
            self.up_down = 1
            self.rotation = 64
        if pyxel.btn(self.down):
            self.y += self.speed
            self.up_down = -1
            self.rotation = 64
        if pyxel.btn(self.left):
            self.x -= self.speed
            self.left_right = 1
            self.rotation = 0
        if pyxel.btn(self.right):
            self.x += self.speed
            self.left_right = -1
            self.rotation = 0

    def draw(self):
        pyxel.blt(self.x, self.y, 0, self.u * 16, self.h * 16 + self.rotation, char_W * self.left_right, char_H * self.up_down, 0)

class Map:
    SIZE = 16 # チップサイズ
    CHIP_WIDTH = 16
    CHIP_HEIGHT = 1
    
    # マップチップ座標をスクリーン座標に変換
    @classmethod
    def to_screen(cls, i, j):
        return (i * cls.SIZE, j * cls.SIZE)
    
    # マップチップの描画
    @classmethod
    def draw_chip(cls, i, j, val):
        # スクリーン座標に変換
        x, y = cls.to_screen(i, j)
        # マップチップの番号を取得
        no = str(val[2])
        no = 10 if no == "a" else int(no)
               
        # チップ画像の座標を計算
        u = (no % cls.CHIP_WIDTH) * cls.SIZE
        v = (math.floor(no / cls.CHIP_WIDTH)) * cls.SIZE
       
        # チップ反転
        w_reverse = -1 if val[0] == "1" else 1
        h_reverse = -1 if val[1] == "1" else 1


        pyxel.blt(x, y, 1, u, v, cls.SIZE * w_reverse, cls.SIZE * h_reverse, 0)

class App:
    def __init__(self):
        pyxel.init(WINDOW_W, WINDOW_H, caption="Hello Pyxel")

        # マップデータの定義
        self.map = [
            [ "000", "000", "000", "000", "000", "000", "000", "002", "000", "000", "000", "000", "000", "000", "000"],
            [ "000", "005", "105", "000", "005", "105", "000", "014", "000", "005", "105", "000", "005", "105", "000"],
            [ "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000", "000"],
            [ "000", "005", "105", "000", "004", "000", "005", "006", "105", "000", "004", "000", "005", "105", "000"],
            [ "000", "000", "000", "000", "002", "000", "000", "002", "000", "000", "002", "000", "000", "000", "000"],
            [ "008", "008", "101", "000", "007", "105", "000", "014", "000", "005", "107", "000", "001", "008", "008"],
            [ "018", "018", "111", "000", "014", "000", "000", "000", "000", "000", "014", "000", "011", "018", "018"],
            [ "000", "000", "000", "000", "000", "000", "005", "003", "105", "000", "000", "000", "000", "000", "000"],
            [ "101", "000", "004", "000", "004", "000", "000", "000", "000", "000", "004", "000", "004", "000", "001"],
            [ "111", "000", "014", "000", "014", "000", "005", "006", "105", "000", "014", "000", "014", "000", "011"],
            [ "000", "000", "000", "000", "000", "000", "000", "002", "000", "000", "000", "000", "000", "000", "000"],
            [ "000", "005", "10a", "000", "005", "105", "000", "014", "000", "005", "105", "000", "00a", "105", "000"],
            [ "000", "000", "002", "000", "000", "000", "000", "000", "000", "000", "000", "000", "002", "000", "000"],
            [ "101", "000", "014", "000", "004", "000", "005", "003", "105", "000", "004", "000", "014", "000", "001"],
            [ "109", "000", "000", "000", "002", "000", "000", "000", "000", "000", "002", "000", "000", "000", "009"],
        ]
        pyxel.image(0).load(0, 0, "img/img.png")
        pyxel.image(1).load(0, 0, "img/stage.png")
        

        # フレーム更新時に1加算する変数
        self.frame = 0

        # Playerクラスのインスタンスを生成、初期位置の座標を引数に渡す
        self.players = {}
        self.player_1 = Player( 48,  32, 0)
        self.player_2 = Player(176, 192, 1)
        self.player_3 = Player(176,  32, 2)
        self.player_4 = Player( 48, 192, 3)

        # 参加するプレイヤーを登録
        # [TODO] プレイ人数に応じて変更
        self.players = [self.player_1, self.player_2]

        pyxel.run(self.update, self.draw)

    def update(self):
        
        self.frame += 1

        # 3フレームごとにキャラグラフィックを更新
        if self.frame % 3 == 0:
            for player in self.players:
                player.update()

        # キャラクターの座標を更新
        for player in self.players:
            player.move()

        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

    def draw(self):
        pyxel.cls(0)
        # マップの描画
        self.draw_map()

        # キャラクターを描画
        for player in self.players:
            player.draw()
    
    def draw_map(self):
        # 各チップの描画
        for j, arr in enumerate(self.map):
            for i, d in enumerate(arr):
                Map.draw_chip(i, j, d)

App()

実行結果

pacman
室井

マップが描画されました。
次回は当たり判定を実装していきます。