3D 体素世界
by ElectricShark88625 lines20.9 KB
<!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>3D 体素世界</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;touch-action:none}
html,body{width:100%;height:100%;overflow:hidden;background:#87c5ff;font-family:system-ui,-apple-system,sans-serif}
canvas{display:block;width:100%;height:100%}
#ui{position:fixed;inset:0;pointer-events:none;z-index:10}
#joyBase{position:absolute;left:20px;bottom:30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.12);border:2px solid rgba(255,255,255,.3);pointer-events:auto;touch-action:none}
#joyKnob{position:absolute;left:50%;top:50%;width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,.6);transform:translate(-50%,-50%);box-shadow:0 2px 10px rgba(0,0,0,.3)}
.rBtn{position:absolute;border-radius:50%;background:rgba(255,255,255,.15);border:2px solid rgba(255,255,255,.45);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;pointer-events:auto;text-shadow:0 1px 4px rgba(0,0,0,.6);touch-action:none;font-size:15px}
.rBtn:active{background:rgba(255,255,255,.45)}
#btnJump{right:24px;bottom:120px;width:80px;height:80px;font-size:18px}
#btnDig{right:120px;bottom:30px;width:70px;height:70px}
#btnPlace{right:30px;bottom:30px;width:70px;height:70px}
#btnBag{right:24px;top:60px;width:60px;height:60px;font-size:13px;border-radius:12px}
#hotbar{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:4px;pointer-events:auto;background:rgba(0,0,0,.35);padding:4px;border-radius:8px}
.slot{width:40px;height:40px;border-radius:4px;border:2px solid rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;position:relative}
.slot.active{border-color:#fff;box-shadow:0 0 0 2px rgba(255,255,255,.5)}
.slot .dot{width:26px;height:26px;border-radius:3px;box-shadow:inset 0 -4px 0 rgba(0,0,0,.18),inset 0 3px 0 rgba(255,255,255,.15)}
#bagPanel{position:absolute;inset:0;background:rgba(0,0,0,.75);display:none;align-items:center;justify-content:center;pointer-events:auto;z-index:20}
#bagPanel.open{display:flex}
#bagBox{width:88%;max-width:480px;max-height:75%;background:#2a2a35;border:2px solid #555;border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
#bagTitle{padding:14px 16px;font-size:15px;font-weight:700;color:#eee;border-bottom:1px solid #444;display:flex;justify-content:space-between;align-items:center}
#bagClose{background:#444;border:none;color:#fff;border-radius:6px;padding:6px 14px;font-size:13px}
#bagGrid{flex:1;overflow-y:auto;padding:12px;display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.bagItem{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 4px;background:#1e1e28;border-radius:8px;border:2px solid transparent}
.bagItem:active{background:#333;border-color:#7dff5a}
.bagItem .dot{width:36px;height:36px;border-radius:5px;box-shadow:inset 0 -5px 0 rgba(0,0,0,.18),inset 0 4px 0 rgba(255,255,255,.12)}
.bagItem span{font-size:10px;color:#ccc}
#hint{position:absolute;top:calc(12px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);background:rgba(0,0,0,.45);color:#fff;padding:6px 16px;border-radius:16px;font-size:12px;white-space:nowrap;pointer-events:none}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
<div id="hint">摇杆移动 · 滑动转视角 · 挖/放/背包</div>
<div id="joyBase"><div id="joyKnob"></div></div>
<div class="rBtn" id="btnJump">跳</div>
<div class="rBtn" id="btnDig">挖</div>
<div class="rBtn" id="btnPlace">放</div>
<div class="rBtn" id="btnBag">背包</div>
<div id="hotbar"></div>
</div>
<div id="bagPanel">
<div id="bagBox">
<div id="bagTitle"><span>方块背包</span><button id="bagClose">关闭</button></div>
<div id="bagGrid"></div>
</div>
</div>
<script>
(function(){
'use strict';
/* ================= 基础 ================= */
const canvas = document.getElementById('game');
const gl = canvas.getContext('webgl', { antialias: true });
function resize(){
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = innerWidth * dpr;
canvas.height = innerHeight * dpr;
canvas.style.width = innerWidth + 'px';
canvas.style.height = innerHeight + 'px';
gl.viewport(0, 0, canvas.width, canvas.height);
}
addEventListener('resize', resize);
resize();
/* ================= 着色器 ================= */
const vsSrc = `
attribute vec3 aPos;
attribute vec3 aColor;
uniform mat4 uMVP;
varying vec3 vColor;
void main(){
gl_Position = uMVP * vec4(aPos, 1.0);
vColor = aColor;
}`;
const fsSrc = `
precision mediump float;
varying vec3 vColor;
void main(){
gl_FragColor = vec4(vColor, 1.0);
}`;
function compile(type, src){
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s));
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(prog);
gl.useProgram(prog);
const aPos = gl.getAttribLocation(prog, 'aPos');
const aColor = gl.getAttribLocation(prog, 'aColor');
const uMVP = gl.getUniformLocation(prog, 'uMVP');
gl.clearColor(0.53, 0.77, 1.0, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
/* ================= 矩阵 ================= */
function perspective(fovy, aspect, near, far){
const f = 1 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
const o = new Float32Array(16);
o[0] = f / aspect;
o[5] = f;
o[10] = (far + near) * nf;
o[11] = -1;
o[14] = 2 * far * near * nf;
return o;
}
function lookAt(eye, target, up){
let zx = eye[0] - target[0], zy = eye[1] - target[1], zz = eye[2] - target[2];
let len = Math.hypot(zx, zy, zz);
if(len < 1e-6){ zx = 0; zy = 0; zz = 1; len = 1; }
zx /= len; zy /= len; zz /= len;
let xx = up[1]*zz - up[2]*zy;
let xy = up[2]*zx - up[0]*zz;
let xz = up[0]*zy - up[1]*zx;
len = Math.hypot(xx, xy, xz);
if(len < 1e-6){ xx = 1; xy = 0; xz = 0; len = 1; }
xx /= len; xy /= len; xz /= len;
const yx = zy*xz - zz*xy;
const yy = zz*xx - zx*xz;
const yz = zx*xy - zy*xx;
const o = new Float32Array(16);
o[0] = xx; o[1] = yx; o[2] = zx; o[3] = 0;
o[4] = xy; o[5] = yy; o[6] = zy; o[7] = 0;
o[8] = xz; o[9] = yz; o[10] = zz; o[11] = 0;
o[12] = -(xx*eye[0] + xy*eye[1] + xz*eye[2]);
o[13] = -(yx*eye[0] + yy*eye[1] + yz*eye[2]);
o[14] = -(zx*eye[0] + zy*eye[1] + zz*eye[2]);
o[15] = 1;
return o;
}
function mul(a, b){
const o = new Float32Array(16);
for(let c = 0; c < 4; c++){
for(let r = 0; r < 4; r++){
o[c*4+r] = a[r]*b[c*4] + a[4+r]*b[c*4+1] + a[8+r]*b[c*4+2] + a[12+r]*b[c*4+3];
}
}
return o;
}
/* ================= 世界数据 ================= */
const W = 32, D = 32, H = 16;
const world = new Uint8Array(W * D * H);
const BLOCK_DEFS = [
null,
{ name:'草方块', top:[0.37,0.64,0.23], side:[0.48,0.35,0.20], bottom:[0.54,0.35,0.17] },
{ name:'泥土', all:[0.54,0.35,0.17] },
{ name:'石头', all:[0.54,0.54,0.54] },
{ name:'沙子', all:[0.91,0.86,0.63] },
{ name:'木头', all:[0.42,0.29,0.17] },
{ name:'树叶', all:[0.18,0.49,0.20] },
{ name:'玻璃', all:[0.75,0.88,0.95] },
{ name:'砖块', all:[0.65,0.29,0.23] },
{ name:'木板', all:[0.76,0.60,0.35] },
{ name:'水', all:[0.18,0.43,0.77] },
{ name:'雪块', all:[0.95,0.97,1.0] },
{ name:'煤矿石', all:[0.29,0.29,0.29] },
{ name:'铁矿石', all:[0.79,0.64,0.49] },
{ name:'金矿石', all:[0.91,0.75,0.29] },
{ name:'钻石矿', all:[0.29,0.85,0.85] },
{ name:'圆石', all:[0.48,0.48,0.48] }
];
const B = { AIR:0, GRASS:1, DIRT:2, STONE:3, SAND:4, WOOD:5, LEAF:6, GLASS:7, BRICK:8, PLANK:9, WATER:10, SNOW:11, COAL:12, IRON:13, GOLD:14, DIAMOND:15, COBBLE:16 };
function idx(x,y,z){ return (y * D + z) * W + x; }
function getBlock(x,y,z){
if(x<0||x>=W||y<0||y>=H||z<0||z>=D) return 0;
return world[idx(x,y,z)];
}
function setBlock(x,y,z,id){
if(x<0||x>=W||y<0||y>=H||z<0||z>=D) return;
world[idx(x,y,z)] = id;
}
function isTransparent(x,y,z){
const id = getBlock(x,y,z);
return id === 0 || id === B.GLASS || id === B.WATER;
}
/* ================= 生成世界 ================= */
function generateWorld(){
for(let x=0;x<W;x++){
for(let z=0;z<D;z++){
const h = Math.max(2, Math.min(H-2, Math.floor(
6 + Math.sin(x*0.25)*2 + Math.cos(z*0.3)*2 + Math.sin((x+z)*0.18)*1.5
)));
for(let y=0;y<=h;y++){
let id = B.STONE;
if(y === h) id = h > 10 ? B.SNOW : (h < 5 ? B.SAND : B.GRASS);
else if(y >= h-2) id = B.DIRT;
else {
const r = Math.random();
if(r > 0.98) id = B.DIAMOND;
else if(r > 0.95) id = B.GOLD;
else if(r > 0.90) id = B.IRON;
else if(r > 0.85) id = B.COAL;
}
setBlock(x,y,z,id);
}
if(h < 5){
for(let y=h+1;y<=5;y++) setBlock(x,y,z,B.WATER);
}
}
}
for(let i=0;i<25;i++){
const tx = 2 + Math.floor(Math.random()*(W-4));
const tz = 2 + Math.floor(Math.random()*(D-4));
let ty = H-1;
while(ty > 0 && getBlock(tx,ty,tz) === 0) ty--;
if(getBlock(tx,ty,tz) !== B.GRASS) continue;
const trunkH = 3 + Math.floor(Math.random()*2);
for(let j=1;j<=trunkH;j++) setBlock(tx,ty+j,tz,B.WOOD);
const topY = ty + trunkH;
for(let dx=-2;dx<=2;dx++) for(let dz=-2;dz<=2;dz++) for(let dy=-1;dy<=2;dy++){
if(Math.abs(dx)+Math.abs(dz)+Math.abs(dy) > 3) continue;
const bx = tx+dx, by = topY+dy, bz = tz+dz;
if(bx>=0&&bx<W&&by>=0&&by<H&&bz>=0&&bz<D && getBlock(bx,by,bz) === 0){
setBlock(bx,by,bz,B.LEAF);
}
}
}
}
/* ================= 网格构建 ================= */
const MAX_VERTS = 200000;
const vertexData = new Float32Array(MAX_VERTS * 6);
let vertexCount = 0;
const FACE_VERTS = {
px: [[1,0,0],[1,0,1],[1,1,1],[1,1,0]],
nx: [[0,0,0],[0,1,0],[0,1,1],[0,0,1]],
py: [[0,1,0],[0,1,1],[1,1,1],[1,1,0]],
ny: [[0,0,0],[1,0,0],[1,0,1],[0,0,1]],
pz: [[0,0,1],[0,1,1],[1,1,1],[1,0,1]],
nz: [[0,0,0],[1,0,0],[1,1,0],[0,1,0]]
};
const FACE_LIGHT = { px:0.82, nx:0.72, py:1.0, ny:0.5, pz:0.9, nz:0.7 };
function addFace(x,y,z,dir,def){
if(vertexCount >= MAX_VERTS) return;
let color;
if(dir === 'py') color = def.top || def.all;
else if(dir === 'ny') color = def.bottom || def.side || def.all;
else color = def.side || def.all;
const light = FACE_LIGHT[dir];
const r = color[0] * light, g = color[1] * light, b = color[2] * light;
const corners = FACE_VERTS[dir];
const order = [0,1,2, 0,2,3];
for(const oi of order){
const c = corners[oi];
const off = vertexCount * 6;
vertexData[off] = x + c[0];
vertexData[off+1] = y + c[1];
vertexData[off+2] = z + c[2];
vertexData[off+3] = r;
vertexData[off+4] = g;
vertexData[off+5] = b;
vertexCount++;
}
}
function buildMesh(){
vertexCount = 0;
for(let y=0;y<H;y++){
for(let z=0;z<D;z++){
for(let x=0;x<W;x++){
const id = getBlock(x,y,z);
if(id === 0) continue;
const def = BLOCK_DEFS[id];
if(!def) continue;
if(isTransparent(x+1,y,z)) addFace(x+1,y,z,'px',def);
if(isTransparent(x-1,y,z)) addFace(x,y,z,'nx',def);
if(isTransparent(x,y+1,z)) addFace(x,y+1,z,'py',def);
if(isTransparent(x,y-1,z)) addFace(x,y,z,'ny',def);
if(isTransparent(x,y,z+1)) addFace(x,y,z+1,'pz',def);
if(isTransparent(x,y,z-1)) addFace(x,y,z,'nz',def);
}
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertexData.subarray(0, vertexCount * 6));
}
/* ================= VBO ================= */
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertexData.byteLength, gl.DYNAMIC_DRAW);
/* ================= 玩家 ================= */
let px = W/2, py = 10, pz = D/2;
let vy = 0, yaw = 0, pitch = -0.15;
let onGround = false;
let jumpHeld = false;
function isSolidAt(x,y,z){
const id = getBlock(Math.floor(x), Math.floor(y), Math.floor(z));
return id !== 0 && id !== B.GLASS && id !== B.WATER;
}
function collides(x,y,z){
const r = 0.3;
for(let bx = Math.floor(x-r); bx <= Math.floor(x+r); bx++){
for(let by = Math.floor(y); by <= Math.floor(y+1.7); by++){
for(let bz = Math.floor(z-r); bz <= Math.floor(z+r); bz++){
if(isSolidAt(bx,by,bz)) return true;
}
}
}
return false;
}
function updatePlayer(dt){
const speed = 4.5;
const sinY = Math.sin(yaw), cosY = Math.cos(yaw);
let mx = 0, mz = 0;
if(joy.active){
mx = -sinY * (-joy.y) + cosY * joy.x;
mz = -cosY * (-joy.y) + (-sinY) * joy.x;
}
const mag = Math.hypot(mx, mz);
if(mag > 1){ mx /= mag; mz /= mag; }
const dx = mx * speed * dt;
const dz = mz * speed * dt;
if(dx !== 0 && !collides(px+dx, py, pz)) px += dx;
if(dz !== 0 && !collides(px, py, pz+dz)) pz += dz;
vy -= 25 * dt;
if(vy < -30) vy = -30;
const prevY = py;
if(!collides(px, py + vy*dt, pz)){
py += vy * dt;
onGround = false;
} else {
if(vy < 0){ py = prevY; onGround = true; }
vy = 0;
}
if(jumpHeld && onGround){ vy = 8; onGround = false; jumpHeld = false; }
px = Math.max(0.5, Math.min(W-0.5, px));
pz = Math.max(0.5, Math.min(D-0.5, pz));
if(py < -10){ px = W/2; py = 15; pz = D/2; vy = 0; }
}
/* ================= 射线 ================= */
function raycast(){
const eye = [px, py + 1.62, pz];
const dir = [-Math.sin(yaw)*Math.cos(pitch), Math.sin(pitch), -Math.cos(yaw)*Math.cos(pitch)];
let last = null;
for(let t=0; t<6; t+=0.05){
const bx = Math.floor(eye[0] + dir[0]*t);
const by = Math.floor(eye[1] + dir[1]*t);
const bz = Math.floor(eye[2] + dir[2]*t);
if(bx<0||bx>=W||by<0||by>=H||bz<0||bz>=D) break;
const id = getBlock(bx,by,bz);
if(id !== 0) return { x:bx, y:by, z:bz, prev:last };
last = { x:bx, y:by, z:bz };
}
return null;
}
/* ================= 挖 / 放 ================= */
let selectedBlock = B.GRASS;
function dig(){
const hit = raycast();
if(!hit) return;
const id = getBlock(hit.x, hit.y, hit.z);
if(BLOCK_DEFS[id] && BLOCK_DEFS[id].name === '基岩') return;
setBlock(hit.x, hit.y, hit.z, 0);
buildMesh();
}
function place(){
const hit = raycast();
if(!hit || !hit.prev) return;
const p = hit.prev;
if(collides(p.x+0.5, p.y, p.z+0.5)) return;
setBlock(p.x, p.y, p.z, selectedBlock);
buildMesh();
}
/* ================= 渲染 ================= */
function render(){
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const proj = perspective(Math.PI/3, canvas.width/canvas.height, 0.1, 100);
const cp = Math.cos(pitch), sp = Math.sin(pitch);
const cy = Math.cos(yaw), sy = Math.sin(yaw);
const eye = [px, py + 1.62, pz];
const forward = [-sy*cp, sp, -cy*cp];
const target = [eye[0]+forward[0], eye[1]+forward[1], eye[2]+forward[2]];
const view = lookAt(eye, target, [0,1,0]);
const mvp = mul(proj, view);
gl.useProgram(prog);
gl.uniformMatrix4fv(uMVP, false, mvp);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 24, 0);
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 24, 12);
gl.enableVertexAttribArray(aColor);
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
}
/* ================= 主循环 ================= */
let last = performance.now();
function loop(now){
requestAnimationFrame(loop);
let dt = (now - last)/1000;
last = now;
if(dt > 0.1) dt = 0.1;
updatePlayer(dt);
render();
}
/* ================= 摇杆 ================= */
const joy = { x:0, y:0, active:false, 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();
const t = e.changedTouches[0];
joy.id = t.identifier;
joy.active = true;
joyMove(t);
}, { 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(e){
for(const t of e.changedTouches){
if(t.identifier === joy.id){
joy.active = false; 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, lookX = 0, lookY = 0;
canvas.addEventListener('touchstart', e => {
for(const t of e.changedTouches){
if(lookId === null){ lookId = t.identifier; lookX = t.clientX; lookY = t.clientY; break; }
}
}, { passive: true });
canvas.addEventListener('touchmove', e => {
for(const t of e.changedTouches){
if(t.identifier === lookId){
yaw -= (t.clientX - lookX) * 0.005;
pitch -= (t.clientY - lookY) * 0.005;
pitch = Math.max(-1.4, Math.min(1.4, pitch));
lookX = t.clientX; lookY = 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 btnDig = document.getElementById('btnDig');
const btnPlace = document.getElementById('btnPlace');
btnJump.addEventListener('touchstart', e => { e.preventDefault(); jumpHeld = true; }, { passive: false });
btnJump.addEventListener('touchend', e => { e.preventDefault(); jumpHeld = false; }, { passive: false });
btnJump.addEventListener('touchcancel', () => { jumpHeld = false; });
btnDig.addEventListener('touchstart', e => { e.preventDefault(); dig(); }, { passive: false });
btnPlace.addEventListener('touchstart', e => { e.preventDefault(); place(); }, { passive: false });
/* ================= 快捷栏 ================= */
const HOTBAR = [B.GRASS, B.DIRT, B.STONE, B.SAND, B.WOOD, B.LEAF, B.GLASS, B.BRICK, B.PLANK];
const hotbarEl = document.getElementById('hotbar');
function colorOf(id){
const def = BLOCK_DEFS[id];
const c = def.top || def.all;
return `rgb(${(c[0]*255)|0},${(c[1]*255)|0},${(c[2]*255)|0})`;
}
function buildHotbar(){
hotbarEl.innerHTML = '';
HOTBAR.forEach((id, i) => {
const d = document.createElement('div');
d.className = 'slot' + (id === selectedBlock ? ' active' : '');
const dot = document.createElement('div');
dot.className = 'dot';
dot.style.background = colorOf(id);
d.appendChild(dot);
d.addEventListener('touchstart', e => {
e.preventDefault();
selectBlock(id);
}, { passive: false });
hotbarEl.appendChild(d);
});
}
function selectBlock(id){
selectedBlock = id;
[...hotbarEl.children].forEach((c, i) => {
c.classList.toggle('active', HOTBAR[i] === id);
});
}
buildHotbar();
/* ================= 背包 ================= */
const PLACEABLE = [B.GRASS,B.DIRT,B.STONE,B.SAND,B.WOOD,B.LEAF,B.GLASS,B.BRICK,B.PLANK,B.WATER,B.SNOW,B.COAL,B.IRON,B.GOLD,B.DIAMOND,B.COBBLE];
const bagPanel = document.getElementById('bagPanel');
const bagGrid = document.getElementById('bagGrid');
const bagClose = document.getElementById('bagClose');
const btnBag = document.getElementById('btnBag');
function buildBag(){
bagGrid.innerHTML = '';
PLACEABLE.forEach(id => {
const def = BLOCK_DEFS[id];
const d = document.createElement('div');
d.className = 'bagItem';
const dot = document.createElement('div');
dot.className = 'dot';
dot.style.background = colorOf(id);
const span = document.createElement('span');
span.textContent = def.name;
d.appendChild(dot);
d.appendChild(span);
d.addEventListener('touchstart', e => {
e.preventDefault();
selectBlock(id);
if(!HOTBAR.includes(id)){
HOTBAR[HOTBAR.length - 1] = id;
buildHotbar();
selectBlock(id);
}
bagPanel.classList.remove('open');
}, { passive: false });
bagGrid.appendChild(d);
});
}
btnBag.addEventListener('touchstart', e => {
e.preventDefault();
buildBag();
bagPanel.classList.add('open');
}, { passive: false });
bagClose.addEventListener('click', () => bagPanel.classList.remove('open'));
bagPanel.addEventListener('touchstart', e => {
if(e.target === bagPanel) bagPanel.classList.remove('open');
}, { passive: false });
/* ================= 初始化 ================= */
generateWorld();
buildMesh();
// 找出生点
px = W/2; pz = D/2;
for(let y = H-1; y >= 0; y--){
if(getBlock(Math.floor(px), y, Math.floor(pz)) !== 0){
py = y + 1;
break;
}
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 3D 体素世界
Creator: ElectricShark88
Libraries: none
Complexity: complex (625 lines, 20.9 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: 3d-electricshark88" to link back to the original. Then publish at arcadelab.ai/publish.