🎮ArcadeLab

方块世界 · VoxelCraft

by RocketDolphin44
1394 lines43.2 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方块世界 · VoxelCraft</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
<style>
:root {
    --bg: #0a0e0c;
    --grass: #6cbf3a;
    --grass-dark: #4a8f2a;
    --dirt: #8b6a3a;
    --dirt-dark: #6b5028;
    --wood: #6b4a2a;
    --stone: #888;
    --sand: #e6d8a0;
    --leaves: #4a8f3a;
    --water: #3a7ab8;
    --accent: #ff8c42;
    --accent-glow: rgba(255, 140, 66, 0.4);
    --fg: #f5f1e8;
    --muted: rgba(245, 241, 232, 0.65);
    --panel: rgba(15, 18, 16, 0.85);
    --border: rgba(255, 255, 255, 0.12);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
    height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--fg);
    font-family: 'VT323', monospace;
    user-select: none;
    -webkit-user-select: none;
}

canvas { display: block; }

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

.crosshair {
    position: absolute;
    top: 50%; left: 50%;
    width: 22px; height: 22px;
    transform: translate(-50%, -50%);
    opacity: 0.85;
}
.crosshair::before, .crosshair::after {
    content: '';
    position: absolute;
    background: #fff;
    mix-blend-mode: difference;
}
.crosshair::before {
    top: 50%; left: 0; right: 0; height: 2px;
    transform: translateY(-50%);
}
.crosshair::after {
    left: 50%; top: 0; bottom: 0; width: 2px;
    transform: translateX(-50%);
}

.topbar {
    position: absolute;
    top: 16px; left: 16px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 16px;
    background: var(--panel);
    border: 2px solid var(--border);
    border-radius: 6px;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    font-size: 18px;
    text-shadow: 1px 1px 0 #000;
    min-width: 220px;
}
.topbar .row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}
.topbar .label {
    color: var(--muted);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 1px;
}
.topbar .value {
    color: var(--accent);
    font-weight: bold;
}

.hotbar {
    position: absolute;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 4px;
    padding: 6px;
    background: var(--panel);
    border: 2px solid var(--border);
    border-radius: 8px;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    pointer-events: auto;
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}

.slot {
    width: 56px; height: 56px;
    background: rgba(0,0,0,0.6);
    border: 2px solid rgba(255,255,255,0.08);
    border-radius: 4px;
    position: relative;
    cursor: pointer;
    transition: all 0.15s ease;
}
.slot:hover {
    border-color: rgba(255,255,255,0.25);
    background: rgba(255,255,255,0.05);
}
.slot.active {
    border-color: var(--accent);
    background: rgba(255, 140, 66, 0.18);
    transform: translateY(-6px) scale(1.05);
    box-shadow: 0 6px 16px var(--accent-glow);
}

.slot .icon {
    width: 36px; height: 36px;
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    border-radius: 3px;
    box-shadow: 
        inset 0 5px 0 rgba(255,255,255,0.2),
        inset 0 -5px 0 rgba(0,0,0,0.3),
        inset 5px 0 0 rgba(0,0,0,0.15),
        inset -5px 0 0 rgba(0,0,0,0.15),
        0 2px 4px rgba(0,0,0,0.4);
}

.slot .num {
    position: absolute;
    top: 3px; left: 5px;
    font-size: 14px;
    color: var(--muted);
    font-family: 'VT323', monospace;
    text-shadow: 1px 1px 0 #000;
}

.slot .name {
    position: absolute;
    bottom: -22px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 16px;
    color: var(--fg);
    white-space: nowrap;
    opacity: 0;
    transition: opacity 0.2s;
    text-shadow: 1px 1px 0 #000;
    pointer-events: none;
}
.slot.active .name { opacity: 1; }

.icon-grass { background: linear-gradient(180deg, #6cbf3a 0% 50%, #9b7a4a 50% 100%); }
.icon-dirt { background: #8b6a3a; }
.icon-stone { background: repeating-linear-gradient(45deg, #aaa 0 4px, #888 4px 8px); }
.icon-wood { background: linear-gradient(180deg, #c8a070 0% 25%, #6b4a2a 25% 75%, #c8a070 75% 100%); }
.icon-leaves { background: radial-gradient(circle at 30% 30%, #6cbf3a, #3a7a2a); }
.icon-sand { background: #e6d8a0; }
.icon-water { background: linear-gradient(180deg, #4a8fc8 0%, #2a5a98 100%); }

.controls-panel {
    position: absolute;
    bottom: 24px;
    right: 16px;
    background: var(--panel);
    border: 2px solid var(--border);
    border-radius: 6px;
    padding: 12px 16px;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    min-width: 200px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.controls-panel h3 {
    font-family: 'Press Start 2P', monospace;
    font-size: 10px;
    color: var(--accent);
    margin-bottom: 8px;
    letter-spacing: 1px;
}
.controls-panel .row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 3px 0;
    font-size: 16px;
    color: var(--muted);
}
.controls-panel .key {
    color: var(--fg);
    background: rgba(255,255,255,0.08);
    padding: 1px 8px;
    border-radius: 3px;
    border: 1px solid var(--border);
    font-size: 14px;
    font-family: 'VT323', monospace;
}

#start-screen {
    position: absolute;
    inset: 0;
    background: 
        radial-gradient(ellipse at center, rgba(40,50,30,0.7), rgba(0,0,0,0.95)),
        repeating-linear-gradient(45deg, transparent, transparent 100px, rgba(255,255,255,0.02) 100px, rgba(255,255,255,0.02) 101px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 30;
    pointer-events: auto;
    transition: opacity 0.4s;
    padding: 20px;
}

#start-screen.hidden {
    opacity: 0;
    pointer-events: none;
}

.title-block {
    text-align: center;
    margin-bottom: 32px;
}

#start-screen h1 {
    font-family: 'Press Start 2P', monospace;
    font-size: clamp(36px, 8vw, 64px);
    color: var(--grass);
    text-shadow: 
        4px 4px 0 var(--dirt-dark),
        8px 8px 0 rgba(0,0,0,0.6);
    letter-spacing: 4px;
    animation: titleBob 3s ease-in-out infinite;
    margin-bottom: 12px;
}

@keyframes titleBob {
    0%, 100% { transform: translateY(0) rotate(-1deg); }
    50% { transform: translateY(-8px) rotate(1deg); }
}

#start-screen .subtitle {
    font-size: 22px;
    color: var(--muted);
    letter-spacing: 2px;
}

#start-btn {
    font-family: 'Press Start 2P', monospace;
    font-size: 18px;
    padding: 18px 40px;
    background: linear-gradient(180deg, var(--grass) 0%, var(--grass-dark) 100%);
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    letter-spacing: 3px;
    box-shadow: 
        0 6px 0 #2a5a1a,
        0 8px 16px rgba(0,0,0,0.5),
        inset 0 2px 0 rgba(255,255,255,0.3);
    transition: all 0.15s;
    text-shadow: 2px 2px 0 rgba(0,0,0,0.4);
}
#start-btn:hover {
    transform: translateY(-2px);
    box-shadow: 
        0 8px 0 #2a5a1a,
        0 10px 20px rgba(0,0,0,0.6),
        inset 0 2px 0 rgba(255,255,255,0.4);
}
#start-btn:active {
    transform: translateY(4px);
    box-shadow: 
        0 2px 0 #2a5a1a,
        inset 0 2px 0 rgba(255,255,255,0.2);
}

.instructions {
    margin-top: 32px;
    padding: 20px 28px;
    background: rgba(0,0,0,0.5);
    border: 2px solid var(--border);
    border-radius: 6px;
    min-width: 360px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.instructions h2 {
    font-family: 'Press Start 2P', monospace;
    font-size: 12px;
    color: var(--accent);
    margin-bottom: 14px;
    letter-spacing: 2px;
    text-align: center;
}
.instructions .row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 6px 0;
    font-size: 18px;
    color: var(--muted);
}
.instructions .key {
    color: var(--fg);
    background: rgba(255,255,255,0.08);
    padding: 2px 10px;
    border-radius: 3px;
    border: 1px solid var(--border);
    font-size: 16px;
    font-family: 'VT323', monospace;
}

#toast {
    position: absolute;
    top: 90px;
    left: 50%;
    transform: translateX(-50%) translateY(-20px);
    background: var(--panel);
    border: 2px solid var(--accent);
    color: var(--fg);
    padding: 8px 18px;
    border-radius: 4px;
    font-size: 18px;
    opacity: 0;
    transition: all 0.3s;
    pointer-events: none;
    box-shadow: 0 4px 12px rgba(0,0,0,0.5);
    text-shadow: 1px 1px 0 #000;
    white-space: nowrap;
}
#toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

#loading {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: rgba(0,0,0,0.85);
    z-index: 25;
}
#loading.hidden { display: none; }
.loading-text {
    font-family: 'Press Start 2P', monospace;
    font-size: 14px;
    color: var(--accent);
    margin-bottom: 16px;
    letter-spacing: 2px;
}
.loading-bar {
    width: 300px;
    height: 12px;
    background: rgba(0,0,0,0.6);
    border: 2px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}
.loading-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--grass), var(--accent));
    animation: loadingAnim 1.5s ease-in-out infinite;
}
@keyframes loadingAnim {
    0% { width: 0%; }
    50% { width: 80%; }
    100% { width: 100%; }
}

</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="ui">
    <div class="crosshair"></div>
    
    <div class="topbar">
        <div class="row"><span class="label">坐标</span><span class="value" id="pos">0, 0, 0</span></div>
        <div class="row"><span class="label">FPS</span><span class="value" id="fps">0</span></div>
        <div class="row"><span class="label">方块数</span><span class="value" id="block-count">0</span></div>
        <div class="row"><span class="label">模式</span><span class="value" id="mode">行走</span></div>
    </div>
    
    <div class="hotbar" id="hotbar"></div>
    
    <div class="controls-panel">
        <h3>操作面板</h3>
        <div class="row"><span>移动</span><span class="key">WASD</span></div>
        <div class="row"><span>跳跃</span><span class="key">空格</span></div>
        <div class="row"><span>飞行</span><span class="key">F</span></div>
        <div class="row"><span>破坏</span><span class="key">左键</span></div>
        <div class="row"><span>放置</span><span class="key">右键</span></div>
        <div class="row"><span>切换</span><span class="key">1-7</span></div>
    </div>
    
    <div id="toast"></div>
    
    <div id="loading">
        <div class="loading-text">正在生成世界...</div>
        <div class="loading-bar"><div class="loading-fill"></div></div>
    </div>
    
    <div id="start-screen" class="hidden">
        <div class="title-block">
            <h1>VOXELCRAFT</h1>
            <p class="subtitle">方块世界 · 创造与生存</p>
        </div>
        <button id="start-btn">开始游戏</button>
        <div class="instructions">
            <h2>操作说明</h2>
            <div class="row"><span>移动</span><span class="key">W A S D</span></div>
            <div class="row"><span>跳跃 / 向上飞</span><span class="key">空格</span></div>
            <div class="row"><span>向下飞</span><span class="key">Shift</span></div>
            <div class="row"><span>切换飞行</span><span class="key">F</span></div>
            <div class="row"><span>破坏方块</span><span class="key">鼠标左键</span></div>
            <div class="row"><span>放置方块</span><span class="key">鼠标右键</span></div>
            <div class="row"><span>选择方块</span><span class="key">1 - 7</span></div>
            <div class="row"><span>滚轮切换</span><span class="key">滚轮</span></div>
            <div class="row"><span>解锁鼠标</span><span class="key">ESC</span></div>
        </div>
    </div>
</div>

<script type="importmap">
{
    "imports": {
        "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
    }
}
</script>

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

// === 常量配置 ===
const WORLD_SIZE = 128;
const WORLD_HEIGHT = 40;
const WATER_LEVEL = 12;
const GRAVITY = 28;
const JUMP_SPEED = 9;
const MOVE_SPEED = 5.5;
const FLY_SPEED = 14;
const FLY_UP_SPEED = 8;
const PLAYER_HEIGHT = 1.8;
const EYE_HEIGHT = 1.62;
const PLAYER_RADIUS = 0.3;
const REACH = 6;
const FOG_NEAR = 30;
const FOG_FAR = 110;

const BLOCKS = {
    GRASS: 1, DIRT: 2, STONE: 3, WOOD: 4, LEAVES: 5, SAND: 6, WATER: 7,
};

const BLOCK_INFO = [
    null,
    { name: '草方块', top: 0x6cbf3a, side: 0x9b7a4a, bottom: 0x6b5028, icon: 'grass' },
    { name: '泥土', top: 0x9b7a4a, side: 0x8b6a3a, bottom: 0x6b5028, icon: 'dirt' },
    { name: '石头', top: 0x9a9a9a, side: 0x808080, bottom: 0x6a6a6a, icon: 'stone' },
    { name: '木头', top: 0xc8a070, side: 0x6b4a2a, bottom: 0xc8a070, icon: 'wood' },
    { name: '树叶', top: 0x5ca043, side: 0x4a8f3a, bottom: 0x3a7a2a, icon: 'leaves' },
    { name: '沙子', top: 0xe8dca0, side: 0xd8c890, bottom: 0xb8a878, icon: 'sand' },
    { name: '水', top: 0x4a8fc8, side: 0x3a7ab8, bottom: 0x2a5a98, icon: 'water' },
];

// === 噪声函数(用于地形生成)===
function hash2D(x, z) {
    let h = Math.sin(x * 127.1 + z * 311.7) * 43758.5453;
    return h - Math.floor(h);
}

function smoothNoise(x, z) {
    const ix = Math.floor(x), iz = Math.floor(z);
    const fx = x - ix, fz = z - iz;
    const a = hash2D(ix, iz);
    const b = hash2D(ix + 1, iz);
    const c = hash2D(ix, iz + 1);
    const d = hash2D(ix + 1, iz + 1);
    const ux = fx * fx * (3 - 2 * fx);
    const uz = fz * fz * (3 - 2 * fz);
    return a * (1 - ux) * (1 - uz) + b * ux * (1 - uz) + c * (1 - ux) * uz + d * ux * uz;
}

function fbm(x, z) {
    let v = 0, amp = 1, freq = 0.022;
    for (let i = 0; i < 4; i++) {
        v += smoothNoise(x * freq, z * freq) * amp;
        amp *= 0.5;
        freq *= 2;
    }
    return v / 1.875;
}

// === 世界数据存储 ===
const worldData = new Map();
const keyStr = (x, y, z) => `${x},${y},${z}`;
const getBlock = (x, y, z) => worldData.get(keyStr(x, y, z)) || 0;
function setBlockData(x, y, z, type) {
    if (type === 0) worldData.delete(keyStr(x, y, z));
    else worldData.set(keyStr(x, y, z), type);
}

// === 程序化地形生成 ===
function generateWorld() {
    // 高度图与基础地形
    for (let x = 0; x < WORLD_SIZE; x++) {
        for (let z = 0; z < WORLD_SIZE; z++) {
            const n = fbm(x, z);
            const n2 = fbm(x + 500, z + 500);
            const h = Math.floor(n * 14 + 6 + n2 * 6); // 高度范围约 6-26
            for (let y = 0; y <= h; y++) {
                let type;
                if (y === h) {
                    // 顶层:水位以下用沙子,以上用草
                    type = h <= WATER_LEVEL + 1 ? BLOCKS.SAND : BLOCKS.GRASS;
                } else if (y > h - 3) {
                    type = BLOCKS.DIRT;
                } else {
                    type = BLOCKS.STONE;
                }
                setBlockData(x, y, z, type);
            }
            // 填充水
            for (let y = h + 1; y <= WATER_LEVEL; y++) {
                setBlockData(x, y, z, BLOCKS.WATER);
            }
        }
    }
    
    // 随机种树
    const treePositions = [];
    let attempts = 0;
    while (treePositions.length < 240 && attempts < 2000) {
        attempts++;
        const x = 3 + Math.floor(Math.random() * (WORLD_SIZE - 6));
        const z = 3 + Math.floor(Math.random() * (WORLD_SIZE - 6));
        for (let y = WORLD_HEIGHT - 1; y >= 0; y--) {
            if (getBlock(x, y, z) === BLOCKS.GRASS) {
                let tooClose = false;
                for (const p of treePositions) {
                    if (Math.abs(p.x - x) < 4 && Math.abs(p.z - z) < 4) {
                        tooClose = true; break;
                    }
                }
                if (!tooClose) treePositions.push({ x, y, z });
                break;
            }
        }
    }
    for (const p of treePositions) placeTree(p.x, p.y, p.z);
}

function placeTree(x, y, z) {
    const h = 4 + Math.floor(Math.random() * 3);
    // 树干
    for (let i = 1; i <= h; i++) {
        setBlockData(x, y + i, z, BLOCKS.WOOD);
    }
    // 树冠
    const topY = y + h;
    for (let dy = -2; dy <= 1; dy++) {
        const r = dy === 1 ? 1 : 2;
        for (let dx = -r; dx <= r; dx++) {
            for (let dz = -r; dz <= r; dz++) {
                if (dx === 0 && dz === 0 && dy < 1) continue;
                if (r === 2 && Math.abs(dx) === 2 && Math.abs(dz) === 2 && Math.random() < 0.6) continue;
                if (dy === 1 && Math.abs(dx) + Math.abs(dz) === 2 && Math.random() < 0.4) continue;
                const tx = x + dx, ty = topY + dy, tz = z + dz;
                if (getBlock(tx, ty, tz) === 0) {
                    setBlockData(tx, ty, tz, BLOCKS.LEAVES);
                }
            }
        }
    }
    setBlockData(x, topY + 2, z, BLOCKS.LEAVES);
}

// === Three.js 场景 ===
const scene = new THREE.Scene();
const fogColor = new THREE.Color(0xc8b890);
scene.background = fogColor;
scene.fog = new THREE.Fog(fogColor, FOG_NEAR, FOG_FAR);

const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);

const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;

// 渐变天空盒
const skyGeo = new THREE.SphereGeometry(500, 32, 16);
const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    uniforms: {
        topColor: { value: new THREE.Color(0x6ea4c8) },
        midColor: { value: new THREE.Color(0xc8d8e0) },
        bottomColor: { value: new THREE.Color(0xe8c890) },
    },
    vertexShader: `
        varying vec3 vWorldPosition;
        void main() {
            vec4 wp = modelMatrix * vec4(position, 1.0);
            vWorldPosition = wp.xyz;
            gl_Position = projectionMatrix * viewMatrix * wp;
        }
    `,
    fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 midColor;
        uniform vec3 bottomColor;
        varying vec3 vWorldPosition;
        void main() {
            float h = normalize(vWorldPosition).y;
            vec3 c;
            if (h > 0.0) {
                c = mix(midColor, topColor, smoothstep(0.0, 0.4, h));
            } else {
                c = mix(midColor, bottomColor, smoothstep(0.0, -0.2, h));
            }
            gl_FragColor = vec4(c, 1.0);
        }
    `,
});
scene.add(new THREE.Mesh(skyGeo, skyMat));

// 光照系统
scene.add(new THREE.AmbientLight(0xc8d8e0, 0.45));
const sun = new THREE.DirectionalLight(0xfff0d0, 0.85);
sun.position.set(80, 100, 60);
scene.add(sun);
scene.add(new THREE.HemisphereLight(0xb0d8ff, 0x6a5a3a, 0.35));

// 漂浮的云
const clouds = new THREE.Group();
const cloudMat = new THREE.MeshBasicMaterial({
    color: 0xffffff,
    transparent: true,
    opacity: 0.7,
    depthWrite: false,
    fog: false,
});
for (let i = 0; i < 18; i++) {
    const w = 15 + Math.random() * 30;
    const d = 15 + Math.random() * 30;
    const cloud = new THREE.Mesh(new THREE.PlaneGeometry(w, d), cloudMat);
    cloud.position.set(
        (Math.random() - 0.5) * 500,
        75 + Math.random() * 15,
        (Math.random() - 0.5) * 500
    );
    cloud.rotation.x = -Math.PI / 2;
    cloud.userData.speed = 0.5 + Math.random() * 1.5;
    clouds.add(cloud);
}
scene.add(clouds);

// === 方块纹理生成(程序化) ===
function createBlockTexture(type) {
    const c = document.createElement('canvas');
    c.width = c.height = 16;
    const ctx = c.getContext('2d');
    
    // 基础灰度
    ctx.fillStyle = '#e8e8e8';
    ctx.fillRect(0, 0, 16, 16);
    
    let s = type * 9301 + 49297;
    const rand = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
    
    // 随机噪点
    for (let i = 0; i < 80; i++) {
        const x = Math.floor(rand() * 16);
        const y = Math.floor(rand() * 16);
        const v = Math.floor(rand() * 60 + 170);
        ctx.fillStyle = `rgb(${v},${v},${v})`;
        ctx.fillRect(x, y, 1, 1);
    }
    
    // 不同方块的特定图案
    if (type === BLOCKS.STONE) {
        ctx.strokeStyle = 'rgba(80,80,80,0.7)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(2, 4); ctx.lineTo(6, 8); ctx.lineTo(5, 12);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(10, 3); ctx.lineTo(13, 8);
        ctx.stroke();
    } else if (type === BLOCKS.WOOD) {
        ctx.fillStyle = 'rgba(100,60,30,0.5)';
        for (let x = 0; x < 16; x += 4) {
            ctx.fillRect(x, 0, 1, 16);
        }
    } else if (type === BLOCKS.LEAVES) {
        for (let i = 0; i < 25; i++) {
            const x = Math.floor(rand() * 16);
            const y = Math.floor(rand() * 16);
            ctx.fillStyle = `rgba(80,160,40,0.5)`;
            ctx.fillRect(x, y, 1, 1);
        }
    } else if (type === BLOCKS.SAND) {
        for (let i = 0; i < 30; i++) {
            const x = Math.floor(rand() * 16);
            const y = Math.floor(rand() * 16);
            ctx.fillStyle = `rgba(200,170,100,0.4)`;
            ctx.fillRect(x, y, 1, 1);
        }
    } else if (type === BLOCKS.GRASS) {
        for (let i = 0; i < 20; i++) {
            const x = Math.floor(rand() * 16);
            const y = Math.floor(rand() * 16);
            ctx.fillStyle = `rgba(60,150,30,0.5)`;
            ctx.fillRect(x, y, 1, 1);
        }
    } else if (type === BLOCKS.DIRT) {
        for (let i = 0; i < 15; i++) {
            const x = Math.floor(rand() * 16);
            const y = Math.floor(rand() * 16);
            ctx.fillStyle = `rgba(100,70,40,0.4)`;
            ctx.fillRect(x, y, 1, 1);
        }
    }
    
    const tex = new THREE.CanvasTexture(c);
    tex.magFilter = THREE.NearestFilter;
    tex.minFilter = THREE.NearestMipmapLinearFilter;
    tex.colorSpace = THREE.SRGBColorSpace;
    return tex;
}

// 创建方块几何体(带顶点色,用于区分顶/侧/底面)
function createBlockGeometry(type) {
    const g = new THREE.BoxGeometry(1, 1, 1);
    const colors = [];
    const info = BLOCK_INFO[type];
    const top = new THREE.Color(info.top);
    const side = new THREE.Color(info.side);
    const bottom = new THREE.Color(info.bottom);
    // BoxGeometry 面顺序:+x, -x, +y, -y, +z, -z
    const faceColors = [side, side, top, bottom, side, side];
    for (let f = 0; f < 6; f++) {
        const c = faceColors[f];
        for (let v = 0; v < 4; v++) colors.push(c.r, c.g, c.b);
    }
    g.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    return g;
}

function createBlockMaterial(type) {
    if (type === BLOCKS.WATER) {
        return new THREE.MeshLambertMaterial({
            color: 0x4a8fc8,
            transparent: true,
            opacity: 0.7,
            depthWrite: false,
        });
    }
    return new THREE.MeshLambertMaterial({
        vertexColors: true,
        map: createBlockTexture(type),
    });
}

// === VoxelRenderer:基于 InstancedMesh 的高效方块渲染 ===
const MAX_INSTANCES = 60000;

class VoxelRenderer {
    constructor() {
        this.meshes = {};
        this.positions = {};
        this.pos2idx = {};
        this.freeIndices = {};
        this.dummy = new THREE.Object3D();
        for (let t = 1; t <= 7; t++) {
            const mesh = new THREE.InstancedMesh(createBlockGeometry(t), createBlockMaterial(t), MAX_INSTANCES);
            mesh.count = 0;
            mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
            mesh.frustumCulled = false;
            scene.add(mesh);
            this.meshes[t] = mesh;
            this.positions[t] = [];
            this.pos2idx[t] = new Map();
            this.freeIndices[t] = [];
        }
    }
    
    add(x, y, z, type) {
        if (type < 1 || type > 7) return;
        const k = keyStr(x, y, z);
        if (this.pos2idx[type].has(k)) return;
        let idx;
        if (this.freeIndices[type].length > 0) {
            idx = this.freeIndices[type].pop();
            this.positions[type][idx] = { x, y, z };
        } else {
            idx = this.positions[type].length;
            if (idx >= MAX_INSTANCES) return;
            this.positions[type].push({ x, y, z });
        }
        this.pos2idx[type].set(k, idx);
        this.dummy.position.set(x + 0.5, y + 0.5, z + 0.5);
        this.dummy.updateMatrix();
        this.meshes[type].setMatrixAt(idx, this.dummy.matrix);
        this.meshes[type].instanceMatrix.needsUpdate = true;
        if (this.meshes[type].count < idx + 1) {
            this.meshes[type].count = idx + 1;
        }
    }
    
    remove(x, y, z) {
        const k = keyStr(x, y, z);
        for (let t = 1; t <= 7; t++) {
            if (this.pos2idx[t].has(k)) {
                const idx = this.pos2idx[t].get(k);
                this.pos2idx[t].delete(k);
                this.freeIndices[t].push(idx);
                this.meshes[t].setMatrixAt(idx, new THREE.Matrix4().makeScale(0, 0, 0));
                this.meshes[t].instanceMatrix.needsUpdate = true;
                return t;
            }
        }
        return 0;
    }
    
    has(x, y, z) {
        const k = keyStr(x, y, z);
        for (let t = 1; t <= 7; t++) {
            if (this.pos2idx[t].has(k)) return true;
        }
        return false;
    }
    
    getMeshes() {
        return Object.values(this.meshes);
    }
}

const voxelRenderer = new VoxelRenderer();

// === 可见性剔除 ===
function isExposed(x, y, z) {
    return getBlock(x+1, y, z) === 0 || getBlock(x-1, y, z) === 0
        || getBlock(x, y+1, z) === 0 || getBlock(x, y-1, z) === 0
        || getBlock(x, y, z+1) === 0 || getBlock(x, y, z-1) === 0;
}

function buildRenderData() {
    let count = 0;
    for (const [k, type] of worldData) {
        const [x, y, z] = k.split(',').map(Number);
        if (type === BLOCKS.WATER) {
            // 水方块只渲染顶层(性能优化)
            if (getBlock(x, y+1, z) !== BLOCKS.WATER) {
                voxelRenderer.add(x, y, z, type);
                count++;
            }
        } else if (isExposed(x, y, z)) {
            voxelRenderer.add(x, y, z, type);
            count++;
        }
    }
    return count;
}

// === 玩家状态 ===
const player = {
    pos: new THREE.Vector3(WORLD_SIZE / 2, 25, WORLD_SIZE / 2),
    vel: new THREE.Vector3(),
    onGround: false,
    flying: false,
    inWater: false,
};

let yaw = 0, pitch = 0;
let isLocked = false;

function findSpawn() {
    const cx = Math.floor(WORLD_SIZE / 2);
    const cz = Math.floor(WORLD_SIZE / 2);
    for (let r = 0; r < 30; r++) {
        for (let dx = -r; dx <= r; dx++) {
            for (let dz = -r; dz <= r; dz++) {
                if (Math.max(Math.abs(dx), Math.abs(dz)) !== r) continue;
                const x = cx + dx, z = cz + dz;
                for (let y = WORLD_HEIGHT - 1; y >= 0; y--) {
                    const t = getBlock(x, y, z);
                    if (t !== 0 && t !== BLOCKS.WATER) {
                        return new THREE.Vector3(x + 0.5, y + 1, z + 0.5);
                    }
                }
            }
        }
    }
    return new THREE.Vector3(cx + 0.5, 25, cz + 0.5);
}

// === 输入处理 ===
const keys = {};
let currentBlock = BLOCKS.GRASS;

window.addEventListener('keydown', (e) => {
    keys[e.code] = true;
    if (e.code.startsWith('Digit')) {
        const n = parseInt(e.code.slice(5));
        if (n >= 1 && n <= 7) selectBlock(n);
    }
    if (e.code === 'KeyF' && isLocked) {
        player.flying = !player.flying;
        player.vel.set(0, 0, 0);
        document.getElementById('mode').textContent = player.flying ? '飞行' : '行走';
        showToast(player.flying ? '飞行模式' : '行走模式');
    }
});
window.addEventListener('keyup', (e) => keys[e.code] = false);

document.addEventListener('mousemove', (e) => {
    if (!isLocked) return;
    yaw -= e.movementX * 0.0025;
    pitch -= e.movementY * 0.0025;
    pitch = Math.max(-Math.PI/2 + 0.01, Math.min(Math.PI/2 - 0.01, pitch));
});

document.addEventListener('mousedown', (e) => {
    if (!isLocked) return;
    if (e.button === 0) breakBlock();
    else if (e.button === 2) placeBlock();
});

document.addEventListener('contextmenu', (e) => e.preventDefault());

document.addEventListener('wheel', (e) => {
    if (!isLocked) return;
    e.preventDefault();
    const dir = e.deltaY > 0 ? 1 : -1;
    let next = currentBlock + dir;
    if (next < 1) next = 7;
    if (next > 7) next = 1;
    selectBlock(next);
}, { passive: false });

const startBtn = document.getElementById('start-btn');
startBtn.addEventListener('click', () => {
    const p = document.body.requestPointerLock();
    if (p && p.catch) p.catch(err => console.warn('Pointer lock failed:', err));
});

document.addEventListener('pointerlockchange', () => {
    isLocked = document.pointerLockElement === document.body;
    if (isLocked) {
        document.getElementById('start-screen').classList.add('hidden');
    } else {
        document.getElementById('start-screen').classList.remove('hidden');
    }
});

// === 射线检测与方块交互 ===
const raycaster = new THREE.Raycaster();
raycaster.far = REACH;

// 选中方块的高亮线框
const highlight = new THREE.LineSegments(
    new THREE.EdgesGeometry(new THREE.BoxGeometry(1.002, 1.002, 1.002)),
    new THREE.LineBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.6, depthTest: true })
);
highlight.visible = false;
scene.add(highlight);

function getTargetBlock() {
    raycaster.setFromCamera({ x: 0, y: 0 }, camera);
    const intersects = raycaster.intersectObjects(voxelRenderer.getMeshes(), false);
    if (intersects.length === 0) return null;
    const hit = intersects[0];
    const n = hit.face.normal;
    const bx = Math.floor(hit.point.x - n.x * 0.5);
    const by = Math.floor(hit.point.y - n.y * 0.5);
    const bz = Math.floor(hit.point.z - n.z * 0.5);
    return {
        x: bx, y: by, z: bz,
        normal: n.clone(),
        point: hit.point.clone(),
        type: getBlock(bx, by, bz),
    };
}

function breakBlock() {
    const t = getTargetBlock();
    if (!t || t.type === 0 || t.type === BLOCKS.WATER) return;
    
    // 生成破坏粒子
    spawnParticles(t.x, t.y, t.z, t.type);
    
    setBlockData(t.x, t.y, t.z, 0);
    voxelRenderer.remove(t.x, t.y, t.z);
    
    // 暴露周围原本被遮挡的方块
    const dirs = [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
    for (const [dx, dy, dz] of dirs) {
        const nx = t.x+dx, ny = t.y+dy, nz = t.z+dz;
        const tt = getBlock(nx, ny, nz);
        if (tt !== 0 && !voxelRenderer.has(nx, ny, nz) && isExposed(nx, ny, nz)) {
            voxelRenderer.add(nx, ny, nz, tt);
        }
    }
}

function placeBlock() {
    const t = getTargetBlock();
    if (!t) return;
    const nx = t.x + Math.round(t.normal.x);
    const ny = t.y + Math.round(t.normal.y);
    const nz = t.z + Math.round(t.normal.z);
    
    if (getBlock(nx, ny, nz) !== 0) return;
    
    // 不能放置在玩家所在位置(水方块除外)
    if (currentBlock !== BLOCKS.WATER) {
        const pBox = getPlayerAABB();
        const bBox = { minX: nx, maxX: nx+1, minY: ny, maxY: ny+1, minZ: nz, maxZ: nz+1 };
        if (boxesIntersect(pBox, bBox)) return;
    }
    
    setBlockData(nx, ny, nz, currentBlock);
    voxelRenderer.add(nx, ny, nz, currentBlock);
}

function boxesIntersect(a, b) {
    return a.maxX > b.minX && a.minX < b.maxX
        && a.maxY > b.minY && a.minY < b.maxY
        && a.maxZ > b.minZ && a.minZ < b.maxZ;
}

function getPlayerAABB() {
    return {
        minX: player.pos.x - PLAYER_RADIUS,
        maxX: player.pos.x + PLAYER_RADIUS,
        minY: player.pos.y,
        maxY: player.pos.y + PLAYER_HEIGHT,
        minZ: player.pos.z - PLAYER_RADIUS,
        maxZ: player.pos.z + PLAYER_RADIUS,
    };
}

// === 破坏方块时的粒子效果 ===
const particles = new THREE.Group();
scene.add(particles);
const particleGeo = new THREE.BoxGeometry(0.18, 0.18, 0.18);
const particleMatCache = {};
function getParticleMat(type) {
    if (!particleMatCache[type]) {
        particleMatCache[type] = new THREE.MeshLambertMaterial({ color: BLOCK_INFO[type].top });
    }
    return particleMatCache[type];
}

function spawnParticles(x, y, z, type) {
    const mat = getParticleMat(type);
    for (let i = 0; i < 8; i++) {
        const p = new THREE.Mesh(particleGeo, mat);
        p.position.set(
            x + 0.5 + (Math.random() - 0.5) * 0.5,
            y + 0.5 + (Math.random() - 0.5) * 0.5,
            z + 0.5 + (Math.random() - 0.5) * 0.5
        );
        p.userData = {
            vel: new THREE.Vector3(
                (Math.random() - 0.5) * 5,
                Math.random() * 5 + 3,
                (Math.random() - 0.5) * 5
            ),
            life: 0.8 + Math.random() * 0.5,
            maxLife: 1.3,
        };
        particles.add(p);
    }
}

function updateParticles(dt) {
    for (let i = particles.children.length - 1; i >= 0; i--) {
        const p = particles.children[i];
        p.userData.life -= dt;
        if (p.userData.life <= 0) {
            particles.remove(p);
            continue;
        }
        p.userData.vel.y -= 25 * dt;
        p.position.x += p.userData.vel.x * dt;
        p.position.y += p.userData.vel.y * dt;
        p.position.z += p.userData.vel.z * dt;
        p.rotation.x += dt * 8;
        p.rotation.z += dt * 6;
        p.scale.setScalar(Math.max(0.01, p.userData.life / p.userData.maxLife));
    }
}

// === 玩家物理与碰撞 ===
function collideAxis(axis, delta) {
    player.pos[axis] += delta;
    
    let iter = 0;
    while (iter++ < 3) {
        const box = getPlayerAABB();
        let collided = false;
        
        const baseX = Math.floor(player.pos.x);
        const baseY = Math.floor(player.pos.y);
        const baseZ = Math.floor(player.pos.z);
        
        for (let x = baseX - 1; x <= baseX + 1; x++) {
            for (let y = baseY - 1; y <= baseY + 2; y++) {
                for (let z = baseZ - 1; z <= baseZ + 1; z++) {
                    const t = getBlock(x, y, z);
                    if (t === 0 || t === BLOCKS.WATER) continue;
                    
                    if (box.maxX > x && box.minX < x + 1
                        && box.maxY > y && box.minY < y + 1
                        && box.maxZ > z && box.minZ < z + 1) {
                        
                        if (axis === 'x') {
                            if (delta > 0) player.pos.x = x - PLAYER_RADIUS - 0.0001;
                            else player.pos.x = x + 1 + PLAYER_RADIUS + 0.0001;
                            player.vel.x = 0;
                        } else if (axis === 'y') {
                            if (delta > 0) {
                                player.pos.y = y - PLAYER_HEIGHT - 0.0001;
                            } else {
                                player.pos.y = y + 1 + 0.0001;
                                player.onGround = true;
                            }
                            player.vel.y = 0;
                        } else {
                            if (delta > 0) player.pos.z = z - PLAYER_RADIUS - 0.0001;
                            else player.pos.z = z + 1 + PLAYER_RADIUS + 0.0001;
                            player.vel.z = 0;
                        }
                        collided = true;
                        break;
                    }
                }
                if (collided) break;
            }
            if (collided) break;
        }
        
        if (!collided) break;
    }
}

function updatePlayer(dt) {
    // 检测是否在水中
    player.inWater = getBlock(
        Math.floor(player.pos.x),
        Math.floor(player.pos.y + 0.5),
        Math.floor(player.pos.z)
    ) === BLOCKS.WATER;
    
    // 计算前向与右向(基于相机朝向,投影到水平面)
    const euler = new THREE.Euler(pitch, yaw, 0, 'YXZ');
    const forward = new THREE.Vector3(0, 0, -1).applyEuler(euler);
    forward.y = 0;
    if (forward.lengthSq() > 0) forward.normalize();
    const right = new THREE.Vector3(1, 0, 0).applyEuler(euler);
    right.y = 0;
    if (right.lengthSq() > 0) right.normalize();
    
    if (player.flying) {
        // 飞行模式
        const speed = FLY_SPEED;
        const move = new THREE.Vector3();
        if (keys['KeyW']) move.add(forward);
        if (keys['KeyS']) move.sub(forward);
        if (keys['KeyA']) move.sub(right);
        if (keys['KeyD']) move.add(right);
        if (move.lengthSq() > 0) move.normalize().multiplyScalar(speed);
        
        player.pos.x += move.x * dt;
        player.pos.z += move.z * dt;
        if (keys['Space']) player.pos.y += FLY_UP_SPEED * dt;
        if (keys['ShiftLeft'] || keys['ShiftRight']) player.pos.y -= FLY_UP_SPEED * dt;
    } else {
        // 行走模式
        const speed = player.inWater ? MOVE_SPEED * 0.5 : MOVE_SPEED;
        const move = new THREE.Vector3();
        if (keys['KeyW']) move.add(forward);
        if (keys['KeyS']) move.sub(forward);
        if (keys['KeyA']) move.sub(right);
        if (keys['KeyD']) move.add(right);
        if (move.lengthSq() > 0) move.normalize().multiplyScalar(speed);
        
        player.vel.x = move.x;
        player.vel.z = move.z;
        
        // 重力(水中减速)
        const g = player.inWater ? GRAVITY * 0.3 : GRAVITY;
        player.vel.y -= g * dt;
        
        // 跳跃 / 水中上浮
        if (keys['Space']) {
            if (player.inWater) {
                player.vel.y = 4;
            } else if (player.onGround) {
                player.vel.y = JUMP_SPEED;
                player.onGround = false;
            }
        }
        
        // 水中阻力
        if (player.inWater) {
            player.vel.y *= 0.9;
        }
        
        player.onGround = false;
        collideAxis('x', player.vel.x * dt);
        collideAxis('z', player.vel.z * dt);
        collideAxis('y', player.vel.y * dt);
        
        // 防止掉出世界
        if (player.pos.y < -10) {
            player.pos.copy(findSpawn());
            player.vel.set(0, 0, 0);
        }
    }
    
    // 更新相机
    camera.position.set(player.pos.x, player.pos.y + EYE_HEIGHT, player.pos.z);
    camera.rotation.set(pitch, yaw, 0, 'YXZ');
}

// === UI 构建 ===
function selectBlock(n) {
    currentBlock = n;
    document.querySelectorAll('.slot').forEach(s => {
        s.classList.toggle('active', parseInt(s.dataset.type) === n);
    });
    showToast(BLOCK_INFO[n].name);
}

function buildHotbar() {
    const hb = document.getElementById('hotbar');
    hb.innerHTML = '';
    for (let i = 1; i <= 7; i++) {
        const slot = document.createElement('div');
        slot.className = 'slot';
        slot.dataset.type = i;
        slot.innerHTML = `
            <div class="icon icon-${BLOCK_INFO[i].icon}"></div>
            <div class="num">${i}</div>
            <div class="name">${BLOCK_INFO[i].name}</div>
        `;
        slot.addEventListener('click', (e) => {
            e.stopPropagation();
            selectBlock(i);
        });
        hb.appendChild(slot);
    }
    // 默认激活第一个
    document.querySelector('.slot[data-type="1"]').classList.add('active');
    currentBlock = 1;
}

let toastTimer = null;
function showToast(msg) {
    const t = document.getElementById('toast');
    t.textContent = msg;
    t.classList.add('show');
    if (toastTimer) clearTimeout(toastTimer);
    toastTimer = setTimeout(() => t.classList.remove('show'), 1500);
}

// === FPS 与信息更新 ===
let lastTime = performance.now();
let fpsTime = 0, fpsCount = 0, fps = 0;

function updateInfo() {
    document.getElementById('pos').textContent = 
        `${player.pos.x.toFixed(1)}, ${player.pos.y.toFixed(1)}, ${player.pos.z.toFixed(1)}`;
    document.getElementById('fps').textContent = fps;
    const counts = Object.values(voxelRenderer.meshes).reduce((s, m) => s + m.count, 0);
    document.getElementById('block-count').textContent = counts;
}

// === 主循环 ===
function animate() {
    requestAnimationFrame(animate);
    
    const now = performance.now();
    const dt = Math.min((now - lastTime) / 1000, 0.05);
    lastTime = now;
    
    // FPS 统计
    fpsTime += dt;
    fpsCount++;
    if (fpsTime > 0.5) {
        fps = Math.round(fpsCount / fpsTime);
        fpsTime = 0;
        fpsCount = 0;
    }
    
    if (isLocked) {
        updatePlayer(dt);
        
        // 高亮当前指向的方块
        const target = getTargetBlock();
        if (target) {
            highlight.position.set(target.x + 0.5, target.y + 0.5, target.z + 0.5);
            highlight.visible = true;
        } else {
            highlight.visible = false;
        }
    } else {
        highlight.visible = false;
    }
    
    // 粒子更新
    updateParticles(dt);
    
    // 云朵移动
    clouds.children.forEach(c => {
        c.position.x += dt * (c.userData.speed || 1);
        if (c.position.x > 250) c.position.x = -250;
    });
    
    renderer.render(scene, camera);
    updateInfo();
}

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

// === 启动流程 ===
buildHotbar();

// 延迟一帧让 loading 屏幕显示,再生成世界
setTimeout(() => {
    const start = performance.now();
    generateWorld();
    const genTime = performance.now() - start;
    console.log(`世界生成: ${genTime.toFixed(0)}ms, ${worldData.size} 方块`);
    
    const buildStart = performance.now();
    const renderCount = buildRenderData();
    const buildTime = performance.now() - buildStart;
    console.log(`渲染构建: ${buildTime.toFixed(0)}ms, ${renderCount} 可见方块`);
    
    player.pos.copy(findSpawn());
    
    document.getElementById('loading').classList.add('hidden');
    document.getElementById('start-screen').classList.remove('hidden');
    
    animate();
}, 100);

</script>
</body>
</html>

Game Source: 方块世界 · VoxelCraft

Creator: RocketDolphin44

Libraries: three

Complexity: complex (1394 lines, 43.2 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: voxelcraft-rocketdolphin44" to link back to the original. Then publish at arcadelab.ai/publish.