Pong

A complete demo with input and simple collision. Use W/S and arrow keys to move the paddles.

Transport

You are viewing: Browser (WASM) · Switch

In the browser, use a canvas with id vulfram-canvas.
Controls: use W / S and the Up / Down arrows. Click the canvas to focus.

Key steps

1. Create paddles, ball, and the backdrop.

2. Read the keyboard with isKeyPressed.

3. Update positions and do simple collision each frame.

Full example

ts
import {
  initEngine,
  createWorld,
  createWindow,
  createEntity,
  createCamera,
  createLight,
  createGeometry,
  createMaterial,
  createModel,
  createTexture,
  updateTransform,
  isKeyPressed,
  tick,
} from '@vulfram/engine';
import { initWasmTransport, transportWasm } from '@vulfram/transport-wasm';

const WINDOW_ID = 1;
const KEY = {
  KeyW: 41,
  KeyS: 37,
  ArrowUp: 74,
  ArrowDown: 71,
} as const;

function createColorMaterial(color: [number, number, number, number]): number {
  const texId = createTexture(WINDOW_ID, { source: { type: 'color', color }, srgb: true });
  return createMaterial(WINDOW_ID, {
    kind: 'standard',
    options: {
      type: 'standard',
      content: {
        baseColor: [1, 1, 1, 1],
        surfaceType: 'opaque',
        baseTexId: texId,
        baseSampler: 'linear-clamp',
        flags: 0,
      },
    },
  });
}

async function boot() {
  await initWasmTransport();
  initEngine({ transport: transportWasm });
  createWorld(WINDOW_ID);
  createWindow(WINDOW_ID, {
    title: 'Pong',
    size: [1100, 700],
    position: [0, 0],
    canvasId: 'vulfram-canvas',
  });

  const camera = createEntity(WINDOW_ID);
  updateTransform(WINDOW_ID, camera, { position: [0, 0, 12], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  createCamera(WINDOW_ID, camera, { kind: 'perspective', near: 0.1, far: 100.0 });

  const light = createEntity(WINDOW_ID);
  updateTransform(WINDOW_ID, light, { position: [2, 4, 6], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  createLight(WINDOW_ID, light, { kind: 'point', intensity: 16, range: 30 });

  const paddleGeom = createGeometry(WINDOW_ID, { type: 'primitive', shape: 'cube' });
  const ballGeom = createGeometry(WINDOW_ID, { type: 'primitive', shape: 'sphere', options: { radius: 0.4, sectors: 24, stacks: 16 } });
  const backdropGeom = createGeometry(WINDOW_ID, { type: 'primitive', shape: 'plane', options: { size: [18, 10, 1], subdivisions: 1 } });

  const redMat = createColorMaterial([0.95, 0.25, 0.25, 1]);
  const blueMat = createColorMaterial([0.25, 0.4, 0.95, 1]);
  const yellowMat = createColorMaterial([1, 0.9, 0.2, 1]);
  const backdropMat = createColorMaterial([0.12, 0.12, 0.15, 1]);

  const backdrop = createEntity(WINDOW_ID);
  updateTransform(WINDOW_ID, backdrop, { position: [0, 0, -2], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  createModel(WINDOW_ID, backdrop, { geometryId: backdropGeom, materialId: backdropMat });

  const left = createEntity(WINDOW_ID);
  const right = createEntity(WINDOW_ID);
  const ball = createEntity(WINDOW_ID);

  const fieldW = 10;
  const fieldH = 7;
  const paddleH = 1.7;
  const paddleW = 0.3;
  const ballSize = 0.45;

  let leftY = 0;
  let rightY = 0;
  let ballX = 0;
  let ballY = 0;
  let ballVelX = 4.2;
  let ballVelY = 2.6;

  updateTransform(WINDOW_ID, left, { position: [-fieldW / 2 + 0.6, 0, 0], rotation: [0, 0, 0, 1], scale: [paddleW, paddleH, 0.3] });
  updateTransform(WINDOW_ID, right, { position: [fieldW / 2 - 0.6, 0, 0], rotation: [0, 0, 0, 1], scale: [paddleW, paddleH, 0.3] });
  updateTransform(WINDOW_ID, ball, { position: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [ballSize, ballSize, ballSize] });

  createModel(WINDOW_ID, left, { geometryId: paddleGeom, materialId: redMat });
  createModel(WINDOW_ID, right, { geometryId: paddleGeom, materialId: blueMat });
  createModel(WINDOW_ID, ball, { geometryId: ballGeom, materialId: yellowMat });

  let last = performance.now();
  function frame(now: number) {
    const deltaMs = now - last;
    const delta = deltaMs / 1000;
    last = now;

    const speed = 6.0;
    if (isKeyPressed(WINDOW_ID, KEY.KeyW)) leftY += speed * delta;
    if (isKeyPressed(WINDOW_ID, KEY.KeyS)) leftY -= speed * delta;
    if (isKeyPressed(WINDOW_ID, KEY.ArrowUp)) rightY += speed * delta;
    if (isKeyPressed(WINDOW_ID, KEY.ArrowDown)) rightY -= speed * delta;

    leftY = Math.max(-fieldH / 2 + paddleH / 2, Math.min(fieldH / 2 - paddleH / 2, leftY));
    rightY = Math.max(-fieldH / 2 + paddleH / 2, Math.min(fieldH / 2 - paddleH / 2, rightY));

    ballX += ballVelX * delta;
    ballY += ballVelY * delta;

    if (ballY > fieldH / 2 - ballSize || ballY < -fieldH / 2 + ballSize) {
      ballVelY *= -1;
      ballY = Math.max(-fieldH / 2 + ballSize, Math.min(fieldH / 2 - ballSize, ballY));
    }

    const leftX = -fieldW / 2 + 0.6;
    const rightX = fieldW / 2 - 0.6;
    const hitLeft = ballX - ballSize < leftX + paddleW && ballX > leftX && Math.abs(ballY - leftY) < paddleH / 2 + ballSize * 0.3;
    const hitRight = ballX + ballSize > rightX - paddleW && ballX < rightX && Math.abs(ballY - rightY) < paddleH / 2 + ballSize * 0.3;

    if (hitLeft) {
      ballVelX = Math.abs(ballVelX);
    } else if (hitRight) {
      ballVelX = -Math.abs(ballVelX);
    }

    if (ballX > fieldW / 2 + 2 || ballX < -fieldW / 2 - 2) {
      ballX = 0;
      ballY = 0;
      ballVelX = Math.sign(ballVelX) * 4.2;
    }

    updateTransform(WINDOW_ID, left, { position: [leftX, leftY, 0] });
    updateTransform(WINDOW_ID, right, { position: [rightX, rightY, 0] });
    updateTransform(WINDOW_ID, ball, { position: [ballX, ballY, 0] });

    tick(now, deltaMs);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}

boot().catch(console.error);
Live demo canvas