JavaScript Game Tutorial
How to Make Pong in JavaScript: Two Players, Fixed Timestep and AI
Pong is the smallest real-time game worth building: two paddles, one ball, simultaneous input and a score. It is also where many beginners meet their first refresh-rate bug, so this tutorial fixes it properly.
Production context
This guide studies Table Tennis, a published Supagames game. Repository source: games/lvl01/04-tennis.html.
Play Table Tennis with a friend on one keyboard: W and S on the left, the arrow keys on the right, first to five points. The code below follows that game, including the fixed timestep we added so the ball moves at the same speed on 60, 120 and 144 Hz screens.
The court is a 600 by 400 canvas. Everything else is a few numbers for the paddles and the ball.
1. Describe the Court, Paddles and Ball
Each paddle only moves vertically, so a single y value per player is enough. The ball has a position and a velocity, and the two scores decide when the match ends.
Keeping the court size in W and H makes it easy to scale the canvas with CSS later without touching the game logic.
const W = 600, H = 400;
const paddleW = 10, paddleH = 70, ballSize = 8;
let leftY = H / 2 - paddleH / 2;
let rightY = H / 2 - paddleH / 2;
let ballX = W / 2, ballY = H / 2;
let ballDX = 4, ballDY = 3;
let leftScore = 0, rightScore = 0, gameRunning = true;
2. Track Keys for Two Players at Once
Two players press keys at the same time, so reacting inside keydown does not work: holding one key would block the other. Store which keys are down in an object, and let the update read it every step.
Arrow keys need preventDefault, otherwise the right player scrolls the page while moving the paddle.
const keys = {};
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowUp" || e.key === "ArrowDown") e.preventDefault();
keys[e.key] = true;
});
document.addEventListener("keyup", (e) => { keys[e.key] = false; });
function movePaddles() {
if (keys.w || keys.W) leftY = Math.max(0, leftY - 5);
if (keys.s || keys.S) leftY = Math.min(H - paddleH, leftY + 5);
if (keys.ArrowUp) rightY = Math.max(0, rightY - 5);
if (keys.ArrowDown) rightY = Math.min(H - paddleH, rightY + 5);
}
3. Move the Ball and Bounce It
The ball moves by its velocity every step and bounces off the top and bottom walls. When it reaches a paddle's x position while overlapping the paddle vertically, the horizontal speed flips and grows by five percent, so long rallies get faster.
After a bounce the ball is placed just outside the paddle. Without that, a fast ball can remain inside the paddle for two steps and flip back into the goal.
function moveBall() {
ballX += ballDX;
ballY += ballDY;
if (ballY <= 0 || ballY >= H - ballSize) ballDY = -ballDY;
if (ballX <= paddleW && ballY + ballSize > leftY && ballY < leftY + paddleH) {
ballDX = -ballDX * 1.05;
ballX = paddleW + 1;
}
if (ballX >= W - paddleW - ballSize && ballY + ballSize > rightY && ballY < rightY + paddleH) {
ballDX = -ballDX * 1.05;
ballX = W - paddleW - ballSize - 1;
}
}
4. Use a Fixed Timestep So Speed Does Not Depend on the Screen
requestAnimationFrame fires once per screen refresh. If you move the ball once per frame, it moves 60 times per second on a 60 Hz monitor and 144 times on a 144 Hz gaming monitor, so the game is more than twice as fast for some players.
The fix is an accumulator. Add the real elapsed time each frame, then run the simulation in fixed 1/60 second steps while enough time has built up. Drawing still happens once per frame. Capping the elapsed time avoids a burst of catch-up steps after the tab was in the background.
const STEP_MS = 1000 / 60;
let lastTime = performance.now();
let accumulator = 0;
function loop(now) {
accumulator += Math.min(250, now - lastTime);
lastTime = now;
while (accumulator >= STEP_MS) {
update(); // movePaddles + moveBall + scoring
accumulator -= STEP_MS;
}
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
5. Score Points and Serve Again
A ball that leaves the court on the left is a point for the right player, and the other way round. After each point the ball returns to the centre with a random direction, so neither player can predict the serve.
The first player to five points wins and the update loop stops until New Game resets everything.
function resetBall() {
ballX = W / 2;
ballY = H / 2;
ballDX = (Math.random() > 0.5 ? 1 : -1) * 4;
ballDY = (Math.random() > 0.5 ? 1 : -1) * 3;
}
function checkScore() {
if (ballX < 0) { rightScore++; resetBall(); }
if (ballX > W) { leftScore++; resetBall(); }
if (leftScore >= 5 || rightScore >= 5) gameRunning = false;
}
6. Let Two Players Share a Touch Screen
Multi-touch makes Pong a great two-player phone game. Every touch on the left half of the canvas moves the left paddle to that finger, and every touch on the right half moves the right paddle.
Convert each touch from screen pixels into canvas units, because the canvas is usually scaled down on phones.
canvas.style.touchAction = "none";
function handleTouches(e) {
const rect = canvas.getBoundingClientRect();
for (const t of e.touches) {
const x = (t.clientX - rect.left) / rect.width * W;
const y = (t.clientY - rect.top) / rect.height * H - paddleH / 2;
const clamped = Math.max(0, Math.min(H - paddleH, y));
if (x < W / 2) leftY = clamped; else rightY = clamped;
}
}
canvas.addEventListener("touchstart", handleTouches, { passive: true });
canvas.addEventListener("touchmove", handleTouches, { passive: true });
7. Add a Computer Opponent
The Supagames version is a two-player game, but a single-player mode only needs a few lines. The computer moves its paddle toward the ball, limited by a maximum speed, so it can be beaten by sharp angles and fast rallies.
Tune difficulty with the speed limit and a dead zone. Making the AI react only when the ball comes toward it also makes it feel more human.
const AI_SPEED = 3.6; // lower = easier
function moveAiPaddle() {
if (ballDX < 0) return; // ball moving away: wait
const paddleCenter = rightY + paddleH / 2;
const diff = ballY - paddleCenter;
if (Math.abs(diff) < 10) return; // dead zone avoids jitter
rightY += Math.sign(diff) * Math.min(AI_SPEED, Math.abs(diff));
rightY = Math.max(0, Math.min(H - paddleH, rightY));
}
8. Build checklist
- Store the paddles as y values and the ball as position plus velocity.
- Read input from a keys object so two players can press keys together.
- Push the ball out of the paddle after every bounce.
- Run the simulation in fixed steps, independent of the refresh rate.
- Serve from the centre in a random direction after every point.
- Support multi-touch, and add an AI with a speed limit for single player.
9. FAQ
Why is my Pong game faster on some computers?
Because the ball moves once per animation frame, and high refresh rate screens draw more frames per second. Use a fixed timestep or multiply movement by delta time so the speed is the same everywhere.
Who created the original Pong?
Atari released Pong in 1972. Allan Alcorn built it as a training project, and its success helped start the arcade video game industry.
How do I change the bounce angle based on where the ball hits the paddle?
Measure the hit position relative to the paddle centre and use it to set the vertical speed, for example (hitY - paddleCenter) / (paddleH / 2) * maxSpeed. The edges then send the ball off at steep angles.
How do I make the AI beatable?
Limit how fast its paddle can move, add a dead zone and let it react only when the ball approaches. Raising those limits as the score grows creates a natural difficulty curve.