step2 倉庫番

荷物を押して所定の位置へ運ぶ。10 ステージすべてを解く

← トップページへもどる

1. 完成イメージ

プレイヤーを矢印キーで動かし、荷物を押して、すべてのゴールに乗せると 1 ステージクリアです。 荷物は押すことしかできません(引けません)。壁ぎわに押しつけてしまうと二度と動かせなくなるので、 動かす前に考えるのがこのゲームの面白さです。

10 ステージ。最初のステージは荷物 1 個から始まり、だんだん難しくなります。 詰んでしまったときは、R で最初からやり直します。 1 手戻す(アンドゥ)はあえて付けません。 気軽に戻せてしまうと、押す前に一手先を読むというこのゲームのいちばんの面白さが薄れるからです。

主人公は向いている方向の絵になり、歩くときは足踏みしながら隣のマスへすべっていきます。 盤面そのものは静かなゲームで、動くのは主人公と荷物だけ——だからこそ、そこは丁寧に作り込みます。

そして今回は スマホでも遊べるようにします。Pyxel の Web 版はタッチ端末で開くと 画面の下に十字キーとボタンを自動で出してくれるので、コード側は 「キーボードも、その画面ボタンも、どちらも見る」ようにしておくだけで済みます。

STAGE 3 MOVES 24 荷物 ゴールに乗った荷物 ゴール
完成イメージ(実際の画面は 160×120 ドット、1 マス 8 ドット。図は概念図で、 主人公は実際には向きに応じた 4 方向のドット絵になります)
項目内容
画面サイズ160 × 120 ドット(step0・step1 と同じ)
1 マスの大きさ8 × 8 ドット。盤面は最大 18 × 12 マスまで置ける
操作(PC)矢印キーで移動・荷物を押す / R でやり直し / Enter で決定
操作(スマホ)画面下の十字キーで移動 / B ボタンでやり直し / A ボタンで決定。Pyxel がタッチ端末に自動で出す仮想ゲームパッドを使う
主人公の表示4 方向 × 2 コマのドット絵。歩くときは隣のマスへ 8 フレームかけてなめらかにすべる
やり直しR(スマホは B ボタン)でステージの最初から。 1 手戻す機能はなし
クリア条件すべての荷物がゴールに乗ること。制限時間や手数制限はなし
ステージ数10。番号順に進む。クリアしたステージは選び直せる
画面タイトル/ステージ選択/プレイ/ステージクリア/全ステージクリアの 5 画面
表示ステージ番号と手数(画面の上)
step1 といちばん違うのは「時間の進み方」です step1 は毎フレーム世界が動いていました。手を止めても、お化けは近づいてきます。
倉庫番はプレイヤーがキーを押したときだけ世界が動きます。 押さないかぎり、盤面は 1 ドットも変わりません。考える時間は無限にあるゲームです。
この違いはコードにも出ます。update() でやることが 「キーが押されたか」を見るだけになり、ぬるぬる動く処理がなくなります。 そのぶん、「動かしていいかどうかの判断」が中身の主役になります。
作品の規模感 最終的なコードはおよそ 280〜330 行です。step1(150〜200 行)より増えますが、 そのうち 3 割ほどは「ステージの地図」そのもので、考えて書くコードの量は step1 と大きくは変わりません。11 のフェーズに分けて進めます。
参考にした記事 ゲームの題材は こちらの記事を参考にしています。 コードや画像は転載せず、ドット絵はリソースエディタで自分で描き、コードもフェーズごとに 一から組み立てます参考記事にステージの概念はないので、 10 ステージ制とタイトル画面はこの学習用に足した拡張です。

2. フェーズ分割

解説ページは、フェーズを進めるたびに追加していきます。

フェーズやること主に使う技術
1 タイルの絵を描き、ステージを画面に並べる
壁・床・荷物・ゴールと、主人公の絵 1 枚を描き、地図のとおりに置く
二次元リスト、マス目とドットの変換、二重ループ
2 プレイヤーを矢印キーで動かす
壁にぶつかったら動かない。主人公の残り 3 枚を描いて、向いた方向の絵にする
動かす前に行き先を調べるbtnp、タプル
3 荷物を押す
押した先が空いているときだけ、荷物ごと動く。荷物とゴールを 2 組に増やす
2 マス先まで見る、リストの中身を書き換える
4 歩くアニメを入れる(step2 で追加した内容)
足踏みの 2 コマ目 4 枚を描き、隣のマスへ 8 フレームかけてすべらせて歩かせる。 押した荷物も一緒にすべる
マス目の位置と見た目の位置を分ける、途中の位置の計算、 btnp から btn への切り替え
5 クリア判定と、やり直し
全部の荷物がゴールに乗ったらクリア。R で最初から。 ゴールに乗った荷物の絵を 1 枚描き足す
all()リストのコピー(浅いコピーの落とし穴)、透過色の選び方
6 ステージを 10 個にする
クリアしたら Enter で次のステージへ。10 個ぜんぶで全クリア。 ステージの作り方と、解けるかどうかを調べる検算ツールも
ステージ一覧の持ち方、進行状態の管理、len()早い return
7 タイトル画面とステージ選択 画面遷移(step1 の復習)、カーソルの移動
8 手数の表示とクリア演出
ステージ番号・手数を出し、クリアの瞬間に間をつくる
HUD、frame_count による演出
9 効果音と BGM をつける play / playm(step1 の復習)
10 スマホで遊べるようにする(step2 で追加した内容)
仮想ゲームパッドの十字キーとボタンも受け付ける。 試しにパッケージして、自分のスマホで実際に動かして確かめる
GAMEPAD1_BUTTON_*入力の判定を自分の関数にまとめる
11 パッケージ化して Web で公開する pyxel package / app2html(step1 の復習)
進め方 1 フェーズごとに「考え方 → 必要な API → コード → 解説」を読み、コードを自分で入力して 実行します。想定どおり動くことを確認できてから、次のフェーズに進みます。 疑問が出たときは、その内容を備忘録に残していきます。
はじめて読むときは、この下の3. 学ぶ技術項目4. 先に押さえておきたい 3 つの土台に目を通してから、フェーズ 1 へ進んでください。

3. この step で学ぶ技術項目

step1 が「ゲームとして成立させるための仕組み」だったのに対し、step2 は 「データでゲームを作る」のが柱です。 ステージを増やすのに、コードを書き足さない—— 地図のデータを 1 つ足すだけでステージが増える、という形を目指します。

分類項目登場フェーズ
データの持ち方 二次元リストで盤面を持つstage[y][x]1
ステージを「文字の地図」で書き、読んで組み立てる1
10 個のステージをまとめて持ち、番号で取り出す6
座標 マス目の座標とドットの座標を行き来する1
位置やずれを (x, y) の組(タプル)として扱う2・3
マス目の位置と、画面に見えている位置を別々に持つ4
移動の判定 動かす前に、行き先を調べる(壁なら動かない)2
2 マス先まで見る(荷物を押せるかどうか)3
絵とアニメーション 向きを覚えて、絵を差し替える(4 方向)2
出発マスと行き先のあいだを、少しずつ埋める4
足踏みのコマ替えと、歩いている間は次の入力を待たせる4
状態の管理 すべての荷物がゴールに乗ったかを調べる5
初期状態を保存しておき、いつでも戻す5
ゲーム進行 ステージを順に進める/全クリア6
タイトルとステージ選択(step1 の画面遷移の応用)7
画面表示ステージ番号・手数の表示、クリア演出8
サウンド押す音・ゴールに乗った音・クリア音(step1 の復習)9
スマホ対応 仮想ゲームパッドのボタンを受け取るGAMEPAD1_BUTTON_*10
キーとボタンの判定を自分の関数にまとめる10
公開パッケージ化と Web 公開(step1 の復習)11

あわせて新しく出てくる Python の概念

概念なぜ必要になるか登場フェーズ
二次元リスト
(リストの中のリスト)
盤面は「縦にも横にも広がる表」です。1 本のリストでは扱いにくいので、 行のリストを、さらにリストで束ねますstage[y][x]縦が先になる理由も、ここで理解します。 1
リストのコピーは「参照」 R でやり直すには、始めの盤面を取っておく必要があります。 ところが backup = stage と書いても控えになりません。 片方を書き換えると両方変わります。 step2 でいちばん引っかかりやすいところです。 5
all()
「全部そろったか」
「すべての荷物がゴールに乗ったか」は、 for と旗(フラグ)でも書けますが、all() を使うと 1 行で、読んだとおりの意味になります。 5
タプル (x, y) 位置は2 つの数がいつも一組で動きます。 別々の変数で持つと引数が増えて散らかるので、組にしてまとめます。 向き(上下左右)も (-1, 0) のような組で表しておくと、 4 方向ぶんの処理が 1 本のコードで書けます 3・4
「いま動いている途中」を
変数で覚えておく
歩くアニメの 8 フレームのあいだ、プログラムは「いま移動中である」ことを 覚えていないといけません。数えるための変数(タイマー)を 1 つ持ち、 0 なら止まっている・0 でなければ歩いている途中と決めます。 状態を数で表すという、ゲームでいちばんよく使う考え方です。 4
step1 で作ったものは、そのまま使えます 画面遷移(scene による振り分け)、btnp の使い分け、 frame_count を使った演出の間、チャンネルを分けた効果音、 pyxel package での公開——これらは新しく学び直しません。 step2 で頭を使うのは、データの持ち方と、動かす前の判断の 2 つに集中します。

4. 先に押さえておきたい 3 つの土台

土台 1:マス目で考え、描くときだけドットに直す

step1 では、男の子の位置はドットの座標boy_x = 76 など)でした。 倉庫番ではマス目の座標(左から 3 番目、上から 1 番目)で考えます。 「3 番目のマス」なら、壁があるかどうかを表から一発で引けるからです。

012 34 x(左から何番目) 01 23 y このマスを指すには stage[1][3] 縦(y)が先、横(x)があと 画面に描くときだけ、ドットに直す px = OX + 3 * 8 py = OY + 1 * 8 OX・OY は盤面を中央に寄せるためのずらし幅
stage[y][x] の順番は、慣れるまで必ず一度は間違えます。「行を選んでから、その中の列を選ぶ」と覚えます
なぜ「縦が先」なのか 地図を行(横 1 列ぶんの文字列)を上から並べたものとして書くからです。
"#########"      ← stage[0]
"#..O....#"      ← stage[1]
"#..$....#"      ← stage[2]
まず何行目かを選び、その行の中で何文字目かを選ぶ—— この順番がそのまま stage[y][x] になります。

土台 2:動かす前に、2 マス先まで調べる

step1 の当たり判定は「動かしたあとで、重なっていないか」を見ていました。 倉庫番は逆で、「動かす前に、動いていいか」を調べます。 めり込んでから直すのではなく、そもそも動かさないわけです。

調べるのはすぐ前のマスと、荷物があったときのその 1 つ先——最大 2 マスです。

いま 1 つ前 2 つ前 前が床 → プレイヤーだけ 1 マス進む 前が壁 → 何も起こらない(手数も増やさない) 前が荷物で、その先が床 → 荷物とプレイヤーが 1 マスずつ動く その先が壁(または別の荷物) → 動かない この 4 つだけです。荷物を 2 つまとめて押すことはできません 3 番目の絵の「その先」に荷物があるときは、4 番目と同じ扱いになります。
この表が、そのままフェーズ 2 とフェーズ 3 のコードになります
「引けない」から詰みが生まれます 荷物を壁ぎわに押しつけると、もう動かせません。 角に入れてしまえば、そこが正解のゴールでないかぎりそのステージは詰みです。 これは不具合ではなく、このゲームの本質です。 だからこそ R(やり直し)を、いつでも押せるようにしておきます。
1 手戻す機能はあえて付けません。戻せないとわかっているから、 押す前に「このあとどうなるか」を読むようになります。そこがこのゲームの遊びどころです。

土台 3:マス目の位置と、目に見えている位置は別もの

歩くアニメを入れると、「盤面の上でどのマスにいるか」と「画面のどこに描かれているか」がずれます。 キーを押したその瞬間に、盤面の上ではもう隣のマスに着いています。 壁の判定もクリア判定も、その新しいマスで行われます。あとから追いつくのは、絵だけです。

右キーを押してから 8 フレームのあいだに起きていること 0 フレーム目 コマ A 4 フレーム目 コマ B 8 フレーム目 コマ A にもどる 緑のマス=stage の上での位置。いちばん左の絵の時点で、もう右のマスに移っています 画面に描くドット位置は、こう出します px = OX + from_x * CELL + (to_x - from_x) * CELL * timer // MOVE CELL は 8 ドット、MOVE は 8 フレーム。 つまり1 フレームにちょうど 1 ドットずつ進みます timer が 8 になったら 0 に戻し、 from_xto_x に合わせて「到着」とします
この 2 つは別の変数で持ちます。1 つにまとめようとすると、必ず判定がおかしくなります
ターン制なのに、時間が流れる? 「倉庫番はキーを押したときだけ世界が動く」と書きましたが、 歩いている 8 フレームのあいだだけは時間が流れます。 そのあいだは次の入力を受け付けません
これは制限のようでいて、じつは都合がよいところです。 この 8 フレームが、そのままキーリピートの間隔になります。 矢印キーを押しっぱなしにすると、8 フレームおきに 1 マスずつ、とことこ歩き続けます。