GameNode.js

Vite + React Canvas Game

A tiny 2D coin-collector game on <canvas> with a requestAnimationFrame loop and keyboard input.

Open in builder →

7 steps

Shell

Shown with defaults: npm, pip, Node LTS, Python 3.12 and the template's default add-ons.

  1. 1. Install and select Node.js with nvm

    runtime

    nvm lets you install several Node.js versions side by side and switch per project. `nvm use` activates it for this shell.

    bash
    nvm install --ltsnvm use --lts
    Expected result
    `node -v` prints the selected version.
    Verify
    node -v && npm -v
    OS notes
    macOS/Linux/WSL: install nvm with `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash`, then reopen the terminal. Windows: use nvm-windows (github.com/coreybutler/nvm-windows) from an elevated terminal, or fnm.
  2. 2. Scaffold a Vite + React app

    template

    create-vite copies the React template. --no-interactive skips prompts; the extra '--' is required by npm to forward flags.

    bash
    npm create vite@latest nvx-game -- --template react-ts --no-interactive
    Expected result
    A ./nvx-game folder with index.html, src/ and package.json.
    Verify
    ls nvx-game/src
    OS notes
    create-vite 9 needs Node.js 20.19+ or 22.12+.
  3. 3. Enter the project folder

    template

    The scaffolder created ./nvx-game. Move into it before installing anything else.

    bash
    cd nvx-game
    Expected result
    You are inside ./nvx-game
    Verify
    ls package.json
  4. 4. Install dependencies

    template

    Download every package listed in package.json into node_modules.

    bash
    npm install
    Expected result
    node_modules/ exists and a lockfile is created.
    Verify
    ls node_modules
  5. 5. Write the game loop

    template

    Replace App with a canvas game: keyboard input, collision detection and a score counter.

    Files written by this step: src/App.tsx, src/index.css
    src/App.tsx
    import { useEffect, useRef, useState } from "react";
    
    const W = 640;
    const H = 400;
    
    export default function App() {
      const canvasRef = useRef<HTMLCanvasElement>(null);
      const [score, setScore] = useState(0);
    
      useEffect(() => {
        const canvas = canvasRef.current;
        const context = canvas?.getContext("2d");
        if (!context) return;
        const ctx: CanvasRenderingContext2D = context;
        const keys: Record<string, boolean> = {};
        const player = { x: W / 2, y: H / 2, r: 14, speed: 4 };
        const coin = { x: 120, y: 120, r: 8 };
        let points = 0;
        let raf = 0;
    
        const down = (e: KeyboardEvent) => { keys[e.key] = true; };
        const up = (e: KeyboardEvent) => { keys[e.key] = false; };
        window.addEventListener("keydown", down);
        window.addEventListener("keyup", up);
    
        const loop = () => {
          if (keys.ArrowLeft || keys.a) player.x -= player.speed;
          if (keys.ArrowRight || keys.d) player.x += player.speed;
          if (keys.ArrowUp || keys.w) player.y -= player.speed;
          if (keys.ArrowDown || keys.s) player.y += player.speed;
          player.x = Math.max(player.r, Math.min(W - player.r, player.x));
          player.y = Math.max(player.r, Math.min(H - player.r, player.y));
    
          if (Math.hypot(player.x - coin.x, player.y - coin.y) < player.r + coin.r) {
            points += 1;
            setScore(points);
            coin.x = 20 + Math.random() * (W - 40);
            coin.y = 20 + Math.random() * (H - 40);
          }
    
          ctx.fillStyle = "#0f172a";
          ctx.fillRect(0, 0, W, H);
          ctx.fillStyle = "#facc15";
          ctx.beginPath();
          ctx.arc(coin.x, coin.y, coin.r, 0, Math.PI * 2);
          ctx.fill();
          ctx.fillStyle = "#38bdf8";
          ctx.beginPath();
          ctx.arc(player.x, player.y, player.r, 0, Math.PI * 2);
          ctx.fill();
          raf = requestAnimationFrame(loop);
        };
        loop();
    
        return () => {
          cancelAnimationFrame(raf);
          window.removeEventListener("keydown", down);
          window.removeEventListener("keyup", up);
        };
      }, []);
    
      return (
        <main className="game">
          <h1>nvx-game</h1>
          <p aria-live="polite">Score: {score}</p>
          <canvas ref={canvasRef} width={W} height={H} aria-label="Game canvas: move with arrow keys or WASD" />
          <p>Move with arrow keys or WASD. Collect the coins!</p>
        </main>
      );
    }
    
    src/index.css
    :root { color-scheme: dark; font-family: system-ui, sans-serif; }
    body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #020617; color: #e2e8f0; }
    .game { text-align: center; }
    canvas { border: 2px solid #334155; border-radius: 12px; max-width: 100%; }
    
    Expected result
    src/App contains the game.
  6. 6. Play it in the dev server

    templaterun manually · dev server

    Vite serves the game with instant hot module reload.

    bash
    npm run dev
    Expected result
    http://localhost:5173 shows the canvas; arrow keys move the ball.
    Verify
    curl -I http://localhost:5173
  7. 7. Build static files

    template

    Outputs an optimized static site in dist/ that any static host can serve.

    bash
    npm run build
    Expected result
    dist/index.html and hashed assets.
    Verify
    ls dist