5 枚のタイルを描き、文字で書いた地図を盤面として表示する
リソースエディタで 8×8 ドットのタイルを 5 枚描き、 文字で書いた地図をプログラムに読ませて、盤面を画面の中央に並べます。 このフェーズで書くコードは 94 行(空行とコメントを除くと 70 行)で、まだ何も動きません。
step2/ フォルダを作り、その中に次の 2 つを置きます。
フォルダとファイルはご自身で作ってください。
step2/
├── sokoban.py これから書くコード
└── sokoban.pyxres リソースエディタで作る絵
名前は好きなもので構いませんが、以降のページはこの名前で説明します。
step1 の絵(男の子・お化け・お菓子)は、画面のどこにでも置ける独立した絵でした。 倉庫番の絵はちがいます。8 ドットごとにきっちり区切られたマス目の、その 1 マスを埋める絵です。 だから 1 枚 8×8 ドット、すきまなく塗りつぶすのが基本になります。
用意するのは次の 5 枚です。
| タイル | 地図での文字 | 役割 | 透過 |
|---|---|---|---|
| 床 | . |
荷物も主人公も自由に通れるマス。いちばん地味に描く | なし |
| 壁 | # |
誰も通れないマス。床とはっきり区別がつく色に | なし |
| ゴール | O |
荷物を乗せたい場所。床の一種なので、通ることはできる | なし |
| 荷物 | $ |
押して動かすもの。画面でいちばん目立つ色に | あり |
| 主人公 | @ |
今回は下向きの 1 枚だけ。上・左・右の 3 枚はフェーズ 2、 足踏みの 2 コマ目 4 枚はフェーズ 4 で描く | あり |
step1 フェーズ 1 と同じで、イメージバンクのどこに描くかが、そのままコードの数字になります。 今回はさらに厄介で、フェーズ 4 で主人公が 8 枚に増えます。 あとから場所を動かすとコードを全部直すことになるので、 いま描かない 8 枚ぶんの場所まで、先に決めてしまいます。
(0, 1)(下)、(0, -1)(上)、(-1, 0)(左)、(1, 0)(右)
という並びで持つので、同じ番号で絵も引けるようにしておくと、あとで話が早くなります。
いまは「そういう約束にした」とだけ覚えておいてください。
盤面をコードで表すのに、文字を並べた地図を使います。
STAGE1 = [
"########",
"#......#",
"#..O...#",
"#..$...#",
"#..@...#",
"########",
]
このやり方の良いところは、コードの上で盤面がそのまま目に見えることです。
数字の表で書くと [[1,1,1,1],[1,0,0,1]] のようになり、
どんな形なのか読み取れません。文字なら、書きながら形を確かめられます。
フェーズ 6 でステージを 10 個に増やすとき、この読みやすさが効いてきます。
| 文字 | 意味 | 覚え方 |
|---|---|---|
# | 壁 | 格子模様が壁っぽい |
. | 床 | 何もない |
O | ゴール | 丸い目印(数字のゼロではなく、大文字のオー) |
$ | 荷物 | 倉庫番の慣例。荷物=お金 |
@ | 主人公 | 倉庫番の慣例。人の頭 |
"#......#" は 8 文字です。どの行も同じ文字数にします。
1 行だけ短いと、盤面の右端が欠けたり、あとのフェーズで
IndexError(範囲の外を見にいった、というエラー)が出たりします。
地図を書いたら、いちど縦に見て、右端がまっすぐそろっているかを確かめてください。
ここがこのフェーズの本題です。読み込んだ地図を、そのまま 1 つの表として持ってはいけません。 3 つに分けて持ちます。
盤面のマス目は (x, y)=「左から何番目・上から何番目」ですが、
pyxel.blt() が求めるのはドットの座標です。変換はかけ算 1 つで済みます。
ドットの x = ox + マス目の x × 8
ドットの y = oy + マス目の y × 8
この ox・oy が盤面を中央に寄せるためのずらし幅です。
ステージによって盤面の大きさが変わるので、読み込むたびに計算し直します。
| 機能 | 役割 |
|---|---|
pyxel edit ファイル名.pyxres | リソースエディタを開く(step0 フェーズ 4 で扱いました) |
pyxel.load("ファイル名.pyxres") | 作った絵をプログラムから読み込む |
pyxel.blt(x, y, img, u, v, w, h, colkey) | イメージバンクから切り出して画面に貼る |
list("文字列") | 文字列を1 文字ずつのリストにばらす |
len(リスト) | 要素の数を返す。行の数・列の数を知るのに使う |
リスト.append(値) | リストの末尾に足す。荷物を集めるのに使う |
a // b | 整数の割り算(小数点以下を切り捨てる) |
step2 フォルダを作ったら、その中で次を実行します。
cd step2
pyxel edit sokoban.pyxres
イメージバンク 0 のタブを開き、上の配置図のとおりに描きます。
1 段目に 4 枚(床・壁・ゴール・荷物)、2 段目の左端に主人公 1 枚です。
1 枚描くごとに Ctrl + S で保存しておくと安心です。
Ctrl + S で保存し、Esc でエディタを閉じます。
step2 フォルダに sokoban.pyxres ができていれば完了です。
step2/sokoban.py に次を入力してください。
# sokoban.py
# step2 フェーズ1:ステージの地図を読み込んで、画面に並べる
import pyxel
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
TILE = 8 # 1 マスの大きさ(ドット)
HUD_HEIGHT = 12 # 画面の上、手数などを出すために空けておく高さ
# イメージバンク 0 の、切り出し位置
V_TILE = 0 # タイルの段
V_PLAYER = 8 # 主人公の段(コマ A)
U_FLOOR = 0
U_WALL = 8
U_GOAL = 16
U_BOX = 24
# ステージの地図
# # 壁 . 床 O ゴール $ 荷物 @ 主人公
STAGE1 = [
"########",
"#......#",
"#..O...#",
"#..$...#",
"#..@...#",
"########",
]
class App:
def __init__(self):
"""起動時の設定"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Sokoban")
pyxel.load("sokoban.pyxres")
self.load_stage(STAGE1)
pyxel.run(self.update, self.draw)
def load_stage(self, rows):
"""文字の地図を読んで、地形・主人公・荷物に分ける"""
self.tiles = []
self.boxes = []
for y in range(len(rows)):
line = []
for x in range(len(rows[y])):
mark = rows[y][x]
if mark == "@":
self.player_x = x
self.player_y = y
mark = "." # 主人公の足もとは床
elif mark == "$":
self.boxes.append([x, y])
mark = "." # 荷物の下も床
line.append(mark)
self.tiles.append(line)
# 盤面を画面の中央に置くための、ずらし幅
board_w = len(self.tiles[0]) * TILE
board_h = len(self.tiles) * TILE
self.ox = (SCREEN_WIDTH - board_w) // 2
self.oy = HUD_HEIGHT + (SCREEN_HEIGHT - HUD_HEIGHT - board_h) // 2
def update(self):
"""フレーム毎の更新処理(まだ何もしない)"""
pass
def draw(self):
"""描画処理"""
pyxel.cls(pyxel.COLOR_BLACK)
# 地形を、上の行から 1 マスずつ描く
for y in range(len(self.tiles)):
for x in range(len(self.tiles[y])):
mark = self.tiles[y][x]
if mark == "#":
u = U_WALL
elif mark == "O":
u = U_GOAL
else:
u = U_FLOOR
pyxel.blt(self.ox + x * TILE, self.oy + y * TILE,
0, u, V_TILE, TILE, TILE)
# 荷物
for box in self.boxes:
pyxel.blt(self.ox + box[0] * TILE, self.oy + box[1] * TILE,
0, U_BOX, V_TILE, TILE, TILE, pyxel.COLOR_BLACK)
# 主人公
pyxel.blt(self.ox + self.player_x * TILE, self.oy + self.player_y * TILE,
0, 0, V_PLAYER, TILE, TILE, pyxel.COLOR_BLACK)
App()
cd step2
python sokoban.py
黒い画面の中央(やや下)に、8×6 マスの盤面が出れば成功です。 外周が壁、中は床、上のほうにゴールの印、その下に荷物、いちばん下に主人公—— という並びになります。まだ何も動きません。
ox は 48、oy は 42 になります。
load_stage() の最後に print(self.ox, self.oy) を書き足して実行すると、
ターミナルに 48 42 と出ます。確かめ終わったら消してください。
list() を使わずに、1 文字ずつ足している
文字列をリストにばらす方法として list("#..O") があり、
これは ['#', '.', '.', 'O'] を返します。ところが今回のコードでは使っていません。
ばらしながら、@ と $ を見つけて取り除く必要があるからです。
for x in range(len(rows[y])):
mark = rows[y][x] # 1 文字取り出す
if mark == "@": # 主人公だったら
self.player_x = x # 位置を覚えて
self.player_y = y
mark = "." # 床にすり替える
elif mark == "$": # 荷物だったら
self.boxes.append([x, y])
mark = "."
line.append(mark) # 1 文字ずつ line に足す
mark = "." と入れ物の中身を差し替えてから append しているのがポイントです。
こうすると self.tiles には地形しか残りません。
rows[y] は "#..O...#" という文字列です。
文字列は rows[y][x] = "." のように一部だけ書き換えることができません
(TypeError になります)。line はリストなので、line[3] = "." と書き換えられます。
フェーズ 3 で荷物を動かすときにこれが必要になるので、
いま文字列からリストに直しておきます。
for の中に for が入っている形です。難しく見えますが、
やっていることは紙の表を指でなぞるのと同じです。
for y in range(len(self.tiles)): # 上の行から順に
for x in range(len(self.tiles[y])): # その行を、左から順に
mark = self.tiles[y][x] # 1 マス取り出す
| 書き方 | 意味 | この地図での値 |
|---|---|---|
len(self.tiles) | 行の数(縦のマス数) | 6 |
len(self.tiles[y]) | その行の文字数(横のマス数) | 8 |
self.tiles[y][x] | 上から y、左から x のマス | '#' や '.' |
外側が y、内側が x です。この順番は逆にしないでください。
self.tiles[y][x] という書き方に合わせておくと、
「先に行を選び、次にその中の列を選ぶ」という読み方が、コードの形とそろいます。
// は「整数のままの割り算」
Python では 96 / 2 は 48.0(小数)になります。
96 // 2 なら 48(整数)です。
ドットの座標に小数が混じっても Pyxel は描いてくれますが、
座標は整数で持っておくほうが、あとの計算で混乱しません。
中央寄せのように「割り切れないこともある」計算では、
// を使って切り捨てると決めておきます。
self.ox = (SCREEN_WIDTH - board_w) // 2 # (160 - 64) // 2 = 48
たとえば盤面が 9 マス(72 ドット)なら (160 - 72) // 2 = 44 です。
左右の余白は 44 と 44 でぴったり。11 マス(88 ドット)なら 36 となり、
右が 1 ドット多くなりますが、見て分かる差ではありません。
colkey を付けるものと、付けないものpyxel.blt(..., 0, u, V_TILE, TILE, TILE) # 地形:透過なし
pyxel.blt(..., 0, U_BOX, V_TILE, TILE, TILE, pyxel.COLOR_BLACK) # 荷物:黒を透過
地形はいちばん下に敷くので、透過する必要がありません。 むしろ透過しないほうが、前に描いたものを確実に塗りつぶせます。
荷物と主人公は地形の上に重ねるので、絵の余白を透過させないと
黒い四角ごと貼りつくことになります。
pyxel.COLOR_BLACK(色番号 0)を透明として指定します。
blt() を呼ぶたびに指定するものなので、絵によって違う色を抜けます。
実際にこのあと、主人公の目を黒く描くために、主人公だけ透過色をグレーに変えました
(フェーズ 5 / 備忘録)。
透過色に選ぶのは、その絵の中で背景以外に 1 ドットも使っていない色です。
self. が付く名前| 書き方 | 意味 | 例 |
|---|---|---|
TILE(大文字) |
最後まで変わらない値。ファイルの先頭にまとめて置く | TILE, U_WALL, STAGE1 |
self.ox |
ゲームの進行につれて変わる値。ステージが変われば作り直される | self.tiles, self.player_x, self.boxes |
mark, line |
その場かぎりの値。関数を抜けたら消える | x, y, u |
self.ox を OX という定数にしなかったのは、
ステージごとに盤面の大きさが変わるからです。
フェーズ 6 で 10 ステージに増やすと、この値はステージを切り替えるたびに変わります。
| 症状 | 原因と対処 |
|---|---|
FileNotFoundError と出る |
sokoban.py と sokoban.pyxres が
同じフォルダにあるか確認してください。
pyxel.load() の相対パスは、実行するスクリプトの置き場所が基準です |
AttributeError: 'App' object has no attribute 'player_x' |
地図に @ が 1 つも入っていません。
STAGE1 を見直してください。
主人公がいないと、位置を覚える行が一度も実行されません |
| 盤面がガタガタ/右端が欠ける | 地図の行ごとに文字数が違います。中央寄せの計算には
self.tiles[0](1 行目)の長さを使っているので、
1 行目だけ長いと右がはみ出します。全部そろえてください |
| 盤面が画面の左上に寄っている | blt() に self.ox +、self.oy + を
足し忘れています |
| 荷物や主人公のまわりに黒い四角が見える | blt() の最後の pyxel.COLOR_BLACK が抜けています |
| 主人公や荷物の輪郭が、床のように抜けて見える | 絵の輪郭を黒(色番号 0)で描いています。黒は透明として扱われるので、 輪郭には色番号 1 などの黒以外の暗い色を使ってください |
| 床が真っ黒で、盤面の形が分からない | 床のタイル(u = 0)を描いていないか、
全面を色番号 0 で塗っています。黒以外の暗い色で塗り直してください |
| となりのタイルが混ざって表示される | 8 ドットの区切りをまたいで描いています。 エディタで、はみ出した部分を消してください |
| ゴールが見えない | ゴールの上に荷物や主人公が乗っていませんか。 いまはそれで正しい動きです。フェーズ 5 で 「ゴールに乗った荷物」の絵に切り替えて、見て分かるようにします |
盤面が出せました。次は主人公を矢印キーで動かします。
このフェーズで作った self.tiles・self.player_x・self.player_y が
そのまま土台になり、update() の pass が中身に置き換わります。