倉庫番の作り方 — JavaScriptでブラウザゲームを作ろう

このゲームで遊ぶ → ソースコード全文 →

このチュートリアルで学べること

倉庫番は、箱をゴールまで押して運ぶ定番パズルゲームです。シンプルなルールですが、2次元配列を使ったマップ管理や、箱を押せるかどうかの判定など、ゲームプログラミングの基本がたくさん詰まっています。このチュートリアルでは、JavaScriptとCanvasを使って倉庫番を一歩ずつ作っていきましょう。

HTMLとCanvasの準備をしよう

まずはゲームを描画するためのCanvasを準備しましょう。Canvas要素をHTMLに配置して、JavaScriptから描画できるようにします。デバイスの画面解像度に合わせて、高精細ディスプレイでもきれいに表示されるようにしましょう。

var canvas, ctx, W, H, dpr;

function initCanvas() {
  canvas = document.getElementById('game-canvas');
  ctx = canvas.getContext('2d');
  dpr = window.devicePixelRatio || 1;
  var rect = canvas.getBoundingClientRect();
  W = rect.width;
  H = rect.height;
  canvas.width = W * dpr;
  canvas.height = H * dpr;
  ctx.scale(dpr, dpr);
}

devicePixelRatioを使うと、Retinaディスプレイなどの高精細画面でもぼやけずに描画できます。getContext('2d')で取得したctxを通じて、図形や文字を描いていきます。

マップデータと2次元配列を作ろう

ステージを文字列で定義する

倉庫番のステージは、壁・床・箱・ゴール・プレイヤーの情報を持っています。文字列の配列として定義すると、見た目でステージの形がわかりやすくなります。この文字列を2次元の数値配列に変換して、ゲームのロジックで扱いやすくしましょう。

// マップタイル定数
var EMPTY = 0, WALL = 1, GOAL = 2;
var BOX = 3, BOX_ON_GOAL = 4;

// ステージデータ(文字列で視覚的に定義)
var LEVELS = [
  [
    '  ###  ',
    '  #.#  ',
    '  # #  ',
    '###$###',
    '#. @ .#',
    '###$###',
    '  # #  ',
    '  #.#  ',
    '  ###  '
  ]
];

// 文字列を2次元配列に変換
function loadLevel(idx) {
  var data = LEVELS[idx];
  mapH = data.length;
  mapW = 0;
  for (var i = 0; i < data.length; i++) {
    if (data[i].length > mapW) mapW = data[i].length;
  }

  map = [];
  for (var y = 0; y < mapH; y++) {
    map[y] = [];
    for (var x = 0; x < mapW; x++) {
      var ch = data[y][x] || ' ';
      switch (ch) {
        case '#': map[y][x] = WALL; break;
        case '.': map[y][x] = GOAL; break;
        case '$': map[y][x] = BOX; break;
        case '*': map[y][x] = BOX_ON_GOAL; break;
        case '@': map[y][x] = EMPTY;
                  playerX = x; playerY = y; break;
        default:  map[y][x] = EMPTY; break;
      }
    }
  }
}

#が壁、.がゴール、$が箱、@がプレイヤーです。switch文で1文字ずつ数値に変換し、map[y][x]の2次元配列に格納します。プレイヤーの位置も同時に記録しています。

プレイヤー移動と箱のプッシュ判定を実装しよう

移動の条件を1つずつチェックする

プレイヤーが移動するとき、移動先のマスに何があるかによって動作が変わります。壁なら動けません。箱があるなら、箱の先も空いているかチェックが必要です。この「段階的な条件チェック」がプッシュ判定のポイントです。

var DIR = {
  up:    { dx: 0, dy: -1 },
  down:  { dx: 0, dy: 1 },
  left:  { dx: -1, dy: 0 },
  right: { dx: 1, dy: 0 }
};

function move(dir) {
  if (!running) return;
  var d = DIR[dir];
  var nx = playerX + d.dx;
  var ny = playerY + d.dy;

  // 範囲外チェック
  if (nx < 0 || nx >= mapW || ny < 0 || ny >= mapH) return;
  var target = map[ny][nx];

  // 壁チェック
  if (target === WALL) return;

  // 箱の処理
  if (target === BOX || target === BOX_ON_GOAL) {
    var bx = nx + d.dx;
    var by = ny + d.dy;
    if (bx < 0 || bx >= mapW || by < 0 || by >= mapH) return;
    var behind = map[by][bx];
    if (behind === WALL || behind === BOX || behind === BOX_ON_GOAL) return;

    // 箱を移動
    map[by][bx] = (behind === GOAL) ? BOX_ON_GOAL : BOX;
    map[ny][nx] = (target === BOX_ON_GOAL) ? GOAL : EMPTY;
  }

  playerX = nx;
  playerY = ny;
  moves++;
  draw();
  checkClear();
}

移動先に箱がある場合、さらにその先(bx, by)が壁や別の箱でないかチェックします。箱がゴールの上に乗ったかどうかは、BOX_ON_GOALという特別なタイル値で管理しています。

Undo機能(履歴管理)を実装しよう

パズルゲームでは「一手戻る」機能がとても重要です。移動のたびに、プレイヤーの位置と箱の状態を履歴配列に保存しておけば、いつでも前の状態に戻すことができます。

var history = [];

// 移動時に履歴を保存(move関数の中で呼ぶ)
history.push({
  px: playerX, py: playerY,
  bfx: nx, bfy: ny, bft: target,
  btx: bx, bty: by, btt: behind
});

// 一手戻る
function undo() {
  if (!running || history.length === 0) return;
  var h = history.pop();

  // 箱を戻す
  if (h.bfx >= 0) {
    map[h.bfy][h.bfx] = h.bft;
    map[h.bty][h.btx] = h.btt;
  }

  playerX = h.px;
  playerY = h.py;
  moves--;
  draw();
}

history配列にpushで追加し、popで取り出すことで「スタック」(後入れ先出し)の仕組みを使っています。箱の元の位置と元のタイル値を保存しているので、正確に元の状態に戻すことができます。

クリア判定を実装しよう

すべての箱がゴールの上に乗ったらステージクリアです。マップ全体をスキャンして、ゴールに乗っていない箱(BOX)が1つも残っていなければクリアと判定します。

function checkClear() {
  for (var y = 0; y < mapH; y++) {
    for (var x = 0; x < mapW; x++) {
      if (map[y][x] === BOX) return; // ゴールに乗っていない箱がある
    }
  }
  // クリア!
  running = false;
  saveProgress();
}

ゴールに乗っている箱はBOX_ON_GOALなので、BOXが1つも見つからなければ全部ゴールに乗っている、と判断できるシンプルなロジックです。

まとめ — 次のステップ

このチュートリアルでは、2次元配列でマップを管理し、箱を押せるかどうかのプッシュ判定を実装し、履歴管理でUndoを実現する方法を学びました。ここからさらに発展させるアイデアとして、ステージエディタを作ってオリジナルのパズルを作成したり、最小手数の記録機能を追加したり、アニメーション付きの移動を実装してみるのも楽しいでしょう。

よくある質問

Q: プログラミング初心者でも作れますか?

A: はい、大丈夫です。このチュートリアルではステップごとにコードを書いていくので、初めての方でも順番に進めれば完成できます。わからないところがあれば、ひなテックの教室で質問もできますよ。

Q: どのくらい時間がかかりますか?

A: 基本部分は約30分〜1時間で作れます。見た目をこだわったり機能を追加すると、さらに楽しく発展させられます。