🎮ArcadeLab

JUMP简单跳一跳

by RocketTiger92
748 lines30.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, maximum-scale=1.0, user-scalable=no">
    <title>JUMP简单跳一跳</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        /* 核心修改:改为纵向排列布局 */
        html, body {
            width: 100%;
            min-height: 100vh; /* 保证能包裹下方额外元素 */
            background: #E0E0E0;
            font-family: "Comic Sans MS", "Marker Felt", "Chalkboard SE", "华文细黑", sans-serif;
            user-select: none;
            -webkit-user-select: none;
            -webkit-tap-highlight-color: transparent;
            display: flex;
            flex-direction: column; /* 垂直堆叠 */
            justify-content: center;
            align-items: center;
            overflow-y: auto; /* 允许屏幕过短时滚动 */
            padding: 20px 0; /* 顶部和底部留白,避免阴影被切 */
        }

        .game-container {
            position: relative;
            width: 90vw;
            max-width: 850px;
            height: 50vh;
            max-height: 480px;
            aspect-ratio: 16 / 9;
            background-color: #ffffff;
            /* 纸张横线 */
            background-image: repeating-linear-gradient(
                to bottom,
                transparent,
                transparent 49px,
                #ebebeb 49px,
                #ebebeb 50px
            );
            border: 3px solid #000000;
            border-radius: 20px;
            box-shadow: 8px 8px 0 #00000020;
            overflow: hidden;
            touch-action: none;
            flex-shrink: 0; /* 防止被压缩 */
        }
         .game-wrapper:active { transform: scale(0.995); }
        #gameCanvas {
            display: block;
            width: 100%;
            height: 100%;
            background: transparent;
            touch-action: none;
        }

        /* 遮罩层 */
        .overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background: rgba(255, 255, 255, 0.85);
            z-index: 10;
            text-align: center;
            cursor: pointer;
            border-radius: 18px;
        }

        .overlay.hidden { display: none; }

        .overlay h1 {
            font-size: 48px;
            font-weight: bold;
            margin-bottom: 20px;
            letter-spacing: 4px;
            color: #000000;
        }

        .overlay h2 {
            font-size: 36px;
            font-weight: bold;
            margin-bottom: 15px;
            color: #000000;
        }

        .overlay p {
            font-size: 16px;
            margin: 6px 0;
            color: #333333;
        }

        .overlay .tip {
            margin-top: 20px;
            font-size: 15px;
            color: #666666;
            animation: blink 1.5s infinite;
        }

        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.4; }
        }

        .score-display {
            position: absolute;
            top: 20px;
            left: 20px;
            background: #ffffff;
            border: 2px solid #000000;
            border-radius: 20px;
            padding: 0 18px;
            font-size: 28px;
            font-weight: bold;
            color: #000000;
            z-index: 5;
            min-width: 30px;
            text-align: center;
            line-height: 1.4;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }

        /* 核心修改:蓄力条移除绝对定位,放到画框外面 */
        .charge-bar {
            position: relative; /* 改为普通文档流 */
            margin-top: 25px;   /* 与画框保持间距 */
            width: 200px;
            height: 10px;
            border: 2px solid #000000;
            border-radius: 5px;
            background: #ffffff;
            opacity: 0;
            transition: opacity 0.1s;
            flex-shrink: 0;
            align-self: center;
        }
        .charge-bar.active { opacity: 1; }
        .charge-fill {
            width: 0%;
            height: 100%;
            background: #000000;
            border-radius: 3px;
            transition: width 0.05s linear;
        }

        .shake {
            animation: canvasShake 0.2s ease-in-out;
        }
        @keyframes canvasShake {
            0%, 100% { transform: translate(0, 0); }
            25% { transform: translate(-2px, -2px); }
            75% { transform: translate(2px, 2px); }
        }

        @media (max-width: 768px) {
            .overlay h1 { font-size: 32px; }
            .overlay h2 { font-size: 26px; }
            .overlay p { font-size: 13px; }
            .score-display { font-size: 22px; padding: 0 14px; top: 12px; left: 12px; }
            .charge-bar { width: 150px; margin-top: 15px; }
        }
    </style>
</head>
<body>
    <!-- 游戏画框 -->
    <div class="game-container">
        <canvas id="gameCanvas"></canvas>
        
        <div id="startScreen" class="overlay">
            <h1>简单跳一跳</h1> 
            <p>长按 W 键 / 按住屏幕 蓄力</p>
            <p>松开跳跃,落在柱子上方</p>
            <p class="tip">点击 或 按空格键开始</p>
        </div>
        
        <div id="gameOverScreen" class="overlay hidden">
            <h2>游戏结束</h2>
            <p>得分:<span id="finalScore">0</span></p>
            <p>最高分:<span id="bestScore">0</span></p>
            <p class="tip">点任意处重新开始</p>
        </div>
        
        <div id="scoreDisplay" class="score-display">0</div>
    </div>

    <!-- 蓄力进度条(移到画框正下方) -->
    <div id="chargeBar" class="charge-bar">
        <div id="chargeFill" class="charge-fill"></div>
    </div>
    
    <script>
        const CONFIG = {
            gravity: 0.6,
            maxChargeTime: 1000,
            minJumpPower: 7,
            maxJumpPower: 16,
            jumpAngle: -45,
            
            pillarMinWidth: 27,
            pillarMaxWidth: 90,
            pillarMinGap: 60,
            pillarMaxGap: 180,
            pillarMinHeight: 160,
            pillarMaxHeight: 100,
            
            playerWidth: 20,
            playerHeight: 40,
            
            cameraSmooth: 0.08,
        };

        const GameState = {
            MENU: 'menu',
            PLAYING: 'playing',
            CHARGING: 'charging',
            JUMPING: 'jumping',
            GAMEOVER: 'gameover'
        };

        class Game {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                
                this.startScreen = document.getElementById('startScreen');
                this.gameOverScreen = document.getElementById('gameOverScreen');
                this.scoreDisplay = document.getElementById('scoreDisplay');
                this.finalScoreEl = document.getElementById('finalScore');
                this.bestScoreEl = document.getElementById('bestScore');
                this.chargeBar = document.getElementById('chargeBar');
                this.chargeFill = document.getElementById('chargeFill');
                
                this.state = GameState.MENU;
                this.score = 0;
                this.bestScore = parseInt(localStorage.getItem('jumpBestScore')) || 0;
                
                this.cameraX = 0;
                this.targetCameraX = 0;
                this.cameraLocked = true;
                
                this.chargeStartTime = 0;
                this.chargePower = 0;
                
                this.player = null;
                this.pillars = [];
                this.audioContext = null;
                
                // 记录缩放比例与逻辑宽高
                this.scale = 1;
                this.logicalWidth = 0;
                this.logicalHeight = 0;
                this.dpr = 1;
                
                this.init();
            }
            
            init() {
                this.resizeCanvas();
                window.addEventListener('resize', () => this.resizeCanvas());
                this.bindEvents();
                this.initAudio();
                this.gameLoop();
            }
            
            // ========== 高清渲染且不破坏坐标 ==========
            resizeCanvas() {
                this.dpr = window.devicePixelRatio || 1;
                const rect = this.canvas.parentElement.getBoundingClientRect();
                
                this.logicalWidth = rect.width;
                this.logicalHeight = rect.height;
                
                this.canvas.width = this.logicalWidth * this.dpr;
                this.canvas.height = this.logicalHeight * this.dpr;
                
                this.canvas.style.width = this.logicalWidth + 'px';
                this.canvas.style.height = this.logicalHeight + 'px';
                
                this.ctx.setTransform(1, 0, 0, 1, 0, 0);
                this.ctx.scale(this.dpr, this.dpr);
                
                this.scale = this.logicalWidth / 850;
            }
            
            bindEvents() {
                const startGameHandler = () => {
                    if (this.state === GameState.MENU || this.state === GameState.GAMEOVER) this.startGame();
                };
                this.startScreen.addEventListener('click', startGameHandler);
                this.gameOverScreen.addEventListener('click', startGameHandler);
                this.startScreen.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    startGameHandler();
                }, { passive: false });
                this.gameOverScreen.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    startGameHandler();
                }, { passive: false });

                document.addEventListener('keydown', (e) => {
                    if (e.code === 'Space') {
                        e.preventDefault();
                        if (this.state === GameState.MENU || this.state === GameState.GAMEOVER) startGameHandler();
                    }
                    if (e.code === 'KeyW' && this.state === GameState.PLAYING) {
                        e.preventDefault();
                        this.startCharge();
                    }
                });
                
                document.addEventListener('keyup', (e) => {
                    if (e.code === 'KeyW' && this.state === GameState.CHARGING) {
                        e.preventDefault();
                        this.releaseJump();
                    }
                });
                
                const startChargeHandler = (e) => {
                    e.preventDefault();
                    if (this.state === GameState.PLAYING) this.startCharge();
                };
                this.canvas.addEventListener('mousedown', startChargeHandler);
                this.canvas.addEventListener('touchstart', startChargeHandler, { passive: false });
                
                const releaseHandler = (e) => {
                    e.preventDefault();
                    if (this.state === GameState.CHARGING) this.releaseJump();
                };
                document.addEventListener('mouseup', releaseHandler);
                this.canvas.addEventListener('touchend', releaseHandler, { passive: false });
            }
            
            initAudio() {
                try {
                    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
                } catch (e) {
                    console.log('Web Audio API not supported');
                }
            }
            
            playSound(type) {
                if (!this.audioContext) return;
                const oscillator = this.audioContext.createOscillator();
                const gainNode = this.audioContext.createGain();
                oscillator.connect(gainNode);
                gainNode.connect(this.audioContext.destination);
                switch(type) {
                    case 'charge':
                        oscillator.frequency.setValueAtTime(200, this.audioContext.currentTime);
                        oscillator.frequency.exponentialRampToValueAtTime(600, this.audioContext.currentTime + 0.1);
                        gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
                        gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
                        oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.1); break;
                    case 'jump':
                        oscillator.frequency.setValueAtTime(400, this.audioContext.currentTime);
                        oscillator.frequency.exponentialRampToValueAtTime(800, this.audioContext.currentTime + 0.15);
                        gainNode.gain.setValueAtTime(0.15, this.audioContext.currentTime);
                        gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
                        oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.15); break;
                    case 'land':
                        oscillator.type = 'square';
                        oscillator.frequency.setValueAtTime(150, this.audioContext.currentTime);
                        oscillator.frequency.exponentialRampToValueAtTime(80, this.audioContext.currentTime + 0.08);
                        gainNode.gain.setValueAtTime(0.12, this.audioContext.currentTime);
                        gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.08);
                        oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.08); break;
                    case 'gameover':
                        oscillator.type = 'sawtooth';
                        oscillator.frequency.setValueAtTime(300, this.audioContext.currentTime);
                        oscillator.frequency.exponentialRampToValueAtTime(100, this.audioContext.currentTime + 0.4);
                        gainNode.gain.setValueAtTime(0.15, this.audioContext.currentTime);
                        gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.4);
                        oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.4); break;
                }
            }
            
            vibrate(duration = 50) {
                if (navigator.vibrate) navigator.vibrate(duration);
                this.canvas.classList.add('shake');
                setTimeout(() => this.canvas.classList.remove('shake'), 200);
            }
            
            startGame() {
                this.score = 0;
                this.cameraX = 0;
                this.targetCameraX = 0;
                this.cameraLocked = true;
                this.pillars = [];
                this.state = GameState.PLAYING;
                
                this.startScreen.classList.add('hidden');
                this.gameOverScreen.classList.add('hidden');
                this.scoreDisplay.textContent = '0';
                
                this.generateFirstPillar();
                
                const firstPillar = this.pillars[0];
                this.player = {
                    x: firstPillar.x + firstPillar.width / 2 - CONFIG.playerWidth * this.scale / 2,
                    y: firstPillar.topY - CONFIG.playerHeight * this.scale,
                    vx: 0, vy: 0,
                    width: CONFIG.playerWidth * this.scale,
                    height: CONFIG.playerHeight * this.scale,
                    squash: 1,
                    currentPillarIndex: 0
                };
                
                this.generateNextPillar();
                if (this.audioContext && this.audioContext.state === 'suspended') this.audioContext.resume();
            }
            
            generateFirstPillar() {
                const baseY = this.logicalHeight;
                const height = 120 * this.scale;
                this.pillars.push({
                    x: 80 * this.scale,
                    topY: baseY - height,
                    width: 30 * this.scale,
                    height: height,
                    bottomY: baseY
                });
            }
            
            generateNextPillar() {
                const lastPillar = this.pillars[this.pillars.length - 1];
                const baseY = this.logicalHeight;
                
                const gap = (CONFIG.pillarMinGap + Math.random() * (CONFIG.pillarMaxGap - CONFIG.pillarMinGap)) * this.scale;
                const width = (CONFIG.pillarMinWidth + Math.random() * (CONFIG.pillarMaxWidth - CONFIG.pillarMinWidth)) * this.scale;
                const height = (CONFIG.pillarMinHeight + Math.random() * (CONFIG.pillarMaxHeight - CONFIG.pillarMinHeight)) * this.scale;
                const topY = baseY - height;
                
                this.pillars.push({
                    x: lastPillar.x + lastPillar.width + gap,
                    topY: topY,
                    width: width,
                    height: height,
                    bottomY: baseY
                });
            }
            
            startCharge() {
                this.state = GameState.CHARGING;
                this.cameraLocked = true;
                this.chargeStartTime = performance.now();
                this.chargePower = 0;
                this.chargeBar.classList.add('active');
                this.playSound('charge');
                this.vibrate(30);
            }
            
            releaseJump() {
                const chargeTime = performance.now() - this.chargeStartTime;
                const chargeRatio = Math.min(chargeTime / CONFIG.maxChargeTime, 1);
                const power = CONFIG.minJumpPower + (CONFIG.maxJumpPower - CONFIG.minJumpPower) * chargeRatio;
                const angleRad = CONFIG.jumpAngle * Math.PI / 180;
                
                this.player.vx = Math.cos(angleRad) * power * this.scale * 0.8;
                this.player.vy = Math.sin(angleRad) * power * this.scale * 0.8;
                
                this.state = GameState.JUMPING;
                this.cameraLocked = false;
                this.chargeBar.classList.remove('active');
                this.chargeFill.style.width = '0%';
                this.player.squash = 1;
                this.playSound('jump');
            }
            
            updateCharge() {
                const chargeTime = performance.now() - this.chargeStartTime;
                const chargeRatio = Math.min(chargeTime / CONFIG.maxChargeTime, 1);
                this.chargePower = chargeRatio;
                this.chargeFill.style.width = (chargeRatio * 100) + '%';
                this.player.squash = 1 - chargeRatio * 0.2;
            }
            
            updatePlayer() {
                if (this.state !== GameState.JUMPING) return;
                
                this.player.vy += CONFIG.gravity * this.scale;
                this.player.x += this.player.vx;
                this.player.y += this.player.vy;
                
                this.checkCollisions();
                this.targetCameraX = this.player.x - this.logicalWidth * 0.3;
            }
            
            checkCollisions() {
                const player = this.player;
                const playerBottom = player.y + player.height;
                const playerLeft = player.x;
                const playerRight = player.x + player.width;
                
                for (let i = 0; i < this.pillars.length; i++) {
                    const pillar = this.pillars[i];
                    
                    if (pillar.x + pillar.width < player.x - 50 * this.scale) continue;
                    if (pillar.x > player.x + 300 * this.scale) break;
                    
                    if (playerRight > pillar.x && playerLeft < pillar.x + pillar.width) {
                        if (player.vy > 0 && playerBottom >= pillar.topY && playerBottom <= pillar.topY + 20 * this.scale) {
                            this.landOnPillar(i, pillar);
                            return;
                        }
                        if (playerBottom > pillar.topY + 10 * this.scale) {
                            if (playerRight > pillar.x && playerLeft < pillar.x && player.x + player.width / 2 < pillar.x) {
                                this.gameOver();
                                return;
                            }
                        }
                    }
                }
                if (player.y > this.logicalHeight + 100 * this.scale) this.gameOver();
            }
            
            landOnPillar(pillarIndex, pillar) {
                if (pillarIndex > this.player.currentPillarIndex) {
                    this.score += pillarIndex - this.player.currentPillarIndex;
                    this.scoreDisplay.textContent = this.score;
                    this.player.currentPillarIndex = pillarIndex;
                    this.generateNextPillar();
                }
                
                this.player.y = pillar.topY - this.player.height;
                this.player.vx = 0;
                this.player.vy = 0;
                this.state = GameState.PLAYING;
                this.cameraLocked = true;
                
                this.player.squash = 0.7;
                setTimeout(() => { if (this.player) this.player.squash = 1; }, 100);
                
                this.playSound('land');
                this.vibrate(40);
            }
            
            gameOver() {
                this.state = GameState.GAMEOVER;
                this.cameraLocked = true;
                if (this.score > this.bestScore) {
                    this.bestScore = this.score;
                    localStorage.setItem('jumpBestScore', this.bestScore.toString());
                }
                this.finalScoreEl.textContent = this.score;
                this.bestScoreEl.textContent = this.bestScore;
                this.gameOverScreen.classList.remove('hidden');
                this.playSound('gameover');
                this.vibrate(200);
            }
            
            updateCamera() {
                if (!this.cameraLocked) {
                    this.cameraX += (this.targetCameraX - this.cameraX) * CONFIG.cameraSmooth;
                }
                if (this.cameraX < 0) this.cameraX = 0;
            }
            
            update() {
                if (this.state === GameState.CHARGING) this.updateCharge();
                if (this.state === GameState.JUMPING) this.updatePlayer();
                this.updateCamera();
            }
            
            // ======================== 绘制区域 ========================
            
            drawPlayer() {
                if (!this.player) return;
                
                const ctx = this.ctx;
                const p = this.player;
                const screenX = p.x - this.cameraX;
                
                const squash = p.squash || 1;
                const width = p.width;
                const height = p.height * squash;
                const baseY = p.y + p.height - height;
                const centerX = screenX + width / 2;
                
                ctx.save();
                ctx.strokeStyle = '#000000';
                ctx.lineWidth = 2 * this.scale;
                ctx.lineCap = 'round';
                ctx.lineJoin = 'round';
                
                const headRadius = 11 * this.scale;
                const headY = baseY + headRadius;
                ctx.beginPath();
                ctx.arc(centerX, headY, headRadius, 0, Math.PI * 2);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.arc(centerX + 1 * this.scale, headY + 3 * this.scale, 4 * this.scale, 0, Math.PI);
                ctx.stroke();
                
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(centerX - 4 * this.scale, headY - 2 * this.scale, 1.5 * this.scale, 0, Math.PI * 2);
                ctx.fill();
                ctx.beginPath();
                ctx.arc(centerX + 6 * this.scale, headY - 2 * this.scale, 1.5 * this.scale, 0, Math.PI * 2);
                ctx.fill();
                
                const bodyTop = headY + headRadius + 2 * this.scale;
                const bodyBottom = baseY + height * 0.7;
                ctx.beginPath();
                ctx.moveTo(centerX, bodyTop);
                ctx.lineTo(centerX, bodyBottom);
                ctx.stroke();
                
                const armY = bodyTop + (bodyBottom - bodyTop) * 0.3;
                const armLength = 8 * this.scale;
                ctx.beginPath();
                if (this.state === GameState.CHARGING) {
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength, armY - 8 * this.scale);
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength, armY - 8 * this.scale);
                } else if (this.state === GameState.JUMPING) {
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength - 2 * this.scale, armY - 4 * this.scale);
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength + 2 * this.scale, armY - 4 * this.scale);
                } else {
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength, armY + 5 * this.scale);
                    ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength, armY + 5 * this.scale);
                }
                ctx.stroke();

                const legTop = bodyBottom;
                const legBottom = baseY + height;
                const legSpread = 5 * this.scale;
                ctx.beginPath();
                if (this.state === GameState.JUMPING) {
                    ctx.moveTo(centerX, legTop); ctx.lineTo(centerX - legSpread, legBottom - 5 * this.scale);
                    ctx.moveTo(centerX, legTop); ctx.lineTo(centerX + legSpread, legBottom - 5 * this.scale);
                } else {
                    ctx.moveTo(centerX, legTop); ctx.lineTo(centerX - legSpread, legBottom);
                    ctx.moveTo(centerX, legTop); ctx.lineTo(centerX + legSpread, legBottom);
                }
                ctx.stroke();
                
                ctx.restore();
            }
            
            drawPillars() {
                const ctx = this.ctx;
                
                ctx.save();
                ctx.strokeStyle = '#000000';
                ctx.fillStyle = '#ffffff';
                ctx.lineWidth = 2 * this.scale;
                
                for (const pillar of this.pillars) {
                    const screenX = pillar.x - this.cameraX;
                    // 修复了此处:将 this.canvas.width 替换为逻辑宽度 this.logicalWidth
                    if (screenX + pillar.width < -50 * this.scale || screenX > this.logicalWidth + 50 * this.scale) continue;
                    
                    const cX = screenX + pillar.width / 2;
                    
                    // 1. 画主体柱子
                    ctx.beginPath();
                    ctx.rect(screenX, pillar.topY, pillar.width, pillar.height);
                    ctx.fill();
                    ctx.stroke();

                    // 2. 中间竖线 (没到顶)
                   
                    ctx.save();          // 保存当前线宽状态
                    ctx.lineWidth = 2.5;   // 修改粗细:1=更细,3=更粗,默认你柱子边框是2

                    const lineTop = pillar.topY + 10 * this.scale;
                    ctx.beginPath();
                    ctx.moveTo(cX, lineTop);
                    ctx.lineTo(cX, pillar.bottomY);
                    ctx.stroke();

                    ctx.restore();       // 恢复原本的线宽,不影响后面的线条


                    // ======= 3. 链锯一样的锯齿边缘 =======
                    const sawWidth = 6 * this.scale;  // 锯齿宽度
                    const sawHeight = 8 * this.scale; // 锯齿高度
                    const gap = 14 * this.scale;       // 间距
                    let yPos = pillar.bottomY - 20 * this.scale;
                    let count = 0;
                    
                    // 循环生成链锯般错综的小方块
                    while (yPos > pillar.topY + 4 * this.scale && count < 10) {
                        // 错位排列(像链锯齿交错)
                        let offsetLeft = (count % 2 === 0) ? 0 : (4 * this.scale);
                        let offsetRight = (count % 2 === 0) ? (4 * this.scale) : 0;

                        // 左侧锯齿块
                        ctx.fillRect(screenX - sawWidth - offsetLeft, yPos - sawHeight/2, sawWidth, sawHeight);
                        ctx.strokeRect(screenX - sawWidth - offsetLeft, yPos - sawHeight/2, sawWidth, sawHeight);
                        
                        // 右侧锯齿块(和左侧错开)
                        ctx.fillRect(screenX + pillar.width + offsetRight, yPos - sawHeight/2, sawWidth, sawHeight);
                        ctx.strokeRect(screenX + pillar.width + offsetRight, yPos - sawHeight/2, sawWidth, sawHeight);

                        yPos -= gap;
                        count++;
                    }
                }
                
                ctx.restore();
            }
            
            render() {
                const ctx = this.ctx;
                
                ctx.save();
                ctx.beginPath();
                ctx.rect(0, 0, this.logicalWidth, this.logicalHeight);
                ctx.clip(); 

                ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
                if (this.state === GameState.MENU) {
                    ctx.restore();
                    return;
                }
                
                this.drawPillars();
                this.drawPlayer();
                
                ctx.restore();
            }
            
            gameLoop() {
                this.update();
                this.render();
                requestAnimationFrame(() => this.gameLoop());
            }
        }
        
        window.addEventListener('load', () => {
            new Game();
        });
    </script>
</body>
</html>

Game Source: JUMP简单跳一跳

Creator: RocketTiger92

Libraries: none

Complexity: complex (748 lines, 30.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" to link back to the original. Then publish at arcadelab.ai/publish.