MC 仿制 · 真实贴图 + 设置界面
by ElectricShark88693 lines30.5 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>MC 仿制 · 真实贴图 + 设置界面</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;touch-action:none}
canvas{display:block}
/* 顶部菜单 */
#topBar{position:fixed;top:calc(6px + env(safe-area-inset-top));left:8px;right:8px;display:flex;justify-content:space-between;z-index:40;pointer-events:none}
#topLeft,#topRight{display:flex;gap:6px;pointer-events:auto}
.topBtn{background:rgba(0,0,0,.45);color:#fff;border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:7px 11px;font-size:12px;text-shadow:0 1px 2px #000}
.topBtn:active{background:rgba(255,255,255,.22)}
/* 摇杆 */
#joyBase{position:fixed;left:calc(18px + env(safe-area-inset-left));bottom:calc(22px + env(safe-area-inset-bottom));width:126px;height:126px;border-radius:50%;background:rgba(255,255,255,.08);border:2px solid rgba(255,255,255,.25);z-index:30;pointer-events:auto;touch-action:none}
#joyKnob{position:absolute;left:50%;top:50%;width:52px;height:52px;border-radius:50%;background:rgba(255,255,255,.55);transform:translate(-50%,-50%);box-shadow:0 2px 8px rgba(0,0,0,.35)}
/* 右侧按钮 */
#rightBtns{position:fixed;right:calc(14px + env(safe-area-inset-right));bottom:calc(22px + env(safe-area-inset-bottom));width:166px;height:186px;z-index:30;pointer-events:none}
.tBtn{position:absolute;pointer-events:auto;border-radius:50%;background:rgba(255,255,255,.13);border:2px solid rgba(255,255,255,.4);color:#fff;font-weight:600;display:flex;align-items:center;justify-content:center;text-shadow:0 1px 4px rgba(0,0,0,.6);touch-action:none}
.tBtn:active{background:rgba(255,255,255,.4)}
#btnJump{right:0;bottom:58px;width:74px;height:74px;font-size:15px}
#btnSneak{right:86px;bottom:8px;width:56px;height:56px;font-size:12px}
#btnJump.fly{background:rgba(100,180,255,.3);border-color:#9cf}
#btnSneak.fly{background:rgba(100,180,255,.2);border-color:#9cf}
/* 生存 HUD */
#survivalHUD{position:fixed;left:50%;bottom:calc(76px + env(safe-area-inset-bottom));transform:translateX(-50%);z-index:20;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:3px}
#statsRow{display:flex;gap:12px;align-items:center}
.hearts,.hunger{display:flex;gap:1px;line-height:1}
.heart,.food{width:9px;height:9px;image-rendering:pixelated}
#xpBarOuter{width:200px;height:6px;background:rgba(0,0,0,.55);border-radius:3px;overflow:hidden;position:relative}
#xpBarInner{height:100%;width:0%;background:#7dff5a;transition:width .1s}
#xpLevel{position:absolute;left:50%;bottom:7px;transform:translateX(-50%);color:#7dff5a;font-size:10px;font-weight:700;text-shadow:0 1px 2px #000}
body.creative #survivalHUD{display:none}
/* 快捷栏 */
#hotbar{position:fixed;left:50%;bottom:calc(8px + env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;gap:3px;z-index:35;pointer-events:auto;background:rgba(0,0,0,.28);padding:3px;border-radius:5px}
.slot{width:38px;height:38px;border-radius:3px;border:2px solid rgba(255,255,255,.16);display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.slot.active{border-color:#fff;box-shadow:0 0 0 1px #fff}
.slot img{width:32px;height:32px;image-rendering:pixelated}
.slot .count{position:absolute;right:1px;bottom:-1px;font-size:8px;color:#fff;text-shadow:0 1px 1px #000}
/* 准星 */
#cross{position:fixed;top:50%;left:50%;width:18px;height:18px;transform:translate(-50%,-50%);z-index:15;pointer-events:none;display:none}
body.split #cross{display:block}
#cross::before,#cross::after{content:'';position:absolute;background:rgba(255,255,255,.85);box-shadow:0 0 2px #000}
#cross::before{left:50%;top:0;width:2px;height:100%;transform:translateX(-50%)}
#cross::after{top:50%;left:0;height:2px;width:100%;transform:translateY(-50%)}
/* 设置面板 */
#settingsPanel{position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:100;display:none;align-items:center;justify-content:center;padding:16px}
#settingsPanel.open{display:flex}
#settingsBox{width:100%;max-width:520px;max-height:82vh;background:#2b2b2b;border:2px solid #555;border-radius:8px;display:flex;flex-direction:column;overflow:hidden;color:#ddd}
#settingsTitle{background:#1e1e1e;padding:12px 16px;font-size:15px;font-weight:600;border-bottom:1px solid #444;display:flex;justify-content:space-between;align-items:center}
#settingsClose{background:#444;border:none;color:#fff;border-radius:4px;padding:4px 10px;font-size:13px}
#settingsTabs{display:flex;background:#222;border-bottom:1px solid #444}
.stab{flex:1;padding:10px 4px;text-align:center;font-size:12px;color:#999;border:none;background:transparent;border-bottom:2px solid transparent}
.stab.active{color:#fff;border-bottom-color:#7dff5a;background:#2e2e2e}
#settingsBody{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:14px}
.setRow{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}
.setRow label{color:#ccc;flex-shrink:0}
.setRow input[type=range]{flex:1;accent-color:#7dff5a;max-width:180px}
.setRow select,.setRow input[type=text]{background:#1a1a1a;border:1px solid #555;color:#fff;padding:6px 8px;border-radius:4px;font-size:12px;max-width:170px}
.setRow .val{color:#7dff5a;font-size:12px;min-width:32px;text-align:right}
.setGroup{font-size:11px;color:#777;text-transform:uppercase;letter-spacing:1px;margin-top:4px;border-bottom:1px solid #3a3a3a;padding-bottom:4px}
#hint{position:fixed;top:calc(48px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,.5);padding:5px 12px;border-radius:12px;font-size:11px;z-index:45;pointer-events:none;white-space:nowrap}
</style>
</head>
<body class="creative">
<canvas id="game"></canvas>
<div id="topBar">
<div id="topLeft">
<div class="topBtn" id="btnPause">‖ 暂停</div>
<div class="topBtn" id="btnMode">创造</div>
</div>
<div id="topRight">
<div class="topBtn" id="btnSettings">设置</div>
<div class="topBtn" id="btnPersp">视角</div>
<div class="topBtn" id="btnSplit">分离</div>
</div>
</div>
<div id="survivalHUD">
<div id="statsRow">
<div class="hearts" id="hearts"></div>
<div class="hunger" id="hunger"></div>
</div>
<div style="position:relative">
<div id="xpBarOuter"><div id="xpBarInner"></div></div>
<div id="xpLevel">0</div>
</div>
</div>
<div id="cross"></div>
<div id="joyBase"><div id="joyKnob"></div></div>
<div id="rightBtns">
<div class="tBtn" id="btnJump">跳跃</div>
<div class="tBtn" id="btnSneak">潜行</div>
</div>
<div id="hotbar"></div>
<div id="hint">创造模式 · 双击跳跃飞行</div>
<!-- 设置面板 -->
<div id="settingsPanel">
<div id="settingsBox">
<div id="settingsTitle">
<span>设置</span>
<button id="settingsClose">关闭</button>
</div>
<div id="settingsTabs">
<button class="stab active" data-tab="video">视频</button>
<button class="stab" data-tab="audio">音频</button>
<button class="stab" data-tab="control">控制</button>
<button class="stab" data-tab="touch">触控</button>
</div>
<div id="settingsBody"></div>
</div>
</div>
<script type="module">
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
/* ============================================================
0. 贴图加载(ProgrammerArt CC0)
============================================================ */
const TEX_BASE = 'https://cdn.jsdelivr.net/npm/programmerart-textures@2.1.0/textures/';
const loader = new THREE.TextureLoader();
function loadTex(path){
const t = loader.load(TEX_BASE + path + '.png');
t.magFilter = THREE.NearestFilter;
t.minFilter = THREE.NearestFilter;
t.colorSpace = THREE.SRGBColorSpace;
return t;
}
const TEX = {
grass_top: loadTex('blocks/grass_top'),
grass_side: loadTex('blocks/grass_side'),
dirt: loadTex('blocks/dirt'),
stone: loadTex('blocks/stone'),
sand: loadTex('blocks/sand'),
log: loadTex('blocks/log_oak'),
leaf: loadTex('blocks/leaves'),
glass: loadTex('blocks/glass'),
brick: loadTex('blocks/brick'),
plank: loadTex('blocks/wood'),
};
const BLOCK_TEX = {
grass: { top: TEX.grass_top, side: TEX.grass_side, bottom: TEX.dirt },
dirt: { all: TEX.dirt },
stone: { all: TEX.stone },
sand: { all: TEX.sand },
log: { all: TEX.log },
leaf: { all: TEX.leaf, transparent: true },
glass: { all: TEX.glass, transparent: true },
brick: { all: TEX.brick },
plank: { all: TEX.plank },
};
/* ============================================================
1. 渲染器
============================================================ */
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87c5ff);
scene.fog = new THREE.Fog(0x87c5ff, 22, 55);
const camera = new THREE.PerspectiveCamera(72, innerWidth/innerHeight, 0.08, 120);
camera.rotation.order = 'YXZ';
scene.add(new THREE.HemisphereLight(0xffffff, 0x556677, 1.05));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(30, 60, 20);
scene.add(sun);
/* ============================================================
2. 体素世界
============================================================ */
const WORLD_W = 32, WORLD_H = 12, WORLD_D = 32;
const world = {};
const key = (x,y,z) => `${x},${y},${z}`;
function generateWorld(){
for(let x=0;x<WORLD_W;x++){
for(let z=0;z<WORLD_D;z++){
const h = 2 + Math.floor(Math.sin(x*0.4)*1.2 + Math.cos(z*0.35)*1.0);
for(let y=0;y<=h;y++){
let type = 'stone';
if(y === h) type = 'grass';
else if(y >= h-1) type = 'dirt';
world[key(x,y,z)] = type;
}
}
}
// 几棵树
for(let i=0;i<6;i++){
const tx = 4 + Math.floor(Math.random()*(WORLD_W-8));
const tz = 4 + Math.floor(Math.random()*(WORLD_D-8));
let gy = 0;
for(let y=WORLD_H-1;y>=0;y--){ if(world[key(tx,y,tz)]){ gy=y; break; } }
for(let j=1;j<=4;j++) world[key(tx, gy+j, tz)] = 'log';
for(let dx=-2;dx<=2;dx++) for(let dz=-2;dz<=2;dz++) for(let dy=-1;dy<=1;dy++){
if(Math.abs(dx)+Math.abs(dz)+Math.abs(dy) > 3) continue;
const bx=tx+dx, by=gy+4+dy+1, bz=tz+dz;
if(!world[key(bx,by,bz)]) world[key(bx,by,bz)] = 'leaf';
}
}
}
generateWorld();
/* 每种方块一个 InstancedMesh,用对应纹理 */
const boxGeo = new THREE.BoxGeometry(1,1,1);
const instanced = {};
function buildMeshes(){
Object.values(instanced).forEach(m => scene.remove(m));
for(const k in instanced) delete instanced[k];
const groups = {};
for(const k in world){
const [x,y,z] = k.split(',').map(Number);
const t = world[k];
if(!groups[t]) groups[t] = [];
groups[t].push([x,y,z]);
}
const dummy = new THREE.Object3D();
for(const type in groups){
const list = groups[type];
const def = BLOCK_TEX[type];
if(!def) continue;
let mat;
if(def.all){
mat = new THREE.MeshLambertMaterial({
map: def.all,
transparent: !!def.transparent,
side: def.transparent ? THREE.DoubleSide : THREE.FrontSide,
alphaTest: def.transparent ? 0.5 : 0
});
} else {
// 多面纹理(草方块):用材质数组
const mTop = new THREE.MeshLambertMaterial({ map: def.top });
const mSide = new THREE.MeshLambertMaterial({ map: def.side });
const mBottom = new THREE.MeshLambertMaterial({ map: def.bottom });
mat = [mSide, mSide, mTop, mBottom, mSide, mSide];
}
const mesh = new THREE.InstancedMesh(boxGeo, mat, list.length);
list.forEach((p,i)=>{
dummy.position.set(p[0], p[1], p[2]);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
});
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
instanced[type] = mesh;
}
}
buildMeshes();
/* ============================================================
3. 玩家
============================================================ */
const player = {
pos: new THREE.Vector3(WORLD_W/2, 6, WORLD_D/2),
yaw: 0, pitch: -0.12,
vy: 0, onGround: false,
flying: false, sneaking: false,
thirdPerson: false,
};
/* 史蒂夫贴图皮肤(ProgrammerArt 没有玩家皮肤,用肤色+衣服色块拼) */
function createSteve(){
const g = new THREE.Group();
const skin = new THREE.MeshLambertMaterial({ color: 0xe6b98a });
const shirt = new THREE.MeshLambertMaterial({ color: 0x18a5a5 });
const pants = new THREE.MeshLambertMaterial({ color: 0x3a3a8c });
const head = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.5,0.5), skin);
head.position.y = 1.75; g.add(head);
const body = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.75,0.26), shirt);
body.position.y = 1.125; g.add(body);
const armGeo = new THREE.BoxGeometry(0.24,0.72,0.24);
const armL = new THREE.Group(); armL.position.set(-0.37,1.47,0);
const amL = new THREE.Mesh(armGeo, shirt); amL.position.y=-0.36; armL.add(amL); g.add(armL);
const armR = new THREE.Group(); armR.position.set(0.37,1.47,0);
const amR = new THREE.Mesh(armGeo, skin); amR.position.y=-0.36; armR.add(amR); g.add(armR);
const legGeo = new THREE.BoxGeometry(0.24,0.75,0.24);
const legL = new THREE.Group(); legL.position.set(-0.13,0.75,0);
const lmL = new THREE.Mesh(legGeo, pants); lmL.position.y=-0.375; legL.add(lmL); g.add(legL);
const legR = new THREE.Group(); legR.position.set(0.13,0.75,0);
const lmR = new THREE.Mesh(legGeo, pants); lmR.position.y=-0.375; legR.add(lmR); g.add(legR);
g.userData = { armL, armR, legL, legR };
return g;
}
const steve = createSteve();
steve.visible = false;
scene.add(steve);
/* ============================================================
4. 物理
============================================================ */
function solidAt(x,y,z){
const t = world[key(Math.floor(x), Math.floor(y), Math.floor(z))];
return t && t !== 'glass' && t !== 'leaf';
}
function moveAxis(axis, delta){
const p = player.pos;
const nx = axis==='x' ? p.x+delta : p.x;
const ny = axis==='y' ? p.y+delta : p.y;
const nz = axis==='z' ? p.z+delta : p.z;
const r = 0.3;
for(let dx=-r;dx<=r;dx+=r) for(let dz=-r;dz<=r;dz+=r){
if(solidAt(nx+dx, ny, nz+dz) || solidAt(nx+dx, ny+1.7, nz+dz)) return false;
}
p[axis] += delta;
return true;
}
/* ============================================================
5. 触控
============================================================ */
const joy = { x:0, y:0, id:null };
const joyBase = document.getElementById('joyBase');
const joyKnob = document.getElementById('joyKnob');
function joyMove(t){
const r = joyBase.getBoundingClientRect();
let dx = t.clientX - (r.left+r.width/2);
let dy = t.clientY - (r.top+r.height/2);
const max = r.width/2 - 14;
const d = Math.hypot(dx,dy);
if(d>max){ dx=dx/d*max; dy=dy/d*max; }
joy.x = dx/max; joy.y = dy/max;
joyKnob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
}
joyBase.addEventListener('touchstart', e=>{ e.preventDefault(); joy.id=e.changedTouches[0].identifier; joyMove(e.changedTouches[0]); }, {passive:false});
joyBase.addEventListener('touchmove', e=>{ e.preventDefault(); for(const t of e.changedTouches) if(t.identifier===joy.id) joyMove(t); }, {passive:false});
function joyEnd(){ joy.x=0; joy.y=0; joy.id=null; joyKnob.style.transform='translate(-50%,-50%)'; }
joyBase.addEventListener('touchend', joyEnd, {passive:false});
joyBase.addEventListener('touchcancel', joyEnd, {passive:false});
/* 右侧视角 */
let lookId=null, lx=0, ly=0;
canvas.addEventListener('touchstart', e=>{
for(const t of e.changedTouches){
if(t.clientX > innerWidth*0.3 && lookId===null){ lookId=t.identifier; lx=t.clientX; ly=t.clientY; break; }
}
}, {passive:true});
canvas.addEventListener('touchmove', e=>{
for(const t of e.changedTouches){
if(t.identifier===lookId){
player.yaw -= (t.clientX-lx)*0.005;
player.pitch -= (t.clientY-ly)*0.005;
player.pitch = Math.max(-1.45, Math.min(1.45, player.pitch));
lx=t.clientX; ly=t.clientY;
}
}
}, {passive:true});
canvas.addEventListener('touchend', e=>{ for(const t of e.changedTouches) if(t.identifier===lookId) lookId=null; }, {passive:true});
/* 跳跃 / 双击飞行 */
const btnJump = document.getElementById('btnJump');
const btnSneak = document.getElementById('btnSneak');
let jumpHeld=false, sneakHeld=false, lastTap=0;
btnJump.addEventListener('touchstart', e=>{
e.preventDefault(); jumpHeld=true;
const now = performance.now();
if(state.mode==='creative'){
if(now-lastTap<320){ player.flying=!player.flying; player.vy=0; updateFlyUI(); lastTap=0; return; }
lastTap=now;
}
if(!player.flying && player.onGround){ player.vy=8.4; player.onGround=false; }
}, {passive:false});
btnJump.addEventListener('touchend', ()=>{ jumpHeld=false; });
btnJump.addEventListener('touchcancel', ()=>{ jumpHeld=false; });
btnSneak.addEventListener('touchstart', e=>{
e.preventDefault(); sneakHeld=true;
player.sneaking = !player.sneaking;
}, {passive:false});
btnSneak.addEventListener('touchend', ()=>{ sneakHeld=false; });
function updateFlyUI(){
btnJump.textContent = player.flying ? '上升' : '跳跃';
btnSneak.textContent = player.flying ? '下降' : '潜行';
btnJump.classList.toggle('fly', player.flying);
btnSneak.classList.toggle('fly', player.flying);
}
/* ============================================================
6. 状态 & 生存 HUD
============================================================ */
const state = { mode:'creative', hp:20, maxHp:20, food:20, maxFood:20, xp:0, xpLevel:0, selected:0 };
/* 心形 SVG(像素风) */
const HEART_FULL = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><path d="M1 1h2v1h1V1h2v1h1v3H6v1H5v1H4V6H3V5H2V4H1z" fill="#e03030"/></svg>');
const HEART_EMPTY = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><path d="M1 1h2v1h1V1h2v1h1v3H6v1H5v1H4V6H3V5H2V4H1z" fill="none" stroke="#555" stroke-width="1"/></svg>');
const FOOD_FULL = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><rect x="1" y="2" width="7" height="5" rx="1" fill="#c98a3a"/><rect x="2" y="1" width="2" height="2" fill="#c98a3a"/></svg>');
function renderHUD(){
const hearts = document.getElementById('hearts');
const hunger = document.getElementById('hunger');
hearts.innerHTML = '';
for(let i=0;i<10;i++){
const img = new Image(); img.src = (state.hp >= (i+1)*2) ? HEART_FULL : HEART_EMPTY;
img.className = 'heart'; hearts.appendChild(img);
}
hunger.innerHTML = '';
for(let i=0;i<10;i++){
const img = new Image(); img.src = FOOD_FULL;
img.style.opacity = state.food >= (i+1)*2 ? 1 : 0.2;
img.className = 'food'; hunger.appendChild(img);
}
const need = 10 + state.xpLevel*5;
document.getElementById('xpBarInner').style.width = Math.min(100, state.xp/need*100) + '%';
document.getElementById('xpLevel').textContent = state.xpLevel;
}
/* ============================================================
7. 快捷栏(带贴图预览)
============================================================ */
const HOTBAR = [
{ type:'grass', name:'草方块' },
{ type:'dirt', name:'泥土' },
{ type:'stone', name:'石头' },
{ type:'sand', name:'沙子' },
{ type:'log', name:'原木' },
{ type:'leaf', name:'树叶' },
{ type:'glass', name:'玻璃' },
{ type:'brick', name:'砖块' },
{ type:'plank', name:'木板' },
];
const hotbarEl = document.getElementById('hotbar');
function buildHotbar(){
hotbarEl.innerHTML = '';
HOTBAR.forEach((item,i)=>{
const d = document.createElement('div');
d.className = 'slot' + (i===0?' active':'');
const img = new Image();
img.src = TEX_BASE + 'blocks/' + (item.type==='grass'?'grass_side':item.type==='log'?'log_oak':item.type==='leaf'?'leaves':item.type==='plank'?'wood':item.type) + '.png';
img.onerror = ()=>{ img.style.display='none'; };
d.appendChild(img);
d.addEventListener('touchstart', e=>{
e.preventDefault();
state.selected = i;
[...hotbarEl.children].forEach((c,idx)=>c.classList.toggle('active', idx===i));
}, {passive:false});
hotbarEl.appendChild(d);
});
}
buildHotbar();
/* ============================================================
8. 设置面板
============================================================ */
const settingsPanel = document.getElementById('settingsPanel');
const settingsBody = document.getElementById('settingsBody');
const settingsTabs = document.querySelectorAll('.stab');
const settings = {
renderDistance: 8, fov: 72, uiScale: 1, brightness: 1,
masterVol: 80, musicVol: 60, sfxVol: 80,
sensitivity: 100, invertY: false,
controlMode: 'joystick', splitControls: false,
autoJump: false, flyDoubleTap: true,
};
function buildSettingsBody(tab){
settingsBody.innerHTML = '';
const add = (html)=>{ const div=document.createElement('div'); div.innerHTML=html; settingsBody.appendChild(div); };
if(tab==='video'){
add('<div class="setGroup">显示</div>');
add(`<div class="setRow"><label>视野 FOV</label><input type="range" min="50" max="110" value="${settings.fov}" data-k="fov"><span class="val">${settings.fov}</span></div>`);
add(`<div class="setRow"><label>亮度</label><input type="range" min="30" max="150" value="${settings.brightness*100}" data-k="brightness"><span class="val">${Math.round(settings.brightness*100)}%</span></div>`);
add(`<div class="setRow"><label>渲染距离</label><input type="range" min="4" max="16" value="${settings.renderDistance}" data-k="renderDistance"><span class="val">${settings.renderDistance}</span></div>`);
add(`<div class="setRow"><label>UI 缩放</label><input type="range" min="70" max="130" value="${settings.uiScale*100}" data-k="uiScale"><span class="val">${Math.round(settings.uiScale*100)}%</span></div>`);
}
if(tab==='audio'){
add('<div class="setGroup">音量</div>');
add(`<div class="setRow"><label>主音量</label><input type="range" min="0" max="100" value="${settings.masterVol}" data-k="masterVol"><span class="val">${settings.masterVol}%</span></div>`);
add(`<div class="setRow"><label>音乐</label><input type="range" min="0" max="100" value="${settings.musicVol}" data-k="musicVol"><span class="val">${settings.musicVol}%</span></div>`);
add(`<div class="setRow"><label>音效</label><input type="range" min="0" max="100" value="${settings.sfxVol}" data-k="sfxVol"><span class="val">${settings.sfxVol}%</span></div>`);
}
if(tab==='control'){
add('<div class="setGroup">视角</div>');
add(`<div class="setRow"><label>灵敏度</label><input type="range" min="20" max="200" value="${settings.sensitivity}" data-k="sensitivity"><span class="val">${settings.sensitivity}%</span></div>`);
add(`<div class="setRow"><label>反转 Y 轴</label><select data-k="invertY"><option value="false"${!settings.invertY?' selected':''}>关</option><option value="true"${settings.invertY?' selected':''}>开</option></select></div>`);
add('<div class="setGroup">移动</div>');
add(`<div class="setRow"><label>自动跳跃</label><select data-k="autoJump"><option value="false"${!settings.autoJump?' selected':''}>关</option><option value="true"${settings.autoJump?' selected':''}>开</option></select></div>`);
}
if(tab==='touch'){
add('<div class="setGroup">触控模式</div>');
add(`<div class="setRow"><label>控制方式</label><select data-k="controlMode"><option value="joystick"${settings.controlMode==='joystick'?' selected':''}>摇杆</option><option value="dpad"${settings.controlMode==='dpad'?' selected':''}>十字键</option><option value="crosshair"${settings.controlMode==='crosshair'?' selected':''}>摇杆+准星</option></select></div>`);
add(`<div class="setRow"><label>分离控制</label><select data-k="splitControls"><option value="false"${!settings.splitControls?' selected':''}>关</option><option value="true"${settings.splitControls?' selected':''}>开</option></select></div>`);
add(`<div class="setRow"><label>双击飞行</label><select data-k="flyDoubleTap"><option value="true"${settings.flyDoubleTap?' selected':''}>开</option><option value="false"${!settings.flyDoubleTap?' selected':''}>关</option></select></div>`);
}
// 绑定事件
settingsBody.querySelectorAll('input[type=range]').forEach(el=>{
el.addEventListener('input', ()=>{
const k = el.dataset.k;
const v = Number(el.value);
const span = el.parentElement.querySelector('.val');
if(k==='fov'){ settings.fov=v; camera.fov=v; camera.updateProjectionMatrix(); span.textContent=v; }
if(k==='brightness'){ settings.brightness=v/100; scene.children.forEach(c=>{ if(c.isHemisphereLight) c.intensity=settings.brightness*1.05; }); span.textContent=v+'%'; }
if(k==='renderDistance'){ settings.renderDistance=v; span.textContent=v; }
if(k==='uiScale'){ settings.uiScale=v/100; document.documentElement.style.fontSize=(14*settings.uiScale)+'px'; span.textContent=v+'%'; }
if(k==='masterVol'||k==='musicVol'||k==='sfxVol'){ settings[k]=v; span.textContent=v+'%'; }
if(k==='sensitivity'){ settings.sensitivity=v; span.textContent=v+'%'; }
});
});
settingsBody.querySelectorAll('select').forEach(el=>{
el.addEventListener('change', ()=>{
const k = el.dataset.k;
const v = el.value;
if(k==='invertY') settings.invertY = v==='true';
if(k==='autoJump') settings.autoJump = v==='true';
if(k==='controlMode') settings.controlMode = v;
if(k==='splitControls'){ settings.splitControls = v==='true'; document.body.classList.toggle('split', settings.splitControls); }
if(k==='flyDoubleTap') settings.flyDoubleTap = v==='true';
});
});
}
function openSettings(){
settingsPanel.classList.add('open');
buildSettingsBody('video');
settingsTabs.forEach(t=>t.classList.toggle('active', t.dataset.tab==='video'));
}
function closeSettings(){ settingsPanel.classList.remove('open'); }
settingsTabs.forEach(tab=>{
tab.addEventListener('click', ()=>{
settingsTabs.forEach(t=>t.classList.remove('active'));
tab.classList.add('active');
buildSettingsBody(tab.dataset.tab);
});
});
document.getElementById('settingsClose').addEventListener('click', closeSettings);
document.getElementById('btnSettings').addEventListener('touchstart', e=>{ e.preventDefault(); openSettings(); }, {passive:false});
/* 顶部按钮 */
const btnMode = document.getElementById('btnMode');
const btnPersp = document.getElementById('btnPersp');
const btnSplit = document.getElementById('btnSplit');
const hint = document.getElementById('hint');
btnMode.addEventListener('touchstart', e=>{
e.preventDefault();
state.mode = state.mode==='creative'?'survival':'creative';
document.body.classList.toggle('creative', state.mode==='creative');
btnMode.textContent = state.mode==='creative'?'创造':'生存';
if(state.mode==='survival') player.flying=false;
updateFlyUI();
hint.textContent = state.mode==='creative'?'创造模式 · 双击跳跃飞行':'生存模式';
renderHUD();
}, {passive:false});
btnPersp.addEventListener('touchstart', e=>{ e.preventDefault(); player.thirdPerson=!player.thirdPerson; hint.textContent = player.thirdPerson?'第三人称':'第一人称'; }, {passive:false});
btnSplit.addEventListener('touchstart', e=>{
e.preventDefault();
settings.splitControls = !settings.splitControls;
document.body.classList.toggle('split', settings.splitControls);
hint.textContent = settings.splitControls?'分离控制 · 准星瞄准':'触控模式';
}, {passive:false});
/* ============================================================
9. 主循环
============================================================ */
let last = performance.now();
function animate(){
requestAnimationFrame(animate);
const now = performance.now();
let dt = (now-last)/1000; last = now;
if(dt>0.1) dt=0.1;
// 移动
const speed = player.flying ? 7.5 : (player.sneaking ? 1.8 : 4.4);
const sinY = Math.sin(player.yaw), cosY = Math.cos(player.yaw);
const mvx = (-sinY)*(-joy.y) + cosY*joy.x;
const mvz = (-cosY)*(-joy.y) + (-sinY)*joy.x;
if(Math.abs(mvx)>0.05 || Math.abs(mvz)>0.05){
moveAxis('x', mvx*speed*dt);
moveAxis('z', mvz*speed*dt);
}
// 垂直
if(player.flying){
let vy = 0;
if(jumpHeld) vy += speed;
if(sneakHeld) vy -= speed;
moveAxis('y', vy*dt);
player.vy = 0;
} else {
if(jumpHeld && player.onGround){ player.vy=8.4; player.onGround=false; }
player.vy -= 26*dt;
if(player.vy < -40) player.vy = -40;
const before = player.pos.y;
const ok = moveAxis('y', player.vy*dt);
if(!ok && player.vy <= 0){ player.onGround = true; player.vy = 0; }
else player.onGround = false;
}
// 边界
player.pos.x = Math.max(1, Math.min(WORLD_W-1, player.pos.x));
player.pos.z = Math.max(1, Math.min(WORLD_D-1, player.pos.z));
// 史蒂夫
steve.position.copy(player.pos);
steve.rotation.y = player.yaw + Math.PI;
steve.visible = player.thirdPerson;
if(player.thirdPerson){
const moving = Math.abs(joy.x)>0.15 || Math.abs(joy.y)>0.15;
if(moving && player.onGround) steve.userData.phase = (steve.userData.phase||0) + dt*9;
const sw = Math.sin(steve.userData.phase||0)*0.6;
steve.userData.armL.rotation.x = sw;
steve.userData.armR.rotation.x = -sw;
steve.userData.legL.rotation.x = -sw;
steve.userData.legR.rotation.x = sw;
}
// 相机
const eye = new THREE.Vector3(player.pos.x, player.pos.y+0.1, player.pos.z);
if(player.thirdPerson){
const dist = 4.5;
const cp = Math.cos(player.pitch);
const target = new THREE.Vector3(
eye.x + Math.sin(player.yaw)*cp*dist,
eye.y - Math.sin(player.pitch)*dist + 1.2,
eye.z + Math.cos(player.yaw)*cp*dist
);
camera.position.lerp(target, Math.min(1, dt*12));
camera.lookAt(eye);
} else {
camera.position.copy(eye);
camera.rotation.set(player.pitch, player.yaw, 0, 'YXZ');
}
// 生存 HUD 缓慢更新
if(state.mode==='survival'){
state.xp += dt*1.5;
const need = 10 + state.xpLevel*5;
if(state.xp >= need){ state.xp -= need; state.xpLevel++; }
renderHUD();
}
renderer.render(scene, camera);
}
/* 初始化 */
player.pos.y = 6;
updateFlyUI();
renderHUD();
animate();
addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
document.addEventListener('gesturestart', e=>e.preventDefault());
</script>
</body>
</html>Game Source: MC 仿制 · 真实贴图 + 设置界面
Creator: ElectricShark88
Libraries: three
Complexity: complex (693 lines, 30.5 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: mc-electricshark88" to link back to the original. Then publish at arcadelab.ai/publish.