パックマンの作り方 — JavaScriptでブラウザゲームを作ろう

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

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

パックマンは、迷路の中でドットを食べながらゴーストを避けるクラシックアクションゲームです。このゲームを作ることで、Canvasを使った2Dゲーム描画の基本、タイルマップによる迷路表現、そして簡単なAIの実装まで幅広く学べます。

ステップ1: 迷路データとCanvas描画

まずは迷路を2次元配列(タイルマップ)で定義しましょう。各セルに「壁」「ドット」「パワーエサ」などの種類を数字で割り当て、それをCanvasに描画します。

var TILE_SIZE = 20;
var COLS = 21;
var ROWS = 23;

// タイルの種類
var W = 1; // 壁
var D = 0; // ドット(通路)
var E = 2; // 空(ドットなし)
var P = 3; // パワーエサ

// 迷路定義(一部抜粋)
var BASE_MAP = [
  [W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W,W],
  [W,D,D,D,D,D,D,D,D,D,W,D,D,D,D,D,D,D,D,D,W],
  [W,D,W,W,D,W,W,W,D,D,W,D,D,W,W,W,D,W,W,D,W],
  [W,P,W,W,D,W,W,W,D,D,W,D,D,W,W,W,D,W,W,P,W],
  // ...(21x23の配列が続く)
];

配列の値によってセルの種類が決まります。Wは壁、Dはドット(通路)、Pはパワーエサです。この配列を元に、Canvasで迷路を描いていきます。

Canvasに迷路を描く

タイルマップを1マスずつループして、壁のセルに青い四角を描きます。ドットとパワーエサも同様に丸を描きます。

function drawMaze() {
  for (var r = 0; r < ROWS; r++) {
    for (var c = 0; c < COLS; c++) {
      if (map[r][c] === W) {
        // 壁を青い四角で描く
        ctx.fillStyle = '#1a1aff';
        ctx.fillRect(c * tileW, r * tileH, tileW, tileH);
      }
    }
  }
}

function drawDots() {
  for (var r = 0; r < ROWS; r++) {
    for (var c = 0; c < COLS; c++) {
      var cx = c * tileW + tileW / 2;
      var cy = r * tileH + tileH / 2;
      if (map[r][c] === D) {
        // 小さな白丸でドットを描く
        ctx.fillStyle = '#FFE0B2';
        ctx.beginPath();
        ctx.arc(cx, cy, tileW * 0.1, 0, Math.PI * 2);
        ctx.fill();
      } else if (map[r][c] === P) {
        // 大きな丸でパワーエサを描く
        ctx.fillStyle = '#FFE0B2';
        ctx.beginPath();
        ctx.arc(cx, cy, tileW * 0.3, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  }
}

tileWtileHはCanvas全体のサイズを列数・行数で割った1マスの幅と高さです。c * tileWのように掛けることで、配列のインデックスをCanvas上の座標に変換しています。

ステップ2: プレイヤーの移動と壁判定

パックマンは「タイル単位」で移動します。プレイヤーのキー入力を受け取り、次に進もうとするタイルが壁かどうかを確認してから移動させます。これが壁衝突判定の基本です。

// キー入力で「次に進みたい方向」を記録する
document.addEventListener('keydown', function (e) {
  var keyMap = {
    ArrowLeft:  { x: -1, y:  0 },
    ArrowRight: { x:  1, y:  0 },
    ArrowUp:    { x:  0, y: -1 },
    ArrowDown:  { x:  0, y:  1 },
    a: { x: -1, y:  0 },
    d: { x:  1, y:  0 },
    w: { x:  0, y: -1 },
    s: { x:  0, y:  1 }
  };
  var dir = keyMap[e.key];
  if (dir) {
    e.preventDefault(); // ページのスクロールを防ぐ
    player.nextDir = dir;
  }
});

player.nextDirに「次に行きたい方向」を保存しておき、プレイヤーがタイルの中心に達したタイミングで方向転換を試みます。これで壁に向かっているときに方向キーを押しても、曲がれる場所まで自動的に待ってくれます。

壁判定と滑らかな移動

プレイヤーはタイルの中心を目指して毎フレーム少しずつ動き、中心に達したときに次のタイルへ進めるか判定します。

function canMove(col, row) {
  // 画面外はトンネル(ワープ)
  if (col < 0 || col >= COLS) return true;
  var tile = map[row][col];
  return tile !== W; // 壁でなければ通れる
}

function movePlayer() {
  var speed = PLAYER_SPEED;
  // 目標座標(タイルの中心)
  var targetX = player.col * tileW + tileW / 2;
  var targetY = player.row * tileH + tileH / 2;
  var dx = targetX - player.x;
  var dy = targetY - player.y;
  var dist = Math.sqrt(dx * dx + dy * dy);

  if (dist < speed) {
    // タイル中心に到達 → 方向転換を試みる
    player.x = targetX;
    player.y = targetY;

    // 次の方向に曲がれるか確認
    var nc = player.col + player.nextDir.x;
    var nr = player.row + player.nextDir.y;
    if (canMove(nc, nr)) {
      player.dir = player.nextDir; // 曲がる
    }

    // 現在の方向に進めるか確認
    var fc = player.col + player.dir.x;
    var fr = player.row + player.dir.y;
    if (canMove(fc, fr)) {
      player.col += player.dir.x;
      player.row += player.dir.y;
    }
  } else {
    // 目標に向かって少しずつ動く
    player.x += (dx / dist) * speed;
    player.y += (dy / dist) * speed;
  }
}

毎フレームで「目標(タイル中心)との距離」を計算し、速度分だけ近づきます。距離が速度より小さくなったら到達とみなして次の判定へ進みます。この「補間移動」のパターンはゲーム開発でよく使われます。

ステップ3: ゴーストAIと衝突判定

ゴーストはプレイヤーを「追いかける」AIを持っています。シンプルなAIの基本は「目標地点に最も近づける方向を選ぶ」という貪欲法(グリーディ法)です。

function getGhostTarget(ghost) {
  // パワーエサを食べられてイジケ中はランダムに逃げる
  if (ghost.frightened) {
    return {
      col: Math.floor(Math.random() * COLS),
      row: Math.floor(Math.random() * ROWS)
    };
  }
  // 通常時はプレイヤーを直接追う(blinky)
  return { col: player.col, row: player.row };
}

function moveGhost(ghost) {
  // 4方向の中から「後戻り以外で目標に最も近い方向」を選ぶ
  var dirs = [
    { x: 0, y: -1 }, // 上
    { x: -1, y: 0 }, // 左
    { x: 0, y: 1 },  // 下
    { x: 1, y: 0 }   // 右
  ];
  var target = getGhostTarget(ghost);
  var bestDir = null;
  var bestDist = Infinity;

  for (var i = 0; i < dirs.length; i++) {
    var d = dirs[i];
    // 後戻りは禁止(逆方向をスキップ)
    if (d.x === -ghost.dir.x && d.y === -ghost.dir.y) continue;
    var nc = ghost.col + d.x;
    var nr = ghost.row + d.y;
    if (!canGhostMove(nc, nr)) continue;
    // 目標地点との距離(二乗距離で比較)
    var ddx = target.col - nc;
    var ddy = target.row - nr;
    var dd = ddx * ddx + ddy * ddy;
    if (dd < bestDist) {
      bestDist = dd;
      bestDir = d;
    }
  }
  if (bestDir) {
    ghost.dir = bestDir;
    ghost.col += ghost.dir.x;
    ghost.row += ghost.dir.y;
  }
}

ゴーストは「後戻り禁止」ルールがあるため、同じ場所を往復せず迷路を探索します。目標との距離はdx*dx + dy*dy(二乗距離)で比較しているため、Math.sqrt()を呼ばなくて済み処理が軽くなります。

衝突判定とライフ管理

プレイヤーとゴーストの距離が一定以下になったら「捕まった」と判定します。

// 毎フレーム、全ゴーストとの距離を確認
for (var j = 0; j < ghosts.length; j++) {
  var g = ghosts[j];
  var gdx = player.x - g.x;
  var gdy = player.y - g.y;
  var gdist = Math.sqrt(gdx * gdx + gdy * gdy);

  if (gdist < tileW * 0.7) {
    if (g.frightened) {
      // パワーエサ中はゴーストを食べる(200点)
      score += GHOST_EAT_SCORE;
      g.eyeOnly = true;    // 目だけの状態で巣に帰る
      g.frightened = false;
    } else {
      // 捕まった → ライフ減少
      lives--;
      deathTimer = 60;     // 死亡アニメーション60フレーム
    }
  }
}

ゴーストが「イジケ状態(frightened)」のときに触れると、逆にゴーストを食べられます。eyeOnlyフラグを立てることで、ゴーストを「目だけ」の状態にして巣へ帰還させています。

まとめ — 次のステップ

おつかれさまでした!パックマンの3つの核心部分、「タイルマップによる迷路描画」「壁判定付きの滑らかな移動」「ゴーストAIと衝突判定」を学びました。さらに発展させるなら、4種類のゴーストにそれぞれ違う追跡パターンを実装したり、レベルが上がるごとにゴーストのスピードを上げたり、フルーツアイテムを追加してみましょう。

よくある質問

Q: Canvasとは何ですか?

A: CanvasはHTMLの要素で、JavaScriptから図形・画像・アニメーションを自由に描ける「お絵描きボード」です。パックマンのような動くゲームを作るときにとても便利です。最初は難しく感じるかもしれませんが、fillRect(四角)とarc(円)の2つを覚えるだけでも多くのものが作れます。

Q: ゴーストAIをもっと賢くするにはどうすればよいですか?

A: 今回の「最も近い方向を選ぶ」グリーディ法に加えて、BFS(幅優先探索)を使うと迷路の構造を考慮した最短経路を見つけられます。また、本物のパックマンのように「先読み」や「挟み撃ち」などの戦略を各ゴーストに割り当てると、より本格的なゲームになります。