ping-pong

This sketch is a modern Pong game where a glowing ball bounces between two paddles—one controlled by the mouse, the other by simple AI. Each collision triggers synthesized sound effects, and the first player to reach 5 points wins the game.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the AI — The AI will react faster and become much harder to beat, making rallies more intense
  2. Make the ball much bigger — The ball grows to a larger diameter, making it easier to see and hit but more dramatic visually
  3. Slow down the game — The ball moves more slowly, giving you more time to react and the AI more time to respond
  4. Change the winning score — The game now ends at 10 points instead of 5, making matches last much longer
  5. Darken the canvas — The background becomes a very dark gray, making the white paddles and ball pop more visually
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates Pong, the classic arcade game, with a glowing ball bouncing between two paddles on a black canvas. One paddle follows your mouse, while the other is controlled by simple AI that tracks the ball. Every collision produces a unique synthesized sound—different tones for paddle hits, wall bounces, and scoring—creating an immersive audio-visual experience. The sketch uses p5.Oscillator objects to generate those sounds in real-time, making it a perfect introduction to both game mechanics and audio synthesis.

The code is organized into a setup() function that initializes the canvas, paddles, ball, and three oscillators, and a draw() function that runs the game loop every frame. Inside draw(), you'll see paddle movement logic, ball physics, collision detection with both paddles and walls, score tracking, and game state management. Helper functions handle ball reset, collision checking, score display, and game restart. By studying this sketch, you'll learn how to build a playable game with AI, trigger sounds on events, and manage game states.

⚙️ How It Works

  1. When the sketch loads, setup() creates a fullscreen canvas, initializes two paddle objects (paddleA on the left, paddleB on the right), spawns a ball in the center with random direction, and creates three p5.Oscillator objects (sine for paddles, triangle for walls, sawtooth for scoring) set to silent initially.
  2. Every frame, draw() clears the canvas with a black background and displays the current scores at the top. The left paddle (paddleA) follows the mouse's y-position, constrained to stay within canvas bounds. The right paddle (paddleB) uses simple AI: it moves toward the ball if the ball is below its center, or away if the ball is above.
  3. The ball moves by adding its velocity (dx, dy) to its position each frame. When the ball hits the top or bottom edge, its vertical velocity reverses and a wall sound plays. The checkCollision() function tests if the ball touches either paddle—if so, it bounces back horizontally and plays a paddle sound.
  4. When the ball leaves the canvas on the left, paddleB scores and a scoring sound plays; when it leaves on the right, paddleA scores. After each point, the ball resets to the center with random direction.
  5. The game checks if either player has reached the scoreLimit (5 points). If so, gameState changes to 'gameOver' and the screen displays a winner message and restart prompt.
  6. Pressing SPACE after game over calls resetGame(), clearing scores and resetting the ball, then returns gameState to 'playing' so the game resumes.

🎓 Concepts You'll Learn

Game loop and frame-based animationCollision detection (AABB bounds testing)Game state managementAI logic (simple tracking)Sound synthesis with oscillatorsObject-based data structuresConstrain and boundary checking

📝 Code Breakdown

setup()

setup() runs once at the start of the sketch. It is where you initialize your canvas, create objects, and set up resources like sound oscillators that need to exist for the entire program.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke(); // Pas de contour pour les formes

  // Initialisation des raquettes
  paddleA = { x: 20, y: height / 2, w: 10, h: 80 };
  paddleB = { x: width - 30, y: height / 2, w: 10, h: 80 };

  // Initialisation de la balle
  resetBall();

  // Initialisation des oscillateurs p5.sound
  // Son de la raquette: Sinusoïde, fréquence 440Hz, volume 0 (silencieux au début)
  paddleOsc = new p5.Oscillator();
  paddleOsc.setType('sine');
  paddleOsc.amp(0);
  paddleOsc.freq(440);
  paddleOsc.start();

  // Son du mur: Triangle, fréquence 220Hz, volume 0
  wallOsc = new p5.Oscillator();
  wallOsc.setType('triangle');
  wallOsc.amp(0);
  wallOsc.freq(220);
  wallOsc.start();

  // Son du score: Dent de scie, fréquence 110Hz, volume 0
  scoreOsc = new p5.Oscillator();
  scoreOsc.setType('sawtooth');
  scoreOsc.amp(0);
  scoreOsc.freq(110);
  scoreOsc.start();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

object-creation Paddle initialization paddleA = { x: 20, y: height / 2, w: 10, h: 80 };

Creates two paddle objects with x, y center position, width, and height properties

loop Oscillator initialization paddleOsc = new p5.Oscillator();

Creates three p5.Oscillator objects (paddle, wall, score) and configures each with type, amplitude, frequency, and starts audio

createCanvas(windowWidth, windowHeight);
Creates a fullscreen canvas that fills the entire browser window
noStroke();
Disables outlines on all shapes drawn after this, so rectangles and ellipses appear solid
paddleA = { x: 20, y: height / 2, w: 10, h: 80 };
Creates an object representing the left paddle with position (20, center-y), width 10, height 80
paddleB = { x: width - 30, y: height / 2, w: 10, h: 80 };
Creates the right paddle positioned 30 pixels from the right edge, same height as paddleA
resetBall();
Calls the resetBall() helper function to initialize the ball at the center with random direction
paddleOsc.setType('sine');
Sets the paddle oscillator to generate a sine wave, a smooth, pure musical tone
paddleOsc.freq(440);
Sets the paddle sound frequency to 440 Hz (the musical note A4)
paddleOsc.start();
Begins the oscillator—it will now generate sound when amp() is called to increase volume

draw()

draw() is the main game loop, running 60 times per second. It handles paddle movement, ball physics, collision detection, scoring, and game state. Notice how it uses gameState to control whether the game is active or showing the end screen—this is called a state machine and is essential for managing different phases of interactive programs.

🔬 This is the AI logic: it moves toward the ball. What happens if you change paddleSpeed to paddleSpeed * 0.5? To paddleSpeed * 2? Which feels harder to beat?

    if (ball.y > paddleB.y) {
      paddleB.y += paddleSpeed;
    } else if (ball.y < paddleB.y) {
      paddleB.y -= paddleSpeed;
    }

🔬 When the ball hits a wall, it reverses direction. What if you change ball.dy *= -1 to ball.dy *= -0.95 so the ball loses a tiny bit of energy on each bounce? Does the game feel different?

    if (ball.y - ball.r < 0 || ball.y + ball.r > height) {
      ball.dy *= -1; // Inverser la direction verticale
      wallOsc.amp(0.3, 0.1); // Jouer le son du mur
      wallOsc.amp(0, 0.5);   // L'éteindre après 0.5 seconde
    }
function draw() {
  background(0); // Fond noir

  // Afficher le score
  displayScores();

  // Vérifier l'état du jeu
  if (gameState === "playing") {
    // --- Logique du jeu en cours ---

    // Mouvement de la raquette A (contrôlée par la souris)
    paddleA.y = mouseY;
    paddleA.y = constrain(paddleA.y, paddleA.h / 2, height - paddleA.h / 2);

    // Mouvement de la raquette B (IA simple)
    if (ball.y > paddleB.y) {
      paddleB.y += paddleSpeed;
    } else if (ball.y < paddleB.y) {
      paddleB.y -= paddleSpeed;
    }
    paddleB.y = constrain(paddleB.y, paddleB.h / 2, height - paddleB.h / 2);

    // Dessiner les raquettes
    fill(255); // Couleur blanche
    rect(paddleA.x, paddleA.y - paddleA.h / 2, paddleA.w, paddleA.h);
    rect(paddleB.x, paddleB.y - paddleB.h / 2, paddleB.w, paddleB.h);

    // Mouvement de la balle
    ball.x += ball.dx;
    ball.y += ball.dy;

    // Dessiner la balle
    ellipse(ball.x, ball.y, ball.r * 2);

    // Collision de la balle avec les murs haut et bas
    if (ball.y - ball.r < 0 || ball.y + ball.r > height) {
      ball.dy *= -1; // Inverser la direction verticale
      wallOsc.amp(0.3, 0.1); // Jouer le son du mur
      wallOsc.amp(0, 0.5);   // L'éteindre après 0.5 seconde
    }

    // Collision de la balle avec les raquettes
    checkCollision(ball, paddleA);
    checkCollision(ball, paddleB);

    // Balle hors limites (point marqué)
    if (ball.x - ball.r < 0) {
      scoreB++; // Raquette B marque un point
      scoreOsc.amp(0.7, 0.1); // Jouer le son du score
      scoreOsc.amp(0, 1);     // L'éteindre après 1 seconde
      resetBall();
    }
    if (ball.x + ball.r > width) {
      scoreA++; // Raquette A marque un point
      scoreOsc.amp(0.7, 0.1); // Jouer le son du score
      scoreOsc.amp(0, 1);     // L'éteindre après 1 seconde
      resetBall();
    }

    // Vérifier si un joueur a atteint la limite de score
    if (scoreA >= scoreLimit || scoreB >= scoreLimit) {
      gameState = "gameOver"; // Changer l'état du jeu
    }

  } else if (gameState === "gameOver") {
    // --- Écran de Game Over ---
    fill(255);
    textSize(64);
    textAlign(CENTER);
    text("GAME OVER", width / 2, height / 2 - 50);

    textSize(48);
    let winner = scoreA >= scoreLimit ? "PLAYER A" : "PLAYER B";
    text(winner + " WINS!", width / 2, height / 2 + 20);

    textSize(24);
    text("Press SPACE to restart", width / 2, height / 2 + 80);
  }
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

assignment Left paddle (mouse) movement paddleA.y = mouseY;

Tracks the left paddle to the mouse's vertical position

conditional Right paddle (AI) movement if (ball.y > paddleB.y) {

AI logic that moves the right paddle toward or away from the ball's y-position

conditional Top/bottom wall bounce if (ball.y - ball.r < 0 || ball.y + ball.r > height) {

Detects when ball hits top or bottom edge and reverses vertical direction, plays wall sound

conditional Left edge scoring if (ball.x - ball.r < 0) {

Detects when ball passes the left edge, awards point to right paddle, plays score sound

conditional Win condition check if (scoreA >= scoreLimit || scoreB >= scoreLimit) {

Tests if either player has reached the score limit and ends the game if true

conditional Game over display } else if (gameState === "gameOver") {

Displays the game over message and winner when gameState is not 'playing'

background(0);
Fills the entire canvas with black (0), erasing everything from the previous frame
displayScores();
Calls the displayScores() function to draw the current score and dividing line at the top
if (gameState === "playing") {
Checks if the game is active; only run game logic if this is true
paddleA.y = mouseY;
Sets the left paddle's y-position to follow the current mouse y-coordinate
paddleA.y = constrain(paddleA.y, paddleA.h / 2, height - paddleA.h / 2);
Clamps the paddle's y-position so it cannot move above the top or below the bottom of the canvas
if (ball.y > paddleB.y) {
Tests if the ball is below the AI paddle's center
paddleB.y += paddleSpeed;
If ball is below paddle, move the paddle down by paddleSpeed pixels each frame
} else if (ball.y < paddleB.y) {
Otherwise, tests if the ball is above the AI paddle's center
paddleB.y -= paddleSpeed;
If ball is above paddle, move the paddle up by paddleSpeed pixels each frame
ball.x += ball.dx;
Updates the ball's horizontal position by adding its horizontal velocity
ball.y += ball.dy;
Updates the ball's vertical position by adding its vertical velocity
ellipse(ball.x, ball.y, ball.r * 2);
Draws the ball as a white circle (diameter is 2 × radius)
if (ball.y - ball.r < 0 || ball.y + ball.r > height) {
Checks if the ball's top or bottom edge has crossed the canvas boundary
ball.dy *= -1;
Reverses the vertical velocity so the ball bounces back
wallOsc.amp(0.3, 0.1);
Fades the wall oscillator's volume to 0.3 over 0.1 seconds, creating a wall-hit sound
wallOsc.amp(0, 0.5);
Fades the wall oscillator volume back to silence over 0.5 seconds, ending the sound
checkCollision(ball, paddleA);
Calls the collision-checking function to test if the ball hit the left paddle
if (ball.x - ball.r < 0) {
Checks if the ball's left edge has passed the left boundary of the canvas
scoreB++;
Increments the right paddle player's score by 1
scoreOsc.amp(0.7, 0.1);
Fades the score oscillator volume to 0.7 over 0.1 seconds to play a score sound
resetBall();
Resets the ball to the center with a new random direction
if (scoreA >= scoreLimit || scoreB >= scoreLimit) {
Tests if either player has scored enough points to win the game
gameState = "gameOver";
Changes the game state to 'gameOver', which pauses the game and displays the end screen
text("GAME OVER", width / 2, height / 2 - 50);
Draws the 'GAME OVER' text centered on the screen
let winner = scoreA >= scoreLimit ? "PLAYER A" : "PLAYER B";
Uses a ternary operator to determine which player won based on who reached scoreLimit

resetBall()

resetBall() is called whenever a point is scored or the game resets. It reinitializes the ball object with properties for position (x, y), radius (r), and velocity (dx, dy). The ternary operators ensure the ball starts moving in a random direction, preventing predictable gameplay.

🔬 The ball resets to the center with random direction each time a point is scored. What happens if you remove the ternary operators and change dx and dy to always be ballSpeed (no randomness)? Does the game become predictable?

  ball = {
    x: width / 2,
    y: height / 2,
    r: 10,
    dx: ballSpeed * (random() > 0.5 ? 1 : -1), // Direction horizontale aléatoire
    dy: ballSpeed * (random() > 0.5 ? 1 : -1) // Direction verticale aléatoire
  };
function resetBall() {
  ball = {
    x: width / 2,
    y: height / 2,
    r: 10,
    dx: ballSpeed * (random() > 0.5 ? 1 : -1), // Direction horizontale aléatoire
    dy: ballSpeed * (random() > 0.5 ? 1 : -1) // Direction verticale aléatoire
  };
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

ternary-operator Random direction assignment dx: ballSpeed * (random() > 0.5 ? 1 : -1),

Generates a random direction: if random() > 0.5, multiply by 1 (rightward), else by -1 (leftward)

ball = {
Creates a new object called ball that will hold its position, radius, and velocity
x: width / 2,
Places the ball horizontally at the center of the canvas
y: height / 2,
Places the ball vertically at the center of the canvas
r: 10,
Sets the ball's radius to 10 pixels (diameter 20)
dx: ballSpeed * (random() > 0.5 ? 1 : -1),
Creates horizontal velocity: ballSpeed is multiplied by either 1 or -1 (chosen randomly), making the ball start moving left or right
dy: ballSpeed * (random() > 0.5 ? 1 : -1)
Creates vertical velocity: ballSpeed is multiplied by either 1 or -1 (chosen randomly), making the ball start moving up or down

checkCollision(ball, paddle)

checkCollision() implements AABB (Axis-Aligned Bounding Box) collision detection—the industry standard for quick, efficient collision checking in games. It checks both X and Y overlap separately. If both are true, a collision occurred. The function also demonstrates how to add gameplay variety: the angle at which the ball hit the paddle determines its exit angle, rewarding skillful paddle placement.

🔬 These lines make the ball bounce at an angle based on where it hits the paddle. What happens if you change ball.dy = diff * ballSpeed to ball.dy = diff * ballSpeed * 2? Does the ball become harder to control?

      // Calculer la différence entre le centre de la balle et le centre de la raquette
      let diff = ball.y - paddle.y;
      // Normaliser la différence à une plage de -1 à 1 basée sur la hauteur de la raquette
      diff /= paddle.h / 2;
      // Ajuster la vitesse verticale en fonction de l'endroit où la balle a frappé la raquette
      ball.dy = diff * ballSpeed;
function checkCollision(ball, paddle) {
  // Vérifier si la balle est dans la plage X de la raquette
  if (ball.x + ball.r > paddle.x - paddle.w / 2 && ball.x - ball.r < paddle.x + paddle.w / 2) {
    // Vérifier si la balle est dans la plage Y de la raquette
    if (ball.y + ball.r > paddle.y - paddle.h / 2 && ball.y - ball.r < paddle.y + paddle.h / 2) {
      ball.dx *= -1; // Inverser la direction horizontale

      // Jouer le son de la raquette
      paddleOsc.amp(0.5, 0.1); // Monter le volume à 0.5 en 0.1 seconde
      paddleOsc.amp(0, 0.5);   // Le redescendre à 0 en 0.5 seconde

      // Calculer la différence entre le centre de la balle et le centre de la raquette
      let diff = ball.y - paddle.y;
      // Normaliser la différence à une plage de -1 à 1 basée sur la hauteur de la raquette
      diff /= paddle.h / 2;
      // Ajuster la vitesse verticale en fonction de l'endroit où la balle a frappé la raquette
      ball.dy = diff * ballSpeed;
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional X-axis overlap detection if (ball.x + ball.r > paddle.x - paddle.w / 2 && ball.x - ball.r < paddle.x + paddle.w / 2) {

Tests whether the ball's left and right edges overlap with the paddle's left and right edges

conditional Y-axis overlap detection if (ball.y + ball.r > paddle.y - paddle.h / 2 && ball.y - ball.r < paddle.y + paddle.h / 2) {

Tests whether the ball's top and bottom edges overlap with the paddle's top and bottom edges

calculation Paddle impact angle let diff = ball.y - paddle.y;

Calculates where on the paddle the ball hit, enabling angled bounces

if (ball.x + ball.r > paddle.x - paddle.w / 2 && ball.x - ball.r < paddle.x + paddle.w / 2) {
Axis-Aligned Bounding Box (AABB) collision detection: checks if the ball's horizontal extent overlaps the paddle's horizontal extent
if (ball.y + ball.r > paddle.y - paddle.h / 2 && ball.y - ball.r < paddle.y + paddle.h / 2) {
Checks if the ball's vertical extent overlaps the paddle's vertical extent. Only if both X and Y overlap is there a true collision
ball.dx *= -1;
Reverses the ball's horizontal velocity so it bounces away from the paddle
paddleOsc.amp(0.5, 0.1);
Fades the paddle oscillator to volume 0.5 over 0.1 seconds, creating a sound when the ball hits
paddleOsc.amp(0, 0.5);
Fades the paddle oscillator back to silence over 0.5 seconds, ending the sound
let diff = ball.y - paddle.y;
Calculates the offset between the ball's y-position and the paddle's y-position (negative if above, positive if below)
diff /= paddle.h / 2;
Normalizes diff to a range of -1 to 1 by dividing by half the paddle height—this converts pixel offset to a proportional value
ball.dy = diff * ballSpeed;
Sets the ball's new vertical velocity based on where it hit the paddle: hitting the top adds upward motion, hitting the bottom adds downward motion

displayScores()

displayScores() is a simple utility function called every frame to draw the current scores and center line. It demonstrates text rendering, alignment, and how to use fill() and stroke() to control color. The center line reinforces the Pong aesthetic and helps players distinguish sides.

function displayScores() {
  fill(255);
  textSize(32);
  textAlign(CENTER);
  text(scoreA, width / 4, 50); // Score de la raquette A
  text(scoreB, 3 * width / 4, 50); // Score de la raquette B

  // Dessiner une ligne au milieu
  stroke(255);
  line(width / 2, 0, width / 2, height);
  noStroke();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

sequence Text styling fill(255);

Sets text color to white

drawing Center dividing line line(width / 2, 0, width / 2, height);

Draws a vertical white line dividing the two players' sides

fill(255);
Sets the fill color to white (255) for the text that follows
textSize(32);
Sets the text size to 32 pixels
textAlign(CENTER);
Centers text horizontally on the coordinates specified in text() calls
text(scoreA, width / 4, 50);
Draws the left player's score (scoreA) at 1/4 of the canvas width from the left, 50 pixels down
text(scoreB, 3 * width / 4, 50);
Draws the right player's score (scoreB) at 3/4 of the canvas width from the left, 50 pixels down
stroke(255);
Sets the stroke color to white for the dividing line
line(width / 2, 0, width / 2, height);
Draws a vertical line from the top to the bottom of the canvas, centered horizontally, dividing the two players' sides
noStroke();
Disables stroke so subsequent shapes are drawn without outlines

resetGame()

resetGame() is called when the player presses SPACE on the game-over screen. It clears both scores and resets the ball, then changes the game state back to 'playing' so draw() resumes normal gameplay logic. This demonstrates how game state machines work: different states trigger different code paths in draw().

function resetGame() {
  scoreA = 0;
  scoreB = 0;
  resetBall();
  gameState = "playing"; // Remettre l'état du jeu à "playing"
}
Line-by-line explanation (4 lines)
scoreA = 0;
Resets the left player's score to 0
scoreB = 0;
Resets the right player's score to 0
resetBall();
Calls resetBall() to reinitialize the ball position and velocity
gameState = "playing";
Changes gameState from 'gameOver' back to 'playing', resuming the main game loop

windowResized()

windowResized() is a special p5.js function that runs automatically whenever the user resizes the browser window. This sketch is fullscreen, so resizing must adjust the canvas and re-center game elements. Notice how it checks gameState before resetting the ball—good defensive programming that prevents unexpected behavior during the game-over screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Réinitialiser les positions des raquettes et de la balle
  paddleA.y = height / 2;
  paddleB.y = height / 2;
  if (gameState === "playing") {
    resetBall();
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Conditional ball reset if (gameState === "playing") {

Only resets ball during gameplay, not during game-over screen to preserve continuity

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the current window dimensions when the browser window is resized
paddleA.y = height / 2;
Re-centers the left paddle vertically after the canvas is resized
paddleB.y = height / 2;
Re-centers the right paddle vertically after the canvas is resized
if (gameState === "playing") {
Checks if the game is active before resetting the ball
resetBall();
Resets the ball only during gameplay to prevent disrupting the game-over screen

keyPressed()

keyPressed() is a p5.js event handler that runs whenever the user presses any key. This implementation only responds to SPACE during game-over, providing a clean restart mechanism. The check for gameState prevents accidental resets during active gameplay. This is a simple example of event-driven programming.

function keyPressed() {
  if (gameState === "gameOver" && key === ' ') { // Si le jeu est fini et que la barre d'espace est pressée
    resetGame();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

conditional Space key game reset if (gameState === "gameOver" && key === ' ') {

Tests if the game is over AND the space bar was pressed, triggering a restart

if (gameState === "gameOver" && key === ' ') {
Checks two conditions: the game must be in 'gameOver' state AND the pressed key must be space (' ')
resetGame();
If both conditions are true, calls resetGame() to clear scores, reset the ball, and resume playing

📦 Key Variables

paddleA object

Stores the left (mouse-controlled) paddle's properties: x position, y position, width, and height

paddleA = { x: 20, y: height / 2, w: 10, h: 80 };
paddleB object

Stores the right (AI-controlled) paddle's properties: x position, y position, width, and height

paddleB = { x: width - 30, y: height / 2, w: 10, h: 80 };
paddleSpeed number

Controls how fast the AI paddle moves toward or away from the ball each frame

let paddleSpeed = 6;
ball object

Stores the ball's position (x, y), radius (r), and velocity (dx, dy) for physics calculations

ball = { x: width / 2, y: height / 2, r: 10, dx: 5, dy: -3 };
ballSpeed number

The magnitude of initial ball velocity; used to scale dx and dy in resetBall()

let ballSpeed = 5;
scoreA number

Tracks the left player's (mouse paddle) score

let scoreA = 0;
scoreB number

Tracks the right player's (AI paddle) score

let scoreB = 0;
scoreLimit number

The score threshold—first player to reach this wins the game

let scoreLimit = 5;
paddleOsc p5.Oscillator

Synthesizes the sound played when the ball hits a paddle (sine wave, 440 Hz)

paddleOsc = new p5.Oscillator();
wallOsc p5.Oscillator

Synthesizes the sound played when the ball hits a wall (triangle wave, 220 Hz)

wallOsc = new p5.Oscillator();
scoreOsc p5.Oscillator

Synthesizes the sound played when a player scores (sawtooth wave, 110 Hz)

scoreOsc = new p5.Oscillator();
gameState string

Tracks whether the game is 'playing' or 'gameOver', controlling which code path draw() executes

let gameState = "playing";

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG checkCollision()

Ball can phase through paddle if ballSpeed is very high, because velocity is checked once per frame and might skip over the paddle entirely.

💡 Implement continuous collision detection or clamp the ball's movement: after bouncing, explicitly position the ball at the paddle surface using ball.x = paddle.x + paddle.w / 2 + ball.r to prevent clipping.

PERFORMANCE draw()

Oscillator amplitude is set twice per collision (once to 0.5, once back to 0), which works but isn't the cleanest p5.sound usage.

💡 Use p5.Envelope objects instead for cleaner, more professional audio: create envelopes in setup() and trigger them on collision events for more control over sound shapes.

STYLE paddleA and paddleB initialization

Paddles are initialized with magic numbers (x: 20, x: width - 30) that are not centralized, making it hard to adjust paddle spacing globally.

💡 Create constants like const PADDLE_MARGIN = 20; and use paddleA = { x: PADDLE_MARGIN, ... }; paddleB = { x: width - PADDLE_MARGIN - paddleA.w, ... }; for consistency and easier tweaking.

FEATURE checkCollision()

No visual feedback when the ball bounces (e.g., a flash, screen shake, or color change), making the game feel less responsive.

💡 Add a counter variable that increments on collision and decrements each frame, drawing a flash of color or scaling the paddle when the counter is above zero—this provides satisfying visual feedback.

FEATURE AI logic in draw()

The AI follows the ball perfectly every frame with no reaction time or prediction, making it difficult to beat but not realistic.

💡 Add a 'think_delay' counter: let the AI only update its target position once every N frames, introducing a human-like reaction time that makes the AI beatable but still challenging.

🔄 Code Flow

Code flow showing setup, draw, resetball, checkcollision, displayscores, resetgame, windowresized, keypressed

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" draw --> paddle-a-movement[paddle-a-movement] draw --> paddle-b-ai[paddle-b-ai] draw --> wall-collision[wall-collision] draw --> scoring-left[scoring-left] draw --> game-over-check[game-over-check] draw --> game-over-screen[game-over-screen] draw --> displayscores[displayscores] click paddle-a-movement href "#sub-paddle-a-movement" click paddle-b-ai href "#sub-paddle-b-ai" click wall-collision href "#sub-wall-collision" click scoring-left href "#sub-scoring-left" click game-over-check href "#sub-game-over-check" click game-over-screen href "#sub-game-over-screen" click displayscores href "#fn-displayscores" paddle-a-movement --> paddle-init[paddle-init] click paddle-init href "#sub-paddle-init" paddle-b-ai --> conditional-b-ai[AI Logic] click conditional-b-ai href "#sub-paddle-b-ai" wall-collision --> conditional-wall[Wall Collision Check] click conditional-wall href "#sub-wall-collision" scoring-left --> conditional-scoring[Scoring Check] click conditional-scoring href "#sub-scoring-left" game-over-check --> conditional-win[Win Condition Check] click conditional-win href "#sub-game-over-check" game-over-screen --> text-setup[text-setup] game-over-screen --> dividing-line[dividing-line] click text-setup href "#sub-text-setup" click dividing-line href "#sub-dividing-line" displayscores --> text-setup displayscores --> dividing-line resetgame --> conditional-reset[Conditional Reset] click conditional-reset href "#sub-conditional-reset" keypressed --> space-key-check[Space Key Check] click space-key-check href "#sub-space-key-check" draw -->|loop| draw draw -->|loop| windowresized[windowResized] click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual experience does the ping-pong sketch provide?

The ping-pong sketch creates a modern and elegant visual of a glowing ball bouncing between two paddles against a black background, enhancing the gameplay experience.

How can users engage with the ping-pong game?

Users can interact by controlling one paddle using their mouse, while the other paddle is managed by a simple AI.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates real-time interaction, sound synthesis with p5.sound, and basic game mechanics, combining visual and auditory feedback.

Preview

ping-pong - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of ping-pong - Code flow showing setup, draw, resetball, checkcollision, displayscores, resetgame, windowresized, keypressed
Code Flow Diagram