Skip to content
DasPeTe Tutorials

Pong in TypeScript · Part 1 of 2 in the series

Part 1: Canvas, Game Loop and Paddles

Set up a canvas with Vite and TypeScript, write a frame-rate independent game loop and move two paddles with the keyboard.

Beginner20 min

Quest objectives

  • Create a TypeScript project with Vite
  • Draw on an HTML canvas
  • Write a game loop with requestAnimationFrame and delta time
  • Read keyboard input without any lag

Pong is the perfect first game: it fits into a single file, yet it contains the same building blocks as much bigger games. There is a loop, input, movement, collision and a score. In this part you lay the foundation and get both paddles moving.

Where this series ends up: two paddles, a ball and a score.
Where this series ends up: two paddles, a ball and a score.

Create the project

You need Node.js installed. Vite sets up a TypeScript project in seconds:

bash
npm create vite@latest pong -- --template vanilla-ts
cd pong
npm install
npm run dev

Delete everything in src/ except for main.ts, empty that file, and replace index.html with a page that only holds a canvas:

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Pong</title>
    <style>
      body {
        margin: 0;
        height: 100vh;
        display: grid;
        place-items: center;
        background: #1a1a2e;
      }
    </style>
  </head>
  <body>
    <canvas id="game" width="800" height="500"></canvas>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

The game loop

A game is a loop: it reads input, updates the world and draws the result, over and over. In the browser, requestAnimationFrame calls you right before the next repaint, usually 60 times per second.

"Usually" is the important word. Some screens run at 144 Hz and a busy laptop might drop to 30. If paddles moved a fixed distance per frame, the game would run at a different speed on every machine. That's why everything moves in pixels per second, multiplied by dt: the seconds since the last frame.

Keyboard input

Keydown events alone are not enough: they fire once, pause, and then repeat at the operating system's rate. So you keep a set of the keys that are currently held and check it in every frame. event.code names the physical key, so W and S stay in the same spot on every keyboard layout.

Here is the complete main.ts for this part:

src/main.ts
const canvas = document.querySelector<HTMLCanvasElement>('#game')!;
const ctx = canvas.getContext('2d')!;

const PADDLE_WIDTH = 12;
const PADDLE_HEIGHT = 80;
const PADDLE_SPEED = 400; // pixels per second

type Paddle = { x: number; y: number };

const left: Paddle = { x: 24, y: canvas.height / 2 - PADDLE_HEIGHT / 2 };
const right: Paddle = { x: canvas.width - 24 - PADDLE_WIDTH, y: left.y };

const keys = new Set<string>();
window.addEventListener('keydown', (event) => keys.add(event.code));
window.addEventListener('keyup', (event) => keys.delete(event.code));

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}

function movePaddle(paddle: Paddle, up: string, down: string, dt: number): void {
  if (keys.has(up)) paddle.y -= PADDLE_SPEED * dt;
  if (keys.has(down)) paddle.y += PADDLE_SPEED * dt;
  paddle.y = clamp(paddle.y, 0, canvas.height - PADDLE_HEIGHT);
}

function update(dt: number): void {
  movePaddle(left, 'KeyW', 'KeyS', dt);
  movePaddle(right, 'ArrowUp', 'ArrowDown', dt);
}

function draw(): void {
  ctx.fillStyle = '#1a1a2e';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#ffffff';
  for (const paddle of [left, right]) {
    ctx.fillRect(paddle.x, paddle.y, PADDLE_WIDTH, PADDLE_HEIGHT);
  }
}

let last = performance.now();

function frame(now: number): void {
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  update(dt);
  draw();
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Open the page and move the left paddle with W and S and the right one with the arrow keys. Both stop at the edges thanks to clamp(). In the next part you add the ball and turn this into an actual game.

Comments

The comments are GitHub Discussions: sign in with GitHub to write one.

The comments come from GitHub, through giscus. Loading them sends your IP address to giscus.app and GitHub.