Ever wanted to build your own Pac-Man game? In this tutorial, we will walk through the basics of creating a simple Pac-Man clone using HTML Canvas and vanilla JavaScript. No frameworks needed — just a browser and a text editor.
Start with a simple HTML file that includes a <canvas> element. The canvas is where all the game graphics will be drawn.
<!DOCTYPE html>
<html>
<head>
<title>My Pac-Man</title>
<style>
body { background: #000; display: flex;
justify-content: center; align-items: center;
height: 100vh; margin: 0; }
canvas { border: 2px solid #333; }
</style>
</head>
<body>
<canvas id="game" width="400" height="400"></canvas>
<script src="pacman.js"></script>
</body>
</html>
Pac-Man uses a tile-based map. Each tile can be a wall, a dot, a power pellet, or empty space. We represent this as a 2D array where each number means a different tile type.
const TILE_SIZE = 20;
const map = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,1],
[1,2,1,1,2,1,1,1,2,1,1,2,1,1,1,2,1,1,2,1],
[1,3,1,1,2,1,1,1,2,1,1,2,1,1,1,2,1,1,3,1],
[1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1],
// ... more rows
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
// 0 = empty, 1 = wall, 2 = dot, 3 = power pellet
Use the Canvas 2D API to loop through the map array and draw each tile. Walls are blue rectangles, dots are small white circles, and power pellets are larger flashing circles.
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
function drawMap() {
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[row].length; col++) {
const x = col * TILE_SIZE;
const y = row * TILE_SIZE;
const tile = map[row][col];
if (tile === 1) {
ctx.fillStyle = '#2121DE';
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
} else if (tile === 2) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x + 10, y + 10, 2, 0, Math.PI * 2);
ctx.fill();
} else if (tile === 3) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x + 10, y + 10, 6, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
Pac-Man is a yellow circle with an animated mouth. We track his position, direction, and use a simple mouth animation that opens and closes.
let pacman = { x: 1, y: 1, dir: 0, mouth: 0.2 };
let mouthOpening = true;
function drawPacman() {
const cx = pacman.x * TILE_SIZE + TILE_SIZE / 2;
const cy = pacman.y * TILE_SIZE + TILE_SIZE / 2;
const angle = pacman.dir * Math.PI / 2;
ctx.fillStyle = '#FFFF00';
ctx.beginPath();
ctx.arc(cx, cy, TILE_SIZE / 2 - 2,
angle + pacman.mouth,
angle + Math.PI * 2 - pacman.mouth);
ctx.lineTo(cx, cy);
ctx.fill();
// Animate mouth
if (mouthOpening) {
pacman.mouth += 0.04;
if (pacman.mouth > 0.4) mouthOpening = false;
} else {
pacman.mouth -= 0.04;
if (pacman.mouth < 0.05) mouthOpening = true;
}
}
Listen for arrow key presses to change Pac-Man's direction. We store the desired direction and only apply it when the move is valid (not into a wall).
let nextDir = 0;
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') nextDir = 0;
if (e.key === 'ArrowDown') nextDir = 1;
if (e.key === 'ArrowLeft') nextDir = 2;
if (e.key === 'ArrowUp') nextDir = 3;
});
function movePacman() {
const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
const d = dirs[nextDir];
const nx = pacman.x + d.x;
const ny = pacman.y + d.y;
if (map[ny] && map[ny][nx] !== 1) {
pacman.x = nx;
pacman.y = ny;
pacman.dir = nextDir;
if (map[ny][nx] === 2 || map[ny][nx] === 3) {
map[ny][nx] = 0; // eat the dot
}
}
}
The game loop ties everything together. It clears the canvas, draws the map, moves Pac-Man, and draws him — all at a steady frame rate.
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawMap();
movePacman();
drawPacman();
}
setInterval(gameLoop, 150); // ~7 FPS for tile-based movement
Ghosts move randomly through the maze. Each frame, a ghost picks a random valid direction. This is the simplest AI — you can make it smarter later by chasing Pac-Man's position.
let ghosts = [
{ x: 9, y: 9, dir: 0, color: '#FF0000' },
{ x: 10, y: 9, dir: 2, color: '#FFB8FF' },
];
function moveGhost(ghost) {
const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
// Try random direction, fallback to current
const tryDirs = [0,1,2,3].sort(() => Math.random() - 0.5);
for (const d of tryDirs) {
const nx = ghost.x + dirs[d].x;
const ny = ghost.y + dirs[d].y;
if (map[ny] && map[ny][nx] !== 1) {
ghost.x = nx;
ghost.y = ny;
ghost.dir = d;
return;
}
}
}
This gives you a basic working Pac-Man. From here you can add:
Ready for the next step? Continue to Part 2: Score Tracking where we add dot counting, score display, high scores, and a win condition.