
Build Pong with Phaser.js and AI: Physics, Colliders, and Local Multiplayer
Pong is the perfect first Phaser.js game. The rules fit in one sentence, the code fits in under 100 lines, and yet it teaches every core Phaser concept you’ll need for action games: physics sprites, colliders, world bounds, scoring, and AI. This tutorial walks through each step with the exact prompts that built it.
Built by DeepSeek V4 Flash — final step (two-player mode) with bot vs AI optionGame instructions: P1 (WS) vs P2 (↑↓) · First to 11 wins · Press R to restart · Try Vs AI mode
What Makes Pong an Ideal Learning Game
Pong maps one-to-one with Phaser’s arcade physics system. Every mechanic teaches a transferable concept:
| Concept | What You Learn | Reused In |
|---|---|---|
| Arcade Physics | Velocity, bounce, world bounds | Every action game |
| Colliders | physics.add.collider() for ball-paddle interaction |
Breakout, space shooters |
| World Bounds | setCollideWorldBounds() + worldbounds event |
Scoring boundaries, wall detection |
| Keyboard Input | addKey() for paddle control |
Player movement in any game |
| AI Steering | Simple tracking with ball.y - paddle.y |
Bot mode in any game |
| Audio FX | Web Audio API for collision sounds | Game feel in any project |
The build progressed through five steps, each adding one mechanic. Here’s how they work.
Code Walkthrough
1. Programmatic Textures with make.graphics()
Every visual in the game — paddles and ball — is created in preload() using Phaser’s Graphics API. No image files needed:
preload() {
let g = this.make.graphics({ add: false });
// Paddle — white rectangle
g.fillStyle(0xffffff);
g.fillRect(0, 0, 12, 80);
g.generateTexture('paddle', 12, 80);
g.destroy();
g = this.make.graphics({ add: false });
// Ball — white circle
g.fillStyle(0xffffff);
g.fillCircle(8, 8, 7);
g.generateTexture('ball', 16, 16);
g.destroy();
}
The pattern: create an off-screen Graphics object ({ add: false }), draw on it, call generateTexture(key, width, height), then destroy it. This is the only reliable texture-creation method in Phaser 3.60 — this.textures.generate() with object config creates invisible 1×1 textures in WebGL mode.
2. Physics Sprites and Colliders
Paddles and ball are physics.add.sprite() with arcade bodies. The paddles are immovable (they don’t recoil on hit), and the ball has setBounce(1) for perfect reflection:
this.paddleL = this.physics.add.sprite(30, H / 2, 'paddle');
this.paddleL.setImmovable(true);
this.ball = this.physics.add.sprite(W / 2, H / 2, 'ball');
this.ball.setBounce(1);
this.ball.setCollideWorldBounds(true);
The collider connects ball to paddles with a callback that controls bounce angle:
this.physics.add.collider(this.ball, this.paddleL, this.hitPaddle, null, this);
this.physics.add.collider(this.ball, this.paddleR, this.hitPaddle, null, this);
The hitPaddle callback computes the bounce angle from where the ball hits the paddle — center hit = flat bounce, edge hit = steep angle:
hitPaddle(ball, paddle) {
let diff = ball.y - paddle.y;
ball.setVelocityY(diff * 8);
// Speed cap at 400px/s
let speed = Math.sqrt(ball.body.velocity.x ** 2 + ball.body.velocity.y ** 2);
if (speed < 400) {
ball.body.velocity.x *= 1.05;
ball.body.velocity.y *= 1.05;
}
}
This creates natural-feeling paddle physics where every hit is slightly different.
3. World Bounds for Scoring
Detecting when the ball exits the playfield is done with Phaser’s worldbounds event — not manual y checks:
this.ball.body.onWorldBounds = true;
this.physics.world.on('worldbounds', (body) => {
if (body.gameObject === this.ball && !this.gameOver) {
if (this.ball.x < 30) {
this.scoreR++;
this.afterPoint();
} else if (this.ball.x > W - 30) {
this.scoreL++;
this.afterPoint();
} else {
this.playTone(220, 0.08, 'triangle'); // wall bounce
}
}
});
The onWorldBounds flag enables the event for the ball’s physics body. When the ball hits any world boundary (top, bottom, left, right), the event fires. We check if it exited left or right (past the paddle positions) to award a point. Top/bottom bounces just play a sound.
4. AI Opponent with Simple Tracking
The AI uses a single proportional-control loop:
updateAI(paddle, speed) {
let diff = this.ball.y - paddle.y;
if (Math.abs(diff) > 8) {
paddle.body.setVelocityY(diff > 0 ? speed : -speed);
} else {
paddle.body.setVelocityY(0);
}
paddle.y = Phaser.Math.Clamp(paddle.y, 40, H - 40);
}
The 8-pixel dead zone (the Math.abs(diff) > 8 check) prevents jittering when the paddle is close to the ball’s Y. The AI moves at a fixed speed rather than tracking the ball exactly — this makes it beatable. Adjusting the speed parameter (220 in the final version) controls difficulty.
5. Sound Effects with Web Audio API
Collision sounds are generated on the fly with a tiny AudioContext utility:
playTone(freq, duration, type) {
try {
let ctx = this.audioCtx;
if (ctx.state === 'suspended') ctx.resume();
let osc = ctx.createOscillator();
let gain = ctx.createGain();
osc.type = type || 'square';
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(0.12, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + duration);
} catch (e) {}
}
Three distinct tones: 440Hz square for paddle hits, 220Hz triangle for wall bounces, and a 160Hz sawtooth for scoring. No audio files needed — the tones are synthesized at runtime.
6. Game State Management
The game tracks state with four boolean flags:
gameStarted— controls the intro/vs screen flowgameOver— locks input after a player reaches 11 pointsserving— prevents the ball from being hit during serve recoverybotMode— enables AI control for the right paddle (or both in bot-vs-bot mode)
When a point is scored, afterPoint() resets the ball, checks the win condition, and either triggers game-over or schedules a new serve:
afterPoint() {
this.ball.body.setVelocity(0, 0);
this.ball.setPosition(W / 2, H / 2);
if (this.scoreL >= 11) {
this.gameOver = true;
this.winText.setText('Player 1 Wins!');
this.restartHint.setText('Press R to play again');
} else {
this.serving = true;
this.time.delayedCall(800, () => {
let angle = Phaser.Math.Between(0, 360);
this.ball.body.setVelocity(Math.cos(angle) * 200, Math.sin(angle) * 200);
this.serving = false;
});
}
}
The time.delayedCall(800, ...) gives players a moment to prepare before the ball launches in a random direction.
How AI Helped
The game was built with DeepSeek V4 Flash on DeepSeek’s API (temperature 0.7, max tokens 4,096). The build followed a five-prompt progression, each adding one mechanic to the previous output.
Prompt 1 — Core physics:
“Create a Phaser.js 3.60 game where two paddles (white rectangles) and a ball (white circle) bounce. P1 moves with W/S, P2 with Up/Down. Ball bounces off top/bottom walls and paddles. Ball resets to center when it exits left or right. Use
make.graphics()for textures,setBounce(1)on the ball,setImmovable(true)on paddles.”
What the model got right: Correct generateTexture() pattern, working collider setup, ball reset logic.
What needed adjustment: The first paddle dimensions were too small (60×10) — needed to match the visible area. A quick second pass fixed sizing.
Prompt 2 — Scoring + AI:
“Add scoring: first to 11 wins. Show score at top center. Add AI opponent — right paddle tracks ball Y position with a dead zone. Add
?bot=trueURL parameter for bot mode.”
What the model got right: Working updateAI() function, score tracking with win condition, bot URL parameter detection.
What needed adjustment: The AI speed was too slow on the first pass (150px/s) — bumped to 220px/s for reasonable difficulty. The dead zone needed widening from 4px to 8px to prevent paddle jitter.
Prompt 3 — Sound + polish:
“Add AudioContext sound effects: 440Hz on paddle hit, 220Hz on wall bounce, 160Hz on score. Add camera shake on score. Add center dashed line.”
What the model got right: Working playTone() function with three distinct tones, camera shake with cameras.main.shake().
What needed adjustment: The playTone() function had an unclosed try/catch block on first output. One-line syntactic fix.
Prompt 4 — Two-player mode:
“Add intro/vs screen showing ‘PONG’ title and player labels. Player 1 vs Player 2 mode. Left paddle is cyan label, right paddle is orange label. Press any key to start.”
What the model got right: Working intro screen with player labels, clean startGame() flow that destroys intro elements.
What needed adjustment: The intro text used setOrigin(0) instead of setOrigin(0.5) — text was positioned off-center. Quick fix.
Total build time: ~27 minutes across 5 prompts. Total tokens used: ~54,000. Cost: $0.0121.
Try It Yourself
The game is intentionally minimal — about 100 lines of core logic. Here are ways to remix it:
| Idea | What to Change | Phaser Concept |
|---|---|---|
| Variable paddle size | Power-up that enlarges paddle | setScale() on physics sprite |
| Speed ramp | Ball accelerates faster per hit | Adjust multiplier in hitPaddle() |
| Brick wall | Add breakable bricks between paddles | staticGroup() with collider |
| Trail effect | Ball leaves a fading trail | tweens on temporary sprites |
| Mobile controls | Add touch-based paddle control | pointer events for movement |
| Difficulty levels | Three AI speeds selected at start | Mode selection UI before game |
Each builds on the same foundation: physics sprites, colliders, and event-driven state.
Full Code Reference
The complete source is in the repository — 420 lines of HTML + CSS + JS. The game logic itself is under 100 lines.
Key files:
public/games/pong/index.html— final game page with nav bar, bot mode, and stats tablepublic/games/pong/pong-step-01.htmlthroughpong-step-05.html— incremental build stepssrc/content/blog/phaser-pong-tutorial.md— this tutorial
The game runs on Phaser 3.60.0 loaded from CDN, uses Arcade Physics with no gravity, and has been tested on Chrome, Firefox, and Safari.
What You Learned
- Programmatic textures —
make.graphics()+generateTexture()for zero-asset development - Arcade physics colliders —
physics.add.collider()with per-paddle callbacks - World bounds events —
worldboundsfor scoring and wall detection - AI steering — proportional tracking with dead zone
- Sound synthesis — AudioContext
playTone()for game feel without audio files - Game state management — boolean flags for intro, play, serve, and game-over states
Pong is the starting line. Every concept here — physics bodies, colliders, bound events, AI steering — scales directly to platformers, shooters, and puzzle games. The Phaser pattern stays the same; only the rules change.