🎮ArcadeLab

🚀 3D倪空射击 - 教孊版

by CyberComet32
1094 lines45.6 KB🛠 Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🚀 3D倪空射击 - 教孊版</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            background: #8b0000;
            overflow: hidden;
            color: #e0e0ff;
        }
        #gameCanvas {
            width: 100vw;
            height: 100vh;
            display: block;
        }
        #ui-layer {
            position: fixed;
            top: 0; left: 0;
            width: 100%; height: 100%;
            pointer-events: none;
            z-index: 10;
        }
        /* 只有按钮和匹窗可以接收錠标事件 */
        #ui-layer button, .screen { pointer-events: auto; }
        /* 分数、生呜、准星、提瀺文字䞍拊截錠标 */
        .score, .lives, .crosshair, .controls-hint, #fpsDisplay {
            pointer-events: none;
        }

        .score {
            position: absolute;
            top: 15px; left: 20px;
            font-size: 22px;
            color: #00d4ff;
            text-shadow: 0 0 10px rgba(0,212,255,0.5);
            font-weight: bold;
        }
        .lives {
            position: absolute;
            top: 15px; right: 20px;
            font-size: 22px;
        }
        .crosshair {
            position: absolute;
            top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            font-size: 36px;
            color: rgba(0,212,255,0.7);
        }
        .screen {
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background: rgba(0,0,0,0.85);
        }
        .screen h1 {
            font-size: 3em;
            color: #00d4ff;
            text-shadow: 0 0 20px rgba(0,212,255,0.5);
            margin-bottom: 20px;
        }
        .screen p {
            font-size: 1.2em;
            color: #aaa;
            margin: 8px 0;
        }
        .screen button {
            margin-top: 30px;
            padding: 15px 40px;
            font-size: 1.3em;
            border: none;
            border-radius: 12px;
            background: linear-gradient(135deg, #00d4ff, #0066ff);
            color: white;
            cursor: pointer;
            font-family: inherit;
            font-weight: bold;
            box-shadow: 0 4px 20px rgba(0,212,255,0.3);
            transition: all 0.3s;
        }
        .screen button:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 30px rgba(0,212,255,0.5);
        }
        .hidden { display: none !important; }

        .controls-hint {
            position: absolute;
            bottom: 15px;
            left: 50%;
            transform: translateX(-50%);
            font-size: 0.85em;
            color: #666;
            text-align: center;
        }
        .key {
            display: inline-block;
            background: rgba(255,255,255,0.1);
            padding: 2px 8px;
            border-radius: 4px;
            color: #00d4ff;
            font-family: monospace;
            margin: 0 2px;
        }

        #fpsDisplay {
            position: absolute;
            bottom: 15px;
            right: 20px;
            font-size: 0.8em;
            color: #444;
        }
    </style>
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
        }
    }
    </script>
</head>
<body>
    <div id="gameCanvas"></div>

    <div id="ui-layer">
        <div class="score">埗分: <span id="scoreValue">0</span></div>
        <div class="lives">❀ <span id="livesValue">3</span></div>
        <div class="crosshair">+</div>

        <div id="startScreen" class="screen">
            <h1>🚀 倪空射击</h1>
            <p>WASD 移劚飞船 | 錠标瞄准 | 巊键射击</p>
            <p>击毁红色敌人䞍芁被撞到</p>
            <button id="startBtn">匀始枞戏</button>
        </div>

        <div id="gameOverScreen" class="screen hidden">
            <h1>💥 枞戏结束</h1>
            <p>最终埗分: <span id="finalScore">0</span></p>
            <button id="restartBtn">再玩䞀次</button>
        </div>

        <div class="controls-hint">
            <span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> 移劚
            | <span class="key">錠标</span> 瞄准
            | <span class="key">å·Šé”®</span> 射击
        </div>
        <div id="fpsDisplay">FPS: --</div>
    </div>

    <script type="module">
        import * as THREE from 'three';

        // ═══════════════════════════════════════════════════════════════
        //  【第䞀诟】3D 舞台搭建
        //  每䞪 3D 枞戏郜需芁䞉䞪基本组件
        //  1. Scene场景—— 装所有䞜西的倧盒子
        //  2. Camera盞机—— 玩家的県睛决定看什么
        //  3. Renderer枲染噚—— 画家把3D画成2D星瀺圚屏幕䞊
        // ═══════════════════════════════════════════════════════════════

        // 【实验 1-1】修改背景颜色
        // 把 0x050510 改成其他颜色看看倪空背景变成什么样
        // 试试0x000000纯黑 0x001100深绿 0x110000深红
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x8b0000);

        // 【实验 1-2】修改雟的浓床
        // 第二䞪参数是雟的浓床越倧越浓
        // 试试0.001埈淡 0.005埈浓 0没有雟
        scene.fog = new THREE.FogExp2(0x8b0000, 0.002);

        // 创建盞机PerspectiveCamera 暡拟人県近倧远小
        // 参数视野角床(床), 宜高比, 最近距犻, 最远距犻
        // 【实验 1-3】修改视野角床
        // 试试30望远镜 90广角 120鱌県
        const camera = new THREE.PerspectiveCamera(
            75,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        );
        // ★★★ 修倍讟眮盞机初始䜍眮圚飞船后方 ★★★
        camera.position.set(0, 5, -15);

        // 创建枲染噚
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        document.getElementById('gameCanvas').appendChild(renderer.domElement);


        // ═══════════════════════════════════════════════════════════════
        //  【第二诟】星空背景 - 粒子系统
        //  倪空䞭有埈倚星星我们甚粒子系统䞀次性画几千颗
        //  粒子系统 = 埈倚小点每䞪点有䜍眮(x,y,z)和颜色
        // ═══════════════════════════════════════════════════════════════

        function createStarfield() {
            // 【实验 2-1】修改星星数量
            // 星星越倚越壮观䜆电脑越卡
            // 试试500埈少 5000埈倚 10000超倚可胜卡
            const starCount = 3000;

            // 准倇存傚星星䜍眮的数组
            // 每颗星星需芁3䞪数字x, y, z
            // 所以数组倧小 = 星星数量 × 3
            const positions = new Float32Array(starCount * 3);
            const colors = new Float32Array(starCount * 3);

            for (let i = 0; i < starCount; i++) {
                // 【实验 2-2】修改星星分垃范囎
                // 2000 衚瀺星星分垃圚 ±1000 的范囎内
                // 试试500星星埈近 5000星星埈远
                const range = 2000;
                positions[i * 3]     = (Math.random() - 0.5) * range;  // x
                positions[i * 3 + 1] = (Math.random() - 0.5) * range;  // y
                positions[i * 3 + 2] = (Math.random() - 0.5) * range;  // z

                // 【实验 2-3】修改星星颜色
                // 现圚星星有䞉种颜色癜色、淡蓝、淡黄
                // 试试让所有星星变成同䞀种颜色
                const colorType = Math.random();
                if (colorType < 0.7) {
                    // 癜色星星
                    colors[i * 3] = 1;     // R
                    colors[i * 3 + 1] = 1; // G
                    colors[i * 3 + 2] = 1; // B
                } else if (colorType < 0.85) {
                    // 淡蓝色星星
                    colors[i * 3] = 0.7;
                    colors[i * 3 + 1] = 0.8;
                    colors[i * 3 + 2] = 1;
                } else {
                    // 淡黄色星星
                    colors[i * 3] = 1;
                    colors[i * 3 + 1] = 0.9;
                    colors[i * 3 + 2] = 0.7;
                }
            }

            const starGeometry = new THREE.BufferGeometry();
            starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            starGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

            // 【实验 2-4】修改星星倧小
            // 试试1埈小 5埈倧 10巚倧
            const starMaterial = new THREE.PointsMaterial({
                size: 2,
                transparent: true,
                opacity: 0.8,
                vertexColors: true,  // 䜿甚我们讟眮的颜色
                blending: THREE.AdditiveBlending  // 叠加发光效果
            });

            const stars = new THREE.Points(starGeometry, starMaterial);
            scene.add(stars);
            return stars;
        }

        const starfield = createStarfield();


        // ═══════════════════════════════════════════════════════════════
        //  【第䞉诟】玩家飞船 - 甚几䜕䜓搭积朚
        //  飞船由倚䞪基本几䜕䜓组合而成
        //  - ConeGeometry圆锥→ 飞船䞻䜓
        //  - BoxGeometry长方䜓→ 翅膀
        //  - SphereGeometry球䜓→ 驟驶舱
        //  - PointLight点光源→ 匕擎发光
        // ═══════════════════════════════════════════════════════════════

        class Player {
            constructor(scene) {
                this.scene = scene;
                this.mesh = new THREE.Group();  // Group 可以装倚䞪零件

                // ========== 飞船䞻䜓 ==========
                // 【实验 3-1】修改飞船䞻䜓圢状
                // ConeGeometry(半埄, 高床, 分段数)
                // 试试ConeGeometry(0.5, 5, 4) → 曎尖曎长4蟹圢
                // 试试ConeGeometry(1.5, 2, 16) → 曎胖曎短16蟹圢
                const bodyGeo = new THREE.ConeGeometry(0.8, 3, 8);

                // 【实验 3-2】修改飞船颜色
                // 颜色栌匏0xRRGGBB红绿蓝
                // 试试0xff0000红 0x00ff00绿 0xff00ff玫
                // emissive 是自发光颜色让飞船圚暗倄也胜看见
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: 0x0088ff,           // 基础颜色蓝色
                    emissive: 0x0044aa,        // 自发光颜色
                    emissiveIntensity: 0.5,    // 发光区床
                    metalness: 0.8,            // 金属感0=塑料1=镜子
                    roughness: 0.2             // 粗糙床0=光滑1=粗糙
                });
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.rotation.x = Math.PI / 2;  // 旋蜬90床让圆锥指向前方
                body.castShadow = true;
                this.mesh.add(body);

                // ========== 巊翌 ==========
                // 【实验 3-3】修改翅膀倧小
                // BoxGeometry(宜, 高, 长)
                // 试试BoxGeometry(5, 0.3, 2) → 曎倧曎厚
                const leftWingGeo = new THREE.BoxGeometry(2.5, 0.15, 1.2);
                const wingMat = new THREE.MeshStandardMaterial({
                    color: 0x0066cc,
                    metalness: 0.9,
                    roughness: 0.3
                });
                const leftWing = new THREE.Mesh(leftWingGeo, wingMat);
                leftWing.position.set(-1.2, -0.3, 0.5);  // 䜍眮巊䞋方
                leftWing.castShadow = true;
                this.mesh.add(leftWing);

                // ========== 右翌 ==========
                const rightWing = new THREE.Mesh(leftWingGeo, wingMat);
                rightWing.position.set(1.2, -0.3, 0.5);  // 䜍眮右䞋方
                rightWing.castShadow = true;
                this.mesh.add(rightWing);

                // ========== 驟驶舱 ==========
                // 【实验 3-4】修改驟驶舱颜色
                // 试试把 0x00ffff青色改成 0xffff00黄色
                const cockpitGeo = new THREE.SphereGeometry(0.4, 8, 8);
                const cockpitMat = new THREE.MeshStandardMaterial({
                    color: 0x00ffff,
                    emissive: 0x00ffff,
                    emissiveIntensity: 0.3,
                    transparent: true,
                    opacity: 0.8
                });
                const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
                cockpit.position.set(0, 0.3, -0.5);
                this.mesh.add(cockpit);

                // ========== 匕擎发光 ==========
                // 【实验 3-5】修改匕擎光颜色
                // 试试0xff0000红色火焰 0x00ff00绿色火焰
                const engineLight = new THREE.PointLight(0x00aaff, 3, 15);
                engineLight.position.set(0, -1.5, 0);
                this.mesh.add(engineLight);

                // ========== 匕擎粒子拖尟 ==========
                this.engineParticles = this.createEngineTrail();

                // ★★★ 关键修倍让飞船倎郚朝向 +z 方向朝向盞机/玩家
                // 默讀圆锥尖端指向 -z旋蜬 180 床后指向 +z
                this.mesh.rotation.y = Math.PI;

                scene.add(this.mesh);
                this.velocity = new THREE.Vector3(0, 0, 0);

                // 【实验 3-6】修改飞船移劚速床
                // 越倧飞埗越快
                this.speed = 0.3;
            }

            createEngineTrail() {
                // 【实验 3-7】修改匕擎拖尟粒子数量
                // 试试20短拖尟 100长拖尟
                const particleCount = 50;
                const geometry = new THREE.BufferGeometry();
                const positions = new Float32Array(particleCount * 3);

                for (let i = 0; i < particleCount; i++) {
                    positions[i * 3] = 0;
                    positions[i * 3 + 1] = -2;
                    positions[i * 3 + 2] = 0;
                }

                geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

                const material = new THREE.PointsMaterial({
                    // 【实验 3-8】修改匕擎拖尟颜色
                    color: 0x00aaff,
                    size: 0.5,
                    transparent: true,
                    opacity: 0.6,
                    blending: THREE.AdditiveBlending
                });

                const particles = new THREE.Points(geometry, material);
                this.mesh.add(particles);
                return particles;
            }

            update(delta, keys, mousePos) {
                // 倄理键盘蟓入
                // 【实验 3-9】修改摩擊力
                // 0.92 衚瀺每垧保留 92% 的速床
                // 试试0.99埈滑像冰面 0.8埈快停䞋
                const friction = 0.92;

                if (keys['KeyW'] || keys['ArrowUp']) this.velocity.y += this.speed * delta * 60;
                if (keys['KeyS'] || keys['ArrowDown']) this.velocity.y -= this.speed * delta * 60;
                if (keys['KeyA'] || keys['ArrowLeft']) this.velocity.x -= this.speed * delta * 60;
                if (keys['KeyD'] || keys['ArrowRight']) this.velocity.x += this.speed * delta * 60;

                this.velocity.multiplyScalar(friction);

                // 曎新䜍眮
                this.mesh.position.add(this.velocity.clone().multiplyScalar(delta));

                // 限制飞船䞍芁飞出屏幕
                this.mesh.position.x = Math.max(-20, Math.min(20, this.mesh.position.x));
                this.mesh.position.y = Math.max(-15, Math.min(15, this.mesh.position.y));
                this.mesh.position.z = 0;

                // 錠标瞄准飞船埮埮蜬向錠标方向
                const targetRotationZ = -mousePos.x * 0.3;
                const targetRotationX = Math.PI / 2 + mousePos.y * 0.2;
                this.mesh.rotation.z += (targetRotationZ - this.mesh.rotation.z) * 0.1;
                this.mesh.rotation.x = targetRotationX;

                // 曎新匕擎拖尟
                this.updateEngineTrail();
            }

            updateEngineTrail() {
                const positions = this.engineParticles.geometry.attributes.position.array;
                const count = positions.length / 3;

                for (let i = count - 1; i > 0; i--) {
                    positions[i * 3] = positions[(i - 1) * 3] + (Math.random() - 0.5) * 0.1;
                    positions[i * 3 + 1] = positions[(i - 1) * 3 + 1] - 0.3;
                    positions[i * 3 + 2] = positions[(i - 1) * 3 + 2] + (Math.random() - 0.5) * 0.1;
                }

                positions[0] = (Math.random() - 0.5) * 0.3;
                positions[1] = -1.8;
                positions[2] = (Math.random() - 0.5) * 0.3;

                this.engineParticles.geometry.attributes.position.needsUpdate = true;
            }

            getPosition() {
                return this.mesh.position.clone();
            }
        }


        // ═══════════════════════════════════════════════════════════════
        //  【第四诟】光照系统
        //  没有光3D 䞖界就是䞀片黑
        //  䞉种灯光
        //  - AmbientLight环境光均匀照亮所有䞜西
        //  - DirectionalLight方向光像倪阳有方向产生阎圱
        //  - PointLight点光源像灯泡从䞀点向四呚发光
        // ═══════════════════════════════════════════════════════════════

        // 【实验 4-1】修改环境光颜色
        // 环境光决定敎䞪场景的基本亮床
        // 试试0xffffff纯癜 0x000000党黑只靠其他光源
        const ambientLight = new THREE.AmbientLight(0x112244, 0.5);
        scene.add(ambientLight);

        // 【实验 4-2】修改方向光暡拟恒星
        // 第䞀䞪参数是颜色第二䞪是区床
        // 试试0xffffff癜色倪阳 0xffaa00橙色倪阳
        const sunLight = new THREE.DirectionalLight(0xffeedd, 1.5);
        sunLight.position.set(50, 50, 50);
        sunLight.castShadow = true;
        scene.add(sunLight);

        // 【实验 4-3】添加曎倚装饰光源
        // 这是玫色星云光
        const nebulaLight = new THREE.PointLight(0xff00ff, 0.5, 100);
        nebulaLight.position.set(0, 0, -50);
        scene.add(nebulaLight);

        // 【实验 4-4】添加䞀䞪绿色光源
        // 取消䞋面䞉行的泚释看看有什么效果
        // const greenLight = new THREE.PointLight(0x00ff00, 1, 50);
        // greenLight.position.set(20, 10, -30);
        // scene.add(greenLight);


        // ═══════════════════════════════════════════════════════════════
        //  【第五诟】子匹系统 + 对象池
        //  子匹需芁创建 → 飞行 → 销毁
        //  劂果每发子匹郜创建新对象枞戏䌚变卡
        //  对象池预先创建䞀堆子匹重倍利甚
        // ═══════════════════════════════════════════════════════════════

        class Bullet {
            constructor() {
                this.active = false;

                // 【实验 5-1】修改子匹倧小
                // SphereGeometry(半埄, 氎平分段, 垂盎分段)
                // 试试SphereGeometry(0.5, 16, 16) → 倧子匹
                const geometry = new THREE.SphereGeometry(0.2, 6, 6);

                // 【实验 5-2】修改子匹颜色
                // 试试0xff0000红色子匹 0x00ff00绿色子匹
                const material = new THREE.MeshBasicMaterial({
                    color: 0xffee00,
                    emissive: 0xffaa00,
                    emissiveIntensity: 1
                });
                this.mesh = new THREE.Mesh(geometry, material);
                this.mesh.visible = false;
                scene.add(this.mesh);

                // 子匹拖尟
                const trailGeo = new THREE.BufferGeometry();
                const trailPos = new Float32Array(10 * 3);
                trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
                const trailMat = new THREE.PointsMaterial({
                    // 【实验 5-3】修改拖尟颜色
                    color: 0xffaa00,
                    size: 0.3,
                    transparent: true,
                    opacity: 0.5,
                    blending: THREE.AdditiveBlending
                });
                this.trail = new THREE.Points(trailGeo, trailMat);
                this.trail.visible = false;
                scene.add(this.trail);
            }

            spawn(position) {
                this.active = true;
                this.mesh.visible = true;
                this.trail.visible = true;
                this.mesh.position.copy(position);
                // ★★★ 修倍子匹向 +z 方向发射飞船倎郚方向
                this.mesh.position.z += 1;

                // 重眮拖尟
                const trailPos = this.trail.geometry.attributes.position.array;
                for (let i = 0; i < 10; i++) {
                    trailPos[i * 3] = position.x;
                    trailPos[i * 3 + 1] = position.y;
                    trailPos[i * 3 + 2] = position.z - 1;
                }
                this.trail.geometry.attributes.position.needsUpdate = true;
            }

            update(delta) {
                if (!this.active) return;

                // 【实验 5-4】修改子匹速床
                // 越倧飞埗越快
                const bulletSpeed = 3;
                const oldPos = this.mesh.position.clone();
                // ★★★ 修倍子匹向 +z 方向飞行
                this.mesh.position.z += bulletSpeed * delta * 60;

                // 曎新拖尟
                const trailPos = this.trail.geometry.attributes.position.array;
                for (let i = 9; i > 0; i--) {
                    trailPos[i * 3] = trailPos[(i - 1) * 3];
                    trailPos[i * 3 + 1] = trailPos[(i - 1) * 3 + 1];
                    trailPos[i * 3 + 2] = trailPos[(i - 1) * 3 + 2];
                }
                trailPos[0] = oldPos.x;
                trailPos[1] = oldPos.y;
                trailPos[2] = oldPos.z;
                this.trail.geometry.attributes.position.needsUpdate = true;
                this.trail.position.copy(this.mesh.position);

                // 飞出蟹界销毁
                // ★★★ 修倍子匹飞出 +z 蟹界销毁
                if (this.mesh.position.z > 100) {
                    this.destroy();
                }
            }

            destroy() {
                this.active = false;
                this.mesh.visible = false;
                this.trail.visible = false;
            }
        }

        class BulletPool {
            constructor(size) {
                // 【实验 5-5】修改对象池倧小
                // 越倧胜同时存圚的子匹越倚
                // 试试10埈少 100埈倚
                this.pool = [];
                for (let i = 0; i < size; i++) {
                    this.pool.push(new Bullet());
                }
            }

            get(position) {
                const bullet = this.pool.find(b => !b.active);
                if (bullet) {
                    bullet.spawn(position);
                    return bullet;
                }
                return null;
            }

            update(delta) {
                this.pool.forEach(b => b.update(delta));
            }

            getActive() {
                return this.pool.filter(b => b.active);
            }
        }


        // ═══════════════════════════════════════════════════════════════
        //  【第六诟】敌人系统
        //  敌人 AI 埈简单
        //  1. 圚远倄随机䜍眮生成
        //  2. 朝着玩家飞过来
        //  3. 被子匹打䞭就爆炞
        // ═══════════════════════════════════════════════════════════════

        class Enemy {
            constructor(playerPosition) {
                this.active = true;
                this.mesh = new THREE.Group();

                // 【实验 6-1】修改敌人䞻䜓圢状
                // 试试SphereGeometry(2, 8, 8) → 倧球
                // 试试BoxGeometry(2, 2, 2) → 方块敌人
                const bodyGeo = new THREE.SphereGeometry(1, 8, 8);

                // 【实验 6-2】修改敌人颜色
                // 试试0x00ff00绿色敌人 0x8800ff玫色敌人
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: 0xff2244,
                    emissive: 0xaa0000,
                    emissiveIntensity: 0.5,
                    metalness: 0.6,
                    roughness: 0.4
                });
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.castShadow = true;
                this.mesh.add(body);

                // 䟧翌
                const wingGeo = new THREE.BoxGeometry(2.5, 0.3, 1);
                const wingMat = new THREE.MeshStandardMaterial({ color: 0xcc0000 });
                const wing = new THREE.Mesh(wingGeo, wingMat);
                this.mesh.add(wing);

                // 県睛
                const eyeGeo = new THREE.SphereGeometry(0.3, 6, 6);
                const eyeMat = new THREE.MeshBasicMaterial({
                    color: 0xff0000,
                    emissive: 0xff0000,
                    emissiveIntensity: 1
                });
                const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
                leftEye.position.set(-0.4, 0.3, -0.7);
                this.mesh.add(leftEye);

                const rightEye = new THREE.Mesh(eyeGeo, eyeMat);
                rightEye.position.set(0.4, 0.3, -0.7);
                this.mesh.add(rightEye);

                // 【实验 6-3】修改敌人生成䜍眮
                // radius 越倧敌人分垃越广
                const angle = Math.random() * Math.PI * 2;
                const radius = 8 + Math.random() * 15;
                // ★★★ 修倍敌人圚飞船 +z 方向前方生成
                this.mesh.position.set(
                    Math.cos(angle) * radius,
                    (Math.random() - 0.5) * 12,
                    playerPosition.z + 40 + Math.random() * 60
                );

                // 【实验 6-4】修改敌人速床
                // 越倧飞埗越快枞戏越隟
                // 试试0.1埈慢 0.5埈快 1.0超快
                this.speed = 0.2 + Math.random() * 0.3;

                scene.add(this.mesh);
            }

            update(delta, playerPosition) {
                if (!this.active) return;

                // 朝着玩家移劚
                const direction = new THREE.Vector3().subVectors(
                    playerPosition, this.mesh.position
                ).normalize();

                this.mesh.position.add(direction.multiplyScalar(this.speed * delta * 60));

                // 让敌人面向玩家
                this.mesh.lookAt(playerPosition);

                // 【实验 6-5】修改敌人旋蜬劚画
                // 试试delta * 5蜬埈快 delta * 0.5蜬埈慢
                this.mesh.rotation.z += delta * 2;

                // 飞过玩家销毁
                // ★★★ 修倍敌人飞过飞船 -z 方向后方销毁
                if (this.mesh.position.z < playerPosition.z - 10) {
                    this.destroy();
                }
            }

            destroy() {
                this.active = false;
                scene.remove(this.mesh);
            }
        }

        class EnemyManager {
            constructor() {
                this.enemies = [];
                this.spawnTimer = 0;

                // 【实验 6-6】修改敌人生成闎隔秒
                // 越小生成越快枞戏越隟
                // 试试0.5埈快 5埈慢
                this.spawnInterval = 2;
            }

            update(delta, playerPosition) {
                this.spawnTimer += delta;
                if (this.spawnTimer >= this.spawnInterval && gameState === 'playing') {
                    this.spawnTimer = 0;
                    this.enemies.push(new Enemy(playerPosition));

                    // 随分数增加生成速床加快
                    this.spawnInterval = Math.max(0.5, 2 - score / 500);
                }

                this.enemies = this.enemies.filter(e => {
                    if (e.active) {
                        e.update(delta, playerPosition);
                        return true;
                    }
                    return false;
                });
            }

            getActive() {
                return this.enemies.filter(e => e.active);
            }

            clear() {
                this.enemies.forEach(e => e.destroy());
                this.enemies = [];
            }
        }


        // ═══════════════════════════════════════════════════════════════
        //  【第䞃诟】爆炞效果 - 粒子系统
        //  爆炾 = 埈倚小粒子从䞭心向四面八方飞出去
        //  每䞪粒子有䜍眮、速床、颜色、生呜倌
        // ═══════════════════════════════════════════════════════════════

        class Explosion {
            constructor(position, color) {
                this.active = true;
                this.life = 1.0;  // 生呜倌从1慢慢减到0

                // 【实验 7-1】修改爆炞粒子数量
                // 试试10埈少像小火花 100埈倚像倧烟花
                this.particleCount = 40;

                const geometry = new THREE.BufferGeometry();
                const positions = new Float32Array(this.particleCount * 3);
                this.velocities = [];
                this.colors = new Float32Array(this.particleCount * 3);

                const baseColor = new THREE.Color(color);

                for (let i = 0; i < this.particleCount; i++) {
                    // 所有粒子从爆炞䞭心匀始
                    positions[i * 3] = position.x;
                    positions[i * 3 + 1] = position.y;
                    positions[i * 3 + 2] = position.z;

                    // 【实验 7-2】修改粒子飞散速床
                    // 越倧爆炞越猛烈
                    const speed = 0.3 + Math.random() * 0.5;

                    // 随机方向球面均匀分垃
                    const theta = Math.random() * Math.PI * 2;
                    const phi = Math.random() * Math.PI;
                    this.velocities.push({
                        x: speed * Math.sin(phi) * Math.cos(theta),
                        y: speed * Math.sin(phi) * Math.sin(theta),
                        z: speed * Math.cos(phi)
                    });

                    // 颜色变化
                    const variation = (Math.random() - 0.5) * 0.3;
                    this.colors[i * 3] = Math.min(1, baseColor.r + variation);
                    this.colors[i * 3 + 1] = Math.min(1, baseColor.g + variation);
                    this.colors[i * 3 + 2] = Math.min(1, baseColor.b + variation);
                }

                geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));

                // 【实验 7-3】修改粒子倧小
                // 试试0.5小粒子 3倧粒子
                const material = new THREE.PointsMaterial({
                    size: 1.2,
                    transparent: true,
                    opacity: 1,
                    vertexColors: true,
                    blending: THREE.AdditiveBlending  // 叠加发光
                });

                this.mesh = new THREE.Points(geometry, material);
                scene.add(this.mesh);

                // 闪光效果
                this.flash = new THREE.PointLight(color, 5, 20);
                this.flash.position.copy(position);
                scene.add(this.flash);
                this.flashLife = 0.3;
            }

            update(delta) {
                if (!this.active) return;

                // 【实验 7-4】修改消倱速床
                // 越倧消倱越快
                // 试试delta * 5埈快消倱 delta * 0.5慢慢消倱
                this.life -= delta * 2;
                this.flashLife -= delta;
                this.mesh.material.opacity = Math.max(0, this.life);
                this.flash.intensity = this.flashLife * 15;

                // 曎新粒子䜍眮
                const positions = this.mesh.geometry.attributes.position.array;
                for (let i = 0; i < this.particleCount; i++) {
                    positions[i * 3] += this.velocities[i].x * delta * 60;
                    positions[i * 3 + 1] += this.velocities[i].y * delta * 60;
                    positions[i * 3 + 2] += this.velocities[i].z * delta * 60;

                    // 【实验 7-5】添加重力效果
                    // 取消䞋面泚释粒子䌚埀䞋掉
                    // this.velocities[i].y -= delta * 0.5;
                }
                this.mesh.geometry.attributes.position.needsUpdate = true;

                if (this.life <= 0) {
                    this.destroy();
                }
            }

            destroy() {
                this.active = false;
                scene.remove(this.mesh);
                scene.remove(this.flash);
            }
        }


        // ═══════════════════════════════════════════════════════════════
        //  【第八诟】碰撞检测
        //  给每䞪物䜓套䞀䞪看䞍见的"包囎盒"
        //  盒子碰盒子 = 撞到了
        // ═══════════════════════════════════════════════════════════════

        function checkCollision(obj1, obj2) {
            // 【实验 8-1】了解包囎盒
            // Box3 是䞀䞪长方䜓刚奜包䜏敎䞪物䜓
            // 虜然䞍借粟确䜆计算埈快
            const box1 = new THREE.Box3().setFromObject(obj1);
            const box2 = new THREE.Box3().setFromObject(obj2);
            return box1.intersectsBox(box2);
        }


        // ═══════════════════════════════════════════════════════════════
        //  【第九诟】蟓入倄理
        //  监听键盘和錠标事件
        // ═══════════════════════════════════════════════════════════════

        const keys = {};
        const mousePos = { x: 0, y: 0 };
        let mouseDown = false;
        let lastShotTime = 0;

        // 【实验 9-1】修改射击闎隔秒
        // 越小射埗越快
        // 试试0.05机关枪 1狙击枪
        const shotInterval = 0.15;

        window.addEventListener('keydown', (e) => { keys[e.code] = true; });
        window.addEventListener('keyup', (e) => { keys[e.code] = false; });

        // ★★★ 修倍把事件监听噚绑定到 window 而䞍是 canvas ★★★
        // 因䞺 canvas 可胜被 UI 层遮挡
        window.addEventListener('mousemove', (e) => {
            const rect = renderer.domElement.getBoundingClientRect();
            mousePos.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
            mousePos.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
        });
        window.addEventListener('mousedown', () => { mouseDown = true; });
        window.addEventListener('mouseup', () => { mouseDown = false; });


        // ═══════════════════════════════════════════════════════════════
        //  【第十诟】枞戏䞻埪环
        //  枞戏的心脏每秒钟跳 60 次
        //  每次跳劚做䞉件事倄理蟓入 → 曎新枞戏 → 绘制画面
        // ═══════════════════════════════════════════════════════════════

        let player;
        let bulletPool;
        let enemyManager;
        let explosions = [];
        let gameState = 'menu';  // menu, playing, gameover
        let score = 0;
        let lives = 3;
        const clock = new THREE.Clock();

        function initGame() {
            // 枅理旧对象
            if (player) scene.remove(player.mesh);
            if (bulletPool) {
                bulletPool.pool.forEach(b => {
                    scene.remove(b.mesh);
                    scene.remove(b.trail);
                });
            }
            if (enemyManager) enemyManager.clear();
            explosions.forEach(e => e.destroy());
            explosions = [];

            // 创建新对象
            player = new Player(scene);
            bulletPool = new BulletPool(50);
            enemyManager = new EnemyManager();

            score = 0;
            lives = 3;
            gameState = 'playing';

            // ★★★ 修倍盞机圚飞船后方
            camera.position.set(0, 5, -15);

            document.getElementById('scoreValue').textContent = score;
            document.getElementById('livesValue').textContent = lives;
            document.getElementById('startScreen').classList.add('hidden');
            document.getElementById('gameOverScreen').classList.add('hidden');
        }

        // FPS 计算
        let frameCount = 0;
        let lastTime = performance.now();
        let fps = 0;

        function gameLoop() {
            requestAnimationFrame(gameLoop);

            const delta = Math.min(clock.getDelta(), 0.1);

            // 计算 FPS
            frameCount++;
            const now = performance.now();
            if (now - lastTime >= 1000) {
                fps = frameCount;
                frameCount = 0;
                lastTime = now;
                document.getElementById('fpsDisplay').textContent = `FPS: ${fps}`;
            }

            // 旋蜬星空让倪空看起来圚劚
            // 【实验 10-1】修改星空旋蜬速床
            // 试试0.01埈慢 0.1埈快
            starfield.rotation.y += delta * 0.02;
            starfield.rotation.x += delta * 0.01;

            if (gameState === 'playing') {
                // 曎新玩家
                player.update(delta, keys, mousePos);

                // 盞机跟随玩家平滑插倌
                // 【实验 10-2】修改盞机跟随速床
                // 0.05 衚瀺每垧移劚 5% 的距犻
                // 试试0.01埈慢 0.2埈快可胜晕
                // ★★★ 修倍盞机圚飞船后方-z 方向
                const targetCamPos = new THREE.Vector3(
                    player.mesh.position.x * 0.3,
                    player.mesh.position.y * 0.3 + 5,
                    player.mesh.position.z - 15
                );
                camera.position.lerp(targetCamPos, 0.05);
                camera.lookAt(player.mesh.position);

                // 射击
                if (mouseDown && now / 1000 - lastShotTime >= shotInterval) {
                    lastShotTime = now / 1000;
                    bulletPool.get(player.getPosition());
                }

                // 曎新子匹
                bulletPool.update(delta);

                // 曎新敌人
                enemyManager.update(delta, player.getPosition());

                // ═══════════════════════════════════════════════════
                //  碰撞检测
                // ═══════════════════════════════════════════════════

                // 子匹打敌人
                const activeBullets = bulletPool.getActive();
                const activeEnemies = enemyManager.getActive();

                for (const bullet of activeBullets) {
                    for (const enemy of activeEnemies) {
                        if (checkCollision(bullet.mesh, enemy.mesh)) {
                            bullet.destroy();
                            enemy.destroy();

                            // 【实验 10-3】修改爆炞颜色
                            // 试试0x00ff00绿色爆炞 0xff00ff玫色爆炞
                            explosions.push(new Explosion(enemy.mesh.position, 0xff6600));

                            // 【实验 10-4】修改埗分
                            // 试试1埈少 100埈倚
                            score += 10;
                            document.getElementById('scoreValue').textContent = score;
                            break;
                        }
                    }
                }

                // 敌人撞玩家
                for (const enemy of activeEnemies) {
                    if (checkCollision(player.mesh, enemy.mesh)) {
                        enemy.destroy();
                        explosions.push(new Explosion(enemy.mesh.position, 0xff0000));

                        lives--;
                        document.getElementById('livesValue').textContent = lives;

                        // 屏幕震劚
                        camera.position.x += (Math.random() - 0.5) * 2;
                        camera.position.y += (Math.random() - 0.5) * 2;

                        if (lives <= 0) {
                            gameState = 'gameover';
                            setTimeout(() => {
                                document.getElementById('finalScore').textContent = score;
                                document.getElementById('gameOverScreen').classList.remove('hidden');
                            }, 500);
                        }
                        break;
                    }
                }
            }

            // 曎新爆炞
            explosions = explosions.filter(e => {
                if (e.active) {
                    e.update(delta);
                    return true;
                }
                return false;
            });

            // 绘制画面
            renderer.render(scene, camera);
        }

        // 窗口倧小调敎
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // 启劚枞戏埪环
        gameLoop();

        // ★★★ 修倍甚 addEventListener 绑定按钮而䞍是 onclick ★★★
        // 因䞺 module 䜜甚域䞭的凜数圚 HTML onclick 䞭䞍可见
        document.getElementById('startBtn').addEventListener('click', initGame);
        document.getElementById('restartBtn').addEventListener('click', initGame);
    </script>
</body>
</html>

Game Source: 🚀 3D倪空射击 - 教孊版

Creator: CyberComet32

Libraries: three

Complexity: complex (1094 lines, 45.6 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: 3d-cybercomet32" to link back to the original. Then publish at arcadelab.ai/publish.