The Problem
Traditional Pac-Man is a single-player game, limiting opportunities for competitive and cooperative gameplay.
The Goal
Expand Pac-Man into a two-player experience while maintaining the classic feel and mechanics.
My Role
I was responsible for game design and development, from ideation to prototyping and final implementation,
as part of a team of four.
To ensure PacMan Battle provided an engaging and competitive multiplayer experience, we conducted a competitive audit, analyzing existing projects to understand what works and what doesn’t. Our goal was to convert the iconic game into a multiplayer format while staying true to its retro aesthetic and mechanics.
We explored co-op variations, but due to technical constraints, we ultimately focused on a turn-based competition model. A real-time online multiplayer experience would require high data exchange frequency, which the university’s server could not handle. As a result, some of our competitive benchmarks didn’t fully align with our final approach.
Since extensive research already existed on Pac-Man’s target audience, we conducted a secondary analysis to understand our potential users better. Based on the findings, we developed an ad-hoc persona to capture our target group's needs and frustrations.
To explore different design concepts, we visualized the game layout through digital sketches. Early iterations helped us quickly refine design choices before transitioning into digital prototyping, ensuring the final approach remained true to the Pac-Man aesthetic.
Once our initial game layout was in place, we conducted usability testing to validate our design decisions and test the functionality of our code.
The usability test involved a live demonstration of the prototype to a group of 11 participants. They could play freely and explore the mechanics naturally. We observed this real-world user interactions and conducted interviews to identify areas for improvement.
Tunnel Mechanic
Removed due to a bug where characters could escape the game area
Scoring System
Adjusted points to create a fairer competitive game
Local Score Storage
Information is spread across multiple sources
Interactive Quiz
We introduced a Pac-Man-themed quiz to maintain engagemen
Tunnel Mechanic
Removed due to a bug where characters could escape the game area
Scoring System
Adjusted points to create a fairer competitive game
Local Score Storage
Information is spread across multiple sources
Interactive Quiz
We introduced a Pac-Man-themed quiz to maintain engagemen
The game board is built using a grid-based system, where each tile represents different objects in an array.
// Map Example
const map = [
['1', 'a', 'a', '2'], // Walls = a / Corners = Numbers
['b', '.', '.', 'b'], // Pellets = . / Vertical Walls = b
['b', 'p', 'p', 'b'], // Power-ups = p
['4', 'a', 'a', '3'] // Walls = a / Corners = Numbers
];
// Dynamic Generation
function generateMap () {
map.forEach((row, i) => {
row.forEach((symbol, j) => {
if (symbol === '.') {
pellets.push(new Pellet({ position: { x: j * 32, y: i * 32 }}));
} else if (symbol === 'p') {
powerUps.push(new PowerUp({ position: { x: j * 32, y: i * 32 }}));
} else {
boundaries.push(new Boundary({ position: { x: j * 32, y: i * 32 }}));
}
});
});
}
Pac-Man's movement follows a collision-based system. To avoid conflicts with webpage functions, only W, A, S, D keys were assigned for player controls.
// Player Movement
document.addEventListener("keydown", (event) => {
switch (event.key) {
case 'ArrowUp': player.velocity.y = -3; break;
case 'ArrowDown': player.velocity.y = 3; break;
case 'ArrowLeft': player.velocity.x = -3; break;
case 'ArrowRight': player.velocity.x = 3; break;
}
});
// Collision Detection
boundaries.forEach((boundary) => {
if (circleCollidesWithRectangle({ circle: player, rectangle: boundary })) {
player.velocity.x = 0;
player.velocity.y = 0;
}
});
The ghosts navigate randomly while avoiding walls. At the same time, the AI adjusts movement based on available paths.
// Ghosts Movement
const possibleDirections = ['up', 'down', 'left', 'right'];
const randomDirection = possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
switch (randomDirection) {
case 'up': ghost.velocity.y = -3; break;
case 'down': ghost.velocity.y = 3; break;
case 'left': ghost.velocity.x = -3; break;
case 'right': ghost.velocity.x = 3; break;
}
// Ghosts Change Behavior
ghosts.forEach(ghost => {
ghost.scared = true;
setTimeout(() => ghost.scared = false, 5000);
});
// Round Timer
var timeLeft = 20;
var timerId = setInterval(() => {
timeLeft--;
if (timeLeft === 0) {
clearTimeout(timerId);
switchTurn();
}
}, 1000);
// Switch Turn
function switchTurn() {
currentPlayer = currentPlayer === 1 ? 2 : 1;
resetPlayerPosition();
}
// Power-Up Collision Effect
if (circleCollidesWithRectangle({ circle: player, rectangle: pellet })) {
pellets.splice(i, 1); // Remove pellet
score += 10; // Increase score
}
// Power to Eat Ghosts
if (circleCollidesWithRectangle({ circle: player, rectangle: powerUp })) {
powerUps.splice(i, 1);
ghosts.forEach(ghost => ghost.scared = true);
}