minecraftedit icon

作者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
md
README.md
现在支持上传本地图片了!
index.html
            
            <!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Minecraft Web - Mobile Edition</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; touch-action: none; user-select: none; -webkit-user-select: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
        canvas { display: block; width: 100%; height: 100dvh; }
        
        /* UI 层 */
        #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
        
        /* 准星 */
        #crosshair {
            position: absolute; top: 50%; left: 50%; width: 16px; height: 16px;
            transform: translate(-50%, -50%); pointer-events: none;
            background-image: linear-gradient(to bottom, transparent 45%, rgba(255,255,255,0.8) 45%, rgba(255,255,255,0.8) 55%, transparent 55%),
                              linear-gradient(to right, transparent 45%, rgba(255,255,255,0.8) 45%, rgba(255,255,255,0.8) 55%, transparent 55%);
            z-index: 5;
        }

        /* 横屏提示 */
        #orientation-warning {
            display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: #1a1a1a; color: white; z-index: 100;
            justify-content: center; align-items: center; flex-direction: column; text-align: center;
        }
        @media screen and (orientation: portrait) {
            #orientation-warning { display: flex; }
        }

        /* 移动端控制区 */
        .mobile-control { pointer-events: auto; position: absolute; }
        
        /* 左侧摇杆区 */
        #joystick-zone {
            bottom: 50px; left: 50px; width: 120px; height: 120px;
            background: rgba(255, 255, 255, 0.1); border-radius: 50%;
            border: 2px solid rgba(255, 255, 255, 0.3);
            display: none; /* 默认隐藏,JS 检测触摸设备开启 */
        }
        #joystick-knob {
            position: absolute; top: 50%; left: 50%; width: 50px; height: 50px;
            background: rgba(255, 255, 255, 0.5); border-radius: 50%;
            transform: translate(-50%, -50%);
        }

        /* 右侧按钮区 */
        #action-buttons {
            bottom: 40px; right: 40px; display: none; flex-direction: column; gap: 15px;
        }
        .btn {
            width: 60px; height: 60px; border-radius: 50%;
            background: rgba(0, 0, 0, 0.5); border: 2px solid rgba(255,255,255,0.5);
            color: white; font-weight: bold; font-size: 12px;
            display: flex; justify-content: center; align-items: center;
            backdrop-filter: blur(4px);
        }
        .btn:active { background: rgba(255, 255, 255, 0.3); transform: scale(0.95); }
        .btn-jump { background: rgba(100, 100, 255, 0.5); }
        .btn-break { background: rgba(255, 100, 100, 0.5); }
        .btn-place { background: rgba(100, 255, 100, 0.5); }

        /* 物品栏 */
        #hotbar {
            position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 5px; background: rgba(0,0,0,0.6); padding: 5px;
            border-radius: 8px; pointer-events: auto;
        }
        .slot {
            width: 40px; height: 40px; border: 2px solid #555; background-size: cover;
            image-rendering: pixelated;
        }
        .slot.active { border-color: white; transform: scale(1.1); box-shadow: 0 0 10px rgba(255,255,255,0.5); }

        /* 说明 */
        #info {
            position: absolute; top: 10px; left: 10px; color: rgba(255,255,255,0.7);
            font-size: 12px; pointer-events: none; text-shadow: 1px 1px 2px black;
        }
    </style>
</head>
<body>

    <!-- 3D 容器 -->
    <div id="container"></div>

    <!-- UI 层 -->
    <div id="ui-layer">
        <div id="crosshair"></div>
        <div id="info">
            PC: WASD 移动,鼠标看/操作<br>
            Mobile: 左摇杆移动,右屏看,按钮操作
        </div>
        
        <!-- 移动端控件 -->
        <div id="joystick-zone" class="mobile-control">
            <div id="joystick-knob"></div>
        </div>
        
        <div id="action-buttons" class="mobile-control">
            <div class="btn btn-jump" id="btn-jump">跳跃</div>
            <div class="btn btn-break" id="btn-break">破坏</div>
            <div class="btn btn-place" id="btn-place">放置</div>
        </div>

        <div id="hotbar"></div>
    </div>

    <!-- 横屏提示 -->
    <div id="orientation-warning">
        <h1>⚠️ 请旋转手机</h1>
        <p>为了最佳体验,请使用横屏模式游玩</p>
        <div style="font-size: 50px; margin-top: 20px;">📱 ➡️ 📱</div>
    </div>

    <!-- 引入 Three.js -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>

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

        // --- 配置 ---
        const WORLD_SIZE = 32; 
        const BLOCK_SIZE = 1;
        const RENDER_DISTANCE = 40;
        
        // --- 纹理生成器 (程序化纹理,无需外部图片) ---
        function createBlockTexture(colorHex, border = true) {
            const canvas = document.createElement('canvas');
            canvas.width = 64; canvas.height = 64;
            const ctx = canvas.getContext('2d');
            
            // 底色
            ctx.fillStyle = colorHex;
            ctx.fillRect(0, 0, 64, 64);
            
            // 噪点
            for(let i=0; i<400; i++) {
                ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.15})`;
                ctx.fillRect(Math.random()*64, Math.random()*64, 2, 2);
                ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.1})`;
                ctx.fillRect(Math.random()*64, Math.random()*64, 2, 2);
            }

            // 边框
            if(border) {
                ctx.strokeStyle = 'rgba(0,0,0,0.2)';
                ctx.lineWidth = 4;
                ctx.strokeRect(0, 0, 64, 64);
                ctx.strokeStyle = 'rgba(255,255,255,0.1)';
                ctx.lineWidth = 2;
                ctx.strokeRect(2, 2, 60, 60);
            }
            
            const tex = new THREE.CanvasTexture(canvas);
            tex.magFilter = THREE.NearestFilter; // 像素风
            tex.minFilter = THREE.NearestFilter;
            return tex;
        }

        const materials = {
            grass: new THREE.MeshLambertMaterial({ map: createBlockTexture('#559c44') }),
            dirt: new THREE.MeshLambertMaterial({ map: createBlockTexture('#5d4037') }),
            stone: new THREE.MeshLambertMaterial({ map: createBlockTexture('#757575') }),
            wood: new THREE.MeshLambertMaterial({ map: createBlockTexture('#8d6e63') }),
            brick: new THREE.MeshLambertMaterial({ map: createBlockTexture('#b71c1c') }),
            sand: new THREE.MeshLambertMaterial({ map: createBlockTexture('#fdd835') })
        };
        const materialKeys = Object.keys(materials);
        let currentMaterialIndex = 0;

        // --- 场景初始化 ---
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x87CEEB);
        scene.fog = new THREE.FogExp2(0x87CEEB, 0.02);

        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 10, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 优化移动端性能
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        document.getElementById('container').appendChild(renderer.domElement);

        // --- 光照 ---
        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
        scene.add(hemiLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
        dirLight.position.set(50, 100, 50);
        dirLight.castShadow = true;
        dirLight.shadow.mapSize.width = 1024;
        dirLight.shadow.mapSize.height = 1024;
        dirLight.shadow.camera.near = 0.5;
        dirLight.shadow.camera.far = 200;
        dirLight.shadow.camera.left = -50;
        dirLight.shadow.camera.right = 50;
        dirLight.shadow.camera.top = 50;
        dirLight.shadow.camera.bottom = -50;
        scene.add(dirLight);

        // --- 世界管理 ---
        const objects = [];
        const geometry = new THREE.BoxGeometry(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
        
        // 简单的世界生成
        function generateWorld() {
            for (let x = -WORLD_SIZE / 2; x < WORLD_SIZE / 2; x++) {
                for (let z = -WORLD_SIZE / 2; z < WORLD_SIZE / 2; z++) {
                    const y = Math.floor(Math.sin(x / 8) * 3 + Math.cos(z / 8) * 3);
                    for (let h = -2; h <= y; h++) {
                        let mat = materials.stone;
                        if (h === y) mat = materials.grass;
                        else if (h > y - 2) mat = materials.dirt;
                        if (y < -1) mat = materials.sand; // 低处是沙子
                        createBlock(x, h, z, mat);
                    }
                }
            }
        }

        function createBlock(x, y, z, material) {
            const mesh = new THREE.Mesh(geometry, material);
            mesh.position.set(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE);
            mesh.castShadow = true;
            mesh.receiveShadow = true;
            scene.add(mesh);
            objects.push(mesh);
            return mesh;
        }

        generateWorld();

        // --- 玩家手臂 (视觉细节) ---
        const armGroup = new THREE.Group();
        const armGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2);
        const armMat = new THREE.MeshLambertMaterial({ color: 0xffccaa }); // 皮肤色
        const armMesh = new THREE.Mesh(armGeo, armMat);
        armMesh.position.set(0.3, -0.4, -0.3);
        armGroup.add(armMesh);
        camera.add(armGroup);
        scene.add(camera); // 将相机加入场景以便手臂跟随

        // --- 输入控制系统 (核心:兼容 PC 和 手机) ---
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        
        // 状态变量
        const moveState = { forward: 0, right: 0, jump: false };
        const lookState = { yaw: 0, pitch: 0 };
        
        // 初始化相机角度
        camera.rotation.order = 'YXZ';

        // --- 桌面端控制 (PointerLock) ---
        if (!isMobile) {
            const controls = new (await import('three/addons/controls/PointerLockControls.js')).PointerLockControls(camera, document.body);
            scene.add(controls.getObject());
            // 重新绑定相机,因为 PointerLockControls 会创建一个 Object3D 包裹相机
            // 为了简化,我们这里直接用键盘更新速度,手动更新相机位置,不使用 PointerLockControls 的内部移动逻辑
            // 这样能更好地统一手机和电脑的物理逻辑
            
            document.addEventListener('click', () => {
                // 简单的锁定逻辑,实际项目中需要更完善的 UI 引导
                // document.body.requestPointerLock(); 
            });

            document.addEventListener('keydown', (e) => {
                switch(e.code) {
                    case 'KeyW': moveState.forward = 1; break;
                    case 'KeyS': moveState.forward = -1; break;
                    case 'KeyA': moveState.right = -1; break;
                    case 'KeyD': moveState.right = 1; break;
                    case 'Space': if(canJump) { velocity.y = 12; canJump = false; } break;
                    case 'Digit1': selectMaterial(0); break;
                    case 'Digit2': selectMaterial(1); break;
                    case 'Digit3': selectMaterial(2); break;
                    case 'Digit4': selectMaterial(3); break;
                    case 'Digit5': selectMaterial(4); break;
                }
            });
            document.addEventListener('keyup', (e) => {
                switch(e.code) {
                    case 'KeyW': case 'KeyS': moveState.forward = 0; break;
                    case 'KeyA': case 'KeyD': moveState.right = 0; break;
                }
            });
            
            // 鼠标看视角
            document.addEventListener('mousemove', (e) => {
                if (document.pointerLockElement === document.body) {
                    lookState.yaw -= e.movementX * 0.002;
                    lookState.pitch -= e.movementY * 0.002;
                    lookState.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, lookState.pitch));
                    camera.rotation.set(lookState.pitch, lookState.yaw, 0, 'YXZ');
                }
            });

            // 鼠标点击交互
            document.addEventListener('mousedown', (e) => {
                if (document.pointerLockElement !== document.body && !isMobile) return;
                handleInteraction(e.button === 2); // 右键放置,左键破坏
            });
            
            // 阻止右键菜单
            document.addEventListener('contextmenu', e => e.preventDefault());

        } else {
            // --- 移动端控制 ---
            document.getElementById('joystick-zone').style.display = 'block';
            document.getElementById('action-buttons').style.display = 'flex';

            // 1. 虚拟摇杆 (移动)
            const joystickZone = document.getElementById('joystick-zone');
            const joystickKnob = document.getElementById('joystick-knob');
            let joystickTouchId = null;
            let joystickCenter = { x: 0, y: 0 };

            joystickZone.addEventListener('touchstart', (e) => {
                e.preventDefault();
                const touch = e.changedTouches[0];
                joystickTouchId = touch.identifier;
                const rect = joystickZone.getBoundingClientRect();
                joystickCenter = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
                updateJoystick(touch.clientX, touch.clientY);
            }, { passive: false });

            joystickZone.addEventListener('touchmove', (e) => {
                e.preventDefault();
                for (let i = 0; i < e.changedTouches.length; i++) {
                    if (e.changedTouches[i].identifier === joystickTouchId) {
                        updateJoystick(e.changedTouches[i].clientX, e.changedTouches[i].clientY);
                        break;
                    }
                }
            }, { passive: false });

            const endJoystick = (e) => {
                e.preventDefault();
                for (let i = 0; i < e.changedTouches.length; i++) {
                    if (e.changedTouches[i].identifier === joystickTouchId) {
                        joystickTouchId = null;
                        moveState.forward = 0;
                        moveState.right = 0;
                        joystickKnob.style.transform = `translate(-50%, -50%)`;
                        break;
                    }
                }
            };
            joystickZone.addEventListener('touchend', endJoystick);
            joystickZone.addEventListener('touchcancel', endJoystick);

            function updateJoystick(x, y) {
                const maxDist = 35;
                let dx = x - joystickCenter.x;
                let dy = y - joystickCenter.y;
                const dist = Math.sqrt(dx*dx + dy*dy);
                
                if (dist > maxDist) {
                    const ratio = maxDist / dist;
                    dx *= ratio;
                    dy *= ratio;
                }

                joystickKnob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;

                // 映射到移动状态 (-1 到 1)
                moveState.right = dx / maxDist;
                moveState.forward = -dy / maxDist; // Y 轴反向
            }

            // 2. 右侧屏幕滑动 (视角)
            let lastTouchX = 0;
            let lastTouchY = 0;
            let lookTouchId = null;

            document.addEventListener('touchstart', (e) => {
                // 如果点在按钮或摇杆上,不处理视角
                if (e.target.closest('.mobile-control') || e.target.closest('#hotbar')) return;
                
                // 只在屏幕右半部分触发视角
                const touch = e.changedTouches[0];
                if (touch.clientX > window.innerWidth / 2) {
                    lookTouchId = touch.identifier;
                    lastTouchX = touch.clientX;
                    lastTouchY = touch.clientY;
                }
            }, { passive: false });

            document.addEventListener('touchmove', (e) => {
                if (lookTouchId === null) return;
                for (let i = 0; i < e.changedTouches.length; i++) {
                    if (e.changedTouches[i].identifier === lookTouchId) {
                        const touch = e.changedTouches[i];
                        const deltaX = touch.clientX - lastTouchX;
                        const deltaY = touch.clientY - lastTouchY;
                        
                        lookState.yaw -= deltaX * 0.005;
                        lookState.pitch -= deltaY * 0.005;
                        lookState.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, lookState.pitch));
                        camera.rotation.set(lookState.pitch, lookState.yaw, 0, 'YXZ');

                        lastTouchX = touch.clientX;
                        lastTouchY = touch.clientY;
                        break;
                    }
                }
            }, { passive: false });

            const endLook = (e) => {
                for (let i = 0; i < e.changedTouches.length; i++) {
                    if (e.changedTouches[i].identifier === lookTouchId) {
                        lookTouchId = null;
                        break;
                    }
                }
            };
            document.addEventListener('touchend', endLook);

            // 3. 按钮绑定
            document.getElementById('btn-jump').addEventListener('touchstart', (e) => {
                e.preventDefault();
                if(canJump) { velocity.y = 12; canJump = false; }
            });
            
            document.getElementById('btn-break').addEventListener('touchstart', (e) => {
                e.preventDefault();
                handleInteraction(false); // 左键逻辑
            });

            document.getElementById('btn-place').addEventListener('touchstart', (e) => {
                e.preventDefault();
                handleInteraction(true); // 右键逻辑
            });

            // 物品栏点击
            document.querySelectorAll('.slot').forEach((slot, idx) => {
                slot.addEventListener('click', () => selectMaterial(idx));
            });
        }

        // --- 交互逻辑 (射线检测) ---
        const raycaster = new THREE.Raycaster();
        const highlightGeo = new THREE.BoxGeometry(1.02, 1.02, 1.02);
        const highlightMat = new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true, transparent: true, opacity: 0.5 });
        const highlightMesh = new THREE.Mesh(highlightGeo, highlightMat);
        scene.add(highlightMesh);

        function handleInteraction(isPlace) {
            raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
            const intersects = raycaster.intersectObjects(objects);

            if (intersects.length > 0 && intersects[0].distance < 6) {
                const intersect = intersects[0];
                
                if (!isPlace) {
                    // 破坏
                    scene.remove(intersect.object);
                    objects.splice(objects.indexOf(intersect.object), 1);
                    // 简单的粒子效果占位
                } else {
                    // 放置
                    const pos = intersect.point.add(intersect.face.normal.multiplyScalar(0.5)).floor();
                    // 防止卡住自己
                    const playerPos = camera.position;
                    const dx = Math.abs(playerPos.x - (pos.x + 0.5));
                    const dz = Math.abs(playerPos.z - (pos.z + 0.5));
                    const dy = Math.abs((playerPos.y - 1.5) - (pos.y + 0.5));
                    
                    if (dx < 0.8 && dz < 0.8 && dy < 1.5) return; // 太近了不放置

                    const exists = objects.some(obj => 
                        Math.abs(obj.position.x - (pos.x+0.5)) < 0.1 && 
                        Math.abs(obj.position.y - (pos.y+0.5)) < 0.1 && 
                        Math.abs(obj.position.z - (pos.z+0.5)) < 0.1
                    );
                    
                    if (!exists) {
                        createBlock(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, materials[materialKeys[currentMaterialIndex]]);
                    }
                }
            }
        }

        // --- 物品栏 UI ---
        const hotbar = document.getElementById('hotbar');
        materialKeys.forEach((key, index) => {
            const div = document.createElement('div');
            div.className = 'slot' + (index === 0 ? ' active' : '');
            // 使用生成的纹理作为背景
            const canvas = materials[key].map.image;
            div.style.backgroundImage = `url(${canvas.toDataURL()})`;
            div.onclick = () => selectMaterial(index);
            hotbar.appendChild(div);
        });

        function selectMaterial(index) {
            currentMaterialIndex = index;
            const slots = document.querySelectorAll('.slot');
            slots.forEach((s, i) => {
                if (i === index) s.classList.add('active');
                else s.classList.remove('active');
            });
        }

        // --- 物理与动画循环 ---
        let velocity = new THREE.Vector3();
        let canJump = false;
        let prevTime = performance.now();
        const speed = 10.0;
        const gravity = 30.0;

        // 简单的碰撞检测辅助函数
        const playerBox = new THREE.Box3();
        const blockBox = new THREE.Box3();

        function checkCollision(newPos) {
            // 玩家包围盒 (宽 0.6, 高 1.8)
            playerBox.setFromCenterAndSize(newPos, new THREE.Vector3(0.6, 1.8, 0.6));
            
            for (let obj of objects) {
                // 优化:只检测附近的方块
                if (obj.position.distanceTo(newPos) > 2) continue;
                
                blockBox.setFromObject(obj);
                if (playerBox.intersectsBox(blockBox)) {
                    return true;
                }
            }
            return false;
        }

        function animate() {
            requestAnimationFrame(animate);

            const time = performance.now();
            const delta = Math.min((time - prevTime) / 1000, 0.1); // 防止卡顿时飞出去
            prevTime = time;

            // 1. 视角更新 (统一处理)
            camera.rotation.set(lookState.pitch, lookState.yaw, 0, 'YXZ');

            // 2. 移动计算
            const direction = new THREE.Vector3();
            direction.z = moveState.forward;
            direction.x = moveState.right;
            direction.normalize();

            if (moveState.forward !== 0 || moveState.right !== 0) {
                // 获取相机朝向的水平向量
                const camDir = new THREE.Vector3();
                camera.getWorldDirection(camDir);
                camDir.y = 0;
                camDir.normalize();
                
                const camRight = new THREE.Vector3();
                camRight.crossVectors(camDir, new THREE.Vector3(0, 1, 0));

                const moveVec = new THREE.Vector3();
                moveVec.addScaledVector(camDir, direction.z);
                moveVec.addScaledVector(camRight, direction.x);
                moveVec.normalize().multiplyScalar(speed * delta);

                // 尝试移动 XZ
                const originalPos = camera.position.clone();
                camera.position.x += moveVec.x;
                camera.position.z += moveVec.z;
                
                if (checkCollision(camera.position)) {
                    camera.position.x = originalPos.x;
                    camera.position.z = originalPos.z;
                }

                // 手臂摆动动画
                const swing = Math.sin(time * 0.015) * 0.1;
                armMesh.position.x = 0.3 + swing;
                armMesh.rotation.z = -swing;
                armMesh.rotation.x = Math.abs(swing);
            } else {
                armMesh.position.x = 0.3;
                armMesh.rotation.set(0,0,0);
            }

            // 3. 重力与跳跃
            velocity.y -= gravity * delta;
            camera.position.y += velocity.y * delta;

            // 地面碰撞
            if (checkCollision(camera.position)) {
                // 如果下落时碰撞,说明落地
                if (velocity.y < 0) {
                    // 简单修正:回到上一帧高度附近
                    // 更精确的做法是分离轴测试,这里简化处理
                    velocity.y = 0;
                    canJump = true;
                    // 防止陷入地下
                    camera.position.y = Math.ceil(camera.position.y - 1.5) + 1.5; 
                } else {
                    // 顶头碰撞
                    velocity.y = 0;
                    camera.position.y -= 0.1;
                }
            }

            // 4. 高亮框更新
            raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
            const intersects = raycaster.intersectObjects(objects);
            if (intersects.length > 0 && intersects[0].distance < 6) {
                const target = intersects[0].object;
                highlightMesh.position.copy(target.position);
                highlightMesh.visible = true;
            } else {
                highlightMesh.visible = false;
            }

            renderer.render(scene, camera);
        }

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

        animate();
    </script>
</body>
</html>
        
编辑器加载中
预览
控制台