Dodge game

← back to next-sketch
Drag (or move the pointer) left/right to dodge the falling blocks — click to restart after a hit. Built on <CanvasSketch />'s onPointerMove/onPointerDown.
View the full next-sketch (React) source
'use client';

import { useRef } from 'react';
import { CanvasSketch, clamp, randRange } from 'next-sketch';

const PLAYER_W = 36;

interface Block {
  x: number;
  y: number;
  size: number;
}

export default function DodgeGame() {
  const state = useRef({
    width: 0,
    playerX: 0,
    pointerX: 0,
    blocks: [] as Block[],
    spawnTimer: 0,
    elapsed: 0,
    alive: true,
  });

  const reset = () => {
    const s = state.current;
    s.playerX = s.width / 2;
    s.pointerX = s.width / 2;
    s.blocks = [];
    s.spawnTimer = 0;
    s.elapsed = 0;
    s.alive = true;
  };

  return (
    <CanvasSketch
      style={{ width: '100%', height: 400 }}
      setup={({ width }) => { state.current.width = width; reset(); }}
      onPointerMove={({ x }) => { state.current.pointerX = x; }}
      onPointerDown={() => { if (!state.current.alive) reset(); }}
      draw={({ ctx, width, height, dt }) => {
        const s = state.current;
        s.width = width;
        ctx.fillStyle = '#0f172a';
        ctx.fillRect(0, 0, width, height);

        if (s.alive) {
          s.playerX = clamp(s.pointerX, PLAYER_W / 2, width - PLAYER_W / 2);
          s.elapsed += dt;

          const speed = 140 + s.elapsed * 12;
          s.spawnTimer -= dt;
          if (s.spawnTimer <= 0) {
            s.blocks.push({ x: randRange(20, width - 20), y: -20, size: randRange(18, 32) });
            s.spawnTimer = Math.max(0.35, 0.9 - s.elapsed * 0.02);
          }

          for (const b of s.blocks) b.y += speed * dt;
          s.blocks = s.blocks.filter((b) => b.y < height + 40);

          for (const b of s.blocks) {
            const dx = Math.abs(b.x - s.playerX);
            const dy = Math.abs(b.y - (height - 30));
            if (dx < (b.size + PLAYER_W) / 2 && dy < (b.size + 16) / 2) {
              s.alive = false;
            }
          }
        }

        ctx.fillStyle = '#ef4444';
        for (const b of s.blocks) ctx.fillRect(b.x - b.size / 2, b.y - b.size / 2, b.size, b.size);

        ctx.fillStyle = '#6366f1';
        ctx.fillRect(s.playerX - PLAYER_W / 2, height - 38, PLAYER_W, 16);

        ctx.fillStyle = '#e2e8f0';
        ctx.font = '14px sans-serif';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillText(`Score: ${Math.floor(s.elapsed * 10)}`, 12, 12);

        if (!s.alive) {
          ctx.fillStyle = 'rgba(15, 23, 42, 0.8)';
          ctx.fillRect(0, 0, width, height);
          ctx.fillStyle = '#e2e8f0';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.font = '20px sans-serif';
          ctx.fillText('Game over — click to restart', width / 2, height / 2);
        }
      }}
    />
  );
}