🎮ArcadeLab

🏃 极简线条跑酷 · 黑白版

by RocketTiger92
466 lines17.9 KB
▶ Play
<!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>
        :root { --bg: #f0f0f0; }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: #e0e0e0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: 'Courier New', 'PingFang SC', monospace;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
            cursor: pointer;
        }
        .game-wrapper {
            position: relative;
            border: 4px solid #111;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 8px 8px 0 #00000020;
            transition: transform 0.1s ease;
            max-width: 95vw;
            max-height: 90vh;
        }
        .game-wrapper:active { transform: scale(0.995); }
        canvas { display: block; max-width: 100%; height: auto; }
        .hint-bar {
            position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
            background: #fff; color: #111; padding: 6px 18px; border-radius: 20px;
            font-size: 13px; letter-spacing: 0.5px; pointer-events: none;
            border: 2px solid #111; transition: opacity 0.3s; font-weight: bold;
        }
    </style>
</head>
<body>
<div class="game-wrapper" id="gameWrapper">
    <canvas id="gameCanvas"></canvas>
    <div class="hint-bar" id="hintBar">[ 空格 / 点击 ] 跳跃</div>
</div>
<script>
    (function() {
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const hintBar = document.getElementById('hintBar');

        const WIDTH = 800, HEIGHT = 420;
        canvas.width = WIDTH; canvas.height = HEIGHT;

        function resizeCanvas() {
            const scale = Math.min((window.innerWidth * 0.95) / WIDTH, (window.innerHeight * 0.88) / HEIGHT, 1.0);
            canvas.style.width = WIDTH * scale + 'px';
            canvas.style.height = HEIGHT * scale + 'px';
        }
        resizeCanvas();
        window.addEventListener('resize', resizeCanvas);

        // -------------------- 游戏参数 --------------------
        const GROUND_Y = 340;
        const PLAYER_X = 130;
        const GRAVITY = 1800;        // 重力 px/s²
        const JUMP_VEL = -620;       // 跳跃初速度 px/s
        const SPEED = 320;           // 障碍物速度 px/s
        const SCORE_RATE = 10;       // 每秒得分
        const SPAWN_MIN = 0.5, SPAWN_MAX = 1.5;// 障碍物生成间隔范围 (秒)

        // -------------------- 状态 --------------------
        const STATE = { WAIT: 0, PLAY: 1, OVER: 2 };
        let gameState = STATE.WAIT;
        let score = 0, scoreAcc = 0;
        let playerY = GROUND_Y, playerVy = 0, onGround = true;
        let obstacles = [], spawnTimer = 0;
        let shake = 0, overAlpha = 0, restartCD = 0;
        let lastTime = performance.now();

        // -------------------- 简易音效 (保留,但不影响黑白风格) --------------------
        let audioCtx = null;
        function getCtx() {
            if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
            if (audioCtx?.state === 'suspended') audioCtx.resume();
            return audioCtx;
        }
        function beep(freq, dur, type='square', vol=0.06) {
            const ctx = getCtx(); if(!ctx) return;
            const t = ctx.currentTime;
            const o = ctx.createOscillator(), g = ctx.createGain();
            o.type = type; o.frequency.setValueAtTime(freq, t);
            g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t+dur);
            o.connect(g); g.connect(ctx.destination);
            o.start(t); o.stop(t+dur);
        }
        function sfxJump() { beep(600,0.08); setTimeout(()=>beep(800,0.06),40); }
        function sfxHit() { beep(80,0.3,'triangle',0.1); }
        function sfxScore() { beep(1200,0.04,'sine',0.03); }

        // -------------------- 障碍物生成 --------------------
        function spawn() {
            const sizes = [32, 42, 52, 62, 74];
            const h = sizes[Math.floor(Math.random()*sizes.length)];
            const w = 12 + h*0.15;
            obstacles.push({ x: WIDTH+10, y: GROUND_Y-h, w, h });
        }

        // -------------------- 碰撞检测 (收缩框) --------------------
        function hitTest(pBox, obs) {
            const m = 6;
            const px = pBox.x+m, py = pBox.y+m, pw = pBox.w-m*2, ph = pBox.h-m*2;
            const ox = obs.x - obs.w/2 + m*0.6, oy = obs.y + m*0.4, ow = obs.w - m*1.2, oh = obs.h - m*0.8;
            return px < ox+ow && px+pw > ox && py < oy+oh && py+ph > oy;
        }
        function playerBox() {
            return { x: PLAYER_X-14, y: playerY-58, w: 28, h: 58 };
        }

        // -------------------- 重置 --------------------
        function reset() {
            playerY = GROUND_Y; playerVy = 0; onGround = true;
            obstacles = []; score = 0; scoreAcc = 0; spawnTimer = 0.6;
            shake = 0; overAlpha = 0; restartCD = 0;
            lastTime = performance.now();
            gameState = STATE.PLAY;
            hintBar.style.opacity = '0';
        }

        // -------------------- 绘制函数 (极简黑白线条风格) --------------------
        function drawBackground() {
            // 纯白背景
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, 0, WIDTH, GROUND_Y);
            // 极细横线表示远方
            ctx.strokeStyle = '#ddd';
            ctx.lineWidth = 0.5;
            for (let y=40; y<GROUND_Y; y+=35) {
                ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(WIDTH,y); ctx.stroke();
            }
        }

        function drawGround() {
            // 地面:纯白底色,黑色粗线边缘
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, GROUND_Y, WIDTH, HEIGHT-GROUND_Y);
            ctx.strokeStyle = '#111';
            ctx.lineWidth = 3;
            ctx.beginPath();
            ctx.moveTo(0, GROUND_Y); ctx.lineTo(WIDTH, GROUND_Y);
            ctx.stroke();
            // 地面纹理:短斜线
            ctx.strokeStyle = '#aaa';
            ctx.lineWidth = 1;
            for (let x=0; x<WIDTH; x+=25) {
                const offset = (x*7)%30;
                ctx.beginPath();
                ctx.moveTo(x, GROUND_Y+4);
                ctx.lineTo(x-6, GROUND_Y+14);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(x+12, GROUND_Y+2);
                ctx.lineTo(x+6, GROUND_Y+12);
                ctx.stroke();
            }
            // 底部粗线
            ctx.strokeStyle = '#111';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(0, HEIGHT-1); ctx.lineTo(WIDTH, HEIGHT-1);
            ctx.stroke();
        }

        // 极简火柴人
        function drawPlayer(y, jumping) {
            const headR = 12;
            const headY = y - 58 + headR;
            const bodyTop = headY + headR;
            const bodyBottom = y - 16;
            const legSpread = jumping ? 8 : 11;
            const armY = bodyTop + 5;

            ctx.save();
            if (jumping) { ctx.translate(PLAYER_X, y); ctx.rotate(-0.07); ctx.translate(-PLAYER_X, -y); }

            // 所有绘制使用黑色线条,无填充
            ctx.strokeStyle = '#111';
            ctx.lineWidth = 2.5;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';

            // 腿
            ctx.beginPath();
            ctx.moveTo(PLAYER_X, bodyBottom);
            ctx.lineTo(PLAYER_X-legSpread, y);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(PLAYER_X, bodyBottom);
            ctx.lineTo(PLAYER_X+legSpread, y);
            ctx.stroke();

            // 身体
            ctx.beginPath();
            ctx.moveTo(PLAYER_X, bodyTop);
            ctx.lineTo(PLAYER_X, bodyBottom);
            ctx.stroke();

            // 手臂
            const armAngle = jumping ? -0.8 : 0.3;
            const armLen = 14;
            ctx.beginPath();
            ctx.moveTo(PLAYER_X, armY);
            ctx.lineTo(PLAYER_X - Math.cos(armAngle)*armLen, armY - Math.sin(armAngle)*armLen);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(PLAYER_X, armY);
            ctx.lineTo(PLAYER_X + Math.cos(armAngle)*armLen, armY - Math.sin(armAngle)*armLen);
            ctx.stroke();

            // 头部 (空心圆)
            ctx.lineWidth = 2.2;
            ctx.beginPath();
            ctx.arc(PLAYER_X, headY, headR, 0, Math.PI*2);
            ctx.stroke();

            // 眼睛 (小点)
            ctx.fillStyle = '#111';
            ctx.beginPath();
            ctx.arc(PLAYER_X-3.5, headY-2, 1.8, 0, Math.PI*2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(PLAYER_X+3.5, headY-2, 1.8, 0, Math.PI*2);
            ctx.fill();

            // 微笑
            ctx.strokeStyle = '#111';
            ctx.lineWidth = 1.5;
            ctx.beginPath();
            ctx.arc(PLAYER_X, headY+3, 4, 0.2*Math.PI, 0.8*Math.PI);
            ctx.stroke();

            ctx.restore();
        }

        // 极简仙人掌 (直线条构成)
        function drawCactus(obs) {
            const cx = obs.x, w = obs.w, h = obs.h;
            const left = cx - w/2, right = cx + w/2, top = obs.y, bottom = obs.y + h;

            ctx.strokeStyle = '#111';
            ctx.lineWidth = 3;
            ctx.lineCap = 'round';

            // 主体矩形
            ctx.beginPath();
            ctx.moveTo(left, top);
            ctx.lineTo(right, top);
            ctx.lineTo(right, bottom);
            ctx.lineTo(left, bottom);
            ctx.closePath();
            ctx.stroke();

            // 中间竖线
            ctx.lineWidth = 1.5;
            ctx.strokeStyle = '#333';
            ctx.beginPath();
            ctx.moveTo(cx, top+4);
            ctx.lineTo(cx, bottom-4);
            ctx.stroke();

            // 侧枝 (根据高度)
            if (h >= 42) {
                ctx.strokeStyle = '#111';
                ctx.lineWidth = 2;
                // 左枝
                const branchY = top + h*0.35;
                ctx.beginPath();
                ctx.moveTo(left, branchY);
                ctx.lineTo(left - w*0.55, branchY);
                ctx.lineTo(left - w*0.55, branchY - h*0.2);
                ctx.stroke();
                // 右枝 (更高的仙人掌才有)
                if (h >= 52) {
                    const branchY2 = top + h*0.5;
                    ctx.beginPath();
                    ctx.moveTo(right, branchY2);
                    ctx.lineTo(right + w*0.55, branchY2);
                    ctx.lineTo(right + w*0.55, branchY2 - h*0.18);
                    ctx.stroke();
                }
            }
            // 顶部小刺
            ctx.fillStyle = '#111';
            for (let i=-1; i<=1; i++) {
                ctx.beginPath();
                ctx.arc(cx + i*4, top-1, 1.8, 0, Math.PI*2);
                ctx.fill();
            }
        }

        // UI:左上角分数 (黑白)
        function drawUI() {
            ctx.fillStyle = '#fff';
            ctx.strokeStyle = '#111';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.roundRect(16, 14, 140, 38, 20);
            ctx.fill();
            ctx.stroke();
            ctx.fillStyle = '#111';
            ctx.font = 'bold 18px "Courier New", monospace';
            ctx.textAlign = 'left';
            ctx.fillText(`${Math.floor(score)}`, 30, 40);
        }

        // 游戏结束覆盖层
        function drawGameOver() {
            if (overAlpha <= 0) return;
            const alpha = Math.min(1, overAlpha);
            ctx.fillStyle = `rgba(255,255,255,${alpha*0.85})`;
            ctx.fillRect(0,0,WIDTH,HEIGHT);
            ctx.fillStyle = '#111';
            ctx.font = 'bold 36px "Courier New", monospace';
            ctx.textAlign = 'center';
            ctx.fillText('GAME OVER', WIDTH/2, HEIGHT/2-20);
            ctx.font = 'bold 20px "Courier New", monospace';
            ctx.fillText(`SCORE: ${Math.floor(score)}`, WIDTH/2, HEIGHT/2+30);
            const pulse = 0.7 + 0.3*Math.sin(performance.now()*0.005);
            ctx.font = '16px "Courier New", monospace';
            ctx.fillStyle = `rgba(0,0,0,${pulse})`;
            ctx.fillText('[ 空格键 ] 重新开始', WIDTH/2, HEIGHT/2+65);
        }

        // -------------------- 更新逻辑 --------------------
        function update(dt) {
            const dtClamp = Math.min(dt, 0.1);
            if (shake > 0) shake = Math.max(0, shake - dtClamp*12);
            if (restartCD > 0) restartCD = Math.max(0, restartCD - dtClamp);

            if (gameState === STATE.PLAY) {
                // 计分
                scoreAcc += dtClamp;
                const prev = Math.floor(score);
                score = scoreAcc * SCORE_RATE;
                if (Math.floor(score) > prev && Math.floor(score)%10===0 && score>0) sfxScore();

                // 重力
                if (!onGround) {
                    playerVy += GRAVITY * dtClamp;
                    playerY += playerVy * dtClamp;
                    if (playerY >= GROUND_Y) {
                        playerY = GROUND_Y; playerVy = 0; onGround = true;
                    }
                }

                // 障碍物生成与移动
                spawnTimer -= dtClamp;
                if (spawnTimer <= 0) {
                    spawn();
                    spawnTimer = SPAWN_MIN + Math.random()*(SPAWN_MAX-SPAWN_MIN);
                }
                for (const obs of obstacles) obs.x -= SPEED * dtClamp;
                while (obstacles.length && obstacles[0].x < -60) obstacles.shift();

                // 碰撞检测
                const pBox = playerBox();
                for (const obs of obstacles) {
                    if (hitTest(pBox, obs)) {
                        gameState = STATE.OVER;
                        overAlpha = 0; shake = 0.4; restartCD = 0.5;
                        sfxHit();
                        hintBar.style.opacity = '1';
                        hintBar.textContent = '[ 空格键 ] 重新开始';
                        break;
                    }
                }
            }

            if (gameState === STATE.OVER && overAlpha < 1.5) overAlpha += dtClamp*2;
        }

        // -------------------- 渲染 --------------------
        function render() {
            ctx.clearRect(0,0,WIDTH,HEIGHT);
            let sx=0, sy=0;
            if (shake>0) { sx=(Math.random()-0.5)*shake*12; sy=(Math.random()-0.5)*shake*10; }
            ctx.save();
            ctx.translate(sx, sy);

            drawBackground();
            drawGround();
            for (const obs of obstacles) drawCactus(obs);
            drawPlayer(playerY, !onGround || playerY<GROUND_Y);

            ctx.restore();
            drawUI();
            drawGameOver();

            if (gameState === STATE.WAIT) {
                ctx.fillStyle = '#111';
                ctx.font = 'bold 22px "Courier New", monospace';
                ctx.textAlign = 'center';
                ctx.fillText('点击屏幕 或 按空格键 开始', WIDTH/2, HEIGHT/2-10);
                ctx.font = '14px "Courier New", monospace';
                ctx.fillText('极简跑酷 · 黑白线条', WIDTH/2, HEIGHT/2+25);
            }
        }

        // -------------------- 循环 --------------------
        function loop(now) {
            const dt = (now - lastTime) / 1000;
            lastTime = now;
            if (dt>0 && dt<0.5) update(dt);
            render();
            requestAnimationFrame(loop);
        }

        // -------------------- 输入 --------------------
        function jumpAction() {
            if (gameState === STATE.WAIT) {
                reset();
                if (onGround) { playerVy = JUMP_VEL; onGround = false; sfxJump(); }
                return;
            }
            if (gameState === STATE.PLAY && onGround) {
                playerVy = JUMP_VEL; onGround = false; sfxJump();
            }
            if (gameState === STATE.OVER && restartCD <= 0) {
                reset();
            }
        }

        window.addEventListener('keydown', e => {
            if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') {
                e.preventDefault();
                jumpAction();
            }
        });
        canvas.addEventListener('click', e => { e.preventDefault(); jumpAction(); });
        canvas.addEventListener('touchstart', e => { e.preventDefault(); jumpAction(); }, {passive: false});
        canvas.addEventListener('dblclick', e => e.preventDefault());

        // 初始画面
        function drawInit() {
            ctx.clearRect(0,0,WIDTH,HEIGHT);
            drawBackground();
            drawGround();
            drawPlayer(GROUND_Y, false);
            drawUI();
            ctx.fillStyle = '#111';
            ctx.font = 'bold 22px "Courier New", monospace';
            ctx.textAlign = 'center';
            ctx.fillText('点击屏幕 或 按空格键 开始', WIDTH/2, HEIGHT/2-10);
            ctx.font = '14px "Courier New", monospace';
            ctx.fillText('极简跑酷 · 黑白线条', WIDTH/2, HEIGHT/2+25);
        }
        drawInit();
        hintBar.style.opacity = '1';
        hintBar.textContent = '[ 空格 / 点击 ] 跳跃';
        lastTime = performance.now();
        requestAnimationFrame(loop);
    })();
</script>
</body>
</html>

Game Source: 🏃 极简线条跑酷 · 黑白版

Creator: RocketTiger92

Libraries: none

Complexity: complex (466 lines, 17.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-rockettiger92-mspxnsc0" to link back to the original. Then publish at arcadelab.ai/publish.