<!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>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
margin: 0; overflow: hidden; background-color: #1a1a2e;
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; z-index: 10;
}
/* 准星 */
#crosshair {
position: absolute; top: 50%; left: 50%; width: 20px; height: 20px;
transform: translate(-50%, -50%); pointer-events: none;
z-index: 20;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,255,0.9);
border-radius: 2px;
}
#crosshair::before {
width: 20px; height: 2px; top: 9px; left: 0;
}
#crosshair::after {
width: 2px; height: 20px; top: 0; left: 9px;
}
/* 横屏提示 */
#orientation-warning {
display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white; z-index: 100;
justify-content: center; align-items: center; flex-direction: column;
text-align: center; padding: 20px;
}
@media screen and (orientation: portrait) {
#orientation-warning { display: flex; }
}
#orientation-warning h1 { font-size: 24px; margin-bottom: 10px; }
#orientation-warning p { font-size: 14px; opacity: 0.8; margin-bottom: 30px; }
#orientation-warning .icon { font-size: 60px; animation: rotate 2s infinite; }
@keyframes rotate { 0%,100%{transform:rotate(0)}50%{transform:rotate(90deg)} }
/* 移动端控制区 */
.mobile-control { pointer-events: auto; position: absolute; }
/* 左侧摇杆区 */
#joystick-zone {
bottom: 80px; left: 30px; width: 110px; height: 110px;
background: rgba(255, 255, 255, 0.12); border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.25);
display: none;
backdrop-filter: blur(8px);
}
#joystick-knob {
position: absolute; top: 50%; left: 50%; width: 45px; height: 45px;
background: rgba(255, 255, 255, 0.4); border-radius: 50%;
transform: translate(-50%, -50%);
border: 2px solid rgba(255,255,255,0.5);
transition: background 0.1s;
}
#joystick-knob:active { background: rgba(255, 255, 255, 0.6); }
/* 右侧按钮区 */
#action-buttons {
bottom: 60px; right: 25px; display: none; flex-direction: column; gap: 12px;
}
.btn {
width: 55px; height: 55px; border-radius: 50%;
background: rgba(30, 30, 50, 0.7); border: 2px solid rgba(255,255,255,0.4);
color: white; font-weight: bold; font-size: 11px;
display: flex; justify-content: center; align-items: center;
backdrop-filter: blur(6px);
transition: all 0.1s;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.btn:active {
background: rgba(255, 255, 255, 0.25);
transform: scale(0.92);
border-color: rgba(255,255,255,0.8);
}
.btn-jump { background: rgba(80, 100, 255, 0.6); }
.btn-break { background: rgba(255, 80, 80, 0.6); }
.btn-place { background: rgba(80, 255, 120, 0.6); }
/* 物品栏 */
#hotbar {
position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px; background: rgba(20,20,35,0.85);
padding: 6px 8px; border-radius: 10px; pointer-events: auto;
border: 1px solid rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
}
.slot {
width: 38px; height: 38px; border: 2px solid #444;
border-radius: 6px; background-size: cover; background-position: center;
image-rendering: pixelated; cursor: pointer;
transition: all 0.15s;
}
.slot:hover { border-color: #888; transform: scale(1.05); }
.slot.active {
border-color: #fff; transform: scale(1.12);
box-shadow: 0 0 0 2px rgba(255,255,255,0.4), 0 4px 12px rgba(0,0,0,0.4);
z-index: 2;
}
/* 说明文字 */
#info {
position: absolute; top: 12px; left: 12px; color: rgba(255,255,255,0.85);
font-size: 11px; pointer-events: none; text-shadow: 0 1px 3px rgba(0,0,0,0.8);
background: rgba(20,20,35,0.7); padding: 6px 10px; border-radius: 6px;
border: 1px solid rgba(255,255,255,0.15);
max-width: 280px; line-height: 1.4;
}
/* 加载提示 */
#loading {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: white; font-size: 16px; text-align: center;
background: rgba(30,30,50,0.9); padding: 20px 30px; border-radius: 12px;
border: 1px solid rgba(255,255,255,0.2); z-index: 50;
}
#loading.hidden { display: none; }
</style>
</head>
<body>
<!-- 3D 容器 -->
<div id="container"></div>
<div id="loading">🌍 正在生成世界...</div>
<!-- UI 层 -->
<div id="ui-layer">
<div id="crosshair"></div>
<div id="info">
🎮 <strong>操作指南</strong><br>
📱 左摇杆:移动角色 | 右屏滑动:转动视角<br>
🔘 按钮:跳跃 / 破坏 / 放置方块
</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">
<div class="icon">📱</div>
<h1>请旋转手机</h1>
<p>横屏模式可获得最佳游戏体验</p>
<div style="font-size:40px">↹</div>
</div>
<!-- Three.js 导入 (修复了URL空格问题) -->
<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 CONFIG = {
WORLD_SIZE: 24,
BLOCK_SIZE: 1,
PLAYER_HEIGHT: 1.7,
PLAYER_RADIUS: 0.35,
MOVE_SPEED: 8,
JUMP_FORCE: 10,
GRAVITY: 25,
INTERACT_DISTANCE: 5,
FOG_COLOR: 0x87ceeb,
SKY_COLOR: 0x87ceeb
};
// ========== 程序化纹理生成 ==========
function createBlockTexture(baseColor, pattern = 'default') {
const canvas = document.createElement('canvas');
canvas.width = 64; canvas.height = 64;
const ctx = canvas.getContext('2d');
// 基础填充
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, 64, 64);
// 添加噪点/纹理
for(let i = 0; i < 300; i++) {
const alpha = Math.random() * 0.2;
ctx.fillStyle = Math.random() > 0.5
? `rgba(0,0,0,${alpha})`
: `rgba(255,255,255,${alpha * 0.6})`;
ctx.fillRect(
Math.random() * 64,
Math.random() * 64,
2 + Math.random() * 2,
2 + Math.random() * 2
);
}
// 边框效果
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.lineWidth = 3;
ctx.strokeRect(1, 1, 62, 62);
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.generateMipmaps = false;
return texture;
}
// ========== 材质库 ==========
const MATERIALS = {
grass: new THREE.MeshLambertMaterial({
map: createBlockTexture('#5a9c44'),
color: 0x5a9c44
}),
dirt: new THREE.MeshLambertMaterial({
map: createBlockTexture('#6b4423'),
color: 0x6b4423
}),
stone: new THREE.MeshLambertMaterial({
map: createBlockTexture('#7a7a7a'),
color: 0x7a7a7a
}),
wood: new THREE.MeshLambertMaterial({
map: createBlockTexture('#8b6914'),
color: 0x8b6914
}),
brick: new THREE.MeshLambertMaterial({
map: createBlockTexture('#a52a2a'),
color: 0xa52a2a
}),
sand: new THREE.MeshLambertMaterial({
map: createBlockTexture('#e6c96c'),
color: 0xe6c96c
}),
leaves: new THREE.MeshLambertMaterial({
map: createBlockTexture('#3a6b2e'),
color: 0x3a6b2e,
transparent: true,
opacity: 0.9
})
};
const BLOCK_TYPES = Object.keys(MATERIALS);
// ========== 场景初始化 ==========
const scene = new THREE.Scene();
scene.background = new THREE.Color(CONFIG.SKY_COLOR);
scene.fog = new THREE.FogExp2(CONFIG.FOG_COLOR, 0.018);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
200
);
camera.position.set(0, CONFIG.PLAYER_HEIGHT, 0);
camera.rotation.order = 'YXZ';
const renderer = new THREE.WebGLRenderer({
antialias: false,
powerPreference: "high-performance",
alpha: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('container').appendChild(renderer.domElement);
// ========== 光照 ==========
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
dirLight.position.set(30, 50, 30);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(1024, 1024);
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 100;
dirLight.shadow.camera.left = -40;
dirLight.shadow.camera.right = 40;
dirLight.shadow.camera.top = 40;
dirLight.shadow.camera.bottom = -40;
scene.add(dirLight);
// ========== 世界生成 ==========
const blocks = [];
const blockGeometry = new THREE.BoxGeometry(
CONFIG.BLOCK_SIZE,
CONFIG.BLOCK_SIZE,
CONFIG.BLOCK_SIZE
);
function createBlock(x, y, z, materialKey) {
const material = MATERIALS[materialKey];
const mesh = new THREE.Mesh(blockGeometry, material);
mesh.position.set(x, y, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.userData = { type: materialKey, x, y, z };
scene.add(mesh);
blocks.push(mesh);
return mesh;
}
function generateTerrain() {
const size = CONFIG.WORLD_SIZE;
const half = size / 2;
for (let x = -half; x < half; x++) {
for (let z = -half; z < half; z++) {
// 简单的高度图生成
const height = Math.floor(
Math.sin(x * 0.3) * 1.5 +
Math.cos(z * 0.3) * 1.5 +
Math.sin((x + z) * 0.2) * 0.8
);
// 生成地层
for (let y = -3; y <= height; y++) {
let blockType = 'stone';
if (y === height) {
blockType = height < -1 ? 'sand' : 'grass';
} else if (y > height - 2) {
blockType = 'dirt';
}
createBlock(x, y, z, blockType);
}
// 随机生成树木
if (height >= 0 && Math.random() < 0.03) {
generateTree(x, height + 1, z);
}
}
}
}
function generateTree(x, y, z) {
// 树干
for (let h = 0; h < 4; h++) {
createBlock(x, y + h, z, 'wood');
}
// 树叶
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
for (let dy = 2; dy <= 4; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 3) < 4) {
if (Math.random() > 0.2) {
createBlock(x + dx, y + dy, z + dz, 'leaves');
}
}
}
}
}
}
// ========== 玩家手臂 (视觉效果) ==========
const armGroup = new THREE.Group();
const armMesh = new THREE.Mesh(
new THREE.BoxGeometry(0.15, 0.5, 0.15),
new THREE.MeshLambertMaterial({ color: 0xffccaa })
);
armMesh.position.set(0.25, -0.5, -0.35);
armGroup.add(armMesh);
camera.add(armGroup);
scene.add(camera);
// ========== 输入状态 ==========
const input = {
move: { x: 0, z: 0 },
jump: false,
yaw: 0,
pitch: 0
};
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// ========== 物理系统 ==========
const player = {
position: new THREE.Vector3(0, 10, 0),
velocity: new THREE.Vector3(0, 0, 0),
onGround: false,
box: new THREE.Box3()
};
function getPlayerBox(pos) {
return new THREE.Box3(
new THREE.Vector3(pos.x - CONFIG.PLAYER_RADIUS, pos.y, pos.z - CONFIG.PLAYER_RADIUS),
new THREE.Vector3(pos.x + CONFIG.PLAYER_RADIUS, pos.y + CONFIG.PLAYER_HEIGHT, pos.z + CONFIG.PLAYER_RADIUS)
);
}
function checkCollision(position) {
const playerBox = getPlayerBox(position);
for (const block of blocks) {
const blockBox = new THREE.Box3().setFromObject(block);
if (playerBox.intersectsBox(blockBox)) {
return true;
}
}
return false;
}
// ========== 交互系统 (射线检测) ==========
const raycaster = new THREE.Raycaster();
const highlightMesh = new THREE.Mesh(
new THREE.BoxGeometry(1.02, 1.02, 1.02),
new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true,
transparent: true,
opacity: 0.4
})
);
highlightMesh.visible = false;
scene.add(highlightMesh);
let selectedBlockIndex = 0;
function handleInteract(isPlace) {
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(blocks);
if (intersects.length === 0 || intersects[0].distance > CONFIG.INTERACT_DISTANCE) {
return;
}
const hit = intersects[0];
if (!isPlace) {
// 破坏方块
scene.remove(hit.object);
const idx = blocks.indexOf(hit.object);
if (idx > -1) blocks.splice(idx, 1);
} else {
// 放置方块
const pos = hit.point.clone().add(hit.face.normal.multiplyScalar(0.5)).floor();
// 防止放置在玩家体内
const playerPos = player.position.clone();
playerPos.y -= CONFIG.PLAYER_HEIGHT / 2;
const dist = playerPos.distanceTo(
new THREE.Vector3(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
);
if (dist < 1.2) return;
// 检查是否已存在方块
const exists = blocks.some(b =>
Math.abs(b.position.x - (pos.x + 0.5)) < 0.1 &&
Math.abs(b.position.y - (pos.y + 0.5)) < 0.1 &&
Math.abs(b.position.z - (pos.z + 0.5)) < 0.1
);
if (!exists) {
createBlock(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, BLOCK_TYPES[selectedBlockIndex]);
}
}
}
// ========== 物品栏 ==========
const hotbar = document.getElementById('hotbar');
BLOCK_TYPES.forEach((type, index) => {
const slot = document.createElement('div');
slot.className = 'slot' + (index === 0 ? ' active' : '');
// 使用 canvas 生成预览图
const canvas = MATERIALS[type].map?.image;
if (canvas) {
slot.style.backgroundImage = `url(${canvas.toDataURL()})`;
} else {
slot.style.backgroundColor = MATERIALS[type].color?.getHexString
? '#' + MATERIALS[type].color.getHexString()
: '#888';
}
slot.addEventListener('click', (e) => {
e.stopPropagation();
selectedBlockIndex = index;
document.querySelectorAll('.slot').forEach(s => s.classList.remove('active'));
slot.classList.add('active');
});
hotbar.appendChild(slot);
});
// ========== 移动端控制 ==========
function setupMobileControls() {
if (!isMobile) return;
document.getElementById('joystick-zone').style.display = 'block';
document.getElementById('action-buttons').style.display = 'flex';
// --- 虚拟摇杆 ---
const joystickZone = document.getElementById('joystick-zone');
const joystickKnob = document.getElementById('joystick-knob');
let joyTouchId = null;
let joyCenter = { x: 0, y: 0 };
const joyMaxDist = 32;
joystickZone.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.changedTouches[0];
joyTouchId = touch.identifier;
const rect = joystickZone.getBoundingClientRect();
joyCenter = {
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 (const touch of e.changedTouches) {
if (touch.identifier === joyTouchId) {
updateJoystick(touch.clientX, touch.clientY);
break;
}
}
}, { passive: false });
function endJoystick(e) {
for (const touch of e.changedTouches) {
if (touch.identifier === joyTouchId) {
joyTouchId = null;
input.move.x = 0;
input.move.z = 0;
joystickKnob.style.transform = 'translate(-50%, -50%)';
break;
}
}
}
joystickZone.addEventListener('touchend', endJoystick);
joystickZone.addEventListener('touchcancel', endJoystick);
function updateJoystick(x, y) {
let dx = x - joyCenter.x;
let dy = y - joyCenter.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > joyMaxDist) {
const ratio = joyMaxDist / dist;
dx *= ratio;
dy *= ratio;
}
joystickKnob.style.transform =
`translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
// 映射到移动输入 (-1 ~ 1)
input.move.x = dx / joyMaxDist;
input.move.z = -dy / joyMaxDist; // Y轴反向
}
// --- 右侧屏幕滑动看视角 ---
let lookTouchId = null;
let lastLook = { x: 0, y: 0 };
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 * 0.5) {
lookTouchId = touch.identifier;
lastLook = { x: touch.clientX, y: touch.clientY };
}
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (lookTouchId === null) return;
for (const touch of e.changedTouches) {
if (touch.identifier === lookTouchId) {
const dx = touch.clientX - lastLook.x;
const dy = touch.clientY - lastLook.y;
input.yaw -= dx * 0.004;
input.pitch -= dy * 0.004;
input.pitch = Math.max(-Math.PI/2 + 0.1, Math.min(Math.PI/2 - 0.1, input.pitch));
lastLook = { x: touch.clientX, y: touch.clientY };
break;
}
}
}, { passive: false });
document.addEventListener('touchend', (e) => {
for (const touch of e.changedTouches) {
if (touch.identifier === lookTouchId) {
lookTouchId = null;
break;
}
}
});
// --- 按钮绑定 ---
document.getElementById('btn-jump').addEventListener('touchstart', (e) => {
e.preventDefault();
if (player.onGround) {
player.velocity.y = CONFIG.JUMP_FORCE;
player.onGround = false;
}
});
document.getElementById('btn-break').addEventListener('touchstart', (e) => {
e.preventDefault();
handleInteract(false);
});
document.getElementById('btn-place').addEventListener('touchstart', (e) => {
e.preventDefault();
handleInteract(true);
});
}
// ========== PC端控制 (备用) ==========
function setupDesktopControls() {
if (isMobile) return;
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (['Space', 'ArrowUp', 'ArrowDown'].includes(e.code)) {
e.preventDefault();
}
});
document.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
// 鼠标视角
let isLocked = false;
document.addEventListener('click', () => {
if (!isLocked && document.body.requestPointerLock) {
document.body.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === document.body;
});
document.addEventListener('mousemove', (e) => {
if (isLocked) {
input.yaw -= e.movementX * 0.002;
input.pitch -= e.movementY * 0.002;
input.pitch = Math.max(-Math.PI/2 + 0.1, Math.min(Math.PI/2 - 0.1, input.pitch));
}
});
// 鼠标交互
document.addEventListener('mousedown', (e) => {
if (e.button === 0) handleInteract(false); // 左键破坏
if (e.button === 2) handleInteract(true); // 右键放置
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
// 键盘输入处理(在动画循环中读取)
window.updateKeyboardInput = () => {
input.move.z = (keys['KeyW'] ? 1 : 0) - (keys['KeyS'] ? 1 : 0);
input.move.x = (keys['KeyD'] ? 1 : 0) - (keys['KeyA'] ? 1 : 0);
if (keys['Space'] && player.onGround) {
player.velocity.y = CONFIG.JUMP_FORCE;
player.onGround = false;
keys['Space'] = false;
}
// 物品栏切换
for (let i = 1; i <= 6; i++) {
if (keys[`Digit${i}`]) {
selectedBlockIndex = i - 1;
document.querySelectorAll('.slot').forEach((s, idx) => {
s.classList.toggle('active', idx === selectedBlockIndex);
});
keys[`Digit${i}`] = false;
break;
}
}
};
}
// ========== 主循环 ==========
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const delta = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
// 更新键盘输入 (PC)
if (window.updateKeyboardInput) {
window.updateKeyboardInput();
}
// 更新相机旋转
camera.rotation.set(input.pitch, input.yaw, 0, 'YXZ');
// 计算移动方向
if (input.move.x !== 0 || input.move.z !== 0) {
const forward = new THREE.Vector3(0, 0, -1);
forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), input.yaw);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3(1, 0, 0);
right.applyAxisAngle(new THREE.Vector3(0, 1, 0), input.yaw);
right.y = 0;
right.normalize();
const moveDir = new THREE.Vector3()
.addScaledVector(forward, input.move.z)
.addScaledVector(right, input.move.x)
.normalize()
.multiplyScalar(CONFIG.MOVE_SPEED * delta);
// 尝试移动 XZ
const oldPos = player.position.clone();
player.position.x += moveDir.x;
player.position.z += moveDir.z;
if (checkCollision(player.position)) {
player.position.x = oldPos.x;
player.position.z = oldPos.z;
}
// 手臂摆动
const swing = Math.sin(now * 0.012) * 0.08;
armMesh.position.x = 0.25 + swing;
armMesh.rotation.z = -swing * 0.5;
} else {
armMesh.position.x = 0.25;
armMesh.rotation.set(0, 0, 0);
}
// 重力
player.velocity.y -= CONFIG.GRAVITY * delta;
player.position.y += player.velocity.y * delta;
// 地面检测
if (checkCollision(player.position)) {
if (player.velocity.y < 0) {
// 落地
player.velocity.y = 0;
player.onGround = true;
// 修正位置防止嵌入
player.position.y = Math.ceil(player.position.y - CONFIG.PLAYER_HEIGHT) + CONFIG.PLAYER_HEIGHT + 0.01;
} else {
// 撞头
player.velocity.y = 0;
player.position.y -= 0.05;
}
} else {
player.onGround = false;
}
// 同步相机位置
camera.position.copy(player.position);
// 更新高亮框
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const hits = raycaster.intersectObjects(blocks);
if (hits.length > 0 && hits[0].distance < CONFIG.INTERACT_DISTANCE) {
highlightMesh.position.copy(hits[0].object.position);
highlightMesh.visible = true;
} else {
highlightMesh.visible = false;
}
renderer.render(scene, camera);
}
// ========== 初始化 ==========
function init() {
generateTerrain();
setupMobileControls();
setupDesktopControls();
// 玩家初始位置
player.position.set(0, 20, 0);
camera.position.copy(player.position);
// 隐藏加载提示
setTimeout(() => {
document.getElementById('loading').classList.add('hidden');
}, 800);
// 窗口调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
}
// 启动
init();
</script>
</body>
</html>
index.html
md
README.md
index.html