Files
minecraft-clone/js/world.js
T
minecraft-cloneandClaude 1049253d54 feat: Minecraft Clone MVP – prozedurale Welt, Blöcke, Abbauen/Setzen
- Weltgenerierung: FBM-Terrain, 3 Biome (Plains/Wald/Wüste), Höhlen, Wasser, Bäume
- Blöcke: Grass, Dirt, Stein, Sand, Holz, Blätter, Wasser (+Bedrock)
- Interaktion: DDA-Voxel-Raycast, Abbauen (LMB) & Setzen (RMB), Hotbar 1-6
- Spieler: First-Person, Gravitation, AABB-Kollision, Springen, Fly-Mode (F)
- Engine: Chunk-Streaming (16x16x64), Face-Culling, opaker+transparenter Pass
- Stack: Three.js + simplex-noise via CDN, prozedurale Texturen, kein Build-Step

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-20 23:55:12 +02:00

202 lines
6.3 KiB
JavaScript

// World: Chunk-Storage, Block-Zugriff, Generierung, Mesh-Verwaltung, Streaming.
import * as THREE from 'three';
import {
CHUNK_SIZE, CHUNK_HEIGHT, BLOCK, isSolid,
} from './constants.js';
import { generateChunk } from './worldgen.js';
import { buildChunkGeometry } from './mesher.js';
class Chunk {
constructor(cx, cz) {
this.cx = cx;
this.cz = cz;
// Index: y * (S*S) + z * S + x
this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
this.meshes = null; // { opaque: Mesh|null, water: Mesh|null }
this.dirty = true; // Mesh muss (neu) gebaut werden
}
idx(x, y, z) { return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x; }
get(x, y, z) { return this.blocks[this.idx(x, y, z)]; }
set(x, y, z, v) { this.blocks[this.idx(x, y, z)] = v; }
}
export class World {
constructor(scene, materials, seed) {
this.scene = scene;
this.materials = materials; // { opaque: Material, water: Material }
this.seed = seed;
this.chunks = new Map();
}
key(cx, cz) { return cx + ',' + cz; }
getChunk(cx, cz) { return this.chunks.get(this.key(cx, cz)); }
// Generiert den Chunk (falls nicht vorhanden) inkl. Terrain+Baeume.
// Da der Chunk VOR dem Generieren in die Map eingetragen wird, ist die
// Rekursion (Baeume in Nachbar-Chunks) beschraenkt und terminiert.
ensureChunk(cx, cz) {
const k = this.key(cx, cz);
let c = this.chunks.get(k);
if (c) return c;
c = new Chunk(cx, cz);
this.chunks.set(k, c);
generateChunk(this, c);
return c;
}
// Block-Lesezugriff OHNE Generierung (fuer Rendering/Physik/Raycast).
getBlock(wx, wy, wz) {
if (wy < 0 || wy >= CHUNK_HEIGHT) return BLOCK.AIR;
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const c = this.getChunk(cx, cz);
if (!c) return BLOCK.AIR;
return c.get(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE);
}
// Lesezugriff MIT Generierung (fuer Weltgenerierung / Baeume).
getGen(wx, wy, wz) {
if (wy < 0 || wy >= CHUNK_HEIGHT) return BLOCK.AIR;
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const c = this.ensureChunk(cx, cz);
return c.get(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE);
}
// Schreibzugriff waehrend Generierung.
setGen(wx, wy, wz, v) {
if (wy < 0 || wy >= CHUNK_HEIGHT) return;
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const c = this.ensureChunk(cx, cz);
c.set(wx - cx * CHUNK_SIZE, wy, wz - cz * CHUNK_SIZE, v);
}
// Spieler-Setzen/Abbauen: schreibt Block und markiert betroffene Chunks dirty.
setBlock(wx, wy, wz, v) {
if (wy < 0 || wy >= CHUNK_HEIGHT) return false;
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const lx = wx - cx * CHUNK_SIZE;
const lz = wz - cz * CHUNK_SIZE;
const c = this.ensureChunk(cx, cz);
if (c.get(lx, wy, lz) === v) return false;
c.set(lx, wy, lz, v);
c.dirty = true;
// An Chunk-Grenzen Nachbar-Chunks ebenfalls aktualisieren
if (lx === 0) this._markDirty(cx - 1, cz);
if (lx === CHUNK_SIZE - 1) this._markDirty(cx + 1, cz);
if (lz === 0) this._markDirty(cx, cz - 1);
if (lz === CHUNK_SIZE - 1) this._markDirty(cx, cz + 1);
return true;
}
_markDirty(cx, cz) {
const c = this.getChunk(cx, cz);
if (c) c.dirty = true;
}
// --- Meshing ---
getBlockWorld = (wx, wy, wz) => this.getBlock(wx, wy, wz);
buildChunkMesh(chunk) {
// Fuer korrektes Border-Culling Nachbarn sicherstellen (Blockdaten).
for (let dz = -1; dz <= 1; dz++) {
for (let dx = -1; dx <= 1; dx++) {
this.ensureChunk(chunk.cx + dx, chunk.cz + dz);
}
}
const { opaque, transparent } = buildChunkGeometry(this.getBlockWorld, chunk);
// alte Meshes entfernen
this.disposeChunkMesh(chunk);
const ox = chunk.cx * CHUNK_SIZE;
const oz = chunk.cz * CHUNK_SIZE;
const meshes = { opaque: null, water: null };
if (opaque) {
const m = new THREE.Mesh(opaque, this.materials.opaque);
m.position.set(ox, 0, oz);
m.frustumCulled = true;
this.scene.add(m);
meshes.opaque = m;
}
if (transparent) {
const m = new THREE.Mesh(transparent, this.materials.water);
m.position.set(ox, 0, oz);
m.renderOrder = 1;
this.scene.add(m);
meshes.water = m;
}
chunk.meshes = meshes;
chunk.dirty = false;
}
disposeChunkMesh(chunk) {
if (!chunk.meshes) return;
for (const key of ['opaque', 'water']) {
const m = chunk.meshes[key];
if (m) {
this.scene.remove(m);
m.geometry.dispose();
}
}
chunk.meshes = null;
}
// --- Streaming: Chunks im Radius um Spieler halten ---
updateStreaming(px, pz, renderDistance, maxOpsPerFrame) {
const pcx = Math.floor(px / CHUNK_SIZE);
const pcz = Math.floor(pz / CHUNK_SIZE);
// 1) Entferne zu weit entfernte Chunks
for (const [k, c] of this.chunks) {
const dx = c.cx - pcx;
const dz = c.cz - pcz;
if (Math.max(Math.abs(dx), Math.abs(dz)) > renderDistance + 1) {
this.disposeChunkMesh(c);
this.chunks.delete(k);
}
}
// 2) Sammle fehlende/dirty Chunks im Radius, sortiert nach Distanz
const todo = [];
for (let dz = -renderDistance; dz <= renderDistance; dz++) {
for (let dx = -renderDistance; dx <= renderDistance; dx++) {
const cx = pcx + dx;
const cz = pcz + dz;
const c = this.getChunk(cx, cz);
if (!c) {
todo.push({ cx, cz, d: dx * dx + dz * dz, gen: true });
} else if (c.dirty) {
todo.push({ cx, cz, d: dx * dx + dz * dz, gen: false });
}
}
}
todo.sort((a, b) => a.d - b.d);
let ops = 0;
for (const job of todo) {
if (ops >= maxOpsPerFrame) break;
const c = this.ensureChunk(job.cx, job.cz);
this.buildChunkMesh(c);
ops++;
}
}
// Bei Spielstart: Chunk am Spawn sicherstellen (fuer sicheren Boden)
highestSolidY(wx, wz) {
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const c = this.ensureChunk(cx, cz);
const lx = wx - cx * CHUNK_SIZE;
const lz = wz - cz * CHUNK_SIZE;
for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) {
if (isSolid(c.get(lx, y, lz))) return y;
}
return CHUNK_HEIGHT - 1;
}
}