🎮ArcadeLab

🏃极简线条跑酷PARKOUR

by RocketTiger92
484 lines25.0 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <!-- 页面编码声明:告诉浏览器用 UTF-8 解码,保证中文显示正常 -->
    <meta charset="UTF-8">
    <!-- viewport 设置:让页面在手机和平板上按比例缩放,适配不同设备 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 页面标题:浏览器标签页中显示的内容 -->
    <title>🏃极简线条跑酷PARKOUR</title>
    <style>
        /* :root 用来定义全局变量,统一管理样式颜色,便于后期修改 */
        :root { --bg: #f0f0f0; }
        /* 全局元素重置:清除默认 margin 和 padding,让布局更统一 */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        /* body 是页面主体,设置背景、居中布局和禁止用户选中 */
        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; /* Safari / WebKit 浏览器兼容处理 */
            cursor: pointer; /* 鼠标在页面上时显示手型,提示可点击 */
        }
        /* 游戏容器:包住canvas和提示条,作为游戏整体展示区域 */
        .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; /* 最大宽度不超过视口 95% */
            max-height: 90vh; /* 最大高度不超过视口 90% */
        }
        /* 按下容器时轻微缩小,给予交互反馈 */
        .game-wrapper:active { transform: scale(0.995); }
        /* canvas 画布样式:块级元素,宽高自适应 */
        canvas { display: block; max-width: 100%; height: auto; }
        /* 提示条:显示在游戏下方,提示用户如何操作 */
        .hint-bar {
            position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); /* 左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 负责实际绘制游戏场景(背景、地面、玩家、障碍物等) -->
    <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;

        // 自适应窗口大小:根据浏览器窗口调整 canvas 的显示尺寸,但内部真实分辨率保持不变
        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; // 地面Y坐标:角色脚底落到这个高度时算站在地面上
        const PLAYER_X = 130; // 玩家角色固定的横向位置,便于控制跳跃和碰撞
        const GRAVITY = 1800;        // 重力 px/s²:数值越大,掉落越快
        const JUMP_VEL = -620;       // 跳跃初速度 px/s:负值表示向上,速度越大跳得越高
        const SPEED = 190;           // 障碍物移动速度 px/s:越大,越快向左滑出屏幕
        const SCORE_RATE = 10;       // 每秒得分:每秒持续生存可累积分数
        const SPAWN_MIN = 0.54, SPAWN_MAX = 1.4; // 障碍物生成间隔范围(秒),随机在这个区间内生成

        // -------------------- 状态 --------------------
        const STATE = { WAIT: 0, PLAY: 1, OVER: 2 }; // 定义三种状态:等待、游戏中、结束
        let gameState = STATE.WAIT; // 当前游戏状态,初始为等待开始
        let score = 0, scoreAcc = 0; // score 是实际分数,scoreAcc 是累计时间,用于计算分数
        let playerY = GROUND_Y, playerVy = 0, onGround = true; // 玩家位置、垂直速度、是否在地面
        let obstacles = [], spawnTimer = 0; // obstacles 保存障碍物数组,spawnTimer 控制生成计时
        let shake = 0, overAlpha = 0, restartCD = 0; // shake: 屏幕抖动强度,overAlpha:结束遮罩透明度,restartCD:重启冷却
        let lastTime = performance.now(); // 记住上一帧时间,用于计算 dt(每帧时间差)

        // -------------------- 简易音效 (保留,但不影响黑白风格) --------------------
        let audioCtx = null; // 音频上下文对象,播放音效时需要它
        function getCtx() {
            if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} // 浏览器兼容:创建 AudioContext
            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); // 音量线性衰减到几乎为0
            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(60,0.35,'sawtooth',0.12,25); 
                beep(90,0.25,'triangle',0.08,40); } // 碰撞音效:低频震动感更强,模拟撞击
        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; // 两个矩形相交则返回 true
        }
        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; // 头部中心 y 坐标
            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);
        }

        // 游戏结束覆盖层:显示“GAME OVER”以及分数
        function drawGameOver() {
            if (overAlpha <= 0) return; // 结束动画未开始则不显示
            const alpha = Math.min(1, overAlpha); // 透明度限制在 0~1
            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(); // 每满 10 分播放一次得分音效

                // 重力:玩家没有落地时,持续施加向下加速度
                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.8; 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; // 震动偏移量,先置为 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(); // 画分数 UI
            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('极简跑酷PARKOUR', 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('极简跑酷JUMP', WIDTH/2, HEIGHT/2+25);
        }
        drawInit(); // 初始化界面绘制
        hintBar.style.opacity = '1'; // 显示提示栏
        hintBar.textContent = '[ 空格 / 点击 ] 跳跃'; // 设置提示文案
        lastTime = performance.now(); // 记录开始时间,方便循环计时
        requestAnimationFrame(loop); // 启动主循环,页面开始更新渲染
    })();
</script>
</body>
</html>

Game Source: 🏃极简线条跑酷PARKOUR

Creator: RocketTiger92

Libraries: none

Complexity: complex (484 lines, 25.0 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.