🏓 Ping Pong amb una pala

Tutorial pas a pas · Compatible amb PC i mòbil · Sense errors de còpia

Pas 1: Estructura HTML bàsica

Comencem amb l'esquelet del document. La meta etiqueta viewport és clau per a la responsivitat. Afegim un div contenidor, un canvas i un element per mostrar la puntuació.

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Ping Pong</title>
</head>
<body>
    <div id="game-container">
        <canvas id="gameCanvas"></canvas>
        <div id="score">0</div>
    </div>
</body>
</html>

Pas 2: CSS responsiu

El contenidor fa servir aspect-ratio: 480/700 per mantenir la proporció del joc. max-width i max-height eviten que el canvas sobrepassi la pantalla. touch-action: none impedeix desplaçaments accidentals en mòbils.

* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    background: #111;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    overflow: hidden;
    touch-action: none;
}
#game-container {
    position: relative;
    max-width: 100vw;
    max-height: 100vh;
    aspect-ratio: 480 / 700;
}
canvas {
    width: 100%;
    height: 100%;
    background: #222;
    border: 2px solid #0ff;
    border-radius: 10px;
}
#score {
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    color: white;
    font-family: Arial, sans-serif;
    font-size: clamp(18px, 5vw, 24px);
    pointer-events: none;
}

Pas 3: JavaScript – Configuració del canvas i variables

Definim la resolució interna del joc (480×700 píxels). La pala se situa a baix i la pilota al centre amb una velocitat inicial cap amunt. Tot es dibuixa sobre aquesta resolució fixa, mentre el CSS escala el canvas visualment.

const GAME_WIDTH = 480;
const GAME_HEIGHT = 700;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;

const paddle = {
    x: GAME_WIDTH / 2 - 50,
    y: GAME_HEIGHT - 40,
    width: 100,
    height: 14,
    speed: 7
};

const ball = {
    x: GAME_WIDTH / 2,
    y: GAME_HEIGHT / 2,
    radius: 10,
    dx: 4,
    dy: -4
};

let score = 0;
let gameRunning = true;
let gameOver = false;

Pas 4: Funció per convertir coordenades del punter

Com que el canvas s'escala, cal traduir la posició del ratolí/dit (clientX) a la coordenada X del món del joc.

function getGameX(clientX) {
    const rect = canvas.getBoundingClientRect();
    const scaleX = GAME_WIDTH / rect.width;
    return (clientX - rect.left) * scaleX;
}

Pas 5: Controls de teclat (PC)

Emmagatzemem l'estat de les tecles (fletxes i A/D) per moure la pala amb el teclat.

const keys = {};
document.addEventListener('keydown', function(e) {
    keys[e.key] = true;
    e.preventDefault();
});
document.addEventListener('keyup', function(e) {
    keys[e.key] = false;
    e.preventDefault();
});

Pas 6: Controls de ratolí

La pala segueix el cursor horitzontalment dins del canvas.

canvas.addEventListener('mousemove', function(e) {
    if (gameOver) return;
    const gameX = getGameX(e.clientX);
    paddle.x = gameX - paddle.width / 2;
    if (paddle.x < 0) paddle.x = 0;
    if (paddle.x + paddle.width > GAME_WIDTH) paddle.x = GAME_WIDTH - paddle.width;
});

Pas 7: Controls tàctils (mòbil)

Amb touchstart i touchmove fem el mateix. Si el joc ha acabat, un toc el reinicia.

canvas.addEventListener('touchstart', function(e) {
    e.preventDefault();
    if (gameOver) { restartGame(); return; }
    const gameX = getGameX(e.touches[0].clientX);
    paddle.x = gameX - paddle.width / 2;
    if (paddle.x < 0) paddle.x = 0;
    if (paddle.x + paddle.width > GAME_WIDTH) paddle.x = GAME_WIDTH - paddle.width;
}, { passive: false });

canvas.addEventListener('touchmove', function(e) {
    e.preventDefault();
    if (gameOver) return;
    const gameX = getGameX(e.touches[0].clientX);
    paddle.x = gameX - paddle.width / 2;
    if (paddle.x < 0) paddle.x = 0;
    if (paddle.x + paddle.width > GAME_WIDTH) paddle.x = GAME_WIDTH - paddle.width;
}, { passive: false });

Pas 8: Lògica del joc (moviment i col·lisions)

La funció update() mou la pala amb el teclat, la pilota, gestiona els rebots i detecta la col·lisió amb la pala. Cada cop que la pilota toca la pala, la velocitat augmenta lleugerament i suma un punt.

function update() {
    if (!gameRunning) return;

    // Teclat
    if (keys['ArrowLeft'] || keys['a']) paddle.x -= paddle.speed;
    if (keys['ArrowRight'] || keys['d']) paddle.x += paddle.speed;

    // Limitar pala
    if (paddle.x < 0) paddle.x = 0;
    if (paddle.x + paddle.width > GAME_WIDTH) paddle.x = GAME_WIDTH - paddle.width;

    // Moure pilota
    ball.x += ball.dx;
    ball.y += ball.dy;

    // Rebots laterals i superior
    if (ball.x - ball.radius <= 0 || ball.x + ball.radius >= GAME_WIDTH) ball.dx = -ball.dx;
    if (ball.y - ball.radius <= 0) ball.dy = -ball.dy;

    // Col·lisió amb la pala
    if (ball.dy > 0 &&
        ball.y + ball.radius >= paddle.y &&
        ball.y - ball.radius <= paddle.y + paddle.height &&
        ball.x + ball.radius >= paddle.x &&
        ball.x - ball.radius <= paddle.x + paddle.width) {
        ball.dy = -ball.dy;
        ball.y = paddle.y - ball.radius;
        ball.dx *= 1.02;
        ball.dy *= 1.02;
        score++;
        document.getElementById('score').textContent = score;
    }

    // Game Over
    if (ball.y - ball.radius > GAME_HEIGHT) {
        gameRunning = false;
        gameOver = true;
        document.getElementById('score').textContent =
            'Game Over! Puntuació: ' + score + ' (toca per reiniciar)';
    }
}

Pas 9: Dibuixat

draw() neteja el canvas i pinta el fons, la línia central, la pala i la pilota amb efecte de resplendor. Si el joc s'ha acabat, mostra un missatge semitransparent.

function draw() {
    ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);

    // Línia central discontínua
    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.setLineDash([10, 15]);
    ctx.beginPath();
    ctx.moveTo(0, GAME_HEIGHT / 2);
    ctx.lineTo(GAME_WIDTH, GAME_HEIGHT / 2);
    ctx.stroke();
    ctx.setLineDash([]);

    // Pala
    ctx.fillStyle = '#0ff';
    ctx.shadowColor = '#0ff';
    ctx.shadowBlur = 15;
    ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);

    // Pilota
    ctx.shadowColor = '#fff';
    ctx.shadowBlur = 10;
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;

    // Game Over
    if (gameOver) {
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
        ctx.fillStyle = '#fff';
        ctx.font = '30px Arial';
        ctx.textAlign = 'center';
        ctx.fillText('Game Over!', GAME_WIDTH / 2, GAME_HEIGHT / 2 - 20);
        ctx.font = '18px Arial';
        ctx.fillText('Toca per reiniciar', GAME_WIDTH / 2, GAME_HEIGHT / 2 + 20);
        ctx.textAlign = 'start';
    }
}

Pas 10: Reinici i bucle del joc

restartGame() torna tot a l'estat inicial. gameLoop() s'executa contínuament amb requestAnimationFrame per aconseguir 60 fps.

function restartGame() {
    score = 0;
    document.getElementById('score').textContent = '0';
    ball.x = GAME_WIDTH / 2;
    ball.y = GAME_HEIGHT / 2;
    ball.dx = (Math.random() > 0.5 ? 1 : -1) * 4;
    ball.dy = -4;
    paddle.x = GAME_WIDTH / 2 - 50;
    gameOver = false;
    gameRunning = true;
}

function gameLoop() {
    update();
    draw();
    requestAnimationFrame(gameLoop);
}

gameLoop();

🎮 Prova el joc aquí mateix!

Juga amb ratolí, teclat (fletxes o A/D) o pantalla tàctil.