Zum Inhalt springen
DasPeTe Tutorials

Pong in TypeScript · Teil 1 von 2 der Serie

Teil 1: Canvas, Game Loop und Schläger

Richte mit Vite und TypeScript ein Canvas ein, schreibe eine von der Bildrate unabhängige Game Loop und steuere zwei Schläger mit der Tastatur.

Einsteiger20 Min.

Quest-Ziele

  • Ein TypeScript-Projekt mit Vite anlegen
  • Auf ein HTML-Canvas zeichnen
  • Eine Game Loop mit requestAnimationFrame und Delta Time schreiben
  • Tastatureingaben ohne Verzögerung auslesen

Pong ist das perfekte erste Spiel: Es passt in eine einzige Datei und enthält trotzdem dieselben Bausteine wie viel größere Spiele. Es gibt eine Schleife, Eingaben, Bewegung, Kollisionen und einen Punktestand. In diesem Teil legst du das Fundament und bringst beide Schläger in Bewegung.

Das Ziel dieser Serie: zwei Schläger, ein Ball und ein Punktestand.
Das Ziel dieser Serie: zwei Schläger, ein Ball und ein Punktestand.

Das Projekt anlegen

Du brauchst eine Installation von Node.js. Vite legt ein TypeScript-Projekt in Sekunden an:

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

Lösche alles in src/ außer main.ts, leere diese Datei und ersetze index.html durch eine Seite, die nur ein Canvas enthält:

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>

Die Game Loop

Ein Spiel ist eine Schleife: Es liest Eingaben, aktualisiert die Welt und zeichnet das Ergebnis, immer und immer wieder. Im Browser ruft dich requestAnimationFrame direkt vor dem nächsten Neuzeichnen auf, normalerweise 60-mal pro Sekunde.

„Normalerweise“ ist hier das wichtige Wort. Manche Bildschirme laufen mit 144 Hz, und ein ausgelasteter Laptop fällt vielleicht auf 30. Würden sich die Schläger pro Frame um eine feste Strecke bewegen, liefe das Spiel auf jedem Rechner unterschiedlich schnell. Deshalb bewegt sich alles in Pixeln pro Sekunde, multipliziert mit dt: den Sekunden seit dem letzten Frame.

Tastatureingaben

Keydown-Events allein reichen nicht: Sie feuern einmal, pausieren und wiederholen sich dann im Takt des Betriebssystems. Deshalb führst du ein Set der gerade gedrückten Tasten und prüfst es in jedem Frame. event.code bezeichnet die physische Taste, W und S bleiben also auf jedem Tastaturlayout an derselben Stelle.

Hier ist die vollständige main.ts für diesen Teil:

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);

Öffne die Seite und bewege den linken Schläger mit W und S und den rechten mit den Pfeiltasten. Dank clamp() stoppen beide am Rand. Im nächsten Teil kommt der Ball dazu, und aus dem Ganzen wird ein richtiges Spiel.

Kommentare

Die Kommentare sind GitHub Discussions: Zum Schreiben mit GitHub anmelden.

Die Kommentare kommen über giscus von GitHub. Beim Laden gehen deine IP-Adresse an giscus.app und GitHub.