🏓 Build Pong in React

Learn game development step-by-step. See the game, understand the code, build it yourself!

🎯 Live Pong Game

This is what we'll build! Control the left paddle with your mouse and try to beat the computer. Click the step buttons above to see each feature being added!

🎮Active Features:
🏆 First to 6 Points Wins!
  • Player paddle (mouse control)
  • Computer paddle (AI)
  • Ball movement
  • Wall collisions
  • Paddle collisions
  • Scoring system
  • Particle effects
📚 What You'll Learn:
  • Game loop with requestAnimationFrame
  • Collision detection algorithms
  • AI opponent using lerp
  • Canvas API for rendering
  • Particle systems
STEP 1

🎮 Player Paddle Control

Let's Start Simple: Create a paddle that follows your mouse! This uses the same mouse tracking we learned before. The paddle will only respond to your mouse after you click "Start Game".

🧠 The Concept

We track the mouse Y position and set the paddle's vertical position to follow it. The paddle stays within the canvas boundaries so it can't go off-screen. The tracking is only active during gameplay to keep things clean.

Step 1: Mouse-Controlled Paddle
import { useState, useEffect, useRef } from 'react';

function PongGame() {
  const [paddleY, setPaddleY] = useState(200);
  const canvasRef = useRef(null);
  
  const CANVAS_HEIGHT = 500;
  const PADDLE_HEIGHT = 100;

  // Track mouse movement
  useEffect(() => {
    const handleMouseMove = (e) => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      
      const rect = canvas.getBoundingClientRect();
      const mouseY = e.clientY - rect.top;
      
      // Keep paddle within canvas bounds
      const newY = Math.max(
        0,  // Don't go above top
        Math.min(
          mouseY - PADDLE_HEIGHT / 2,  // Center on mouse
          CANVAS_HEIGHT - PADDLE_HEIGHT  // Don't go below bottom
        )
      );
      
      setPaddleY(newY);
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  // Drawing code (we'll add this next)
  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    
    // Clear canvas
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, 800, 500);
    
    // Draw paddle
    ctx.fillStyle = '#00ff88';
    ctx.fillRect(0, paddleY, 15, 100);
  }, [paddleY]);

  return <canvas ref={canvasRef} width={800} height={500} />;
}
STEP 2

🤖 Computer AI Opponent

Adding Challenge: The computer paddle needs to move automatically! We'll use lerp (linear interpolation) to make it smoothly follow the ball.

🧠 The Concept

The AI tries to center its paddle on the ball's Y position. Instead of instant movement, it moves a percentage of the distance each frame - this makes it beatable!

💡 AI Difficulty:
Speed = 0.08 → Easy (slow reaction)
Speed = 0.15 → Medium (balanced)
Speed = 0.5 → Hard (fast reaction)
Step 2: Computer AI with Lerp
function ComputerAI() {
  const [computerPaddleY, setComputerPaddleY] = useState(200);
  const [ballPos, setBallPos] = useState({ x: 400, y: 250 });

  // Game loop
  useEffect(() => {
    const gameLoop = () => {
      // Computer AI: Follow the ball
      setComputerPaddleY(prev => {
        const targetY = ballPos.y - PADDLE_HEIGHT / 2;
        const speed = 0.08;  // Adjust for difficulty
        
        // Lerp formula: move 8% closer each frame
        return prev + (targetY - prev) * speed;
      });

      requestAnimationFrame(gameLoop);
    };

    const animationId = requestAnimationFrame(gameLoop);
    return () => cancelAnimationFrame(animationId);
  }, [ballPos]);

  return (
    // Canvas rendering...
    <canvas />
  );
}
STEP 3

⚽ Ball Movement

The Core Mechanic: A ball that moves with velocity! Velocity means the ball has both speed and direction.

🧠 The Physics

The ball has velocity in X and Y directions. Each frame, we add the velocity to the position. Positive X = right, negative X = left. Same for Y (up/down).

🎯 Velocity Explained:
velocity = { x: 4, y: 3 }
  • x: 4 means move 4 pixels right per frame
  • y: 3 means move 3 pixels down per frame
  • At 60fps, ball moves 240px/sec right, 180px/sec down
Step 3: Ball Physics
function BallMovement() {
  const [ballPos, setBallPos] = useState({ x: 400, y: 250 });
  const [ballVelocity, setBallVelocity] = useState({ x: 4, y: 3 });

  useEffect(() => {
    const gameLoop = () => {
      // Update ball position based on velocity
      setBallPos(prev => ({
        x: prev.x + ballVelocity.x,  // Add X velocity
        y: prev.y + ballVelocity.y,  // Add Y velocity
      }));

      requestAnimationFrame(gameLoop);
    };

    const animationId = requestAnimationFrame(gameLoop);
    return () => cancelAnimationFrame(animationId);
  }, [ballVelocity]);

  // Draw ball on canvas
  useEffect(() => {
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#ffcc00';
    ctx.beginPath();
    ctx.arc(ballPos.x, ballPos.y, 7.5, 0, Math.PI * 2);
    ctx.fill();
  }, [ballPos]);

  return <canvas />;
}
STEP 4

🧱 Wall Collision Detection

Bounce Back: When the ball hits the top or bottom wall, it should bounce! We do this by reversing the Y velocity.

🧠 Collision Logic

Check if ball's Y position is less than 0 (hit top) or greater than canvas height (hit bottom). When collision detected: multiply Y velocity by -1 to reverse direction!

Step 4: Wall Bounce
function WallCollision() {
  const CANVAS_HEIGHT = 500;
  const BALL_SIZE = 15;

  useEffect(() => {
    const gameLoop = () => {
      // Move ball
      setBallPos(prev => {
        let newY = prev.y + ballVelocity.y;

        // Hit top wall?
        if (newY <= 0) {
          setBallVelocity(v => ({ ...v, y: Math.abs(v.y) }));
          newY = 0;
        }

        // Hit bottom wall?
        if (newY >= CANVAS_HEIGHT - BALL_SIZE) {
          setBallVelocity(v => ({ ...v, y: -Math.abs(v.y) }));
          newY = CANVAS_HEIGHT - BALL_SIZE;
        }

        return { ...prev, y: newY };
      });

      requestAnimationFrame(gameLoop);
    };

    const animationId = requestAnimationFrame(gameLoop);
    return () => cancelAnimationFrame(animationId);
  }, [ballVelocity]);
}
⚡ Why Math.abs()?
If velocity.y = 3 (moving down) and we hit bottom wall:
→ New velocity.y = -Math.abs(3) = -3 (now moving up!)
This ensures the ball always bounces in the correct direction!
STEP 5

🏓 Paddle Collision Detection

The Trickiest Part: Detecting when the ball hits a paddle! We need to check if the ball overlaps with the paddle rectangle.

🧠 Rectangle Collision

For collision to happen, the ball must be:
1) Horizontally aligned with paddle (X check)
2) Vertically overlapping with paddle (Y check)
3) Moving TOWARDS the paddle (prevents double-bounce)

Step 5: Paddle Collision
function PaddleCollision() {
  const PADDLE_WIDTH = 15;
  const PADDLE_HEIGHT = 100;
  const BALL_SIZE = 15;

  useEffect(() => {
    const gameLoop = () => {
      // Check player paddle collision (left side)
      setBallPos(prev => {
        let newX = prev.x + ballVelocity.x;
        
        if (
          newX <= PADDLE_WIDTH &&
          ballVelocity.x < 0 &&  // Moving LEFT toward paddle
          prev.y + BALL_SIZE >= paddleY &&
          prev.y <= paddleY + PADDLE_HEIGHT
        ) {
          // Collision detected!
          setBallVelocity(v => ({
            x: Math.abs(v.x) * 1.05,  // Bounce right (+ speed up 5%)
            y: v.y
          }));
          
          newX = PADDLE_WIDTH;  // Push ball away
        }

        // Check computer paddle (right side)
        if (
          newX >= CANVAS_WIDTH - PADDLE_WIDTH - BALL_SIZE &&
          ballVelocity.x > 0 &&  // Moving RIGHT toward paddle
          prev.y + BALL_SIZE >= computerPaddleY &&
          prev.y <= computerPaddleY + PADDLE_HEIGHT
        ) {
          setBallVelocity(v => ({
            x: -Math.abs(v.x) * 1.05,  // Bounce left (+ speed up)
            y: v.y
          }));
          
          newX = CANVAS_WIDTH - PADDLE_WIDTH - BALL_SIZE;
        }

        return { ...prev, x: newX };
      });

      requestAnimationFrame(gameLoop);
    };

    const animationId = requestAnimationFrame(gameLoop);
    return () => cancelAnimationFrame(animationId);
  }, [paddleY, computerPaddleY, ballVelocity]);
}
🎮 Pro Tip: The velocity direction check (ballVelocity.x < 0 or > 0) prevents the ball from getting "stuck" inside the paddle. It only bounces when approaching!
STEP 6

🎯 Scoring System

Keep Track of Points: When the ball goes past a paddle, the opponent scores! Then reset the ball to center.

🧠 Scoring Logic

If ball X position goes past left edge (X ≤ 0), computer scores. If ball goes past right edge (X ≥ canvas width), player scores.

Step 6: Score Tracking
function ScoringSystem() {
  const [score, setScore] = useState({ player: 0, computer: 0 });
  const CANVAS_WIDTH = 800;

  useEffect(() => {
    const gameLoop = () => {
      // Move ball...

      // Check for scoring
      setBallPos(prev => {
        // Ball went past player paddle (left edge)
        if (prev.x <= 0) {
          setScore(s => ({ ...s, computer: s.computer + 1 }));
          // Reset ball to center
          setTimeout(() => {
            setBallPos({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2 });
            setBallVelocity(getRandomVelocity());
          }, 100);
          return prev;
        }

        // Ball went past computer paddle (right edge)
        if (prev.x >= CANVAS_WIDTH) {
          setScore(s => ({ ...s, player: s.player + 1 }));
          // Reset ball to center
          setTimeout(() => {
            setBallPos({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2 });
            setBallVelocity(getRandomVelocity());
          }, 100);
          return prev;
        }

        return prev;
      });

      requestAnimationFrame(gameLoop);
    };

    const animationId = requestAnimationFrame(gameLoop);
    return () => cancelAnimationFrame(animationId);
  }, []);

  // Draw scores on canvas
  useEffect(() => {
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 48px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(score.player, CANVAS_WIDTH / 4, 60);
    ctx.fillText(score.computer, (CANVAS_WIDTH / 4) * 3, 60);
  }, [score]);
}
STEP 7

✨ Particle Effects

Visual Polish: Add explosion particles when the ball hits something! This makes the game feel more dynamic and satisfying.

🧠 Particle System

When collision happens, spawn 8 particles in a circle pattern. Each particle moves outward and fades away over time.

Step 7: Particle Explosions
function ParticleSystem() {
  const [particles, setParticles] = useState([]);

  // Create particles on collision
  const createParticles = (x, y) => {
    const newParticles = Array.from({ length: 8 }, (_, i) => {
      const angle = (i * Math.PI * 2) / 8;  // Spread in circle
      
      return {
        x,
        y,
        vx: Math.cos(angle) * 3,  // X velocity
        vy: Math.sin(angle) * 3,  // Y velocity
        life: 1,  // Full opacity
        id: Date.now() + i,
      };
    });
    
    setParticles(prev => [...prev, ...newParticles]);
  };

  // Update particles
  useEffect(() => {
    const interval = setInterval(() => {
      setParticles(prev => 
        prev
          .map(p => ({
            ...p,
            x: p.x + p.vx,      // Move
            y: p.y + p.vy,
            life: p.life - 0.05, // Fade out
          }))
          .filter(p => p.life > 0)  // Remove dead particles
      );
    }, 30);

    return () => clearInterval(interval);
  }, []);

  // Draw particles
  useEffect(() => {
    const ctx = canvas.getContext('2d');
    
    particles.forEach(p => {
      ctx.fillStyle = `rgba(255, 204, 0, ${p.life})`;
      ctx.beginPath();
      ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
      ctx.fill();
    });
  }, [particles]);

  // Call createParticles() on collisions
  // Example: createParticles(ballPos.x, ballPos.y);
}
🎨 Math Behind Circular Spread:
angle = (i × 2π) / 8 creates 8 evenly spaced directions
vx = cos(angle) × speed and vy = sin(angle) × speed
This creates the "explosion" effect!

🏆 You Built Pong!

From mouse tracking to particle systems - you mastered game development basics!

🎮

Game Loop

requestAnimationFrame

Physics

Velocity & Collision

🤖

AI

Lerp-based Opponent

Polish

Particles & Effects

Enjoyed this tutorial?

Your support helps me create more free, interactive content!

Buy Me a Coffee