
【ゲーム】JavaScript:17 パックンマン
「パックンマン」は、プレイヤーが😄(パックンマン)を操作して迷路内のドット(●)をすべて食べつくし、おばけ(👻🧟💀)に捕まらないようにクリアを目指すシンプルなアクションゲームです。ステージは全3面で、ステージが上がるごとにおばけの数が増え、難易度が上昇します。
遊び方・操作方法
- 操作キー:キーボードの矢印キー(↑↓←→)でパックンマンを上下左右に移動
- スタート:「▶️ スタート」ボタンをクリックするとゲーム開始
- ライフ:初期ライフは2(😄😄)。おばけに捕まるとライフが1つ減り、ライフが0になるとゲームオーバー
- ステージクリア:迷路内のすべてのドットを食べるとステージクリア
ルール
- ドットを食べる:通過したマスにあるドット(●)を食べるとスコア+10
- おばけを回避:おばけと同じマスに入るとライフが1減少
- 残りライフがある場合:捕まれても同ステージを最初から再スタート
- ライフ0でゲームオーバー:タイトル画面に戻る
- 全3ステージクリア:クリアメッセージを表示してタイトルへ戻る
🎮ゲームプレイ
以下のリンク先から実際にプレイできます。
17 パックンマン
素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
pac_man_bg.png | pac_man_title.png |
---|---|
![]() | ![]() |
ゲーム画面イメージ

プログラム全文(pac_man.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>パックンマン</title>
<style>
/* 画面全体スクロール無効 */
html, body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
}
body {
background: url('pac_man_bg.png') no-repeat center center fixed;
background-size: cover;
font-family: Arial, sans-serif;
color: #fff;
text-align: center;
}
button {
cursor: pointer;
border: none;
border-radius: 5px;
}
/* タイトル画面 */
#titleScreen {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.6);
}
#titleScreen img {
max-width: 80%;
height: auto;
}
#titleScreen h1 {
font-size: 3rem;
margin: 1rem 0;
}
#titleScreen p {
font-size: 1.2rem;
max-width: 60%;
line-height: 1.5;
}
#startButton {
margin-top: 2rem;
padding: 0.8rem 2rem;
font-size: 1.2rem;
background: #ff0;
color: #000;
font-weight: bold;
}
/* ゲーム画面コンテナ */
#gameContainer {
display: none;
position: relative;
width: 100%;
height: 100vh;
}
/* マップラッパー */
#mapWrapper {
position: relative;
width: 600px; /* 20 cols × 30px */
height: 480px; /* 16 rows × 30px */
margin: 2rem auto 0;
}
/* キャンバス(マップ) */
#gameCanvas {
display: block;
width: 600px;
height: 480px;
background: rgba(0,0,0,0.5);
}
/* メッセージエリア:マップ中央 */
#messageArea {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8);
padding: 1.2rem 1.5rem;
border-radius: 10px;
font-size: 1.2rem;
display: none;
max-width: 80%;
line-height: 1.5;
z-index: 10;
}
#messageArea button {
margin-top: 1rem;
padding: 0.6rem 1.2rem;
background: #ff0;
color: #000;
font-weight: bold;
}
/* HUD:マップ下部に表示 */
#hud {
background: rgba(0,0,0,0.7);
padding: 12px 16px;
border-radius: 5px;
font-size: 1.6rem;
display: flex;
gap: 32px;
justify-content: center;
width: max-content;
margin: 1rem auto;
}
</style>
</head>
<body>
<!-- タイトル画面 -->
<div id="titleScreen">
<img src="pac_man_title.png" alt="パックンマン タイトル">
<h1>🎮 パックンマンへようこそ 🎮</h1>
<p>👻 おばけを避けながら、すべてのドットを食べ尽くそう!</p>
<button id="startButton">▶️ スタート</button>
</div>
<!-- ゲーム画面 -->
<div id="gameContainer">
<div id="mapWrapper">
<canvas id="gameCanvas"></canvas>
<div id="messageArea"></div>
</div>
<div id="hud">
<div>ライフ: <span id="lives"></span></div>
<div>ステージ: <span id="stage"></span></div>
<div>スコア: <span id="score"></span></div>
</div>
</div>
<script>
// 要素取得
const titleScreen = document.getElementById('titleScreen');
const startButton = document.getElementById('startButton');
const gameContainer = document.getElementById('gameContainer');
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const messageArea = document.getElementById('messageArea');
const hudLives = document.getElementById('lives');
const hudStage = document.getElementById('stage');
const hudScore = document.getElementById('score');
// マップ設定
const tileSize = 30, rows = 16, cols = 20;
canvas.width = cols * tileSize;
canvas.height = rows * tileSize;
// ゲーム状態
let maze, player, enemies, lives, currentStage, score, inMessage, gameLoopId;
// 1ステージ用マップテンプレート
const originalMaze = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
[1,0,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,0,1],
[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,1,0,1,0,1,1,1,0,0,1,1,1,0,1,0,1,0,1],
[1,0,1,0,1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1],
[1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,1,0,0,0,1],
[1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,1,0,0,0,1],
[1,0,1,0,1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1],
[1,0,1,0,1,0,1,1,1,1,1,1,1,1,0,1,0,1,0,1],
[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,0,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
[1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
// 全モンスター候補
const allEnemies = [
{ x: 18, y: 2, emoji: '👻' },
{ x: 3, y: 14, emoji: '🧟' },
{ x: 10, y: 8, emoji: '💀' }
];
// ゲーム開始
startButton.addEventListener('click', () => {
titleScreen.style.display = 'none';
gameContainer.style.display = 'block';
initGame();
// 敵移動+描画ループ (600ms間隔でモンスターさらにゆっくり)
gameLoopId = setInterval(gameLoop, 600);
});
function showTitle() {
clearInterval(gameLoopId);
gameContainer.style.display = 'none';
titleScreen.style.display = 'flex';
}
// 初期化
function initGame() {
lives = 2;
currentStage = 1;
score = 0;
inMessage = false;
resetStage();
}
// ステージ設定/リセット
function resetStage() {
inMessage = false;
maze = originalMaze.map(r => r.slice());
player = { x: 1, y: 2 };
// 現ステージ分のモンスターを配置
enemies = allEnemies.slice(0, currentStage).map(e => ({ ...e }));
updateHUD();
drawAll();
}
// HUD更新
function updateHUD() {
hudLives.textContent = '😄'.repeat(lives);
hudStage.textContent = currentStage;
hudScore.textContent = score;
}
// メインループ
function gameLoop() {
if (inMessage) return;
moveEnemies();
checkCollision();
drawAll();
checkStageClear();
}
// 描画
function drawAll() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawMaze();
drawPlayer();
drawEnemies();
}
function drawMaze() {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (maze[y][x] === 1) {
ctx.fillStyle = 'blue';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
} else if (maze[y][x] === 0) {
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(
x * tileSize + tileSize / 2,
y * tileSize + tileSize / 2,
5, 0, Math.PI * 2
);
ctx.fill();
}
}
}
}
function drawPlayer() {
ctx.font = `${tileSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
'😄',
player.x * tileSize + tileSize / 2,
player.y * tileSize + tileSize / 2
);
}
function drawEnemies() {
enemies.forEach(e => {
ctx.font = `${tileSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
e.emoji,
e.x * tileSize + tileSize / 2,
e.y * tileSize + tileSize / 2
);
});
}
// プレイヤー移動
document.addEventListener('keydown', e => {
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
e.preventDefault();
}
if (inMessage) return;
let dx = 0, dy = 0;
if (e.key === 'ArrowUp') dy = -1;
if (e.key === 'ArrowDown') dy = 1;
if (e.key === 'ArrowLeft') dx = -1;
if (e.key === 'ArrowRight') dx = 1;
if (dx || dy) movePlayer(dx, dy);
});
function movePlayer(dx, dy) {
const nx = player.x + dx, ny = player.y + dy;
if (maze[ny] && maze[ny][nx] !== 1) {
player.x = nx; player.y = ny;
if (maze[ny][nx] === 0) {
maze[ny][nx] = 2;
score += 10;
hudScore.textContent = score;
}
}
drawAll();
}
// 敵移動
function moveEnemies() {
const dirs = [{dx:0,dy:-1},{dx:0,dy:1},{dx:-1,dy:0},{dx:1,dy:0}];
enemies.forEach(e => {
let best = null, minD = Infinity;
dirs.forEach(d => {
const nx = e.x + d.dx, ny = e.y + d.dy;
if (maze[ny] && maze[ny][nx] !== 1) {
const dist = Math.abs(nx - player.x) + Math.abs(ny - player.y);
if (dist < minD) { minD = dist; best = d; }
}
});
if (best) {
e.x += best.dx;
e.y += best.dy;
}
});
}
// 衝突判定
function checkCollision() {
for (let e of enemies) {
if (e.x === player.x && e.y === player.y) {
handleCaught();
break;
}
}
}
function handleCaught() {
inMessage = true;
lives--;
updateHUD();
if (lives > 0) {
showTemporaryMessage(
`💥 捕まった! 残りライフ: ${lives}`,
3000,
() => resetStage()
);
} else {
showReturnMessage('💀 ゲームオーバー 💀');
}
}
// ステージクリア判定
function checkStageClear() {
if (!maze.some(row => row.includes(0))) {
inMessage = true;
if (currentStage < 3) {
showTemporaryMessage(
`🎉 ステージ${currentStage}クリア! 次へ…`,
5000,
() => {
currentStage++;
resetStage();
}
);
} else {
showReturnMessage('🏆 全ステージクリア! おめでとう! 🏆');
}
}
}
// メッセージ表示
function showTemporaryMessage(text, duration, cb) {
messageArea.innerHTML = `<p>${text}</p>`;
messageArea.style.display = 'block';
setTimeout(() => {
messageArea.style.display = 'none';
cb && cb();
}, duration);
}
function showReturnMessage(text) {
messageArea.innerHTML =
`<p>${text}</p><button id="returnBtn">タイトル画面へ戻る</button>`;
messageArea.style.display = 'block';
document.getElementById('returnBtn').addEventListener('click', () => {
messageArea.style.display = 'none';
showTitle();
});
}
</script>
</body>
</html>
アルゴリズムの流れ
ステップ | 処理内容 | 主な関数・命令 |
---|---|---|
1. 初期化 | ライフ・ステージ・スコアをリセットし、ステージのマップを生成 | initGame() → resetStage() |
2. メインループ | 一定間隔(600ms)でおばけを動かし、衝突判定・描画・クリア判定 | gameLoop() |
3. 描画 | 迷路・プレイヤー・おばけをキャンバスに描画 | drawMaze() , drawPlayer() , drawEnemies() |
4. プレイヤー移動 | 矢印キーで移動可能判定 → ドットを食べる(スコア加算) | movePlayer(dx,dy) |
5. おばけ移動 | 四方の移動先を評価し、プレイヤーへの距離が最小となる方向へ移動 | moveEnemies() |
6. 衝突判定 | プレイヤーとおばけが同座標か判定 | checkCollision() → handleCaught() |
7. ステージクリア判定 | ドットが残っているか判定 → クリア/全ステージクリア処理 | checkStageClear() |
関数の詳しい解説
関数名 | 説明 |
---|---|
initGame() | ゲーム開始時の初期設定。ライフ・ステージ・スコアを初期化し、最初のステージをセット |
resetStage() | マップをコピーしてプレイヤー・おばけを再配置し、HUDを更新 |
updateHUD() | ライフ・ステージ・スコアを画面下部のHUDに反映 |
gameLoop() | メインループ。おばけ移動・衝突判定・描画・クリア判定を順に実行 |
drawMaze() | maze 配列を走査し、壁(1)を青、ドット(0)を黄色の円で描画 |
drawPlayer() | プレイヤー絵文字(😄)を現在座標に描画 |
drawEnemies() | 各おばけの絵文字をそれぞれの座標に描画 |
movePlayer(dx,dy) | 移動先が通行可能(壁でない)ならプレイヤー座標を更新し、ドットならスコア+■ |
moveEnemies() | 四方向の移動先を評価し、プレイヤーとの距離が最小となる方向へおばけを移動 |
checkCollision() | プレイヤーとおばけが同じ座標かチェックし、捕まった場合に handleCaught() を呼び出し。 |
handleCaught() | ライフ減少、再スタート or ゲームオーバーメッセージ表示 |
checkStageClear() | マップ内にドットが残存しているかチェックし、次ステージ or クリア完了メッセージを表示 |
showTemporaryMessage() | 一時メッセージを表示し、一定時間後に非表示→コールバック実行 |
showReturnMessage() | 終了メッセージ+「タイトルへ戻る」ボタンを表示 |
改造のポイント
- タイルサイズの変更:
tileSize
を変えてマップの見た目を拡大/縮小 - ステージ数追加:
allEnemies
配列とif (currentStage < 3)
の条件を調整して、4面以上を追加可能 - おばけの動き多様化:
moveEnemies()
にランダム要素や追跡アルゴリズム(A* など)を導入 - パワーアップアイテム:マップに特殊アイテムを配置し、取得時におばけを一定時間無力化
- BGM/SE導入:
Audio
オブジェクトで効果音やBGMを再生 - スコアランキング機能:
localStorage
へスコアを保存し、ランキング画面を表示 - 難易度調整:
setInterval(gameLoop, 600)
の間隔を短くしてスピードアップ