Learn game development step-by-step. See the game, understand the code, build it yourself!
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!
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".
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.
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} />;
}Adding Challenge: The computer paddle needs to move automatically! We'll use lerp (linear interpolation) to make it smoothly follow the ball.
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!
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 />
);
}The Core Mechanic: A ball that moves with velocity! Velocity means the ball has both speed and direction.
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 = { x: 4, y: 3 }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 />;
}Bounce Back: When the ball hits the top or bottom wall, it should bounce! We do this by reversing the Y velocity.
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!
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]);
}The Trickiest Part: Detecting when the ball hits a paddle! We need to check if the ball overlaps with the paddle rectangle.
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)
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]);
}Keep Track of Points: When the ball goes past a paddle, the opponent scores! Then reset the ball to center.
If ball X position goes past left edge (X ≤ 0), computer scores. If ball goes past right edge (X ≥ canvas width), player scores.
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]);
}Visual Polish: Add explosion particles when the ball hits something! This makes the game feel more dynamic and satisfying.
When collision happens, spawn 8 particles in a circle pattern. Each particle moves outward and fades away over time.
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);
}angle = (i × 2π) / 8 creates 8 evenly spaced directionsvx = cos(angle) × speed and vy = sin(angle) × speedFrom mouse tracking to particle systems - you mastered game development basics!
requestAnimationFrame
Velocity & Collision
Lerp-based Opponent
Particles & Effects