fix(world): leere Chunks, Bodensignal und Fall-Auffang-Schleife (Fix-Runde 1)
Ruling 51: #load() rief addCollider() auch für Chunks ohne jede Kollisions- geometrie auf (offenes Wasser: 4_6, 4_10, 5_6, 5_10 — an den Rohdaten nachgezählt). Das lieferte nur eine Fremdwarnung aus ColliderRegistry und ein handle mit id:-1, das nie ready wird. addCollider läuft jetzt nur noch, wenn die collide-Gruppe Kinder hat; #unload und isReadyAt behandeln einen Eintrag ohne handle korrekt (kein removeCollider nötig, sofort "bereit"). Ruling 52: lastSafe hielt sich an der Höhe fest (p.y > -1), nicht am tatsächlichen Bodenkontakt — der Scheitel eines Sprungs oder ein Dach zählten als "sicher". Prüfung im Submodul ergab: playerIsOnGround (public, via setOnGround() nur aus snapToGround() auf true gesetzt) mit passendem Getter getIsOnGround(). lastSafe aktualisiert sich jetzt nur noch bei echtem Bodenkontakt. Ruling 53: setSkipCapsuleCollision(true) schaltet nur die horizontale Kollision ab, applyGravity läuft unabhängig weiter — die "Schwebephase" war freier Fall. Die alte 15s-Zeitschranke schaltete die Kollision ohne vorhandenen Boden scharf, was bei dauerhaft fehlendem Chunk in eine endlose Fall-Auffang-Schleife lief. Die Figur wird jetzt aktiv auf Spawnhöhe gehalten, bis ihr Chunk bereit ist; nach 15s nur noch eine einmalige Warnung, kein Verhaltenswechsel. Per künstlicher 8s-Verzögerung gemessen: Höhe blieb über den gesamten (durch Worker-Pool-Stau auf über 14s verlängerten) Wartezeitraum innerhalb 0,004 Einheiten der Spawnhöhe. Neue Tests in tests/streaming.test.js decken Ruling 51 ab (ChunkStreamer mit gemocktem fetch/player/scene); Ruling 52/53 sind Physik-Verhalten des Submoduls und über den Browser verifiziert (Bericht: task-8-report.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f7041dab91
commit
289bbbba1d
3 files changed
+102
-15
No files matched your search
+25
-8
@@ -54,25 +54,42 @@ await player.init({
|
||||
});
|
||||
|
||||
// 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).
|
||||
// streaming.js). setSkipCapsuleCollision(true) schaltet nur das horizontale Herausdrücken
|
||||
// ab; applyGravity läuft im Controller unabhängig davon weiter (CharacterMovement.ts), die
|
||||
// Kapsel würde also binnen Sekunden frei fallen, wenn man sie nur "schweben" ließe. Solange
|
||||
// der Chunk unter dem Spawn nicht bereit ist, hält tick() sie deshalb aktiv auf Spawnhöhe
|
||||
// fest (Ruling 53) statt sie fallen und vom Auffangnetz auffangen zu lassen — eine feste
|
||||
// Zeitschranke, die danach die Kollision ohne vorhandenen Boden einschaltet, würde in eine
|
||||
// endlose Fall-Auffang-Schleife laufen, sobald der Chunk dauerhaft ausbleibt. Nach 15s gibt
|
||||
// es nur noch eine einmalige Warnung, kein Verhaltenswechsel.
|
||||
const streamer = new ChunkStreamer({ scene, player, materials });
|
||||
player.setSkipCapsuleCollision(true);
|
||||
let settled = false;
|
||||
let warnedSlowSpawn = 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));
|
||||
if (!settled) {
|
||||
if (streamer.isReadyAt(player.playerCapsule.position)) {
|
||||
settled = true;
|
||||
player.setSkipCapsuleCollision(false);
|
||||
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
|
||||
} else {
|
||||
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
|
||||
if (!warnedSlowSpawn && performance.now() - startedAt > 15000) {
|
||||
warnedSlowSpawn = true;
|
||||
console.warn(`Chunk am Spawn (${SPAWN.x}, ${SPAWN.z}) ist nach 15s immer noch nicht bereit.`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const p = player.playerCapsule.position;
|
||||
// Ruling 52: an ein echtes Bodensignal binden statt an die Höhe — sonst würde auch der
|
||||
// Scheitelpunkt eines Sprungs, ein Dach oder ein Moment im freien Fall als "sicher" gelten.
|
||||
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 };
|
||||
else if (player.getIsOnGround()) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
|
||||
}
|
||||
|
||||
const timer = new Timer();
|
||||
|
||||
+15
-5
@@ -72,9 +72,15 @@ export class ChunkStreamer {
|
||||
group.add(collide);
|
||||
this.scene.add(group);
|
||||
group.updateMatrixWorld(true);
|
||||
const handle = this.player.addCollider({
|
||||
motion: "static", useWorker: true, shape: { kind: "mesh", source: collide },
|
||||
});
|
||||
// Ruling 51: ein Chunk ganz ohne Kollisionsgeometrie (offenes Wasser, z.B. 4_6/4_10/
|
||||
// 5_6/5_10 — per public/data/vice-a nachgezählt) braucht keinen Collider. addCollider
|
||||
// auf einer leeren Gruppe liefert nur eine Fremdwarnung und ein handle mit id:-1, das
|
||||
// nie ready wird — der Chunk bleibt trotzdem in active, nur ohne Collider-Handle.
|
||||
const handle = collide.children.length > 0
|
||||
? this.player.addCollider({
|
||||
motion: "static", useWorker: true, shape: { kind: "mesh", source: collide },
|
||||
})
|
||||
: null;
|
||||
this.active.set(k, { group, handle, chunk });
|
||||
} catch (err) {
|
||||
console.warn(`Chunk ${k} konnte nicht geladen werden:`, err);
|
||||
@@ -97,7 +103,8 @@ export class ChunkStreamer {
|
||||
#unload(k) {
|
||||
const entry = this.active.get(k);
|
||||
if (!entry) return;
|
||||
this.player.removeCollider(entry.handle);
|
||||
// Ruling 51: collider-lose Chunks (offenes Wasser) haben kein handle abzumelden.
|
||||
if (entry.handle) this.player.removeCollider(entry.handle);
|
||||
this.scene.remove(entry.group);
|
||||
entry.group.traverse((o) => o.geometry?.dispose());
|
||||
this.active.delete(k);
|
||||
@@ -108,6 +115,9 @@ export class ChunkStreamer {
|
||||
const i = Math.floor((position.x - SLICE.x0) / CHUNK_SIZE);
|
||||
const j = Math.floor((SLICE.y1 + position.z) / CHUNK_SIZE);
|
||||
const entry = this.active.get(key(i, j));
|
||||
return Boolean(entry && this.player.collisionWorld.get(entry.handle.id)?.ready);
|
||||
if (!entry) return false;
|
||||
// Ruling 51: kein Collider nötig (leerer Chunk) -> nichts, worauf noch zu warten wäre.
|
||||
if (!entry.handle) return true;
|
||||
return Boolean(this.player.collisionWorld.get(entry.handle.id)?.ready);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user