我的世界超级低配edit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
index.html
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0">
    <title>Web Minecraft Fixed</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { overflow: hidden; background: #87CEEB; font-family: sans-serif; touch-action: none; user-select: none; -webkit-user-select: none; }
        canvas { display: block; position: fixed; top: 0; left: 0; z-index: 0; }

        #ui { position: fixed; inset: 0; z-index: 10; pointer-events: none; }

        /* 准星 */
        #crosshair {
            position: absolute; top: 50%; left: 50%; width: 20px; height: 20px;
            transform: translate(-50%, -50%); opacity: 0.85; pointer-events: none;
        }
        #crosshair::before, #crosshair::after { content: ''; position: absolute; background: #fff; border-radius: 1px; }
        #crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; }
        #crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; }

        /* 摇杆 */
        #joystick-zone {
            position: absolute; bottom: 30px; left: 30px; width: 130px; height: 130px;
            background: rgba(255,255,255,0.08); border: 2px solid rgba(255,255,255,0.15);
            border-radius: 50%; pointer-events: auto; touch-action: none;
        }
        #joystick-knob {
            position: absolute; top: 50%; left: 50%; width: 55px; height: 55px;
            background: rgba(255,255,255,0.35); border-radius: 50%;
            transform: translate(-50%, -50%); pointer-events: none;
        }

        /* 右侧按钮 */
        .btn-group { position: absolute; bottom: 35px; right: 30px; display: flex; flex-direction: column; gap: 14px; pointer-events: auto; }
        .btn {
            width: 68px; height: 68px; border-radius: 50%; background: rgba(0,0,0,0.35);
            border: 2px solid rgba(255,255,255,0.25); color: #fff; font-size: 26px;
            display: flex; justify-content: center; align-items: center; backdrop-filter: blur(3px);
            touch-action: none; cursor: pointer;
        }
        .btn:active { background: rgba(255,255,255,0.25); transform: scale(0.92); }

        /* 物品栏 */
        #hotbar {
            position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 5px; padding: 5px; background: rgba(0,0,0,0.45);
            border-radius: 10px; pointer-events: auto; touch-action: none;
        }
        .slot {
            width: 36px; height: 36px; border: 2px solid rgba(255,255,255,0.15);
            border-radius: 4px; transition: all 0.12s; cursor: pointer; touch-action: none;
        }
        .slot.active { border-color: #fff; transform: scale(1.15); box-shadow: 0 0 8px rgba(255,255,255,0.5); }

        /* 模式标签 */
        #mode-tag {
            position: absolute; top: 12px; right: 12px; padding: 6px 14px;
            background: rgba(0,80,0,0.65); color: #fff; border-radius: 16px;
            font-size: 13px; font-weight: bold; pointer-events: auto; cursor: pointer;
            touch-action: none;
        }
    </style>
    <script type="importmap">
        { "imports": { "three": "https://unpkg.com/[email protected]/build/three.module.js" } }
    </script>
</head>
<body>

<div id="ui">
    <div id="crosshair"></div>
    <div id="mode-tag">建造 🧱</div>
    <div id="joystick-zone"><div id="joystick-knob"></div></div>
    <div class="btn-group">
        <div class="btn" id="btn-jump">⬆️</div>
        <div class="btn" id="btn-action">🔨</div>
    </div>
    <div id="hotbar"></div>
</div>

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

/* ===================== 配置 ===================== */
const BLOCKS = [
    { name: '草地', color: 0x5C9E58 },
    { name: '泥土', color: 0x8B5A2B },
    { name: '石头', color: 0x7D7D7D },
    { name: '木头', color: 0xA05F2C },
    { name: '砖块', color: 0xB84A38 },
    { name: '玻璃', color: 0xAADDFF, opacity: 0.5 }
];

const STATE = {
    sel: 0, build: true,
    mx: 0, mz: 0,
    vel: new THREE.Vector3(),
    onGround: false,
    pitch: 0, yaw: 0
};

/* ===================== Three.js 基础 ===================== */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.018);

const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 200);
camera.position.set(0, 3, 0); // 初始位置抬高一点防止卡地

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 0.9);
sun.position.set(30, 80, 40);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
const sd = 40;
sun.shadow.camera.left = -sd; sun.shadow.camera.right = sd;
sun.shadow.camera.top = sd; sun.shadow.camera.bottom = -sd;
scene.add(sun);

/* ===================== 世界与碰撞体 ===================== */
const objects = [];       // 用于射线检测(渲染+交互)
const blockMap = new Map(); // 用于快速碰撞检测 key="x,y,z"

const geo = new THREE.BoxGeometry(1, 1, 1);
const mats = BLOCKS.map(b => new THREE.MeshLambertMaterial({
    color: b.color, transparent: !!b.opacity, opacity: b.opacity || 1
}));

function getBlockKey(x, y, z) { return `${Math.round(x)},${Math.round(y)},${Math.round(z)}`; }

function placeBlock(x, y, z, mi) {
    x = Math.round(x); y = Math.round(y); z = Math.round(z);
    const key = getBlockKey(x, y, z);
    if (blockMap.has(key)) return null; // 已有方块

    const m = new THREE.Mesh(geo, mats[mi]);
    m.position.set(x, y, z);
    m.castShadow = m.receiveShadow = true;
    m.userData = { isBlock: true };
    scene.add(m);
    objects.push(m);
    blockMap.set(key, m);
    return m;
}

function removeBlock(mesh) {
    const p = mesh.position;
    const key = getBlockKey(p.x, p.y, p.z);
    blockMap.delete(key);
    scene.remove(mesh);
    const idx = objects.indexOf(mesh);
    if (idx > -1) objects.splice(idx, 1);
}

// 生成地面 (带一个坑洞用于测试悬空)
for (let x = -15; x <= 15; x++) {
    for (let z = -15; z <= 15; z++) {
        // 故意留一个 3x3 的坑测试掉落
        if (Math.abs(x) <= 1 && Math.abs(z) <= 1) continue; 
        placeBlock(x, 0, z, 0);
        if (Math.random() > 0.93) placeBlock(x, -1, z, 2);
    }
}

/* ===================== 物理碰撞检测 ===================== */
// 检查指定整数坐标是否有方块
function hasBlock(x, y, z) {
    return blockMap.has(getBlockKey(Math.round(x), Math.round(y), Math.round(z)));
}

// 检查玩家脚底是否踩在方块上
function checkGrounded(px, py, pz) {
    // 玩家脚底 y 坐标
    const footY = py - 1.6;
    // 检查脚底正下方的四个角(防止站在边缘滑落)
    const margin = 0.25;
    const checks = [
        [px - margin, footY, pz - margin],
        [px + margin, footY, pz - margin],
        [px - margin, footY, pz + margin],
        [px + margin, footY, pz + margin]
    ];
    for (const [cx, cy, cz] of checks) {
        if (hasBlock(Math.floor(cx), Math.floor(cy), Math.floor(cz))) return true;
    }
    return false;
}

// 检查玩家身体是否与方块重叠
function checkBodyCollision(px, py, pz) {
    const margin = 0.25;
    const minX = Math.floor(px - margin), maxX = Math.floor(px + margin);
    const minZ = Math.floor(pz - margin), maxZ = Math.floor(pz + margin);
    const minY = Math.floor(py - 1.6), maxY = Math.floor(py + 0.2);
    
    for (let x = minX; x <= maxX; x++)
        for (let y = minY; y <= maxY; y++)
            for (let z = minZ; z <= maxZ; z++)
                if (hasBlock(x, y, z)) return true;
    return false;
}

/* ===================== 射线交互 ===================== */
const ray = new THREE.Raycaster();
ray.far = 8;
const screenCenter = new THREE.Vector2(0, 0);

function doAction(build) {
    ray.setFromCamera(screenCenter, camera);
    const hits = ray.intersectObjects(objects);
    if (!hits.length) return;
    const h = hits[0];
    if (build) {
        const p = h.point.clone().add(h.face.normal.clone().multiplyScalar(0.5)).floor().addScalar(0.5);
        // 防止把自己卡住
        if (checkBodyCollision(camera.position.x, camera.position.y, camera.position.z)) {
            // 如果当前位置已经碰撞,尝试只检查新方块位置
            const bx = Math.round(p.x), by = Math.round(p.y), bz = Math.round(p.z);
            const margin = 0.3;
            const cx = camera.position.x, cy = camera.position.y, cz = camera.position.z;
            if (bx >= Math.floor(cx-margin) && bx <= Math.floor(cx+margin) &&
                bz >= Math.floor(cz-margin) && bz <= Math.floor(cz+margin) &&
                by >= Math.floor(cy-1.6) && by <= Math.floor(cy+0.2)) return;
        }
        placeBlock(p.x, p.y, p.z, STATE.sel);
    } else {
        if (h.object.userData.isBlock) removeBlock(h.object);
    }
}

/* ===================== UI 绑定 ===================== */
const hotbarEl = document.getElementById('hotbar');
BLOCKS.forEach((b, i) => {
    const s = document.createElement('div');
    s.className = `slot${i === 0 ? ' active' : ''}`;
    s.style.background = '#' + b.color.toString(16).padStart(6, '0');
    if (b.opacity) s.style.opacity = b.opacity;
    // 使用 pointerdown 而非 click,响应更快且不被 touch 拦截
    s.addEventListener('pointerdown', (e) => {
        e.stopPropagation(); // 阻止冒泡到视角控制
        STATE.sel = i;
        document.querySelectorAll('.slot').forEach((el, j) => el.classList.toggle('active', j === i));
    });
    hotbarEl.appendChild(s);
});

function toggleMode() {
    STATE.build = !STATE.build;
    const t = document.getElementById('mode-tag');
    const b = document.getElementById('btn-action');
    t.textContent = STATE.build ? '建造 🧱' : '破坏 💥';
    t.style.background = STATE.build ? 'rgba(0,80,0,0.65)' : 'rgba(140,0,0,0.65)';
    b.textContent = STATE.build ? '🧱' : '💥';
}

// 所有 UI 按钮统一用 pointerdown + stopPropagation
document.getElementById('mode-tag').addEventListener('pointerdown', (e) => { e.stopPropagation(); toggleMode(); });
document.getElementById('btn-action').addEventListener('pointerdown', (e) => { e.stopPropagation(); doAction(STATE.build); });
document.getElementById('btn-jump').addEventListener('pointerdown', (e) => {
    e.stopPropagation();
    if (STATE.onGround) { STATE.vel.y = 9; STATE.onGround = false; }
});

/* ===================== 输入系统 ===================== */
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;

// --- PC 指针锁定 ---
if (!isTouchDevice) {
    let locked = false;
    renderer.domElement.addEventListener('click', () => { if (!locked) document.body.requestPointerLock(); });
    document.addEventListener('pointerlockchange', () => { locked = !!document.pointerLockElement; });
    document.addEventListener('mousemove', e => {
        if (!locked) return;
        STATE.yaw -= e.movementX * 0.002;
        STATE.pitch -= e.movementY * 0.002;
        STATE.pitch = Math.max(-1.55, Math.min(1.55, STATE.pitch));
    });
    document.addEventListener('mousedown', e => {
        if (!locked) return;
        if (e.button === 0) doAction(false);
        if (e.button === 2) doAction(true);
    });
    document.addEventListener('contextmenu', e => e.preventDefault());
    
    const keys = {};
    document.addEventListener('keydown', e => {
        keys[e.code] = true;
        if (e.code === 'Space' && STATE.onGround) { STATE.vel.y = 9; STATE.onGround = false; }
        if (e.key >= '1' && e.key <= '6') {
            const idx = +e.key - 1;
            if (idx < BLOCKS.length) {
                STATE.sel = idx;
                document.querySelectorAll('.slot').forEach((el, j) => el.classList.toggle('active', j === idx));
            }
        }
        if (e.code === 'KeyQ') toggleMode();
    });
    document.addEventListener('keyup', e => { keys[e.code] = false; });
    
    // PC 移动输入在循环中读取 keys
    STATE._getPCMove = () => {
        let fx = 0, fz = 0;
        if (keys['KeyW'] || keys['ArrowUp']) fz -= 1;
        if (keys['KeyS'] || keys['ArrowDown']) fz += 1;
        if (keys['KeyA'] || keys['ArrowLeft']) fx -= 1;
        if (keys['KeyD'] || keys['ArrowRight']) fx += 1;
        return { x: fx, z: fz };
    };
} else {
    STATE._getPCMove = () => ({ x: 0, z: 0 });
}

// --- 移动端触控 ---
const joyZone = document.getElementById('joystick-zone');
const joyKnob = document.getElementById('joystick-knob');
let joyId = null, joyOx = 0, joyOy = 0;
let lookId = null, lookLx = 0, lookLy = 0;

// 摇杆区域独立监听,不与全局冲突
joyZone.addEventListener('touchstart', e => {
    e.preventDefault();
    e.stopPropagation();
    const t = e.changedTouches[0];
    joyId = t.identifier;
    joyOx = t.clientX; joyOy = t.clientY;
    STATE.mx = 0; STATE.mz = 0;
}, { passive: false });

joyZone.addEventListener('touchmove', e => {
    e.preventDefault();
    e.stopPropagation();
    for (const t of e.changedTouches) {
        if (t.identifier === joyId) {
            const mr = 38;
            let dx = t.clientX - joyOx, dy = t.clientY - joyOy;
            const d = Math.hypot(dx, dy);
            if (d > mr) { dx *= mr / d; dy *= mr / d; }
            joyKnob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
            STATE.mx = dx / mr; STATE.mz = dy / mr;
        }
    }
}, { passive: false });

const endJoy = e => {
    for (const t of e.changedTouches) {
        if (t.identifier === joyId) {
            joyId = null;
            joyKnob.style.transform = 'translate(-50%,-50%)';
            STATE.mx = 0; STATE.mz = 0;
        }
    }
};
joyZone.addEventListener('touchend', endJoy);
joyZone.addEventListener('touchcancel', endJoy);

// 全局视角旋转 (排除 UI 元素)
document.addEventListener('touchstart', e => {
    for (const t of e.changedTouches) {
        // 跳过 UI 元素和摇杆
        if (t.target.closest('.btn') || t.target.closest('.slot') || 
            t.target.closest('#mode-tag') || t.target.closest('#joystick-zone')) continue;
        
        if (lookId === null) {
            lookId = t.identifier;
            lookLx = t.clientX; lookLy = t.clientY;
        }
    }
}, { passive: false });

document.addEventListener('touchmove', e => {
    for (const t of e.changedTouches) {
        if (t.identifier === lookId) {
            const sens = 0.005;
            STATE.yaw -= (t.clientX - lookLx) * sens;
            STATE.pitch -= (t.clientY - lookLy) * sens;
            STATE.pitch = Math.max(-1.55, Math.min(1.55, STATE.pitch));
            lookLx = t.clientX; lookLy = t.clientY;
        }
    }
}, { passive: false });

const endLook = e => {
    for (const t of e.changedTouches) {
        if (t.identifier === lookId) lookId = null;
    }
};
document.addEventListener('touchend', endLook);
document.addEventListener('touchcancel', endLook);

/* ===================== 主循环 ===================== */
const clock = new THREE.Clock();

(function loop() {
    requestAnimationFrame(loop);
    const dt = Math.min(clock.getDelta(), 0.05);

    // 1. 相机旋转
    camera.rotation.order = 'YXZ';
    camera.rotation.y = STATE.yaw;
    camera.rotation.x = STATE.pitch;

    // 2. 计算移动方向
    const pcMove = STATE._getPCMove();
    let inputX = STATE.mx + pcMove.x;
    let inputZ = STATE.mz + pcMove.z;
    
    const fwd = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), STATE.yaw);
    const rgt = new THREE.Vector3(1, 0, 0).applyAxisAngle(new THREE.Vector3(0, 1, 0), STATE.yaw);
    const dir = new THREE.Vector3().addScaledVector(fwd, -inputZ).addScaledVector(rgt, inputX);
    if (dir.lengthSq() > 1) dir.normalize();

    // 3. 物理模拟
    const speed = 7;
    const gravity = 28;
    const jumpForce = 9;

    // 水平速度平滑
    STATE.vel.x += (dir.x * speed - STATE.vel.x) * Math.min(1, 15 * dt);
    STATE.vel.z += (dir.z * speed - STATE.vel.z) * Math.min(1, 15 * dt);
    
    // 重力
    STATE.vel.y -= gravity * dt;

    // 4. 分轴碰撞检测与位移
    const pos = camera.position;
    
    // X 轴
    pos.x += STATE.vel.x * dt;
    if (checkBodyCollision(pos.x, pos.y, pos.z)) {
        pos.x -= STATE.vel.x * dt;
        STATE.vel.x = 0;
    }
    
    // Z 轴
    pos.z += STATE.vel.z * dt;
    if (checkBodyCollision(pos.x, pos.y, pos.z)) {
        pos.z -= STATE.vel.z * dt;
        STATE.vel.z = 0;
    }
    
    // Y 轴
    pos.y += STATE.vel.y * dt;
    if (checkBodyCollision(pos.x, pos.y, pos.z)) {
        pos.y -= STATE.vel.y * dt;
        // 如果是向下碰撞,说明落地了
        if (STATE.vel.y < 0) {
            STATE.onGround = true;
        }
        STATE.vel.y = 0;
    } else {
        STATE.onGround = false;
    }

    // 兜底:防止掉出世界
    if (pos.y < -20) {
        pos.set(0, 5, 0);
        STATE.vel.set(0, 0, 0);
    }

    renderer.render(scene, camera);
})();

window.addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
        
编辑器加载中
预览
控制台