
!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 classgame-wrapper div classheader div classscore-box span idscoreDisplay0/spanm/div div classcontrols-row button idrestartBtn重新开始/button /div /div canvas idgameCanvas width800 height400/canvas div classfooterkbd空格/kbd kbd↑/kbd kbdW/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