Advanced BrowserNodeBun @vulfram/engine

Pong

A complete mini-game demo with keyboard input, collision logic, and deterministic per-frame updates.

What You Will Explore

  • Read keyboard state and route input to gameplay actions.
  • Apply simple AABB collision rules to game entities.
  • Keep simulation updates in sync with render frames.

Prerequisites

  • A running Vulfram project in Browser, Node, or Bun.
  • Canvas focus handling (for browser keyboard input).

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,
  Mount,
  World3D,
  createWindow,
  tick,
} from '@vulfram/engine';
import { initWasmTransport, transportWasm } from '@vulfram/transport-browser';

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

function createColorMaterial(worldId: number, color: [number, number, number, number]): number {
  const texId = World3D.create3DTexture(worldId, { source: { type: 'color', color }, srgb: true });
  return World3D.create3DMaterial(worldId, {
    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 });
  const worldId = World3D.create3DWorld();
  const { windowId } = createWindow({
    title: 'Pong',
    size: [1100, 700],
    position: [0, 0],
    canvasId: 'vulfram-canvas',
  });
Mount.mountWorldToWindow(worldId, windowId);

  const camera = World3D.create3DEntity(worldId);
  World3D.update3DTransform(worldId, camera, { position: [0, 0, 12], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  World3D.create3DCamera(worldId, camera, { kind: 'perspective', near: 0.1, far: 100.0 });

  const light = World3D.create3DEntity(worldId);
  World3D.update3DTransform(worldId, light, { position: [2, 4, 6], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  World3D.create3DLight(worldId, light, { kind: 'point', intensity: 16, range: 30 });

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

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

  const backdrop = World3D.create3DEntity(worldId);
  World3D.update3DTransform(worldId, backdrop, { position: [0, 0, -2], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
  World3D.create3DModel(worldId, backdrop, { geometryId: backdropGeom, materialId: backdropMat });

  const left = World3D.create3DEntity(worldId);
  const right = World3D.create3DEntity(worldId);
  const ball = World3D.create3DEntity(worldId);

  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;

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

  World3D.create3DModel(worldId, left, { geometryId: paddleGeom, materialId: redMat });
  World3D.create3DModel(worldId, right, { geometryId: paddleGeom, materialId: blueMat });
  World3D.create3DModel(worldId, 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 (World3D.is3DKeyPressed(worldId, KEY.KeyW)) leftY += speed * delta;
    if (World3D.is3DKeyPressed(worldId, KEY.KeyS)) leftY -= speed * delta;
    if (World3D.is3DKeyPressed(worldId, KEY.ArrowUp)) rightY += speed * delta;
    if (World3D.is3DKeyPressed(worldId, 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;
    }

    World3D.update3DTransform(worldId, left, { position: [leftX, leftY, 0] });
    World3D.update3DTransform(worldId, right, { position: [rightX, rightY, 0] });
    World3D.update3DTransform(worldId, ball, { position: [ballX, ballY, 0] });

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

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