🎮ArcadeLab

BLOCK WORLD 3D

by AtomicBolt46
419 lines9.0 KB🛠️ Three.js (3D graphics)
▶ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BLOCK WORLD 3D</title>
<style>
* { box-sizing:border-box; }
body {
  margin:0;
  overflow:hidden;
  font-family:Arial;
  background:#87ceeb;
}
#info {
  position:absolute;
  top:12px;
  left:12px;
  background:#17212ddd;
  color:white;
  padding:14px;
  border-radius:10px;
  z-index:2;
}
#info h2 { color:#7dff75; margin:0 0 8px; }
#start {
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  padding:18px 30px;
  background:#58ce48;
  border:3px solid white;
  border-radius:12px;
  font-size:22px;
  font-weight:bold;
  cursor:pointer;
  z-index:5;
}
#crosshair {
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  color:white;
  font-size:30px;
  pointer-events:none;
  text-shadow:1px 1px 3px black;
}
#hotbar {
  position:absolute;
  bottom:20px;
  left:50%;
  transform:translateX(-50%);
  display:flex;
  gap:5px;
}
.slot {
  width:65px;
  height:65px;
  background:#222b;
  border:3px solid #aaa;
  border-radius:7px;
  color:white;
  display:flex;
  flex-direction:column;
  justify-content:center;
  align-items:center;
  font-size:12px;
}
.slot.selected { border:4px solid yellow; }
.emoji { font-size:26px; }
#message {
  position:absolute;
  bottom:100px;
  width:100%;
  text-align:center;
  color:white;
  font-weight:bold;
  text-shadow:1px 1px 4px black;
  pointer-events:none;
}
</style>
</head>
<body>

<div id="info">
  <h2>🌍 BLOCK WORLD</h2>
  <div>WASD - Move</div>
  <div>Mouse - Look around</div>
  <div>SPACE - Jump</div>
  <div>Left click - Break block</div>
  <div>Right click - Place block</div>
  <div>1-5 - Select block</div>
  <div>ESC - Pause</div>
</div>

<button id="start">▶ PLAY GAME</button>
<div id="crosshair">+</div>
<div id="message">Explore and build your world!</div>
<div id="hotbar"></div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
  }
}
</script>

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

// SCENE
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 20, 65);

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

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

// LIGHTS
scene.add(new THREE.HemisphereLight(0xffffff,0x526b42,2));

const sun = new THREE.DirectionalLight(0xffffff,2);
sun.position.set(15,30,10);
sun.castShadow = true;
scene.add(sun);

// MATERIALS
const materials = {
  Grass: new THREE.MeshLambertMaterial({color:0x55a832}),
  Dirt: new THREE.MeshLambertMaterial({color:0x80502d}),
  Stone: new THREE.MeshLambertMaterial({color:0x888888}),
  Wood: new THREE.MeshLambertMaterial({color:0x80502d}),
  Leaves: new THREE.MeshLambertMaterial({
    color:0x237b27, transparent:true, opacity:0.95
  }),
  Sand: new THREE.MeshLambertMaterial({color:0xd9c27d})
};

const blockTypes = ["Grass","Dirt","Stone","Wood","Sand"];
const icons = ["🟩","🟫","🪨","🪵","🟨"];
let selected = 0;

// BLOCK STORAGE
const blocks = new Map();
const meshes = [];

const geometry = new THREE.BoxGeometry(1,1,1);

function key(x,y,z) {
  return `${x},${y},${z}`;
}

function addBlock(x,y,z,type) {
  const k = key(x,y,z);
  if(blocks.has(k)) return;

  const mesh = new THREE.Mesh(geometry,materials[type]);
  mesh.position.set(x,y,z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  mesh.userData.block = true;
  mesh.userData.type = type;

  scene.add(mesh);
  blocks.set(k,mesh);
  meshes.push(mesh);
}

function removeBlock(mesh) {
  const p = mesh.position;
  blocks.delete(key(p.x,p.y,p.z));
  scene.remove(mesh);

  const index = meshes.indexOf(mesh);
  if(index >= 0) meshes.splice(index,1);
}

// TERRAIN
const heights = new Map();

function terrainHeight(x,z) {
  return Math.floor(
    2 + Math.sin(x*0.22)*1.5 + Math.cos(z*0.2)*1.3
  );
}

for(let x=-20;x<=20;x++) {
  for(let z=-20;z<=20;z++) {
    const h = terrainHeight(x,z);
    heights.set(`${x},${z}`,h);

    for(let y=-3;y<=h;y++) {
      let type = y===h ? "Grass" : "Dirt";
      if(y===h && h<=1) type="Sand";
      if(y<h-2) type="Stone";
      addBlock(x,y,z,type);
    }
  }
}

// TREES
function makeTree(x,z) {
  const h = terrainHeight(x,z);
  if(h<=1) return;

  for(let y=1;y<=3;y++)
    addBlock(x,h+y,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)<5) {
          addBlock(x+dx,h+dy,z+dz,"Leaves");
        }
      }
    }
  }
}

[
  [-7,-6],[-12,5],[6,-9],[11,7],
  [15,-3],[-16,-9],[3,13],[-3,8]
].forEach(p=>makeTree(p[0],p[1]));

// PLAYER
let yaw = 0;
let pitch = 0;
let velocityY = 0;
let onGround = false;
const keys = {};

camera.position.set(0,terrainHeight(0,0)+2,0);

document.addEventListener("keydown",e=>{
  keys[e.code]=true;

  if(e.code.startsWith("Digit")) {
    const n=Number(e.code.slice(5))-1;
    if(n>=0 && n<5) selectBlock(n);
  }

  if(e.code==="Space" && onGround) {
    velocityY=0.18;
    onGround=false;
  }
});

document.addEventListener("keyup",e=>{
  keys[e.code]=false;
});

document.addEventListener("mousemove",e=>{
  if(document.pointerLockElement===renderer.domElement) {
    yaw -= e.movementX*0.002;
    pitch -= e.movementY*0.002;
    pitch=Math.max(-1.5,Math.min(1.5,pitch));
    camera.rotation.set(pitch,yaw,0,"YXZ");
  }
});

// START GAME
document.getElementById("start").onclick=()=>{
  renderer.domElement.requestPointerLock();
};

document.addEventListener("pointerlockchange",()=>{
  document.getElementById("start").style.display =
    document.pointerLockElement===renderer.domElement
    ? "none":"block";
});

// HOTBAR
function selectBlock(n) {
  selected=n;
  updateHotbar();
}

function updateHotbar() {
  const bar=document.getElementById("hotbar");
  bar.innerHTML="";

  blockTypes.forEach((type,i)=>{
    const slot=document.createElement("div");
    slot.className="slot"+(selected===i?" selected":"");
    slot.innerHTML=
      `<div class="emoji">${icons[i]}</div>${i+1}. ${type}`;
    bar.appendChild(slot);
  });
}
updateHotbar();

// RAYCASTING
const raycaster = new THREE.Raycaster();
raycaster.far=6;

function getTarget() {
  raycaster.setFromCamera(new THREE.Vector2(0,0),camera);
  return raycaster.intersectObjects(meshes,false)[0];
}

// BREAK BLOCK
renderer.domElement.addEventListener("mousedown",e=>{
  if(document.pointerLockElement!==renderer.domElement) return;

  if(e.button===0) {
    const hit=getTarget();
    if(hit) {
      removeBlock(hit.object);
      showMessage("Block broken!");
    }
  }

  if(e.button===2) {
    const hit=getTarget();

    if(hit) {
      const p=hit.object.position;
      const n=hit.face.normal;

      const x=Math.round(p.x+n.x);
      const y=Math.round(p.y+n.y);
      const z=Math.round(p.z+n.z);

      // Keep blocks away from the player's position
      if(Math.abs(x-camera.position.x)>0.8 ||
         Math.abs(z-camera.position.z)>0.8 ||
         Math.abs(y-camera.position.y)>1.5) {
        addBlock(x,y,z,blockTypes[selected]);
        showMessage(blockTypes[selected]+" placed!");
      }
    }
  }
});

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

function showMessage(msg) {
  const el=document.getElementById("message");
  el.textContent=msg;
  clearTimeout(showMessage.timer);
  showMessage.timer=setTimeout(()=>{
    el.textContent="Explore and build your world!";
  },1500);
}

// MOVEMENT
function updatePlayer() {
  const speed=keys["ShiftLeft"]?0.13:0.09;

  const forward=new THREE.Vector3(
    -Math.sin(yaw),0,-Math.cos(yaw)
  );
  const right=new THREE.Vector3(
    Math.cos(yaw),0,-Math.sin(yaw)
  );

  const move=new THREE.Vector3();

  if(keys["KeyW"]||keys["ArrowUp"]) move.add(forward);
  if(keys["KeyS"]||keys["ArrowDown"]) move.sub(forward);
  if(keys["KeyD"]||keys["ArrowRight"]) move.add(right);
  if(keys["KeyA"]||keys["ArrowLeft"]) move.sub(right);

  if(move.length()>0) {
    move.normalize().multiplyScalar(speed);
    camera.position.x+=move.x;
    camera.position.z+=move.z;
  }

  camera.position.x=THREE.MathUtils.clamp(camera.position.x,-19,19);
  camera.position.z=THREE.MathUtils.clamp(camera.position.z,-19,19);

  const px=Math.round(camera.position.x);
  const pz=Math.round(camera.position.z);
  const ground=heights.get(`${px},${pz}`) ?? 2;

  velocityY-=0.009;
  camera.position.y+=velocityY;

  if(camera.position.y<ground+2) {
    camera.position.y=ground+2;
    velocityY=0;
    onGround=true;
  }
}

// ANIMATION
function animate() {
  requestAnimationFrame(animate);

  if(document.pointerLockElement===renderer.domElement) {
    updatePlayer();
  }

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

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

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

Game Source: BLOCK WORLD 3D

Creator: AtomicBolt46

Libraries: three

Complexity: complex (419 lines, 9.0 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: block-world-3d-atomicbolt46" to link back to the original. Then publish at arcadelab.ai/publish.