Files
gta6/src/main.js
T
knsoandClaude Sonnet 5 f7041dab91 feat(world): Chunk-Streaming mit Collider-Verwaltung und Auffangnetz
wantedChunks() liefert Lade-/Entladelisten mit Hysterese (Laderadius 400,
Entladeradius 600) um die Spielerposition; ChunkStreamer holt Chunks per
fetch, baut sie über buildChunkGeometry() (Task 7) zu Meshes, registriert
die Kollisionsschicht (ground/bridges/buildings) beim Player-Controller
mit asynchronem BVH-Bau und meldet über isReadyAt() wann ein Collider
tatsächlich einsatzbereit ist. Straßen/Sand/Park/Stadtfläche bleiben reine
Auflagen ohne eigene Kollision.

main.js schwebt die Figur kollisionsfrei am Spawn (Vice Beach), bis ihr
Chunk bereit ist oder 15s vergangen sind, und fängt jeden tieferen
Durchfall über ein Auffangnetz ab, das zur letzten sicheren Höhe
zurücksetzt. Der alte Platzhalter-Boden (400x400-Ebene samt Collider)
entfällt zugunsten der echten gestreamten Welt.

Der zweite Testfall aus dem Task-Brief hatte einen Datenfehler: die
"near"-Position {x:1375,z:-1375} traf exakt das Zentrum von Chunk 9_4
(Distanz zu Chunk 5_5 ca. 1030, weit jenseits des Entladeradius 600) und
ließ die eigene Behauptung "not.toContain" fehlschlagen, sobald
streaming.js existierte (RED belegt im Task-8-Report). Position auf
{x:875,z:-1125} korrigiert, exakt 500 Einheiten von Chunk 5_5 entfernt —
außerhalb von 400, innerhalb von 600 — und damit die Hysterese, die der
Testname behauptet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 20:18:54 +02:00

92 lines
4.3 KiB
JavaScript

import { ACESFilmicToneMapping, AmbientLight, DirectionalLight, MeshStandardMaterial,
PerspectiveCamera, Scene, Timer, Vector3, WebGLRenderer } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { playerController } from "three-player-controller";
import { ChunkStreamer } from "./world/streaming.js";
const container = document.querySelector("#container");
const scene = new Scene();
const renderer = new WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.toneMapping = ACESFilmicToneMapping;
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 2500);
// Pflichtoption für player.init: der Controller übernimmt die Kamera, braucht aber controls.domElement als Eingabeziel.
const controls = new OrbitControls(camera, renderer.domElement);
scene.add(new AmbientLight(0xffffff, 1.5));
const sun = new DirectionalLight(0xffffff, 2);
sun.position.set(60, 120, 40);
sun.castShadow = true;
scene.add(sun);
// Ein Material je Schicht aus buildChunkGeometry() (Task 7). buildings trägt die dort erzeugten
// Vertexfarben (Pastellton pro Gebäude) und bleibt deshalb weiß grundiert; die übrigen Schichten
// sind reine Auflagen/Landmassen ohne Vertexfarben und bekommen je einen flachen Grundton.
const materials = {
ground: new MeshStandardMaterial({ color: 0x6d7a52, roughness: 1 }),
roads: new MeshStandardMaterial({ color: 0x3a3a3d, roughness: 0.9 }),
bridges: new MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.8 }),
buildings: new MeshStandardMaterial({ vertexColors: true, roughness: 0.85 }),
sand: new MeshStandardMaterial({ color: 0xd9c48c, roughness: 1 }),
park: new MeshStandardMaterial({ color: 0x4f7a3d, roughness: 1 }),
urban: new MeshStandardMaterial({ color: 0xa8a8a0, roughness: 0.9 }),
};
// Vice Beach; Task 8, Schritt 5. Auch initPos, damit der Streamer von Anfang an den richtigen
// Chunk anfragt statt erst nach dem späteren reset() umzuschwenken.
const SPAWN = { x: 1699, y: 3, z: -743 };
const gltf = await new GLTFLoader().loadAsync("./models/ual.glb");
const player = new playerController();
await player.init({
scene, camera, controls,
initPos: new Vector3(SPAWN.x, SPAWN.y, SPAWN.z),
playerModelConfig: {
model: gltf.scene, animations: gltf.animations, scale: 0.005,
idleAnim: "Idle_Loop", walkAnim: "Walk_Loop", runAnim: "Sprint_Loop",
jumpAnim: ["Jump_Start", "Jump_Loop", "Jump_Land"],
headBoneName: "Head", rotateY: -Math.PI / 2, speed: 100, runSpeed: 600,
},
});
// Chunk-Collider treffen erst nach und nach ein (Fetch + asynchroner BVH-Bau, siehe
// streaming.js). Bis der Chunk unter dem Spawn bereit ist — oder spätestens nach 15s —
// bleibt die Kapselkollision aus, damit die Figur nicht an halb geladenen Gebäuden hängen
// bleibt; das Auffangnetz unten fängt jeden tieferen Durchfall ab (auch während dieser Zeit).
const streamer = new ChunkStreamer({ scene, player, materials });
player.setSkipCapsuleCollision(true);
let settled = false;
const startedAt = performance.now();
let lastSafe = { ...SPAWN };
function tick(delta) {
streamer.update(player.playerCapsule.position);
if (!settled && (streamer.isReadyAt(player.playerCapsule.position) || performance.now() - startedAt > 15000)) {
settled = true;
player.setSkipCapsuleCollision(false);
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
}
const p = player.playerCapsule.position;
if (p.y < -30) player.reset(new Vector3(lastSafe.x, lastSafe.y, lastSafe.z));
else if (p.y > -1) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
}
const timer = new Timer();
timer.connect(document);
renderer.setAnimationLoop((t) => {
timer.update(t);
const delta = Math.min(timer.getDelta(), 1 / 40);
player.update(delta);
tick(delta);
renderer.render(scene, camera);
});
addEventListener("resize", () => {
renderer.setSize(container.clientWidth, container.clientHeight);
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
});