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

游戏开发:2D射击游戏 - 从入门到实践详解

教程简介

射击游戏(Shoot 'em up)是最具爽快感的游戏类型之一,从经典的太空侵略者到现代的弹幕游戏,射击游戏始终拥有大量忠实玩家。本教程将带你从零实现一个完整的2D射击游戏,涵盖射击游戏设计、玩家控制、子弹系统、敌人波次、碰撞检测、爆炸效果、分数系统等核心内容。

核心概念

射击游戏设计

射击游戏的核心要素:

  • 流畅的玩家控制:八方向移动和精准射击

  • 子弹系统:玩家子弹和敌人子弹的分离管理

  • 敌人波次:按波次生成敌人,逐渐增加难度

  • 碰撞检测:子弹与敌人、敌人与玩家、敌人子弹与玩家

  • 爆炸效果:视觉反馈增强打击感

  • 分数系统:连击和倍率激励玩家

子弹系统

代码示例

class BulletSystem {
    constructor() {
        this.playerBullets = [];
        this.enemyBullets = [];
    }

    firePlayerBullet(x, y, vx, vy) {
        this.playerBullets.push({ x, y, vx, vy, w: 4, h: 12, alive: true });
    }

    fireEnemyBullet(x, y, vx, vy) {
        this.enemyBullets.push({ x, y, vx, vy, radius: 4, alive: true });
    }

    update(dt) {
        for (const b of this.playerBullets) {
            b.x += b.vx * dt;
            b.y += b.vy * dt;
            if (b.y < -20 || b.y > 620) b.alive = false;
        }
        for (const b of this.enemyBullets) {
            b.x += b.vx * dt;
            b.y += b.vy * dt;
            if (b.y < -20 || b.y > 620 || b.x < -20 || b.x > 820) b.alive = false;
        }
        this.playerBullets = this.playerBullets.filter(b => b.alive);
        this.enemyBullets = this.enemyBullets.filter(b => b.alive);
    }
}

敌人波次

代码示例

class WaveSystem {
    constructor() {
        this.waves = [];
        this.currentWave = 0;
        this.spawnTimer = 0;
    }

    addWave(enemies, delay) {
        this.waves.push({ enemies, delay, spawned: 0 });
    }

    update(dt) {
        if (this.currentWave >= this.waves.length) return;
        const wave = this.waves[this.currentWave];
        this.spawnTimer += dt;
        if (this.spawnTimer >= wave.delay && wave.spawned < wave.enemies.length) {
            this.spawnTimer = 0;
            wave.spawned++;
            return wave.enemies[wave.spawned - 1];
        }
        if (wave.spawned >= wave.enemies.length) {
            this.currentWave++;
        }
        return null;
    }
}

语法与用法

完整的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: #050510; 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 #1a1a3a; border-radius: 8px; }
        .info { margin-top: 8px; font-size: 12px; color: #444; text-align: center; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div class="info">WASD/方向键移动 | 自动射击 | 消灭所有敌人过关</div>
    <script>
        class ShooterGame {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.W = 800; this.H = 600;
                this.keys = {};
                this.state = 'menu';
                this.score = 0;
                this.lives = 3;
                this.wave = 0;
                this.combo = 0;
                this.comboTimer = 0;
                this.multiplier = 1;
                this.lastTime = 0;
                this.gameTime = 0;
                this.particles = [];
                this.playerBullets = [];
                this.enemyBullets = [];
                this.enemies = [];
                this.powerups = [];
                this.stars = [];
                this.shakeTimer = 0;
                this.shakeIntensity = 0;
                this.waveDelay = 0;
                this.waveCleared = false;

                // 生成星空
                for (let i = 0; i < 100; i++) {
                    this.stars.push({
                        x: Math.random() * this.W, y: Math.random() * this.H,
                        size: Math.random() * 2 + 0.5, speed: 20 + Math.random() * 60,
                        brightness: Math.random()
                    });
                }

                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.wave = 0;
                this.combo = 0; this.comboTimer = 0; this.multiplier = 1;
                this.playerBullets = []; this.enemyBullets = [];
                this.enemies = []; this.powerups = []; this.particles = [];
                this.player = {
                    x: this.W / 2, y: this.H - 80, w: 28, h: 32,
                    vx: 0, vy: 0, speed: 300,
                    fireRate: 0.12, fireTimer: 0,
                    invincible: 0, powerLevel: 1
                };
                this.spawnNextWave();
            }

            spawnNextWave() {
                this.wave++;
                this.waveDelay = 2;
                this.waveCleared = false;

                const enemyCount = 5 + this.wave * 3;
                for (let i = 0; i < enemyCount; i++) {
                    const types = ['basic', 'basic', 'zigzag', 'shooter'];
                    const type = this.wave > 2 ? types[Math.floor(Math.random() * types.length)] : 'basic';
                    this.enemies.push({
                        x: 50 + Math.random() * (this.W - 100),
                        y: -30 - Math.random() * 200,
                        w: 24, h: 24,
                        vx: 0, vy: 40 + Math.random() * 30 + this.wave * 5,
                        type, hp: type === 'shooter' ? 2 : 1,
                        alive: true, fireTimer: Math.random() * 2,
                        zigTimer: Math.random() * Math.PI * 2,
                        points: type === 'shooter' ? 200 : (type === 'zigzag' ? 150 : 100)
                    });
                }

                // Boss每5波
                if (this.wave % 5 === 0) {
                    this.enemies.push({
                        x: this.W / 2, y: -60,
                        w: 60, h: 50,
                        vx: 60, vy: 20,
                        type: 'boss', hp: 10 + this.wave * 2,
                        maxHp: 10 + this.wave * 2,
                        alive: true, fireTimer: 0,
                        points: 2000, phase: 0
                    });
                }
            }

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

                // 玩家移动
                let mx = 0, my = 0;
                if (this.keys['ArrowLeft'] || this.keys['KeyA']) mx = -1;
                if (this.keys['ArrowRight'] || this.keys['KeyD']) mx = 1;
                if (this.keys['ArrowUp'] || this.keys['KeyW']) my = -1;
                if (this.keys['ArrowDown'] || this.keys['KeyS']) my = 1;
                if (mx !== 0 && my !== 0) { mx *= 0.707; my *= 0.707; }
                p.vx = mx * p.speed; p.vy = my * p.speed;
                p.x += p.vx * dt; p.y += p.vy * dt;
                p.x = Math.max(p.w / 2, Math.min(p.x, this.W - p.w / 2));
                p.y = Math.max(p.h / 2, Math.min(p.y, this.H - p.h / 2));
                if (p.invincible > 0) p.invincible -= dt;

                // 自动射击
                p.fireTimer -= dt;
                if (p.fireTimer <= 0) {
                    p.fireTimer = p.fireRate;
                    this.firePlayerBullet(p.x, p.y - p.h / 2, 0, -500);
                    if (p.powerLevel >= 2) {
                        this.firePlayerBullet(p.x - 12, p.y - p.h / 2 + 5, -30, -500);
                        this.firePlayerBullet(p.x + 12, p.y - p.h / 2 + 5, 30, -500);
                    }
                    if (p.powerLevel >= 3) {
                        this.firePlayerBullet(p.x - 20, p.y - p.h / 2 + 10, -60, -480);
                        this.firePlayerBullet(p.x + 20, p.y - p.h / 2 + 10, 60, -480);
                    }
                }

                // 更新子弹
                for (const b of this.playerBullets) {
                    b.x += b.vx * dt; b.y += b.vy * dt;
                    if (b.y < -20) b.alive = false;
                }
                for (const b of this.enemyBullets) {
                    b.x += b.vx * dt; b.y += b.vy * dt;
                    if (b.y > this.H + 20 || b.y < -20 || b.x < -20 || b.x > this.W + 20) b.alive = false;
                }
                this.playerBullets = this.playerBullets.filter(b => b.alive);
                this.enemyBullets = this.enemyBullets.filter(b => b.alive);

                // 更新敌人
                for (const e of this.enemies) {
                    if (!e.alive) continue;
                    switch (e.type) {
                        case 'basic':
                            e.y += e.vy * dt;
                            break;
                        case 'zigzag':
                            e.y += e.vy * dt;
                            e.zigTimer += dt * 3;
                            e.x += Math.sin(e.zigTimer) * 120 * dt;
                            break;
                        case 'shooter':
                            e.y += e.vy * dt;
                            if (e.y > 80) e.vy = Math.max(0, e.vy - 30 * dt);
                            e.fireTimer -= dt;
                            if (e.fireTimer <= 0 && e.y > 0) {
                                e.fireTimer = 1.5 - this.wave * 0.05;
                                const angle = Math.atan2(p.y - e.y, p.x - e.x);
                                this.fireEnemyBullet(e.x, e.y + e.h / 2, Math.cos(angle) * 200, Math.sin(angle) * 200);
                            }
                            break;
                        case 'boss':
                            if (e.y < 80) { e.y += e.vy * dt; }
                            else {
                                e.x += e.vx * dt;
                                if (e.x < 40 || e.x > this.W - 40) e.vx *= -1;
                                e.fireTimer -= dt;
                                if (e.fireTimer <= 0) {
                                    e.fireTimer = 0.3;
                                    e.phase = (e.phase + 1) % 3;
                                    if (e.phase === 0) {
                                        for (let a = -0.5; a <= 0.5; a += 0.2) {
                                            this.fireEnemyBullet(e.x, e.y + e.h / 2, Math.sin(a) * 180, Math.cos(a) * 180);
                                        }
                                    } else {
                                        const angle = Math.atan2(p.y - e.y, p.x - e.x);
                                        this.fireEnemyBullet(e.x, e.y + e.h / 2, Math.cos(angle) * 250, Math.sin(angle) * 250);
                                    }
                                }
                            }
                            break;
                    }
                    if (e.y > this.H + 50 && e.type !== 'boss') e.alive = false;
                }

                // 碰撞:玩家子弹 vs 敌人
                for (const b of this.playerBullets) {
                    if (!b.alive) continue;
                    for (const e of this.enemies) {
                        if (!e.alive) continue;
                        if (b.x > e.x - e.w / 2 && b.x < e.x + e.w / 2 && b.y > e.y - e.h / 2 && b.y < e.y + e.h / 2) {
                            b.alive = false;
                            e.hp--;
                            this.spawnParticles(b.x, b.y, 3, '#fff', -40, 40, -40, 40);
                            if (e.hp <= 0) {
                                e.alive = false;
                                this.combo++;
                                this.comboTimer = 2;
                                this.multiplier = Math.min(8, 1 + Math.floor(this.combo / 5));
                                this.score += e.points * this.multiplier;
                                this.spawnExplosion(e.x, e.y, e.type === 'boss' ? 30 : 10, e.type === 'boss' ? '#f0a500' : '#e94560');
                                this.shakeTimer = e.type === 'boss' ? 0.3 : 0.05;
                                this.shakeIntensity = e.type === 'boss' ? 8 : 2;
                                if (Math.random() < 0.1) {
                                    this.powerups.push({ x: e.x, y: e.y, type: 'power', vy: 80 });
                                }
                            }
                            break;
                        }
                    }
                }

                // 碰撞:敌人子弹 vs 玩家
                if (p.invincible <= 0) {
                    for (const b of this.enemyBullets) {
                        if (!b.alive) continue;
                        const dx = b.x - p.x, dy = b.y - p.y;
                        if (Math.sqrt(dx * dx + dy * dy) < 14) {
                            b.alive = false;
                            this.playerHit();
                            break;
                        }
                    }
                    // 碰撞:敌人 vs 玩家
                    for (const e of this.enemies) {
                        if (!e.alive) continue;
                        const dx = e.x - p.x, dy = e.y - p.y;
                        if (Math.abs(dx) < (e.w + p.w) / 2.5 && Math.abs(dy) < (e.h + p.h) / 2.5) {
                            this.playerHit();
                            break;
                        }
                    }
                }

                // 道具
                for (let i = this.powerups.length - 1; i >= 0; i--) {
                    const pu = this.powerups[i];
                    pu.y += pu.vy * dt;
                    const dx = pu.x - p.x, dy = pu.y - p.y;
                    if (Math.sqrt(dx * dx + dy * dy) < 20) {
                        if (pu.type === 'power') p.powerLevel = Math.min(3, p.powerLevel + 1);
                        this.powerups.splice(i, 1);
                        this.spawnParticles(p.x, p.y, 8, '#16c79a', -60, 60, -60, 20);
                    } else if (pu.y > this.H + 20) {
                        this.powerups.splice(i, 1);
                    }
                }

                // 连击计时
                if (this.comboTimer > 0) {
                    this.comboTimer -= dt;
                    if (this.comboTimer <= 0) { this.combo = 0; this.multiplier = 1; }
                }

                // 检查波次
                this.enemies = this.enemies.filter(e => e.alive);
                if (this.enemies.length === 0 && !this.waveCleared) {
                    this.waveCleared = true;
                    this.waveDelay = 2;
                }
                if (this.waveCleared) {
                    this.waveDelay -= dt;
                    if (this.waveDelay <= 0) {
                        if (this.wave >= 10) {
                            this.state = 'win';
                        } else {
                            this.spawnNextWave();
                        }
                    }
                }

                // 粒子
                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 += 100 * dt; pt.life -= dt;
                    if (pt.life <= 0) this.particles.splice(i, 1);
                }

                // 星空
                for (const s of this.stars) {
                    s.y += s.speed * dt;
                    if (s.y > this.H) { s.y = 0; s.x = Math.random() * this.W; }
                }

                if (this.shakeTimer > 0) this.shakeTimer -= dt;
            }

            firePlayerBullet(x, y, vx, vy) {
                this.playerBullets.push({ x, y, vx, vy, alive: true });
            }

            fireEnemyBullet(x, y, vx, vy) {
                this.enemyBullets.push({ x, y, vx, vy, alive: true });
            }

            playerHit() {
                this.lives--;
                this.player.invincible = 2;
                this.player.powerLevel = Math.max(1, this.player.powerLevel - 1);
                this.combo = 0; this.multiplier = 1;
                this.spawnExplosion(this.player.x, this.player.y, 15, '#4a90d9');
                this.shakeTimer = 0.15; this.shakeIntensity = 5;
                if (this.lives <= 0) this.state = 'gameover';
            }

            spawnExplosion(x, y, count, color) {
                for (let i = 0; i < count; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = 50 + Math.random() * 150;
                    this.particles.push({
                        x, y,
                        vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
                        life: 0.3 + Math.random() * 0.5,
                        size: 2 + Math.random() * 4, color
                    });
                }
            }

            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.2 + Math.random() * 0.3,
                        size: 1 + Math.random() * 2, 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; }

                ctx.save();
                if (this.shakeTimer > 0) {
                    ctx.translate((Math.random()-0.5)*this.shakeIntensity*2, (Math.random()-0.5)*this.shakeIntensity*2);
                }

                // 背景
                ctx.fillStyle = '#050510';
                ctx.fillRect(0, 0, W, H);

                // 星空
                for (const s of this.stars) {
                    ctx.fillStyle = `rgba(255,255,255,${0.3 + s.brightness * 0.5})`;
                    ctx.fillRect(s.x, s.y, s.size, s.size);
                }

                // 玩家子弹
                ctx.fillStyle = '#4aff4a';
                for (const b of this.playerBullets) {
                    ctx.fillRect(b.x - 2, b.y - 6, 4, 12);
                }

                // 敌人子弹
                for (const b of this.enemyBullets) {
                    ctx.fillStyle = '#ff4a4a';
                    ctx.beginPath();
                    ctx.arc(b.x, b.y, 4, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = '#ff8888';
                    ctx.beginPath();
                    ctx.arc(b.x, b.y, 2, 0, Math.PI * 2);
                    ctx.fill();
                }

                // 敌人
                for (const e of this.enemies) {
                    if (!e.alive) continue;
                    ctx.save();
                    ctx.translate(e.x, e.y);

                    if (e.type === 'boss') {
                        // Boss
                        ctx.fillStyle = '#f0a500';
                        ctx.beginPath();
                        ctx.moveTo(0, -e.h / 2);
                        ctx.lineTo(e.w / 2, e.h / 4);
                        ctx.lineTo(e.w / 3, e.h / 2);
                        ctx.lineTo(-e.w / 3, e.h / 2);
                        ctx.lineTo(-e.w / 2, e.h / 4);
                        ctx.closePath();
                        ctx.fill();
                        // 血条
                        ctx.fillStyle = '#333';
                        ctx.fillRect(-30, -e.h / 2 - 12, 60, 6);
                        ctx.fillStyle = '#e94560';
                        ctx.fillRect(-30, -e.h / 2 - 12, 60 * (e.hp / e.maxHp), 6);
                    } else {
                        const colors = { basic: '#e94560', zigzag: '#533483', shooter: '#f0a500' };
                        ctx.fillStyle = colors[e.type] || '#e94560';
                        ctx.beginPath();
                        ctx.moveTo(0, e.h / 2);
                        ctx.lineTo(-e.w / 2, -e.h / 3);
                        ctx.lineTo(-e.w / 4, -e.h / 2);
                        ctx.lineTo(e.w / 4, -e.h / 2);
                        ctx.lineTo(e.w / 2, -e.h / 3);
                        ctx.closePath();
                        ctx.fill();
                        // 眼睛
                        ctx.fillStyle = '#fff';
                        ctx.beginPath(); ctx.arc(-4, -2, 3, 0, Math.PI * 2); ctx.fill();
                        ctx.beginPath(); ctx.arc(4, -2, 3, 0, Math.PI * 2); ctx.fill();
                        ctx.fillStyle = '#111';
                        ctx.beginPath(); ctx.arc(-3, -2, 1.5, 0, Math.PI * 2); ctx.fill();
                        ctx.beginPath(); ctx.arc(5, -2, 1.5, 0, Math.PI * 2); ctx.fill();
                    }
                    ctx.restore();
                }

                // 道具
                for (const pu of this.powerups) {
                    ctx.fillStyle = '#16c79a';
                    ctx.beginPath();
                    ctx.arc(pu.x, pu.y, 8, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.fillStyle = '#fff';
                    ctx.font = 'bold 10px Arial';
                    ctx.textAlign = 'center';
                    ctx.fillText('P', pu.x, pu.y + 4);
                }

                // 玩家
                const p = this.player;
                if (p.invincible <= 0 || Math.floor(p.invincible * 10) % 2 === 0) {
                    ctx.save();
                    ctx.translate(p.x, p.y);
                    // 引擎光
                    ctx.fillStyle = 'rgba(74, 144, 217, 0.3)';
                    ctx.beginPath();
                    ctx.arc(0, p.h / 2 + 5, 10 + Math.sin(this.gameTime * 20) * 3, 0, Math.PI * 2);
                    ctx.fill();
                    // 船体
                    ctx.fillStyle = '#4a90d9';
                    ctx.beginPath();
                    ctx.moveTo(0, -p.h / 2);
                    ctx.lineTo(-p.w / 2, p.h / 2);
                    ctx.lineTo(-p.w / 4, p.h / 3);
                    ctx.lineTo(p.w / 4, p.h / 3);
                    ctx.lineTo(p.w / 2, p.h / 2);
                    ctx.closePath();
                    ctx.fill();
                    // 驾驶舱
                    ctx.fillStyle = '#8ec5fc';
                    ctx.beginPath();
                    ctx.arc(0, -2, 5, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();
                }

                // 粒子
                for (const pt of this.particles) {
                    ctx.globalAlpha = Math.min(1, 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.wave}/10`, 170, 24);
                ctx.fillStyle = '#e94560';
                ctx.fillText(`${''.repeat(this.lives)}`, 310, 24);
                if (this.multiplier > 1) {
                    ctx.fillStyle = '#f0a500';
                    ctx.fillText(`x${this.multiplier}`, 420, 24);
                }
                ctx.fillStyle = '#16c79a';
                ctx.fillText(`火力: ${'I'.repeat(this.player.powerLevel)}`, 500, 24);

                if (this.waveCleared && this.waveDelay > 0) {
                    ctx.fillStyle = '#fff'; ctx.font = '24px Arial'; ctx.textAlign = 'center';
                    ctx.fillText(`第 ${this.wave + 1} 波即将到来...`, W / 2, H / 2);
                }
            }

            renderMenu(ctx, W, H) {
                ctx.fillStyle = '#050510'; ctx.fillRect(0, 0, W, H);
                for (const s of this.stars) { ctx.fillStyle = `rgba(255,255,255,${0.3+s.brightness*0.5})`; ctx.fillRect(s.x, s.y, s.size, s.size); }
                ctx.fillStyle = '#4a90d9'; ctx.font = 'bold 52px Arial'; ctx.textAlign = 'center';
                ctx.fillText('星际突击', W / 2, 180);
                ctx.fillStyle = '#e94560'; ctx.font = '20px Arial';
                ctx.fillText('2D 射击游戏', W / 2, 220);
                ctx.fillStyle = '#fff'; ctx.font = '18px Arial';
                ctx.fillText('点击或按空格开始', W / 2, 350);
                ctx.fillStyle = '#666'; ctx.font = '13px monospace';
                ctx.fillText('WASD/方向键移动 | 自动射击 | 共10波', W / 2, 430);
            }

            renderGameOver(ctx, W, H) {
                ctx.fillStyle = '#0a0505'; 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.wave}`, W / 2, 320);
                ctx.fillStyle = '#888'; ctx.font = '16px Arial';
                ctx.fillText('点击或按空格返回', W / 2, 420);
            }

            renderWin(ctx, W, H) {
                ctx.fillStyle = '#050a05'; 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.fillStyle = '#888'; ctx.font = '16px Arial';
                ctx.fillText('点击或按空格返回', W / 2, 420);
            }

            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 ShooterGame();
        game.start();
    </script>
</body>
</html>

浏览器兼容性

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

注意事项与最佳实践

  1. 子弹对象池:频繁创建销毁子弹对象会触发GC,使用对象池复用。

  2. 弹幕可见性:敌人子弹应使用与背景对比明显的颜色,确保玩家能看清。

  3. 碰撞体小于视觉体:玩家碰撞体应略小于视觉表现,增加容错。

  4. 波次节奏:波次间给予短暂休息,避免持续高压。

  5. 连击倍率:连击系统激励玩家持续击杀,但倍率应有上限。

  6. 火力递增:拾取道具逐步增强火力,给玩家成长感。

  7. Boss战设计:Boss应有明显的攻击模式切换和血条显示。

  8. 性能限制:限制同屏子弹和粒子数量,避免帧率下降。

常见问题与解决方案

问题1:子弹数量过多导致卡顿

解决方案:限制最大子弹数,超出时移除最旧的子弹。

问题2:敌人弹幕无法躲避

解决方案:降低弹幕密度,增加弹幕间隙,使用可辨认的弹道颜色。

问题3:玩家碰撞判定过严

解决方案:缩小玩家碰撞体,仅用中心点判定。

问题4:Boss战缺乏策略性

解决方案:设计Boss攻击模式切换,暴露弱点窗口。

问题5:火力升级后游戏太简单

解决方案:随波次增加敌人数量和血量,平衡火力增长。

总结

本教程带你实现了一个完整的2D射击游戏。我们学习了射击游戏的核心设计,实现了自动射击和火力升级系统,设计了多种敌人类型(基础、蛇行、射击、Boss),实现了波次生成和连击倍率系统,添加了爆炸粒子和屏幕震动等视觉反馈。

关键要点:

  • 子弹系统分离管理玩家和敌人子弹

  • 波次系统控制敌人生成节奏

  • 连击倍率激励玩家持续击杀

  • 碰撞体应小于视觉体增加容错

  • Boss战需要攻击模式切换和弱点窗口

  • 火力升级给予玩家成长感

  • 屏幕震动和粒子效果增强打击感

  • 限制子弹和粒子数量保证性能

掌握这些技术后,你将能够开发出节奏紧凑、打击感强的2D射击游戏。

常见问题

什么是2D射击游戏?

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

如何学习2D射击游戏的实际应用?

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

2D射击游戏有哪些注意事项?

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

2D射击游戏适合初学者吗?

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

2D射击游戏的核心要点是什么?

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

标签: 弹幕 碰撞检测 Shooter HTML5游戏 Canvas 物理引擎 内存管理 射击游戏

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

本文涉及AI创作

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

list快速访问

上一篇: 游戏开发:2D弹球游戏 - 从入门到实践详解 下一篇: 游戏开发:2D益智游戏 - 从入门到实践详解

poll相关推荐