マガジン

30分で作る迷路:配列から壁を描いて歩く

遊びをつくる

執筆にClaudeを使用しています。

増えすぎた部品を脇へよけ、一周できる小さなゲームのコースを完成させる手元。
記事のテーマをもとに制作したAI生成の写真風イメージ。

迷路ゲームを作ろうとして、最初に「壁の絵」から描き始める場面を考えてみます。絵は描けても、キャラクターが壁をすり抜けてしまい、当たり判定の座標を別に書き足すうちに、見た目と判定がずれていきます。この混乱は、壁の情報が絵と判定の二か所に分かれて存在することから起きます。 この記事で確かめるのは「壁の情報を二次元配列の一か所だけに置けば、描画と壁判定の両方がそれを読むだけで済むのか」という問いです。9×9の小さな迷路を文字の配列で書き、そこからキャンバスに壁を描き、矢印キーで壁に当たらず歩いてゴールに着くところまでを、約50行のコードで作ります。外部ライブラリもサーバーも使わず、1つのHTMLファイルだけで動きます。

この記事の目次
  1. 30分の配分と準備
  2. 手順1〜2:配列を書いて、配列から描く
  3. 手順3:壁に当たらない移動を作る
  4. 手順4:キー入力をつなぎ、動作を確かめる
  5. つまずきやすい点と、次の30分の改造案
  6. 次の行動

30分の配分と準備

準備するものは、テキストエディタと、最近のブラウザ(Chrome、Safari、Firefoxなど)だけです。エディタで index.html という名前の空のファイルを作り、保存してブラウザにドラッグすれば表示できます。コードを直したら保存してブラウザを再読み込みします。

30分の目安は次のとおりです。

  • 手順1:迷路データを書く、目安:5分、できあがるもの:配列と変数
  • 手順2:配列から描く、目安:8分、できあがるもの:壁と床とゴールの表示
  • 手順3:壁に当たらない移動、目安:10分、できあがるもの:矢印キーで歩ける丸
  • 手順4:動作確認と改造、目安:7分、できあがるもの:確認済みの迷路

時間が余ったら、記事の後半にある改造案から1つ選んでください。

手順1〜2:配列を書いて、配列から描く

手順1:迷路データを書く

index.html に次の内容を書きます。ここから手順4までのコードは、上から順に同じファイルへ続けて書いていく1つのプログラムです。

html
<!doctype html>
<meta charset="utf-8">
<canvas id="c" width="360" height="360"></canvas>
<p id="m">矢印キーで歩きます</p>
<script>
const map = [
  "#########",
  "#S..#...#",
  "#.#.#.#.#",
  "#.#...#.#",
  "#.#####.#",
  "#...#...#",
  "###.#.###",
  "#...#..G#",
  "#########"
];
const T = 40;
const ctx = c.getContext("2d");
let px = 1, py = 1, steps = 0, done = false;

# が壁、. が通路、S がスタート、G がゴールです。1行が1つの文字列で、map[y][x] と書くと、上から y 行目、左から x 文字目の1マスを取り出せます。順番が「縦、横」になる点に注意してください。数学の (x, y) と逆なので、後で一番間違えやすい箇所になります。

T は1マスの大きさ(ピクセル)です。キャンバスの幅と高さの 360 は、9マス × 40 で決まっています。マス数や T を変えるときは、この掛け算も合わせます。px、py はプレイヤーのいるマスで、最初は S の位置 (1, 1) です。c と m は、HTMLの id と同じ名前でそのまま参照できます。

手順2:配列から描く

続けて、描画の関数を書きます。

js
function draw() {
  for (let y = 0; y < map.length; y++) {
    for (let x = 0; x < map[y].length; x++) {
      const ch = map[y][x];
      ctx.fillStyle = ch === "#" ? "#345" : ch === "G" ? "#fc4" : "#eef";
      ctx.fillRect(x * T, y * T, T, T);
    }
  }
  ctx.fillStyle = "#e44";
  ctx.beginPath();
  ctx.arc(px * T + T / 2, py * T + T / 2, T / 3, 0, Math.PI * 2);
  ctx.fill();
}

二重のループで全マスを順に見て、文字に応じた色で四角を塗ります。マス (x, y) の左上は画面上の (x × T, y × T) です。最後にプレイヤーを、マスの中心に丸で描きます。

ここで一度確かめます。ファイルの末尾(</script> はまだ書かないので、最後の行の下)に draw(); と書いて保存し、ブラウザで開いてみてください。濃い青の壁に囲まれた迷路と、左上近くの赤い丸、右下近くの黄色いマスが見えれば成功です。何も出ない場合は、ブラウザの開発者ツールのコンソールにエラーが出ていないかを見ます。多くは括弧や引用符の閉じ忘れです。確認できたら、この draw(); は手順4で書き直すので消します。

手順3:壁に当たらない移動を作る

移動の考え方は「動いてから戻す」ではなく「動く前に調べる」です。行き先のマスを計算し、そこが壁でないと確かめてから、初めて座標を書き換えます。

js
function move(dx, dy) {
  if (done) return;
  const nx = px + dx, ny = py + dy;
  if (map[ny]?.[nx] === undefined || map[ny][nx] === "#") return;
  px = nx; py = ny; steps++;
  draw();
  if (map[py][px] === "G") {
    done = true;
    m.textContent = steps + " 歩でゴールしました";
  }
}

流れを順に説明します。

  1. dx、dy は、1回の入力で動く量です。右なら (1, 0)、上なら (0, -1) を渡します。
  2. nx、ny は行き先の候補です。この時点では px、py を変えません。
  3. 行き先が配列の外(undefined)か壁(#)なら、何もせず return します。座標が変わらないので、壁に押し付けても丸は動かず、歩数も増えません。
  4. 通れるときだけ座標を更新して歩数を1増やし、描き直します。
  5. 着いたマスが G なら、done を true にして、以降の移動を止めます。

map[ny]?.[nx] の ?. は、map[ny] が存在しないときにエラーにせず undefined を返す書き方です。この迷路は外周がすべて壁なので、範囲外に出ることは本来ありません。それでも、後で外周のない地図に差し替えたときにエラーで止まらないよう、保険として入れておきます。

この関数の重要な点は、壁の判定に使っている情報が、手順2で描画に使った map と同じだということです。壁を1文字書き換えれば、見た目と通れるかどうかが同時に変わります。冒頭の問いに対する答えは、ここに現れます。

手順4:キー入力をつなぎ、動作を確かめる

最後に、キーボードの入力を move につなぎ、初回の描画を呼んで、</script> で閉じます。

js
addEventListener("keydown", e => {
  const d = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }[e.key];
  if (d) { e.preventDefault(); move(...d); }
});
draw();
</script>

キー名から移動量を引く小さな表を作り、対応するキーのときだけ move を呼びます。e.preventDefault() は、矢印キーでページがスクロールしてしまう動作を止めるためのものです。

コードはここまでで完成です。保存して再読み込みし、次の順に確かめてください。

  1. 開始直後に「上」と「左」を押します。どちらも壁なので、丸が動かなければ壁判定は働いています。
  2. 「右」を2回押します。丸が右へ2マス進み、3回目で止まります(右隣が壁のため)。
  3. 「下」を押して、通路の途中から壁へ向かって押し続けます。止まったまま、ほかの方向にはすぐ動ければ問題ありません。
  4. 入口から出口まで歩きます。ゴールすると、下の文字が歩数入りの表示に変わり、それ以降は動かなくなります。

歩数の読み方も決めておきます。壁に当たった入力は歩数に数えないので、表示される歩数は「実際に動いたマス数」です。この地図のスタートからゴールまでを数えると、最短は20歩になるはずです。表示が20なら最短で歩けています。それより大きければ、袋小路に入るなどの遠回りをしています。ご自身の画面の結果を、この数字と見比べてみてください。

つまずきやすい点と、次の30分の改造案

動かないときは、次の点を順に見てください。

  • 丸が壁を抜ける、または見た目と判定がずれる:map[x][y] と逆に書いていないかを確かめます。正しくは map[y][x] です。
  • 迷路の右端や下端が切れる:キャンバスの width と height が、列数や行数に T を掛けた値と合っているかを確かめます。
  • 行の文字数が揃っていない:どれかの行が1文字短いと、その行の描画が欠け、判定も undefined になって通れなくなります。9行すべてが9文字であることを数えてください。
  • キーを押しても反応しない:ページの上を一度クリックして、キー入力の対象を移します。
  • ゴール後も動いてしまう:done の宣言と、move の最初の if (done) return; を確認します。

改造案は、今の構造のまま足せるものを選びました。

  • 壁の種類を増やす:~(水)を通れない文字として追加します。判定の条件に1つ足し、色を決めれば完成です。
  • スタート位置を配列から探す:二重ループで S を探して px、py に入れると、地図だけを書き換えれば遊べる作りになります。
  • 鍵と扉:k を拾うと変数を立て、D はその変数があるときだけ通れる、という分岐を判定に足します。
  • スマホ対応:今の操作はキーボードだけなので、画面上にボタンを4つ置き、同じ move を呼ぶようにします。

次の行動

まず、手順1の地図の # と . を数文字書き換え、ゴールまでの道が1本でも残っているかを歩いて確かめてください。気に入った迷路ができたら、index.html として保存し、アソバースの投稿画面に直接アップロードできます。直下の index.html が入口になり、外部通信を使わないこのコードはそのまま動かせます。投稿の流れは入力、表示の確認、公開の3段階で、公開にはメールアドレスの確認が必要です。投稿前に、スマホで遊ぶ人もいることを考え、画面上のボタンを足すかどうかも決めておくとよいでしょう。