Pyxelで倉庫番ゲームを作る(前編)


始めに

パズルゲームなら簡単に作れるかなーと思い作りました。前回と比べて長めの記事になるので分割して書きます。

作るもの

倉庫番 -Wikipedia
画面には動かせるブロック、動かせないブロック(壁)があり、プレイヤーは動かせるブロックに向かって移動すると、ブロックを押すことが出来る。このブロックをゴールまで全て運べばゲームクリア。ただしブロックは一度に一つしか運べず、引っ張ることは出来ない。

動作環境

Microsoft Windows [Version 10.0.17134.648]
Python 3.7.1
pyxel  1.0.1

グラフィック

今回はプレイヤーやブロックなど様々なものが登場するので、それらの画像を作成するためにPyxelEditorを使用します。PyxelEditorはコンソールから起動します。

pyxeleditor box.pyxel

するとbox.pyxelというファイルが作成されPyxelEditorが起動するので、イメージバンクと呼ばれる場所に使用する画像を描いていきます。
まずはこんな感じで描きましょう。

一応解説を入れると一番右上とその左隣がプレイヤー、下の青い四角は箱のゴール地点、その下の黄色いのが箱、緑がゴール地点に置かれた箱、最後が壁です。

次にタイルマップエディタでゲームの背景(タイルマップ)を作ります。ゲーム開始から終了まで変化がほとんどないものを背景にするので、今回は壁と箱のゴール地点を配置します。
今回使用するのは8*8のサイズだけなので、一番左上のマスだけ編集します。

※タイルマップエディタ起動時はイメージバンクの一番左上の画像で埋め尽くされています

ここで重要になってくるのが先ほどのイメージエディタの時に見えていた紺色の線です。この線は縦横8ドットの広さになっているのでこのマスのことを「タイル」と呼ぶことにします。プログラムでタイルマップの指定した座標のデータを取得する時、「その座標にあるタイルのイメージバンクの座標に対応したデータ」が得られます。先ほどのタイルマップをデータに表すと以下のようになります(色はわかりやすくするために付けただけであり、実際のデータは数字だけです)。

イメージバンクの一番左上のタイルはデフォルトで0のデータを持っています。以降右のタイルへ移動するごとにデータは+1ずつ増えます。イメージバンクの横幅の最大値は255なので32個目のタイルは31のデータで、33個目のタイルは1個目のタイルの1個下になりデータは32です。

今回のプログラムではプレイヤーが進行可能かどうかの判断はタイルマップからデータを取得し、その値がいくつなら壁と認識する、という手法をとっています。なのでタイルマップの黒くなっている部分は青い箱の左隣のタイル(32のデータであるタイル)を使用してください。
・・・本当は34のデータのタイルを使っても問題ありませんが一応。

レイアウト

box.py
import pyxel
class App:
    #初期設定
    def __init__(self):
        pyxel.init(64,64,caption="box")
        #pyxeleditorで作成した画像を読み込む
        #box.pyxelはbox.pyと同じ場所に用意してね
        pyxel.load('box.pyxel')
        pyxel.run(self.update,self.draw)

    #ゲーム管理
    def update(self):
        pass

    #画面描画
    def draw(self):
        #今回は背景を白色で塗りつぶす
        pyxel.cls(7)
        #タイルマップを貼り付ける
        pyxel.bltm(0,0,0,0,0,8,8,0)

App()

実行結果

プレイヤーの実装

プレイヤーのクラスに「座標」と「方向」を作ります。プレイヤーは上下左右の4方向を向くことになるので、全部で4枚の画像が必要かと思いますが、Pyxelの機能で画像を反転させて描画することが出来るので、上下と左右を向いた時の絵はそれぞれ同じ絵を使い、反転させることで4方向を表現します。
・・・なので描いた絵によってはプレイヤーが逆立ちをしてしまいますスウィートホーム。そうなっても不自然でないような絵がいいと思います。

これはその絵を表示させるときに使う関数です。

blt(x, y, img, u, v, w, h, [colkey])

イメージバンクimg(0-2) の (u, v) からサイズ (w, h) の領域を (x, y) にコピーする。w、hそれぞれに負の値を設定すると水平、垂直方向に反転する。colkeyに色を指定すると透明色として扱われる

先ほどのイメージバンクの画像を見ていただければわかると思いますが、プレイヤーが下向きの画像は(u,v)=(0,0)~(7,7)で右向きの画像は(u,v)=(8,0)~(15,0)の範囲に描かれているので、上下を向いていればu=0、左右を向いていればu=8にする変数が必要です。そして画像を(非)反転させるためにw,hを8か-8にする変数を作ります。上向きの時hを-8にする必要がありますが、上下を向いている時点で既に左右対称とするとwとhの両方が-8でも問題ありません。これは左右を向いていても同じなので(上下対称)向きを管理する変数はこうなりました。

上向き:blt(x, y, img, 0, 0, -8, -8)
下向き:blt(x, y, img, 0, 0, 8, 8)
右向き:blt(x, y, img, 8, 0, 8, 8)
左向き:blt(x, y, img, 8, 0, -8, -8)
        ↓
blt(x, y, img,8*self.player.v[0],0,8*self.player.v[1],8*self.player.v[1])
v[0]=0:プレイヤーは上下を向いている
v[0]=1:プレイヤーは左右を向いている
v[1]=1:元の画像を使う(下方向か右方向)
v[1]=-1:元の画像を反転させて使う(上方向か左方向)

・・・うまく説明できてるのかなぁ…?

box.py
#追加
#プレイヤーのクラス
class Player:
    def __init__(self):
        #座標
        self.x=8
        self.y=8
        #方向
        self.v=[0,-1]

class App:
    #追加
    player=Player()

    #追加
    #移動関数 今は移動というより方向転換しかできない
    def move(self,x,y):
        self.player.v=[abs(x),x+y]

    #ゲーム管理
    def update(self):
        #追加
        #それぞれ十字キーの各方向を押したらmove関数を呼び出す
        if pyxel.btn(pyxel.KEY_UP):
            self.move(0,-1)
        elif pyxel.btn(pyxel.KEY_DOWN):
            self.move(0,1)
        elif pyxel.btn(pyxel.KEY_RIGHT):
            self.move(1,0)
        elif pyxel.btn(pyxel.KEY_LEFT):
            self.move(-1,0)
   
    #画面描画
    def draw(self):
        #追加
        pyxel.blt(self.player.x,self.player.y,0,8*self.player.v[0],0,8*self.player.v[1],8*self.player.v[1],11)

App()

実行結果

プレイヤーは動きませんが向きが変わるかと思います。

今度はプレイヤーを移動させましょう。タイル大きさは縦横8ドットなのでプレイヤーも8ドット刻みに移動することになりますが、一瞬のうちに8ドット移動するのもヘンなので、移動しようとしたらmove_count変数を8カウントにし、1カウント以上の間だけプレイヤーが1ドットずつ移動しカウントダウンするというプログラムにしましょう。また壁には移動できないように、移動先のタイルマップから取得したデータが65以上(壁)ならばカウントを0にして移動させないようにします。

#追加
import math

class App:
    #初期設定
    def __init__(self):
        #追加
        #プレイヤーの移動の為に使う変数
        self.move_count=0
        self.move_x=0
        self.move_y=0

    #移動関数
    def move(self,x,y):
        #追加
        #カウントが0なら(移動完了済みなら)移動用の変数に値などを代入する
        if self.move_count==0:
            self.move_count=8
            self.move_x=x
            self.move_y=y
            #↓この命令は前回に書いたものなので注意してね
            self.player.v=[abs(x),x+y]

            #プレイヤー現在の座標をタイルマップの座標に変換し
            #進行方向にあるタイルを調べて65以上(通れない壁)なら移動は取りやめにする
            if pyxel.tilemap(0).get(math.floor(self.player.x/7)+self.move_x,math.floor(self.player.y/7)+self.move_y)>=65:
                self.move_count=0

    #ゲーム管理
    def update(self):
        #追加
        #カウントが8以上で移動する
        if self.move_count>0:
            self.move_count-=1
            self.player.x+=self.move_x
            self.player.y+=self.move_y

実行結果

青い四角は箱のゴール地点なのでその場所はプレイヤーも移動できます。他はちゃんと壁をすり抜けたりしませんね?

後編へ続く

今回はここまでです!次回は箱の実装をします。
Pyxelで倉庫番ゲームを作る(後編)
Pyxel公式マニュアル