pin_drop当前位置:知识文库 ❯ 图文

游戏开发:2D平台跳跃游戏 - 从入门到实践详解

教程简介

平台跳跃游戏(Platformer)是最经典的游戏类型之一,从超级马里奥到空洞骑士,这种游戏类型始终深受玩家喜爱。本教程将带你从零开始实现一个完整的2D平台跳跃游戏,涵盖平台游戏设计、角色控制(跑/跳/蹲)、平台生成、敌人AI、道具系统、关卡设计、计分系统等核心内容。通过本教程的学习,你将掌握开发完整2D平台游戏的全套技能。

核心概念

平台游戏设计

平台跳跃游戏的核心要素:

  • 精确的角色控制:响应灵敏的移动和跳跃是平台游戏的灵魂

  • 多样的平台类型:静止平台、移动平台、消失平台、弹跳平台

  • 敌人与障碍:不同行为模式的敌人和环境危险

  • 收集与成长:金币、道具、能力提升

  • 关卡节奏:难度曲线和节奏感

角色控制

代码示例

class PlayerController {
    constructor() {
        this.speed = 250;
        this.jumpForce = -480;
        this.doubleJumpForce = -400;
        this.gravity = 1200;
        this.canDoubleJump = true;
        this.coyoteTime = 0.1;     // 土狼时间:离开平台后仍可跳跃的时间窗口
        this.jumpBufferTime = 0.1;  // 跳跃缓冲:提前按跳跃键的容错
        this.coyoteTimer = 0;
        this.jumpBufferTimer = 0;
    }

    update(dt, player, keys, platforms) {
        // 水平移动
        let targetVx = 0;
        if (keys['ArrowLeft'] || keys['KeyA']) targetVx = -this.speed;
        if (keys['ArrowRight'] || keys['KeyD']) targetVx = this.speed;
        player.vx += (targetVx - player.vx) * 12 * dt;

        // 跳跃缓冲
        if (keys['Space'] || keys['ArrowUp'] || keys['KeyW']) {
            this.jumpBufferTimer = this.jumpBufferTime;
        }
        this.jumpBufferTimer -= dt;

        // 土狼时间
        if (player.onGround) {
            this.coyoteTimer = this.coyoteTime;
            this.canDoubleJump = true;
        } else {
            this.coyoteTimer -= dt;
        }

        // 跳跃
        if (this.jumpBufferTimer > 0) {
            if (this.coyoteTimer > 0) {
                player.vy = this.jumpForce;
                this.coyoteTimer = 0;
                this.jumpBufferTimer = 0;
            } else if (this.canDoubleJump) {
                player.vy = this.doubleJumpForce;
                this.canDoubleJump = false;
                this.jumpBufferTimer = 0;
            }
        }

        // 可变跳跃高度:松开跳跃键时减少上升速度
        if (!(keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && player.vy < -100) {
            player.vy *= 0.5;
        }

        // 重力
        player.vy += this.gravity * dt;

        // 更新位置
        player.x += player.vx * dt;
        player.y += player.vy * dt;

        // 平台碰撞
        player.onGround = false;
        for (const p of platforms) {
            this.resolvePlatformCollision(player, p);
        }
    }
}

敌人AI

代码示例

class EnemyAI {
    constructor(type) {
        this.type = type;
        this.patrolSpeed = 80;
        this.chaseSpeed = 150;
        this.detectRange = 200;
        this.dir = 1;
    }

    update(dt, enemy, player) {
        switch (this.type) {
            case 'patrol':
                enemy.vx = this.patrolSpeed * this.dir;
                if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) {
                    this.dir *= -1;
                }
                break;
            case 'chase':
                const dx = player.x - enemy.x;
                const dist = Math.abs(dx);
                if (dist < this.detectRange) {
                    enemy.vx = Math.sign(dx) * this.chaseSpeed;
                } else {
                    enemy.vx = this.patrolSpeed * this.dir;
                    if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) {
                        this.dir *= -1;
                    }
                }
                break;
        }
        enemy.x += enemy.vx * dt;
    }
}

语法与用法

完整的2D平台跳跃游戏

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2D平台跳跃游戏</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
        canvas { border: 2px solid #2a2a4a; border-radius: 8px; }
        .info { margin-top: 8px; font-size: 12px; color: #555; text-align: center; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div class="info">A/D或方向键移动 | W/空格跳跃(可二段跳) | 踩敌人消灭 | 收集金币 | 到达旗帜过关</div>
    <script>
        class PlatformerGame {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.W = 800;
                this.H = 600;
                this.keys = {};
                this.state = 'menu'; // menu, playing, gameover, win
                this.score = 0;
                this.lives = 3;
                this.level = 1;
                this.coins = 0;
                this.time = 0;
                this.lastTime = 0;
                this.particles = [];

                this.setupInput();
            }

            setupInput() {
                document.addEventListener('keydown', (e) => {
                    this.keys[e.code] = true;
                    if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
                    if (this.state === 'menu' && e.code === 'Space') this.startGame();
                    if ((this.state === 'gameover' || this.state === 'win') && e.code === 'Space') this.state = 'menu';
                });
                document.addEventListener('keyup', (e) => { this.keys[e.code] = false; });
                this.canvas.addEventListener('click', () => {
                    if (this.state === 'menu') this.startGame();
                    if (this.state === 'gameover' || this.state === 'win') this.state = 'menu';
                });
            }

            startGame() {
                this.state = 'playing';
                this.score = 0;
                this.lives = 3;
                this.level = 1;
                this.coins = 0;
                this.loadLevel(this.level);
            }

            loadLevel(level) {
                this.player = {
                    x: 50, y: 400, w: 24, h: 36,
                    vx: 0, vy: 0, onGround: false,
                    facing: 1, canDoubleJump: true,
                    coyoteTimer: 0, jumpBufferTimer: 0,
                    invincible: 0, animFrame: 0, animTimer: 0
                };
                this.particles = [];
                this.cameraX = 0;
                this.levelWidth = 2400 + level * 400;
                this.flagX = this.levelWidth - 80;

                // 生成平台
                this.platforms = [
                    { x: 0, y: 520, w: 300, h: 80, type: 'ground' }
                ];

                // 地面段
                let gx = 300;
                while (gx < this.levelWidth) {
                    const gw = 150 + Math.random() * 200;
                    const gap = 60 + Math.random() * 80;
                    this.platforms.push({ x: gx + gap, y: 520, w: gw, h: 80, type: 'ground' });
                    gx += gap + gw;
                }

                // 浮空平台
                for (let i = 0; i < 15 + level * 5; i++) {
                    const px = 200 + Math.random() * (this.levelWidth - 400);
                    const py = 250 + Math.random() * 220;
                    const pw = 80 + Math.random() * 100;
                    const types = ['normal', 'normal', 'normal', 'moving', 'bounce'];
                    const type = types[Math.floor(Math.random() * types.length)];
                    const plat = { x: px, y: py, w: pw, h: 16, type, origY: py, moveRange: 40 + Math.random() * 40, moveSpeed: 1 + Math.random() * 1.5 };
                    this.platforms.push(plat);
                }

                // 金币
                this.coinList = [];
                for (let i = 0; i < 20 + level * 5; i++) {
                    this.coinList.push({
                        x: 100 + Math.random() * (this.levelWidth - 200),
                        y: 200 + Math.random() * 280,
                        collected: false, bob: Math.random() * Math.PI * 2
                    });
                }

                // 敌人
                this.enemies = [];
                for (let i = 0; i < 5 + level * 3; i++) {
                    const ex = 300 + Math.random() * (this.levelWidth - 400);
                    const types = ['patrol', 'patrol', 'chase'];
                    this.enemies.push({
                        x: ex, y: 484, w: 28, h: 28,
                        vx: 0, type: types[Math.floor(Math.random() * types.length)],
                        dir: Math.random() > 0.5 ? 1 : -1,
                        patrolMin: ex - 80, patrolMax: ex + 80,
                        alive: true, animTimer: 0
                    });
                }

                // 道具
                this.powerups = [];
                if (Math.random() > 0.5) {
                    this.powerups.push({ x: this.levelWidth / 2, y: 300, type: 'heart', collected: false });
                }
            }

            update(dt) {
                if (this.state !== 'playing') return;
                this.time += dt;
                const p = this.player;

                // === 玩家控制 ===
                const speed = 260;
                const jumpForce = -500;
                const doubleJumpForce = -420;
                const gravity = 1200;

                let targetVx = 0;
                if (this.keys['ArrowLeft'] || this.keys['KeyA']) { targetVx = -speed; p.facing = -1; }
                if (this.keys['ArrowRight'] || this.keys['KeyD']) { targetVx = speed; p.facing = 1; }
                p.vx += (targetVx - p.vx) * 10 * dt;

                // 跳跃缓冲
                if (this.keys['Space'] || this.keys['ArrowUp'] || this.keys['KeyW']) {
                    if (!this._jumpKeyWasDown) {
                        p.jumpBufferTimer = 0.1;
                        this._jumpKeyWasDown = true;
                    }
                } else {
                    this._jumpKeyWasDown = false;
                }
                p.jumpBufferTimer -= dt;

                // 土狼时间
                if (p.onGround) {
                    p.coyoteTimer = 0.08;
                    p.canDoubleJump = true;
                } else {
                    p.coyoteTimer -= dt;
                }

                // 跳跃执行
                if (p.jumpBufferTimer > 0) {
                    if (p.coyoteTimer > 0) {
                        p.vy = jumpForce;
                        p.coyoteTimer = 0;
                        p.jumpBufferTimer = 0;
                        this.spawnParticles(p.x + p.w / 2, p.y + p.h, 5, '#fff', -50, 50, -100, -30);
                    } else if (p.canDoubleJump) {
                        p.vy = doubleJumpForce;
                        p.canDoubleJump = false;
                        p.jumpBufferTimer = 0;
                        this.spawnParticles(p.x + p.w / 2, p.y + p.h, 8, '#4a90d9', -80, 80, -50, 50);
                    }
                }

                // 可变跳跃高度
                if (!(this.keys['Space'] || this.keys['ArrowUp'] || this.keys['KeyW']) && p.vy < -100) {
                    p.vy *= 0.6;
                }

                // 重力
                p.vy += gravity * dt;
                if (p.vy > 600) p.vy = 600;

                // 更新位置
                p.x += p.vx * dt;
                p.y += p.vy * dt;

                // 无敌时间
                if (p.invincible > 0) p.invincible -= dt;

                // 动画
                p.animTimer += dt;
                if (Math.abs(p.vx) > 20) {
                    if (p.animTimer > 0.1) { p.animTimer = 0; p.animFrame = (p.animFrame + 1) % 4; }
                } else {
                    p.animFrame = 0;
                }

                // === 平台碰撞 ===
                p.onGround = false;
                for (const plat of this.platforms) {
                    // 移动平台
                    if (plat.type === 'moving') {
                        plat.y = plat.origY + Math.sin(this.time * plat.moveSpeed) * plat.moveRange;
                    }

                    if (p.x + p.w > plat.x && p.x < plat.x + plat.w &&
                        p.y + p.h > plat.y && p.y + p.h < plat.y + plat.h + p.vy * dt + 10 &&
                        p.vy >= 0) {
                        p.y = plat.y - p.h;
                        p.vy = 0;
                        p.onGround = true;

                        if (plat.type === 'bounce') {
                            p.vy = -650;
                            p.onGround = false;
                            this.spawnParticles(p.x + p.w / 2, plat.y, 8, '#16c79a', -60, 60, -200, -100);
                        }
                    }
                }

                // 掉落
                if (p.y > 650) {
                    this.playerDie();
                    return;
                }

                // === 金币收集 ===
                for (const coin of this.coinList) {
                    if (coin.collected) continue;
                    const dx = (p.x + p.w / 2) - coin.x;
                    const dy = (p.y + p.h / 2) - coin.y;
                    if (Math.sqrt(dx * dx + dy * dy) < 22) {
                        coin.collected = true;
                        this.coins++;
                        this.score += 100;
                        this.spawnParticles(coin.x, coin.y, 6, '#f0a500', -60, 60, -80, -20);
                    }
                }

                // === 道具收集 ===
                for (const pu of this.powerups) {
                    if (pu.collected) continue;
                    if (p.x + p.w > pu.x - 12 && p.x < pu.x + 12 && p.y + p.h > pu.y - 12 && p.y < pu.y + 12) {
                        pu.collected = true;
                        if (pu.type === 'heart') { this.lives = Math.min(this.lives + 1, 5); }
                        this.spawnParticles(pu.x, pu.y, 10, '#e94560', -80, 80, -100, -20);
                    }
                }

                // === 敌人 ===
                for (const enemy of this.enemies) {
                    if (!enemy.alive) continue;
                    enemy.animTimer += dt;

                    // AI
                    if (enemy.type === 'patrol') {
                        enemy.vx = 70 * enemy.dir;
                        if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) enemy.dir *= -1;
                    } else if (enemy.type === 'chase') {
                        const dx = p.x - enemy.x;
                        if (Math.abs(dx) < 250) {
                            enemy.vx = Math.sign(dx) * 120;
                            enemy.dir = Math.sign(dx);
                        } else {
                            enemy.vx = 60 * enemy.dir;
                            if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) enemy.dir *= -1;
                        }
                    }
                    enemy.x += enemy.vx * dt;

                    // 玩家碰撞
                    if (p.invincible <= 0 && p.x + p.w > enemy.x && p.x < enemy.x + enemy.w &&
                        p.y + p.h > enemy.y && p.y < enemy.y + enemy.h) {
                        if (p.vy > 0 && p.y + p.h < enemy.y + enemy.h / 2 + 10) {
                            // 踩杀
                            enemy.alive = false;
                            p.vy = -350;
                            this.score += 200;
                            this.spawnParticles(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2, 10, '#e94560', -100, 100, -150, -50);
                        } else {
                            // 受伤
                            this.playerHit();
                        }
                    }
                }

                // === 旗帜(通关) ===
                if (p.x + p.w > this.flagX) {
                    this.score += 1000;
                    this.level++;
                    if (this.level > 3) {
                        this.state = 'win';
                    } else {
                        this.loadLevel(this.level);
                    }
                    return;
                }

                // === 摄像机 ===
                const targetCamX = p.x - this.W / 3;
                this.cameraX += (targetCamX - this.cameraX) * 5 * dt;
                this.cameraX = Math.max(0, Math.min(this.cameraX, this.levelWidth - this.W));

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

            playerHit() {
                const p = this.player;
                if (p.invincible > 0) return;
                this.lives--;
                p.invincible = 1.5;
                p.vy = -300;
                p.vx = -p.facing * 200;
                this.spawnParticles(p.x + p.w / 2, p.y + p.h / 2, 8, '#e94560', -100, 100, -150, -50);
                if (this.lives <= 0) {
                    this.state = 'gameover';
                }
            }

            playerDie() {
                this.lives--;
                if (this.lives <= 0) {
                    this.state = 'gameover';
                } else {
                    this.player.x = 50;
                    this.player.y = 400;
                    this.player.vx = 0;
                    this.player.vy = 0;
                }
            }

            spawnParticles(x, y, count, color, vxMin, vxMax, vyMin, vyMax) {
                for (let i = 0; i < count; i++) {
                    this.particles.push({
                        x, y,
                        vx: vxMin + Math.random() * (vxMax - vxMin),
                        vy: vyMin + Math.random() * (vyMax - vyMin),
                        life: 0.4 + Math.random() * 0.4,
                        size: 2 + Math.random() * 3,
                        color
                    });
                }
            }

            render() {
                const ctx = this.ctx;
                const W = this.W, H = this.H;

                if (this.state === 'menu') {
                    this.renderMenu(ctx, W, H);
                    return;
                }
                if (this.state === 'gameover') {
                    this.renderGameOver(ctx, W, H);
                    return;
                }
                if (this.state === 'win') {
                    this.renderWin(ctx, W, H);
                    return;
                }

                // === 游戏画面 ===
                // 天空
                const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
                skyGrad.addColorStop(0, '#1a1a4a');
                skyGrad.addColorStop(0.6, '#2a3a5a');
                skyGrad.addColorStop(1, '#3a5a4a');
                ctx.fillStyle = skyGrad;
                ctx.fillRect(0, 0, W, H);

                // 远景山
                ctx.fillStyle = '#1a2a3a';
                ctx.beginPath();
                ctx.moveTo(0, 400);
                for (let x = 0; x <= W; x += 5) {
                    const wx = x + this.cameraX * 0.2;
                    ctx.lineTo(x, 350 - Math.sin(wx * 0.003) * 80 - Math.sin(wx * 0.007) * 30);
                }
                ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.fill();

                ctx.save();
                ctx.translate(-Math.round(this.cameraX), 0);

                // 平台
                for (const plat of this.platforms) {
                    if (plat.x + plat.w < this.cameraX - 50 || plat.x > this.cameraX + W + 50) continue;

                    if (plat.type === 'ground') {
                        ctx.fillStyle = '#2a4a2a';
                        ctx.fillRect(plat.x, plat.y, plat.w, plat.h);
                        ctx.fillStyle = '#3a6a3a';
                        ctx.fillRect(plat.x, plat.y, plat.w, 6);
                        // 草
                        ctx.fillStyle = '#4a8a4a';
                        for (let gx = plat.x; gx < plat.x + plat.w; gx += 8) {
                            ctx.fillRect(gx, plat.y - 2, 3, 4);
                        }
                    } else {
                        let color = '#5a4a3a';
                        if (plat.type === 'moving') color = '#4a5a6a';
                        if (plat.type === 'bounce') color = '#2a6a5a';
                        ctx.fillStyle = color;
                        ctx.fillRect(plat.x, plat.y, plat.w, plat.h);
                        ctx.fillStyle = plat.type === 'bounce' ? '#3a8a7a' : '#7a6a5a';
                        ctx.fillRect(plat.x, plat.y, plat.w, 4);
                    }
                }

                // 旗帜
                ctx.fillStyle = '#8a7a6a';
                ctx.fillRect(this.flagX, 380, 4, 140);
                ctx.fillStyle = '#e94560';
                ctx.beginPath();
                ctx.moveTo(this.flagX + 4, 380);
                ctx.lineTo(this.flagX + 40, 395);
                ctx.lineTo(this.flagX + 4, 410);
                ctx.fill();

                // 金币
                for (const coin of this.coinList) {
                    if (coin.collected) continue;
                    if (coin.x < this.cameraX - 50 || coin.x > this.cameraX + W + 50) continue;
                    const bob = Math.sin(this.time * 3 + coin.bob) * 4;
                    ctx.fillStyle = '#f0a500';
                    ctx.beginPath();
                    ctx.arc(coin.x, coin.y + bob, 8, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = '#ffd700';
                    ctx.beginPath();
                    ctx.arc(coin.x - 2, coin.y + bob - 2, 3, 0, Math.PI * 2);
                    ctx.fill();
                }

                // 道具
                for (const pu of this.powerups) {
                    if (pu.collected) continue;
                    const bob = Math.sin(this.time * 2) * 5;
                    ctx.fillStyle = '#e94560';
                    ctx.beginPath();
                    ctx.arc(pu.x, pu.y + bob, 10, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = '#fff';
                    ctx.font = 'bold 12px Arial';
                    ctx.textAlign = 'center';
                    ctx.fillText('+', pu.x, pu.y + bob + 4);
                }

                // 敌人
                for (const enemy of this.enemies) {
                    if (!enemy.alive) continue;
                    if (enemy.x < this.cameraX - 50 || enemy.x > this.cameraX + W + 50) continue;

                    ctx.save();
                    ctx.translate(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2);
                    ctx.scale(enemy.dir, 1);

                    // 身体
                    ctx.fillStyle = enemy.type === 'chase' ? '#c73652' : '#e94560';
                    ctx.beginPath();
                    ctx.arc(0, 2, 14, 0, Math.PI * 2);
                    ctx.fill();

                    // 眼睛
                    ctx.fillStyle = '#fff';
                    ctx.beginPath(); ctx.arc(3, -3, 5, 0, Math.PI * 2); ctx.fill();
                    ctx.fillStyle = '#111';
                    ctx.beginPath(); ctx.arc(5, -3, 2.5, 0, Math.PI * 2); ctx.fill();

                    // 嘴
                    ctx.strokeStyle = '#111';
                    ctx.lineWidth = 1.5;
                    ctx.beginPath();
                    ctx.arc(2, 5, 5, 0, Math.PI);
                    ctx.stroke();

                    ctx.restore();
                }

                // 玩家
                const p = this.player;
                if (p.invincible <= 0 || Math.floor(p.invincible * 10) % 2 === 0) {
                    ctx.save();
                    ctx.translate(p.x + p.w / 2, p.y + p.h / 2);
                    ctx.scale(p.facing, 1);

                    // 身体
                    ctx.fillStyle = '#4a90d9';
                    ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);

                    // 头
                    ctx.fillStyle = '#ffd5a0';
                    ctx.beginPath();
                    ctx.arc(0, -p.h / 2 - 2, 10, 0, Math.PI * 2);
                    ctx.fill();

                    // 帽子
                    ctx.fillStyle = '#e94560';
                    ctx.fillRect(-10, -p.h / 2 - 12, 20, 6);
                    ctx.fillRect(-6, -p.h / 2 - 16, 12, 6);

                    // 眼睛
                    ctx.fillStyle = '#fff';
                    ctx.beginPath(); ctx.arc(3, -p.h / 2 - 3, 3, 0, Math.PI * 2); ctx.fill();
                    ctx.fillStyle = '#111';
                    ctx.beginPath(); ctx.arc(4, -p.h / 2 - 3, 1.5, 0, Math.PI * 2); ctx.fill();

                    ctx.restore();
                }

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

                ctx.restore();

                // HUD
                ctx.fillStyle = 'rgba(0,0,0,0.5)';
                ctx.fillRect(0, 0, W, 36);
                ctx.fillStyle = '#fff';
                ctx.font = '14px monospace';
                ctx.textAlign = 'left';
                ctx.fillText(`分数: ${this.score}`, 10, 24);
                ctx.fillText(`关卡: ${this.level}/3`, 150, 24);
                ctx.fillText(`金币: ${this.coins}`, 260, 24);
                ctx.fillStyle = '#e94560';
                ctx.fillText(`${''.repeat(this.lives)}`, 370, 24);
            }

            renderMenu(ctx, W, H) {
                const grad = ctx.createLinearGradient(0, 0, 0, H);
                grad.addColorStop(0, '#0a0a2a'); grad.addColorStop(1, '#1a2a4a');
                ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);

                ctx.fillStyle = '#e94560';
                ctx.font = 'bold 52px Arial'; ctx.textAlign = 'center';
                ctx.fillText('超级冒险', W / 2, 180);
                ctx.fillStyle = '#4a90d9';
                ctx.font = '22px Arial';
                ctx.fillText('2D 平台跳跃游戏', W / 2, 220);

                ctx.fillStyle = '#fff';
                ctx.font = '18px Arial';
                ctx.fillText('点击或按空格开始', W / 2, 350);

                ctx.fillStyle = '#888';
                ctx.font = '13px monospace';
                ctx.fillText('A/D移动 | W/空格跳跃 | 踩敌人消灭', W / 2, 450);
                ctx.fillText('收集金币 | 到达旗帜过关 | 共3关', W / 2, 475);
            }

            renderGameOver(ctx, W, H) {
                ctx.fillStyle = '#1a0a0a'; ctx.fillRect(0, 0, W, H);
                ctx.fillStyle = '#e94560';
                ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center';
                ctx.fillText('游戏结束', W / 2, 200);
                ctx.fillStyle = '#fff'; ctx.font = '20px Arial';
                ctx.fillText(`最终分数: ${this.score}`, W / 2, 280);
                ctx.fillText(`收集金币: ${this.coins}`, W / 2, 320);
                ctx.fillStyle = '#888'; ctx.font = '16px Arial';
                ctx.fillText('点击或按空格返回', W / 2, 420);
            }

            renderWin(ctx, W, H) {
                ctx.fillStyle = '#0a1a0a'; ctx.fillRect(0, 0, W, H);
                ctx.fillStyle = '#16c79a';
                ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center';
                ctx.fillText('恭喜通关!', W / 2, 200);
                ctx.fillStyle = '#fff'; ctx.font = '20px Arial';
                ctx.fillText(`最终分数: ${this.score}`, W / 2, 280);
                ctx.fillText(`收集金币: ${this.coins}`, W / 2, 320);
                ctx.fillStyle = '#f0a500'; ctx.font = '16px Arial';
                ctx.fillText('你是真正的冒险家!', W / 2, 380);
                ctx.fillStyle = '#888';
                ctx.fillText('点击或按空格返回', W / 2, 440);
            }

            start() {
                this.lastTime = performance.now();
                requestAnimationFrame((t) => this.loop(t));
            }

            loop(currentTime) {
                const dt = Math.min((currentTime - this.lastTime) / 1000, 0.05);
                this.lastTime = currentTime;
                this.update(dt);
                this.render();
                requestAnimationFrame((t) => this.loop(t));
            }
        }

        const game = new PlatformerGame();
        game.start();
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 移动端Chrome 移动端Safari
Canvas 2D 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
requestAnimationFrame 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
KeyboardEvent 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
Touch Events 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
localStorage 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持

注意事项与最佳实践

  1. 土狼时间(Coyote Time):离开平台后给予短暂跳跃窗口(约0.1秒),大幅提升操作手感。

  2. 跳跃缓冲(Jump Buffer):在落地前短暂按跳跃键也能触发跳跃,提升容错。

  3. 可变跳跃高度:松开跳跃键时减少上升速度,让玩家可以精确控制跳跃高度。

  4. 加速度移动:角色起步和停止使用加速度而非瞬间速度,手感更自然。

  5. 踩杀判定:玩家从上方接触敌人时消灭敌人,需要精确的碰撞判定区域。

  6. 无敌帧:受伤后短暂无敌,避免连续受伤。

  7. 摄像机平滑跟随:摄像机使用插值跟随玩家,避免生硬的视角切换。

  8. 关卡难度曲线:从简单到复杂,逐步引入新机制。

代码规范示例

代码示例

/**
 * 平台游戏角色控制器
 * 实现精确的平台游戏操作手感
 */
class PlatformPlayerController {
    /**
     * @param {Object} config - 控制器配置
     */
    constructor(config = {}) {
        this.moveSpeed = config.moveSpeed || 260;
        this.jumpForce = config.jumpForce || -500;
        this.doubleJumpForce = config.doubleJumpForce || -420;
        this.gravity = config.gravity || 1200;
        this.maxFallSpeed = config.maxFallSpeed || 600;
        this.acceleration = config.acceleration || 10;
        this.coyoteTime = config.coyoteTime || 0.08;
        this.jumpBufferTime = config.jumpBufferTime || 0.1;
        this.jumpCutMultiplier = config.jumpCutMultiplier || 0.6;

        this._coyoteTimer = 0;
        this._jumpBufferTimer = 0;
        this._canDoubleJump = true;
        this._jumpKeyWasDown = false;
    }

    /**
     * 更新玩家物理和控制
     * @param {Object} player - 玩家对象
     * @param {number} dt - 帧间隔时间
     * @param {Object} input - 输入状态
     * @param {Array} platforms - 平台数组
     */
    update(player, dt, input, platforms) {
        this._handleHorizontal(player, dt, input);
        this._handleJump(player, dt, input);
        this._applyGravity(player, dt);
        this._updatePosition(player, dt);
        this._resolveCollisions(player, platforms);
    }

    /** @private */
    _handleHorizontal(player, dt, input) {
        let targetVx = 0;
        if (input.left) { targetVx = -this.moveSpeed; player.facing = -1; }
        if (input.right) { targetVx = this.moveSpeed; player.facing = 1; }
        player.vx += (targetVx - player.vx) * this.acceleration * dt;
    }

    /** @private */
    _handleJump(player, dt, input) {
        // 跳跃缓冲
        if (input.jumpPressed && !this._jumpKeyWasDown) {
            this._jumpBufferTimer = this.jumpBufferTime;
        }
        this._jumpKeyWasDown = input.jumpPressed;
        this._jumpBufferTimer -= dt;

        // 土狼时间
        if (player.onGround) {
            this._coyoteTimer = this.coyoteTime;
            this._canDoubleJump = true;
        } else {
            this._coyoteTimer -= dt;
        }

        // 执行跳跃
        if (this._jumpBufferTimer > 0) {
            if (this._coyoteTimer > 0) {
                player.vy = this.jumpForce;
                this._coyoteTimer = 0;
                this._jumpBufferTimer = 0;
            } else if (this._canDoubleJump) {
                player.vy = this.doubleJumpForce;
                this._canDoubleJump = false;
                this._jumpBufferTimer = 0;
            }
        }

        // 可变跳跃高度
        if (!input.jumpHeld && player.vy < -100) {
            player.vy *= this.jumpCutMultiplier;
        }
    }

    /** @private */
    _applyGravity(player, dt) {
        player.vy += this.gravity * dt;
        if (player.vy > this.maxFallSpeed) player.vy = this.maxFallSpeed;
    }

    /** @private */
    _updatePosition(player, dt) {
        player.x += player.vx * dt;
        player.y += player.vy * dt;
    }

    /** @private */
    _resolveCollisions(player, platforms) {
        player.onGround = false;
        for (const plat of platforms) {
            if (player.x + player.w > plat.x && player.x < plat.x + plat.w &&
                player.y + player.h > plat.y && player.y + player.h < plat.y + plat.h + 15 &&
                player.vy >= 0) {
                player.y = plat.y - player.h;
                player.vy = 0;
                player.onGround = true;
            }
        }
    }
}

常见问题与解决方案

问题1:角色操作手感生硬

原因:缺少加速度、土狼时间和跳跃缓冲。

解决方案:实现加速度移动、土狼时间、跳跃缓冲和可变跳跃高度。

问题2:角色卡在平台边缘

原因:碰撞检测只检查了上方碰撞。

解决方案:分别处理四个方向的碰撞,优先处理最小重叠方向。

问题3:移动平台上角色不跟随

原因:移动平台改变位置时没有带动角色。

解决方案:记录平台位移量,将位移量加到角色位置上。

问题4:敌人行为单一

原因:只有简单的巡逻AI。

解决方案:实现多种AI类型:巡逻、追踪、跳跃、飞行等。

问题5:关卡缺乏趣味性

原因:平台布局随机,缺乏设计。

解决方案:手动设计关键段落,随机填充细节,确保关卡有节奏感。

总结

本教程带你从零实现了一个完整的2D平台跳跃游戏。我们学习了平台游戏的核心设计原则,实现了精确的角色控制(包括土狼时间、跳跃缓冲、可变跳跃高度等提升手感的技巧),设计了多种平台类型(静止、移动、弹跳),实现了敌人AI(巡逻和追踪),添加了金币收集和道具系统,设计了3个关卡的完整游戏流程。

关键要点:

  • 土狼时间和跳跃缓冲大幅提升操作手感

  • 可变跳跃高度让玩家精确控制跳跃

  • 加速度移动比瞬间速度更自然

  • 踩杀判定需要精确的碰撞区域

  • 受伤后无敌帧避免连续受伤

  • 摄像机平滑跟随提升视觉体验

  • 关卡难度应从简单到复杂递进

  • 多种平台和敌人类型增加游戏趣味

掌握这些技术后,你将能够开发出操作手感优秀、内容丰富的2D平台跳跃游戏。

常见问题

什么是2D平台跳跃游戏?

2D平台跳跃游戏是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习2D平台跳跃游戏的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

2D平台跳跃游戏有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

2D平台跳跃游戏适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

2D平台跳跃游戏的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: 碰撞检测 HTML5游戏 Canvas 物理引擎 键盘输入 重力 场景管理 跳跃游戏

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 游戏开发:游戏状态管理 - 从入门到实践详解 下一篇: 游戏开发:2D弹球游戏 - 从入门到实践详解

poll相关推荐