three.js纸飞机飞行撞建筑

  • 流畅的飞行控制:使用鼠标平滑地控制纸飞机的飞行姿态。

  • 无限程序化世界:建筑会在您前方无限生成,每次的飞行体验都独一无二。

  • 碰撞与破坏:撞向建筑时,它们会碎裂成一堆粒子,带来满足感。

  • 飞行辅助:当飞机倾斜角度过大时,会自动平滑地回正,让操控更简单。

  • 动态视角:摄像机将始终跟在纸飞机后方,提供沉浸式的飞行体验。

ini 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>纸飞机飞行</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #0d0c1c; /* 深紫色背景 */
            color: white;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        }
        canvas {
            display: block;
        }
        #info {
            position: absolute;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            padding: 10px 20px;
            background: rgba(0, 0, 0, 0.5);
            border-radius: 15px;
            text-align: center;
            z-index: 100;
            font-size: 16px;
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="info">移动鼠标控制飞机</div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

    <script>
        // --- 核心组件 ---
        let scene, camera, renderer, clock;
        let plane, planeBoundingBox;
        const buildings = new Map();
        const activeExplosions = [];

        // --- 游戏参数 ---
        const PLANE_SPEED = 65; // 降低飞行速度
        const PLANE_TURN_SPEED = 0.05;
        const PLANE_BOUNDS = { x: 40, y: 30 };
        const MAX_TILT = Math.PI / 6; // 最大倾斜角度 (30度)
        const AUTO_CORRECT_SPEED = 0.04;

        // --- 控制变量 ---
        const mousePosition = new THREE.Vector2();
        const targetPlanePosition = new THREE.Vector3();

        // --- 世界生成参数 ---
        const CELL_SIZE = 25; // 增大格子尺寸
        const RENDER_DISTANCE = 35; // 渲染距离(以CELL为单位)
        let lastPlayerCell = { x: 0, z: 0 };

        // --- 美学参数 ---
        const BUILDING_COLORS = [0x2c3e50, 0x34495e, 0x1f3a4e, 0x4a6fa5];
        const EMISSIVE_COLORS = [0xffd700, 0x00f2ff, 0xff4f79, 0x7effa4];


        // --- 初始化 ---
        function init() {
            // 场景和时钟
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog(0x0d0c1c, 100, 450); // 调整雾效颜色和距离
            clock = new THREE.Clock();

            // 相机
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 15, 30);
            camera.lookAt(0, 0, 0);

            // 渲染器
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            document.body.appendChild(renderer.domElement);

            // 光照
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
            scene.add(ambientLight);

            const directionalLight = new THREE.DirectionalLight(0xaaccff, 0.6);
            directionalLight.position.set(10, 30, 20);
            scene.add(directionalLight);
            
            // 创建纸飞机
            plane = createPaperPlane();
            scene.add(plane);
            planeBoundingBox = new THREE.Box3().setFromObject(plane);

            // 事件监听
            window.addEventListener('resize', onWindowResize);
            document.addEventListener('mousemove', onMouseMove);

            // 初始生成建筑
            updateWorld();

            // 开始动画
            animate();
        }

        // --- 创建纸飞机模型 ---
        function createPaperPlane() {
            const geometry = new THREE.BufferGeometry();
            const vertices = new Float32Array([
                 0, 2,  0,  -2, 0,  2,   2, 0,  2,   0, 0, -4,   0, 0,  3,
            ]);
            const indices = [ 3, 2, 0,  3, 0, 1,  3, 1, 4,  3, 4, 2 ];
            geometry.setIndex(indices);
            geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
            geometry.computeVertexNormals();
            const material = new THREE.MeshStandardMaterial({ 
                color: 0xffffff, side: THREE.DoubleSide, metalness: 0.2, roughness: 0.8
            });
            const mesh = new THREE.Mesh(geometry, material);
            mesh.scale.set(1.5, 1.5, 1.5);
            return mesh;
        }

        // --- 创建更好看的建筑 ---
        function createCoolBuilding(x, z) {
            const buildingGroup = new THREE.Group();
            buildingGroup.position.set(x, 0, z);

            const parts = Math.floor(Math.random() * 3) + 1; // 1到3个部分
            let currentHeight = 0;
            let lastWidth = Math.random() * 15 + 10;
            let lastDepth = Math.random() * 15 + 10;

            for (let i = 0; i < parts; i++) {
                const height = Math.random() * 50 + 20;
                const width = Math.max(lastWidth * (Math.random() * 0.4 + 0.6), 5); // 逐渐变窄
                const depth = Math.max(lastDepth * (Math.random() * 0.4 + 0.6), 5);

                const geometry = new THREE.BoxGeometry(width, height, depth);
                const material = new THREE.MeshStandardMaterial({
                    color: BUILDING_COLORS[Math.floor(Math.random() * BUILDING_COLORS.length)],
                    metalness: 0.8,
                    roughness: 0.4,
                    emissive: EMISSIVE_COLORS[Math.floor(Math.random() * EMISSIVE_COLORS.length)],
                    emissiveIntensity: 0.3
                });

                const mesh = new THREE.Mesh(geometry, material);
                mesh.position.y = currentHeight + height / 2;
                buildingGroup.add(mesh);
                
                currentHeight += height;
                lastWidth = width;
                lastDepth = depth;
            }

            buildingGroup.userData.boundingBox = new THREE.Box3().setFromObject(buildingGroup);
            return buildingGroup;
        }

        // --- 动画循环 ---
        function animate() {
            requestAnimationFrame(animate);
            const delta = clock.getDelta();

            // 1. 更新飞机位置
            plane.position.z -= PLANE_SPEED * delta;
            targetPlanePosition.x = mousePosition.x * PLANE_BOUNDS.x;
            targetPlanePosition.y = -mousePosition.y * PLANE_BOUNDS.y + 10;
            plane.position.lerp(new THREE.Vector3(targetPlanePosition.x, targetPlanePosition.y, plane.position.z), PLANE_TURN_SPEED);

            // 2. 更新飞机姿态(倾斜)
            const targetTiltZ = -mousePosition.x * MAX_TILT;
            const targetTiltX = -mousePosition.y * MAX_TILT;
            plane.rotation.z += (targetTiltZ - plane.rotation.z) * PLANE_TURN_SPEED;
            plane.rotation.x += (targetTiltX - plane.rotation.x) * PLANE_TURN_SPEED;

            // 3. 自动回正
            if (Math.abs(plane.rotation.z) > MAX_TILT) {
                plane.rotation.z = THREE.MathUtils.lerp(plane.rotation.z, Math.sign(plane.rotation.z) * MAX_TILT, AUTO_CORRECT_SPEED);
            }
             if (Math.abs(plane.rotation.x) > MAX_TILT) {
                plane.rotation.x = THREE.MathUtils.lerp(plane.rotation.x, Math.sign(plane.rotation.x) * MAX_TILT, AUTO_CORRECT_SPEED);
            }
            plane.rotation.y = 0;

            // 4. 更新相机位置
            camera.position.x = plane.position.x * 0.2;
            camera.position.y = plane.position.y + 10;
            camera.position.z = plane.position.z + 25;
            camera.lookAt(plane.position);
            
            // 5. 更新世界
            updateWorld();

            // 6. 碰撞检测
            checkCollisions();
            
            // 7. 更新爆炸效果
            updateExplosions(delta);

            renderer.render(scene, camera);
        }

        // --- 碰撞检测 ---
        function checkCollisions() {
            planeBoundingBox.setFromObject(plane);
            for (const [key, building] of buildings) {
                if (plane.position.distanceTo(building.position) < 150) { 
                    building.userData.boundingBox.setFromObject(building); // 确保包围盒最新
                    if (planeBoundingBox.intersectsBox(building.userData.boundingBox)) {
                        // 从建筑群组中获取颜色和尺寸信息
                        const mainPart = building.children[0];
                        if (mainPart) {
                           createExplosion(building.position, mainPart.geometry.parameters.width, mainPart.geometry.parameters.height, mainPart.material.color);
                        }
                        scene.remove(building);
                        buildings.delete(key);
                        break;
                    }
                }
            }
        }

        // --- 创建爆炸效果 ---
        function createExplosion(position, width, height, color) {
            const particleCount = 50;
            const particles = [];
            const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
            
            for (let i = 0; i < particleCount; i++) {
                const size = Math.random() * 1.5 + 0.5;
                const particleGeometry = new THREE.BoxGeometry(size, size, size);
                const particle = new THREE.Mesh(particleGeometry, particleMaterial);
                
                particle.position.copy(position);
                
                particle.userData.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 50, (Math.random() - 0.5) * 50, (Math.random() - 0.5) * 50
                );
                particle.userData.life = Math.random() * 1.5 + 0.5;
                
                scene.add(particle);
                particles.push(particle);
            }
            activeExplosions.push(particles);
        }
        
        // --- 更新爆炸粒子 ---
        function updateExplosions(delta) {
            for (let i = activeExplosions.length - 1; i >= 0; i--) {
                const particles = activeExplosions[i];
                for (let j = particles.length - 1; j >= 0; j--) {
                    const particle = particles[j];
                    particle.userData.life -= delta;
                    if (particle.userData.life <= 0) {
                        scene.remove(particle);
                        particles.splice(j, 1);
                    } else {
                        particle.position.add(particle.userData.velocity.clone().multiplyScalar(delta));
                        particle.userData.velocity.y -= 20 * delta; // 重力
                        particle.scale.setScalar(particle.userData.life);
                    }
                }
                if (particles.length === 0) {
                    activeExplosions.splice(i, 1);
                }
            }
        }

        // --- 世界生成与销毁 ---
        function updateWorld() {
            const playerCellX = Math.round(plane.position.x / CELL_SIZE / 2);
            const playerCellZ = Math.round(plane.position.z / CELL_SIZE);

            if (playerCellX === lastPlayerCell.x && playerCellZ === lastPlayerCell.z) return;
            lastPlayerCell = { x: playerCellX, z: playerCellZ };

            const visibleCells = new Set();

            for (let x = -RENDER_DISTANCE; x <= RENDER_DISTANCE; x++) {
                for (let z = -RENDER_DISTANCE; z <= RENDER_DISTANCE; z++) {
                    const cellX = playerCellX + x;
                    const cellZ = playerCellZ + z;
                    const key = `${cellX},${cellZ}`;
                    visibleCells.add(key);
                    
                    if (!buildings.has(key)) {
                        // 降低生成几率,让建筑更稀疏
                        if (Math.random() > 0.85) { 
                             const posX = cellX * CELL_SIZE * 2 + (Math.random() - 0.5) * 15;
                             const posZ = cellZ * CELL_SIZE + (Math.random() - 0.5) * 10;
                             const building = createCoolBuilding(posX, posZ);
                             buildings.set(key, building);
                             scene.add(building);
                        }
                    }
                }
            }
            
            for (const [key, building] of buildings) {
                if (!visibleCells.has(key)) {
                    scene.remove(building);
                    buildings.delete(key);
                }
            }
        }

        // --- 事件处理 ---
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function onMouseMove(event) {
            mousePosition.x = (event.clientX / window.innerWidth) * 2 - 1;
            mousePosition.y = (event.clientY / window.innerHeight) * 2 - 1;
        }

        // --- 启动 ---
        init();
    </script>
</body>
</html>
相关推荐
无光末阳13 小时前
前端 vue3+router 比较好用的自定义日志插件
前端·vue.js
liercats13 小时前
ECharts图表正常但动画不显示?问题排查指南:大概率是宽高问题
前端
摸鱼的鱼lv13 小时前
sonarQube全流程实战:从VSCode开发到CI/CD质量门禁
前端
whysqwhw13 小时前
JS/TS, Java/Kotlin, C/C++ 之间常见的跨语言调用方式
前端
云霄IT13 小时前
vue3前端开发的基础教程——快速上手
前端·javascript·vue.js
whysqwhw13 小时前
Node-API 学习四
前端
阿杆13 小时前
OAuth 图解指南(阮老师推荐)
前端·后端·架构
星哥说事13 小时前
OpenResty 和 Nginx 到底有啥区别?你真的了解吗!
前端
whysqwhw13 小时前
Node-API 学习五
前端