<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>火柴人跑酷</title>
<style>
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #0e0e1a;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Segoe UI', system-ui, sans-serif;
    overflow: hidden;
  }
  .game-wrapper {
    background: #151528;
    border-radius: 18px;
    padding: 20px;
    box-shadow: 0 8px 50px rgba(0,0,0,0.7);
    position: relative;
  }
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
    padding: 0 4px;
  }
  .score-box {
    font-size: 17px;
    font-weight: 600;
    color: #ccc;
  }
  .score-box span { color: #f0c040; }
  .controls-row {
    display: flex;
    gap: 8px;
  }
  .controls-row button {
    background: #2d2d50;
    border: none;
    color: #ccc;
    font-size: 13px;
    padding: 6px 16px;
    border-radius: 6px;
    cursor: pointer;
    font-family: inherit;
    transition: background 0.15s;
  }
  .controls-row button:hover { background: #3d3d62; }
  canvas {
    display: block;
    background: #111124;
    border-radius: 10px;
    width: 800px;
    height: 400px;
    image-rendering: auto;
  }
  .footer {
    margin-top: 10px;
    text-align: center;
    font-size: 13px;
    color: #555;
  }
  .footer kbd {
    background: #222238;
    padding: 2px 9px;
    border-radius: 4px;
    font-family: inherit;
    font-size: 12px;
    color: #999;
  }
  @media (max-width: 860px) {
    .game-wrapper { padding: 12px; border-radius: 12px; }
    canvas { width: calc(100vw - 40px); height: calc((100vw - 40px) * 0.5); }
  }
</style>
</head>
<body>
<div class="game-wrapper">
  <div class="header">
    <div class="score-box">🏃 <span id="scoreDisplay">0</span>m</div>
    <div class="controls-row">
      <button id="restartBtn">重新开始</button>
    </div>
  </div>
  <canvas id="gameCanvas" width="800" height="400"></canvas>
  <div class="footer"><kbd>空格</kbd> <kbd>↑</kbd> <kbd>W</kbd> 跳跃 &nbsp;·&nbsp; 双击/点两下可二段跳</div>
</div>

<script>
(function() {
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const scoreDisplay = document.getElementById('scoreDisplay');

  const W = 800, H = 400;
  const GROUND_Y = 340;        // 地面高度
  const GRAVITY = 0.6;
  const JUMP_VEL = -9;
  const MAX_JUMPS = 2;

  // ---- 状态 ----
  let gameOver = false;
  let score = 0;
  let frameCount = 0;
  let speed = 5;
  let obstacles = [];
  let particles = [];
  let bgStars = [];

  // ---- 火柴人 ----
  const stick = {
    x: 120, y: GROUND_Y,
    vy: 0,
    w: 20, h: 44,           // 碰撞箱
    jumpsLeft: MAX_JUMPS,
    grounded: true,
    runPhase: 0,
  };

  // ---- 星星背景 ----
  for (let i = 0; i < 50; i++) {
    bgStars.push({
      x: Math.random() * W,
      y: Math.random() * (GROUND_Y - 20),
      r: Math.random() * 1.6 + 0.4,
      a: Math.random() * 0.5 + 0.2,
    });
  }

  // ---- 地面块(视觉分割) ----
  let groundOff = 0;

  // ---- 重置 ----
  function reset() {
    gameOver = false;
    score = 0;
    frameCount = 0;
    speed = 5;
    obstacles = [];
    particles = [];
    stick.y = GROUND_Y;
    stick.vy = 0;
    stick.jumpsLeft = MAX_JUMPS;
    stick.grounded = true;
    stick.runPhase = 0;
    scoreDisplay.textContent = '0';
  }

  // ---- 生成障碍物 ----
  function spawnObstacle() {
    const types = [ 
      { w: 14, h: 28 },     // 小石块
      { w: 20, h: 36 },     // 中箱子
      { w: 12, h: 40 },     // 高柱子
      { w: 28, h: 22 },     // 矮宽箱
    ];
    const t = types[Math.floor(Math.random() * types.length)];
    obstacles.push({
      x: W + 20,
      y: GROUND_Y - t.h,
      w: t.w,
      h: t.h,
      passed: false,
      type: t,
    });
  }

  let spawnTimer = 0;

  // ---- 粒子特效 ----
  function emitParticles(x, y, count, color, spread) {
    for (let i = 0; i < count; i++) {
      particles.push({
        x, y,
        vx: (Math.random() - 0.5) * spread,
        vy: -Math.random() * spread * 0.6 - 1,
        life: 1,
        decay: 0.015 + Math.random() * 0.025,
        size: 2 + Math.random() * 3,
        color,
      });
    }
  }

  // ---- 更新 ----
  function update() {
    if (gameOver) return;
    frameCount++;

    // 速度渐增
    speed = 5 + Math.floor(score / 100) * 0.5;
    if (speed > 14) speed = 14;

    // ---- 火柴人物理 ----
    stick.vy += GRAVITY;
    stick.y += stick.vy;

    // 落地检测
    if (stick.y >= GROUND_Y) {
      stick.y = GROUND_Y;
      stick.vy = 0;
      if (!stick.grounded) {
        // 落地粒子
        emitParticles(stick.x, GROUND_Y, 6, 'rgba(200,200,255,0.4)', 4);
      }
      stick.grounded = true;
      stick.jumpsLeft = MAX_JUMPS;
    } else {
      stick.grounded = false;
    }

    // 跑步相位(落地时才积累)
    if (stick.grounded) {
      stick.runPhase += 0.18 * (speed / 5);
    }

    // ---- 障碍物 ----
    for (let i = obstacles.length - 1; i >= 0; i--) {
      const o = obstacles[i];
      o.x -= speed;

      // 计分
      if (!o.passed && o.x + o.w < stick.x) {
        o.passed = true;
        score++;
        scoreDisplay.textContent = score;
      }

      // 碰撞检测 (AABB)
      const sx = stick.x - stick.w/2, sy = stick.y - stick.h;
      if (sx < o.x + o.w && sx + stick.w > o.x &&
          sy < o.y + o.h && sy + stick.h > o.y) {
        gameOver = true;
        emitParticles(stick.x, stick.y - stick.h/2, 30, 'rgba(255,100,100,0.8)', 8);
        return;
      }

      // 移除屏幕外
      if (o.x + o.w < -20) obstacles.splice(i, 1);
    }

    // ---- 生成 ----
    spawnTimer -= speed;
    if (spawnTimer <= 0) {
      spawnObstacle();
      const minGap = Math.max(80, 140 - score * 0.15);
      spawnTimer = minGap + Math.random() * 60;
    }

    // ---- 粒子 ----
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.08;
      p.life -= p.decay;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // 地面滚动
    groundOff = (groundOff - speed) % 40;
  }

  // ---- 绘制火柴人 ----
  function drawStickman() {
    const cx = stick.x;
    const baseY = stick.y;  // 脚底
    const headR = 8;
    const bodyLen = 20;
    const limbLen = 16;
    const headY = baseY - bodyLen - headR;

    ctx.save();
    ctx.strokeStyle = '#e8e8f0';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    // 跑步 / 跳跃姿态
    const inAir = !stick.grounded;
    const phase = stick.runPhase;
    const swing = (inAir ? 0.5 : Math.sin(phase)) * 0.45;

    // 头
    ctx.beginPath();
    ctx.arc(cx, headY, headR, 0, Math.PI * 2);
    ctx.stroke();

    // 身体
    const neckY = headY + headR;
    const hipY = neckY + bodyLen;
    ctx.beginPath();
    ctx.moveTo(cx, neckY);
    ctx.lineTo(cx, hipY);
    ctx.stroke();

    // 左腿(从胯部到脚)
    const legAngleL = 0.6 + swing;
    const legAngle = 0.6;
    const lx1 = cx, ly1 = hipY;
    const lx2 = cx - Math.sin(legAngleL) * limbLen;
    const ly2 = ly1 + Math.cos(legAngleL) * limbLen;
    ctx.beginPath(); ctx.moveTo(lx1, ly1); ctx.lineTo(lx2, ly2); ctx.stroke();

    // 右腿
    const legAngleR = 0.6 - swing;
    const rx2 = cx - Math.sin(legAngleR) * limbLen;
    const ry2 = ly1 + Math.cos(legAngleR) * limbLen;
    ctx.beginPath(); ctx.moveTo(cx, ly1); ctx.lineTo(rx2, ry2); ctx.stroke();

    // 手臂
    const armY = neckY + 4;
    const armSwing = (inAir ? 1 : Math.sin(phase + Math.PI)) * 0.4;
    const ax1 = cx - Math.sin(0.3 + armSwing) * limbLen * 0.9;
    const ay1 = armY + Math.cos(0.3 + armSwing) * limbLen * 0.9;
    ctx.beginPath(); ctx.moveTo(cx, armY); ctx.lineTo(ax1, ay1); ctx.stroke();

    const ax2 = cx - Math.sin(0.3 - armSwing) * limbLen * 0.9;
    const ay2 = armY + Math.cos(0.3 - armSwing) * limbLen * 0.9;
    ctx.beginPath(); ctx.moveTo(cx, armY); ctx.lineTo(ax2, ay2); ctx.stroke();

    // 眼睛
    ctx.fillStyle = '#111';
    ctx.beginPath(); ctx.arc(cx - 3, headY - 1, 1.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(cx + 3, headY - 1, 1.5, 0, Math.PI*2); ctx.fill();

    ctx.restore();
  }

  // ---- 绘制障碍物 ----
  function drawObstacle(o) {
    const hue = 220 + (o.h % 3) * 15;
    ctx.fillStyle = `hsl(${hue}, 20%, 35%)`;
    ctx.fillRect(o.x, o.y, o.w, o.h);
    // 边框高光
    ctx.strokeStyle = `hsl(${hue}, 15%, 50%)`;
    ctx.lineWidth = 1.5;
    ctx.strokeRect(o.x, o.y, o.w, o.h);
    // 顶线高光
    ctx.strokeStyle = `hsl(${hue}, 20%, 55%)`;
    ctx.beginPath();
    ctx.moveTo(o.x + 2, o.y + 2);
    ctx.lineTo(o.x + o.w - 2, o.y + 2);
    ctx.stroke();
  }

  // ---- 绘制场景 ----
  function drawScene() {
    ctx.clearRect(0, 0, W, H);

    // ---- 星空 ----
    for (const s of bgStars) {
      ctx.fillStyle = `rgba(255,255,255,${s.a})`;
      ctx.beginPath();
      ctx.arc(((s.x - frameCount * 0.15) % W + W) % W, s.y, s.r, 0, Math.PI*2);
      ctx.fill();
    }

    // ---- 远景城市剪影 ----
    ctx.fillStyle = '#1a1a32';
    const cityOffset = (frameCount * 0.3) % 200;
    for (let i = -1; i < 6; i++) {
      const bx = i * 160 - cityOffset;
      const bh = 40 + ((i * 7 + 3) % 5) * 20;
      ctx.fillRect(bx, GROUND_Y - bh, 50, bh);
      // 小窗户
      ctx.fillStyle = 'rgba(255,200,100,0.06)';
      for (let wy = GROUND_Y - bh + 8; wy < GROUND_Y - 12; wy += 12)
        for (let wx = bx + 6; wx < bx + 46; wx += 14)
          ctx.fillRect(wx, wy, 6, 5);
      ctx.fillStyle = '#1a1a32';
    }

    // ---- 地面 ----
    ctx.fillStyle = '#1c1c30';
    ctx.fillRect(0, GROUND_Y + 2, W, H - GROUND_Y);

    // 地面分割线
    ctx.strokeStyle = 'rgba(255,255,255,0.04)';
    ctx.lineWidth = 1;
    for (let x = groundOff; x < W; x += 40) {
      ctx.beginPath(); ctx.moveTo(x, GROUND_Y + 6); ctx.lineTo(x, GROUND_Y + 14); ctx.stroke();
    }

    // 地面顶部亮线
    ctx.fillStyle = 'rgba(255,255,255,0.06)';
    ctx.fillRect(0, GROUND_Y, W, 2);

    // ---- 障碍物 ----
    for (const o of obstacles) drawObstacle(o);

    // ---- 火柴人 ----
    drawStickman();

    // ---- 粒子 ----
    for (const p of particles) {
      ctx.globalAlpha = p.life;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    // ---- 影子 ----
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(stick.x, GROUND_Y + 4, 14, 4, 0, 0, Math.PI*2);
    ctx.fill();

    // ---- 游戏结束 ----
    if (gameOver) {
      ctx.fillStyle = 'rgba(0,0,0,0.55)';
      ctx.fillRect(0, 0, W, H);
      ctx.fillStyle = '#fff';
      ctx.font = 'bold 32px system-ui, sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('💀 撞倒了', W/2, 150);
      ctx.font = '20px system-ui, sans-serif';
      ctx.fillStyle = '#f0c040';
      ctx.fillText(`${score}m`, W/2, 200);
      ctx.font = '15px system-ui, sans-serif';
      ctx.fillStyle = '#999';
      ctx.fillText('点击「重新开始」再来一局', W/2, 250);
    }
  }

  // ---- 跳跃 ----
  function jump() {
    if (gameOver) return;
    if (stick.jumpsLeft > 0) {
      stick.vy = JUMP_VEL * (stick.jumpsLeft === MAX_JUMPS ? 1 : 0.85);
      stick.jumpsLeft--;
      if (stick.jumpsLeft === 0 && !stick.grounded) {
        // 二段跳粒子
        emitParticles(stick.x, stick.y, 10, 'rgba(150,200,255,0.5)', 6);
      }
    }
  }

  // ---- 循环 ----
  function gameLoop() {
    update();
    drawScene();
    requestAnimationFrame(gameLoop);
  }

  // ---- 事件 ----
  document.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
      e.preventDefault();
      jump();
    }
  });

  // 触摸 / 点击跳跃(双击触发二段跳)
  let lastTap = 0;
  canvas.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    const now = Date.now();
    if (now - lastTap < 300 && stick.jumpsLeft === 1) {
      // 快速双击视为二段跳
      jump();
      lastTap = 0;
    } else {
      jump();
      lastTap = now;
    }
  });

  document.getElementById('restartBtn').addEventListener('click', () => {
    reset();
  });

  // ---- 启动 ----
  reset();
  gameLoop();
})();
</script>
</body>
</html>

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐