- 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>
202 lines
6.3 KiB
JavaScript
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;
|
|
}
|
|
}
|