倉庫番は、箱をゴールまで押して運ぶ定番パズルゲームです。シンプルなルールですが、2次元配列を使ったマップ管理や、箱を押せるかどうかの判定など、ゲームプログラミングの基本がたくさん詰まっています。このチュートリアルでは、JavaScriptと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次元の数値配列に変換して、ゲームのロジックで扱いやすくしましょう。
// マップタイル定数
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次元配列に格納します。プレイヤーの位置も同時に記録しています。
プレイヤーが移動するとき、移動先のマスに何があるかによって動作が変わります。壁なら動けません。箱があるなら、箱の先も空いているかチェックが必要です。この「段階的な条件チェック」がプッシュ判定のポイントです。
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という特別なタイル値で管理しています。
パズルゲームでは「一手戻る」機能がとても重要です。移動のたびに、プレイヤーの位置と箱の状態を履歴配列に保存しておけば、いつでも前の状態に戻すことができます。
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を実現する方法を学びました。ここからさらに発展させるアイデアとして、ステージエディタを作ってオリジナルのパズルを作成したり、最小手数の記録機能を追加したり、アニメーション付きの移動を実装してみるのも楽しいでしょう。
A: はい、大丈夫です。このチュートリアルではステップごとにコードを書いていくので、初めての方でも順番に進めれば完成できます。わからないところがあれば、ひなテックの教室で質問もできますよ。
A: 基本部分は約30分〜1時間で作れます。見た目をこだわったり機能を追加すると、さらに楽しく発展させられます。