盤面の上ではもう着いている。あとから追いつくのは、絵だけ
いまはキーを押した瞬間に、主人公がカクッと 8 ドット飛びます。 これを8 フレームかけて、1 ドットずつすべるようにします。 押した荷物もいっしょにすべります。あわせて足踏みを入れ、 矢印キーを押しっぱなしにすると歩き続けるようにします。 フェーズ 3 のコードに 36 行ほど書き足して、できあがりは 192 行になります。
「8 フレームかけて動かす」と聞くと、 1 ドット動かすたびに壁や荷物を調べ直す作りを思い浮かべるかもしれません。 それはとても大変です。ドットの途中ではマスとマスのあいだにいるので、 「いまどのマスにいるのか」がはっきりしません。
倉庫番では、そうしません。キーを押したその瞬間に、判定はぜんぶ終わらせます。
壁か、荷物か、押せるか——フェーズ 3 で書いた try_move() が、
今までどおり一瞬で答えを出し、self.player_x はもう新しいマスになります。
そのうえで、絵を描くときだけ「まだ着いていないふり」をします。 これが step2 概要の土台 3 に書いた 「マス目の位置と、目に見えている位置は別もの」です。
player_x と from_x は別の変数で持ちます。1 つにまとめようとすると、必ず判定がおかしくなります| 変数 | 意味 |
|---|---|
self.from_x / self.from_y |
見た目の出発マス。止まっているときは player_x と同じ値 |
self.walk_timer |
歩き始めてから何フレーム経ったか。0 なら止まっている |
self.pushing |
いまの一歩で荷物を押しているか(True / False) |
歩いている 8 フレームのとちゅうで次のキーを受け付けてしまうと、 着く前に次のマスへ移ろうとして、絵の出発点がめちゃくちゃになります。 そこで止まっているときだけキーを見ることにします。
pyxel.btnp() は
「押した瞬間だけ True」でした。これだと押しっぱなしでは 1 歩しか進みません。
今回は pyxel.btn()(押されている間ずっと True)に変えます。
歩いている間は見に行かないので、連打しすぎることもありません。
荷物を押したとき、主人公だけがすべって、荷物は瞬間移動すると台なしです。 押した荷物も、まったく同じ 8 フレームで、同じだけずらして描きます。
self.pushing という「この一歩で押したかどうか」の覚え書きを持ちます。
| 機能 | 役割 |
|---|---|
pyxel.btn(キー) |
押されている間ずっと True。btnp(押した瞬間だけ)と入れ替える |
// |
小数を切り捨てる割り算。ドットは整数なので、こちらを使う |
| 値を返すメソッド | slide_pos() のように、計算結果を返すだけのメソッドを自分で作る |
if ~ else |
荷物ごとに「すべらせる/そのまま描く」を切り替える |
btn だけです
残りはぜんぶ Python の話です。絵を動かしているのは、blt に渡す座標の計算だけで、
Pyxel 側に「アニメーション機能」があるわけではありません。
ここが分かると、step3 以降でどんな動きでも作れるようになります。
8 段階に分けます。3-2 から 3-8 まで書かないと動かないので、 最後まで書いてから実行してください。
リソースエディタ(pyxel edit sokoban.pyxres)を開き、
イメージバンク 0 の 3 段目(v = 16)に、主人公の 4 方向をもう 1 組描きます。
場所は フェーズ 1 の配置図のとおりです。
| 位置 | 描くもの |
|---|---|
u = 0, v = 16 | 下向き・コマ B |
u = 8, v = 16 | 上向き・コマ B |
u = 16, v = 16 | 左向き・コマ B |
u = 24, v = 16 | 右向き・コマ B |
V_PLAYER の名前を変える
ファイルの上のほう、定数を並べているところを書き換えます。
V_PLAYER は V_PLAYER_A に名前を変えます
(B と並べたときに、どちらがどちらか分かるようにするためです)。
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
TILE = 8 # 1 マスの大きさ(ドット)
HUD_HEIGHT = 12 # 画面の上、手数などを出すために空けておく高さ
MOVE_FRAMES = 8 # 隣のマスへ移りきるまでにかけるフレーム数 ← 足す
# イメージバンク 0 の、切り出し位置
V_TILE = 0 # タイルの段
V_PLAYER_A = 8 # 主人公の段(コマ A) ← 名前を変える
V_PLAYER_B = 16 # 主人公の段(コマ B・足を入れ替えた絵) ← 足す
MOVE_FRAMES = 4(1 フレームに 2 ドット)にすると、きびきび歩きます。
あとで好みに合わせて変えてみてください。
load_stage() に、歩きの状態を足す最初の 3 行のうしろに 2 行足し、二重ループのあとにも 2 行足します。
def load_stage(self, rows):
"""文字の地図を読んで、壁・主人公・荷物に分ける"""
self.tiles = []
self.boxes = []
self.player_dir = DIR_DOWN # はじめは下を向いている
self.walk_timer = 0 # 0 なら止まっている。1〜8 は歩いている途中
self.pushing = False # いまの一歩で、荷物を押しているか
self.tiles.append(line)
self.from_x = self.player_x # 見た目の出発マス
self.from_y = self.player_y
# 盤面を画面の中央に置くための、ずらし幅
from_x は、ループのあとに書きます
self.player_x は、地図の中の @ を見つけたときに決まります。
つまり二重ループが終わるまで存在しません。
ループより前に self.from_x = self.player_x と書くと、
AttributeError になります。
update() を書き換える
いまは self.handle_key() の 1 行だけです。ここが今回の心臓部になります。
def update(self):
"""フレーム毎の更新処理"""
if self.walk_timer > 0: # 歩いている途中
self.walk_timer += 1
if self.walk_timer > MOVE_FRAMES: # 隣のマスに着いた
self.walk_timer = 0
self.from_x = self.player_x # 見た目を、いまいるマスに合わせる
self.from_y = self.player_y
self.pushing = False
if self.walk_timer == 0: # 止まっているときだけ、キーを見る
self.handle_key()
if は else ではありません
上の if の中で「着いた」と分かると、その場で walk_timer が 0 になります。
だから下の if が同じフレームで成り立ち、すぐ次の一歩に入れます。else にすると、着いたフレームはキーを見ずに終わってしまい、
1 マスごとに 1 フレームぶん立ち止まります。押しっぱなしで歩いたときに、
かすかにガクガクします。6-1 でくわしく見ます。
btnp を btn に変える
handle_key() の 4 か所を書き換えるだけです。p を消します。
def handle_key(self):
"""矢印キーを見て、押された向きへ 1 歩進もうとする"""
if pyxel.btn(pyxel.KEY_DOWN):
self.try_move(DIR_DOWN)
elif pyxel.btn(pyxel.KEY_UP):
self.try_move(DIR_UP)
elif pyxel.btn(pyxel.KEY_LEFT):
self.try_move(DIR_LEFT)
elif pyxel.btn(pyxel.KEY_RIGHT):
self.try_move(DIR_RIGHT)
try_move() の最後に 2 行足す荷物を動かした直後に 1 行、いちばん下に 1 行です。
self.move_box(next_x, next_y, dx, dy)
self.pushing = True # この一歩は、荷物を押している
self.player_x = next_x
self.player_y = next_y
self.walk_timer = 1 # ここから 8 フレームかけて歩く
self.pushing = True の字下げに注意
self.move_box(...) と同じ深さ——つまり
if self.has_box(next_x, next_y): の中です。self.walk_timer = 1 は外側。押しても押さなくても歩くからです。
slide_pos() を足す
try_move() と draw() のあいだに、新しいメソッドを 1 つ足します。
1-1 の式を、そのままコードにしたものです。
def slide_pos(self, origin, from_cell, to_cell):
"""出発マスから到着マスへ、いまどこまで来たかをドットで返す"""
moved = (to_cell - from_cell) * TILE * self.walk_timer // MOVE_FRAMES
return origin + from_cell * TILE + moved
origin に self.ox を渡せば横の位置、self.oy を渡せば縦の位置です。
「マス目 → ドット」の変換に、途中の分を足しているだけなので、
主人公にも荷物にも同じものが使えます。
同じ計算を 4 か所に書かずにすみます。
draw() の、荷物と主人公を書き換える壁を描く二重ループはそのままです。その下の 2 か所を書き換えます。
# 荷物。押している 1 つだけは、主人公といっしょにすべる
dx, dy = MOVES[self.player_dir]
push_x = self.player_x + dx # 押した荷物は、主人公の目の前にいる
push_y = self.player_y + dy
for box in self.boxes:
if self.pushing and box[0] == push_x and box[1] == push_y:
bx = self.slide_pos(self.ox, box[0] - dx, box[0])
by = self.slide_pos(self.oy, box[1] - dy, box[1])
else:
bx = self.ox + box[0] * TILE
by = self.oy + box[1] * TILE
pyxel.blt(bx, by, 0, U_BOX, V_TILE, TILE, TILE, pyxel.COLOR_BLACK)
# 主人公
px = self.slide_pos(self.ox, self.from_x, self.player_x)
py = self.slide_pos(self.oy, self.from_y, self.player_y)
v = V_PLAYER_A
if self.walk_timer >= MOVE_FRAMES // 2: # 歩きの後半は、足を入れ替える
v = V_PLAYER_B
pyxel.blt(px, py, 0, self.player_dir * TILE, v, TILE, TILE, pyxel.COLOR_BLACK)
box[0] にいて、dx だけ動いたところです。
ということは、出発マスは box[0] - dx。
荷物ごとに出発マスを覚えておく必要はありません。
書き足したのは定数 2 つ、load_stage() の 4 行、update() の中身、
try_move() の 2 行、新しい slide_pos()、
そして draw() の下半分です。
# sokoban.py
# step2 フェーズ4:歩くアニメを入れる
import pyxel
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
TILE = 8 # 1 マスの大きさ(ドット)
HUD_HEIGHT = 12 # 画面の上、手数などを出すために空けておく高さ
MOVE_FRAMES = 8 # 隣のマスへ移りきるまでにかけるフレーム数
# イメージバンク 0 の、切り出し位置
V_TILE = 0 # タイルの段
V_PLAYER_A = 8 # 主人公の段(コマ A)
V_PLAYER_B = 16 # 主人公の段(コマ B・足を入れ替えた絵)
U_FLOOR = 0
U_WALL = 8
U_GOAL = 16
U_BOX = 24
# 向き。この番号がそのまま主人公の絵の切り出し位置になる
DIR_DOWN = 0
DIR_UP = 1
DIR_LEFT = 2
DIR_RIGHT = 3
# 向きごとの、1 歩ぶんのずれ(x のずれ, y のずれ)
MOVES = [
(0, 1), # 下
(0, -1), # 上
(-1, 0), # 左
(1, 0), # 右
]
# ステージの地図
# # 壁 . 床 O ゴール $ 荷物 @ 主人公
STAGE1 = [
"########",
"#......#",
"#..O.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 = []
self.player_dir = DIR_DOWN # はじめは下を向いている
self.walk_timer = 0 # 0 なら止まっている。1〜8 は歩いている途中
self.pushing = False # いまの一歩で、荷物を押しているか
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)
self.from_x = self.player_x # 見た目の出発マス
self.from_y = self.player_y
# 盤面を画面の中央に置くための、ずらし幅
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):
"""フレーム毎の更新処理"""
if self.walk_timer > 0: # 歩いている途中
self.walk_timer += 1
if self.walk_timer > MOVE_FRAMES: # 隣のマスに着いた
self.walk_timer = 0
self.from_x = self.player_x # 見た目を、いまいるマスに合わせる
self.from_y = self.player_y
self.pushing = False
if self.walk_timer == 0: # 止まっているときだけ、キーを見る
self.handle_key()
def handle_key(self):
"""矢印キーを見て、押された向きへ 1 歩進もうとする"""
if pyxel.btn(pyxel.KEY_DOWN):
self.try_move(DIR_DOWN)
elif pyxel.btn(pyxel.KEY_UP):
self.try_move(DIR_UP)
elif pyxel.btn(pyxel.KEY_LEFT):
self.try_move(DIR_LEFT)
elif pyxel.btn(pyxel.KEY_RIGHT):
self.try_move(DIR_RIGHT)
def has_box(self, x, y):
"""そのマスに荷物があるかどうかを答える"""
for box in self.boxes:
if box[0] == x and box[1] == y:
return True
return False
def move_box(self, x, y, dx, dy):
"""(x, y) にある荷物を、(dx, dy) だけ動かす"""
for box in self.boxes:
if box[0] == x and box[1] == y:
box[0] += dx
box[1] += dy
return
def try_move(self, direction):
"""その向きへ動けるか調べて、動けるときだけ 1 マス進む"""
self.player_dir = direction # 動けなくても、向きだけは変える
dx, dy = MOVES[direction]
next_x = self.player_x + dx
next_y = self.player_y + dy
if self.tiles[next_y][next_x] == "#": # 行き先が壁なら
return # 何もしないで戻る
if self.has_box(next_x, next_y): # 行き先に荷物があるなら
far_x = next_x + dx # 荷物の、1 つ先のマス
far_y = next_y + dy
if self.tiles[far_y][far_x] == "#": # その先が壁なら押せない
return
if self.has_box(far_x, far_y): # その先に別の荷物があっても押せない
return
self.move_box(next_x, next_y, dx, dy)
self.pushing = True # この一歩は、荷物を押している
self.player_x = next_x
self.player_y = next_y
self.walk_timer = 1 # ここから 8 フレームかけて歩く
def slide_pos(self, origin, from_cell, to_cell):
"""出発マスから到着マスへ、いまどこまで来たかをドットで返す"""
moved = (to_cell - from_cell) * TILE * self.walk_timer // MOVE_FRAMES
return origin + from_cell * TILE + moved
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)
# 荷物。押している 1 つだけは、主人公といっしょにすべる
dx, dy = MOVES[self.player_dir]
push_x = self.player_x + dx # 押した荷物は、主人公の目の前にいる
push_y = self.player_y + dy
for box in self.boxes:
if self.pushing and box[0] == push_x and box[1] == push_y:
bx = self.slide_pos(self.ox, box[0] - dx, box[0])
by = self.slide_pos(self.oy, box[1] - dy, box[1])
else:
bx = self.ox + box[0] * TILE
by = self.oy + box[1] * TILE
pyxel.blt(bx, by, 0, U_BOX, V_TILE, TILE, TILE, pyxel.COLOR_BLACK)
# 主人公
px = self.slide_pos(self.ox, self.from_x, self.player_x)
py = self.slide_pos(self.oy, self.from_y, self.player_y)
v = V_PLAYER_A
if self.walk_timer >= MOVE_FRAMES // 2: # 歩きの後半は、足を入れ替える
v = V_PLAYER_B
pyxel.blt(px, py, 0, self.player_dir * TILE, v, TILE, TILE, pyxel.COLOR_BLACK)
App()
cd step2
python sokoban.py
見た目のマス配置はフェーズ 3 と同じです。確かめてほしいのは、動きのほうです。
| やってみること | こうなれば成功 |
|---|---|
| 矢印キーを 1 回だけ押す | カクッと飛ばず、すーっと 1 マスすべる。途中で足が入れ替わる |
| 矢印キーを押しっぱなしにする | とことこ歩き続ける。止まらずになめらかにつながる |
| 荷物を押す | ワンコとリンゴが、ぴったりそろってすべる。離れたり重なったりしない |
| 壁に向かって押しっぱなしにする | その場で向きだけ変わる。足踏みはしない(歩いていないので) |
| 少し離れた荷物に向かって歩く | 荷物はじっとしている。ワンコだけが近づいていく |
| 歩いている途中で、別の矢印キーを押す | 着くまで無視される。着いてから、その向きへ歩き出す |
MOVE_FRAMES を 4 にすると 2 倍の速さ、16 にすると半分の速さになります。
倉庫番は「押しまちがえたくない」ゲームなので、少しゆっくりめのほうが落ち着いて遊べます。
実際に指で押してみて、自分がいちばん気持ちいい数字を選んでください。2 / 4 / 8 / 16 がおすすめです。
v = 16)を描いていません。
イメージバンクの何もない場所を切り出すと、透明色(黒)だけが並ぶので、
歩いている後半だけ姿が消えます。3-1 に戻って 4 枚描いてください。
else ではなく、2 つめの if なのか if self.walk_timer > 0:
...
if self.walk_timer > MOVE_FRAMES:
self.walk_timer = 0 # ← ここで 0 になる
if self.walk_timer == 0: # ← だから、同じフレームで成り立つ
self.handle_key()
矢印キーを押しっぱなしにしているときの、フレームごとの動きを追ってみます。 8 フレーム目に着いて、その同じフレームで次の一歩が始まります。
| フレーム | walk_timer | 画面のワンコ | コマ |
|---|---|---|---|
| 1 | 1 | 出発マスから 1 ドット | A |
| 2 〜 3 | 2 〜 3 | 2 〜 3 ドット | A |
| 4 〜 7 | 4 〜 7 | 4 〜 7 ドット | B |
| 8 | 8 | 8 ドット=到着 | B |
| 9 | 9 → 0 → 1 | 次のマスへ 1 ドット | A |
9 フレーム目に注目してください。walk_timer が 9 になって「着いた」と分かり、
0 に戻され、そのまま handle_key() が呼ばれて 1 になります。
1 フレームのあいだに 3 回書き換わっています。おかげで、
ワンコは 1 フレームに 1 ドットずつ、休まずに進み続けます。
もし else にすると、9 フレーム目は「着いた」で終わってしまい、
キーを見るのは 10 フレーム目です。9 フレーム目と 10 フレーム目で、同じ場所に立ち止まります。
1 マスごとに 1 回、わずかに引っかかる歩き方になります。
// は、切り捨ての割り算 moved = (to_cell - from_cell) * TILE * self.walk_timer // MOVE_FRAMES
/ だと 4.0 のような小数になります。
Pyxel の blt() に小数を渡すと、思わぬ位置に描かれたりエラーになったりします。
// は小数点以下を切り捨てて整数を返すので、ドットの計算にはこちらを使います。
>>> 8 * 3 / 8
3.0
>>> 8 * 3 // 8
3
>>> 8 * 3 // 5 # 割り切れないとき
4
TILE * walk_timer // MOVE_FRAMES は、
先にかけてから割ります(8 * 3 // 8 = 3)。TILE * (walk_timer // MOVE_FRAMES) と書くと、
3 // 8 が先に 0 になってしまい、
7 フレームのあいだ 1 ドットも動かず、最後に 8 ドット飛びます。
かっこをつけないほうが正しい、めずらしい例です。
btn と btnp の違いいつ True になるか | 押しっぱなしにすると | |
|---|---|---|
pyxel.btnp(キー) | 押した瞬間の 1 フレームだけ | 何も起きない(1 歩で止まる) |
pyxel.btn(キー) | 押されているあいだ、ずっと | 毎フレーム True |
フェーズ 2・3 では、瞬間移動だったので btnp がちょうどよいブレーキになっていました。
btn にしていたら、1 秒で 30 マス進んでしまいます。
いまは「歩いているあいだはキーを見ない」というブレーキを、自分で作りました。
だから btn に変えても暴走しません。
むしろ btnp のままだと、押しっぱなしで歩き続けられず、
1 マスごとにキーを押し直すことになります。
btnp にはリピート機能もあります
pyxel.btnp(キー, 12, 4) のように書くと、
「12 フレーム押しっぱなしにしたら、そこから 4 フレームおきに True」になります。
これでもキーリピートは作れます。from_x は「見た目の出発点」
self.from_x は、止まっているあいだは self.player_x と同じ値です。
だから slide_pos() の計算はこうなります。
moved = (4 - 4) * 8 * 0 // 8 → 0
return 48 + 4 * 8 + 0 → 80 # マス目どおりの位置
つまり止まっているときも、同じ式で正しい位置が出ます。
「歩いているとき用」と「止まっているとき用」で描き分ける必要はありません。
if が 1 つ減ります。
from を合わせ忘れると
self.from_x = self.player_x を書き忘れると、
次の一歩で、前々回いた場所からすべり始めます。
歩くたびに 1 マスぶんずつ、絵が後ろへ取り残されていきます。
self.pushing がないと、どうなるか「主人公の目の前に荷物がある」だけを条件にしてしまうと、こうなります。
#..@.$.# ← ワンコと、2 マス先のリンゴ
→ を 1 回押す(押していない。ただ近づいただけ)
#...@$.# ← 盤面はこう
でも画面では、リンゴまで 8 ドットすべってしまう
リンゴがワンコの上に重なりながらついてきます。
self.pushing は「この一歩で本当に押したか」を覚えておくための、
True / False ひとつだけの小さな覚え書きです。
self.pushing = True にしたら、着いたときに False に戻します
(update() の中でやっています)。
戻し忘れると、次に止まっているあいだじゅう
目の前の荷物が 1 マス手前に描かれ続けます。 v = V_PLAYER_A
if self.walk_timer >= MOVE_FRAMES // 2: # 歩きの後半は、足を入れ替える
v = V_PLAYER_B
MOVE_FRAMES // 2 は 4 です。つまり
前半(1〜3)はコマ A、後半(4〜8)はコマ B、
着いたら walk_timer が 0 になるのでコマ A に戻ります。
1 マス歩くたびに A → B → A と、足が 1 往復します。
V_PLAYER_A と V_PLAYER_B は、
イメージバンクの切り出す段(縦の位置)です。横の位置
(self.player_dir * TILE)はそのままなので、
向きと足踏みが、きれいに掛け算になっています。4 方向 × 2 コマ = 8 枚の絵を、
数字 2 つで選び分けているわけです。
v を if で選ぶかわりに、
コマを並べたリストから選ぶ形にすると増やしやすくなります。
たとえば v = 24 にコマ C を描き足したうえで、こう書きます。
WALK_STEPS = [8, 16, 24, 16] # 使いたい段を、見せたい順に並べる
v = WALK_STEPS[self.walk_timer * len(WALK_STEPS) // (MOVE_FRAMES + 1)]
いまは 2 コマなので if のほうが読みやすく、この書き方は使いません。
「増えてきたらリストにする」と覚えておいてください。
| 症状 | 原因と対処 |
|---|---|
| まったく動かない/1 歩も進まない | try_move() の最後の self.walk_timer = 1 を書いていません。
0 のままだと、絵は動きますが……そもそも歩き出しません |
| 1 歩動いたきり、二度と動かない | update() の中で self.walk_timer = 0 に戻していません。
ずっと「歩いている途中」のままなので、キーを見に行けなくなっています |
| 相変わらずカクッと飛ぶ | draw() がまだ self.ox + self.player_x * TILE のままです。
self.slide_pos(...) に置き換えてください |
| 歩くたびに、絵がどんどん後ろへ取り残される | 到着したときの self.from_x = self.player_x の 2 行を書いていません
(6-4 参照) |
| 7 フレーム止まって、最後に 8 ドット飛ぶ | slide_pos() で TILE * (walk_timer // MOVE_FRAMES) と、
かっこをつけていませんか。かっこは不要です |
| 歩いている後半だけ、主人公が消える | コマ B(v = 16)を描いていません。3-1 に戻ってください |
| 足踏みしない(ずっとコマ A) | V_PLAYER_B = 16 を 8 のまま書いていないか、
if self.walk_timer >= MOVE_FRAMES // 2: を書き忘れています |
| 押していない荷物までついてくる | self.pushing を条件に入れていません。
if self.pushing and box[0] == push_x ... の self.pushing and が必要です |
| 止まっているのに、目の前の荷物が 1 マス手前にずれて見える | 到着時に self.pushing = False に戻していません |
| 押した荷物だけ瞬間移動する | 荷物の if の中で slide_pos() を使っていない(else 側と同じ式になっている)か、
出発マスを box[0] のまま渡しています。box[0] - dx です |
| 矢印キーを押しっぱなしにしても 1 歩しか進まない | handle_key() が btnp のままです。p を消してください |
| ものすごい速さで走り抜ける | btn に変えたのに、update() の
if self.walk_timer == 0: を書いていません。毎フレーム 1 マス進んでいます |
AttributeError: 'App' object has no attribute 'player_x' |
self.from_x = self.player_x を、
二重ループより前に書いています。ループのあとに移してください |
NameError: name 'V_PLAYER' is not defined |
定数を V_PLAYER_A に変えたのに、draw() の中が
V_PLAYER のままです |
| 斜めに動いてしまう | slide_pos() の origin に、
縦の計算でも self.ox を渡しています。縦は self.oy です |
これで「遊べる」から「気持ちよく遊べる」に一歩近づきました。
次はクリア判定と、やり直しです。
荷物がぜんぶゴールに乗ったらクリア、R キーでステージの最初から——を作ります。
これでようやく、行き詰まってもウィンドウを閉じなくてよくなります。