Ein First-Person-Controller in Babylon.js · Teil 10 von 11 der Serie
Teil 10: Wände hochklettern
Markiere Wände mit einer Kollisionsebene als kletterbar, beginne das Klettern mit einem Sprung, beweg dich an der Wand hinauf, hinunter und seitwärts, halte an ihren Rändern an, klettere oben darüber und spring ab.

Quest-Ziele
- Wände mit einer Kollisionsebene als kletterbar markieren
- Mit einem Sprung zu klettern beginnen, vom Boden oder aus der Luft
- Sich an der Wand hinauf, hinunter und seitwärts bewegen und an ihr bleiben
- An den Seiten anhalten und oben darüber klettern
- Loslassen oder rückwärts abspringen
Leitern gehen hinauf und hinunter. Eine Kletterwand geht überallhin: hinauf, hinunter und seitwärts, so weit ihre Griffe reichen. Diese letzte Art zu klettern baut auf den Teilen der vorigen auf: den Ebenen aus Teil 4, den Modi aus Teil 7 und dem Klettern auf eine Kante aus Teil 8, das die Spielfigur oben über die Kante bringt.
Welche Wände?
Anders als eine Kante lässt sich nicht jede Wand beklettern: Ein Spiel zeigt, welche es sind, mit Griffen, Ranken oder einer Farbe, und der Code muss es auch wissen. Eine Kollisionsebene ist ein guter Ort dafür. Die Wand bleibt für alles andere fest, und ein Strahl, der nur nach der Kletterebene sucht, findet nur kletterbare Wände. Gib LAYER in src/physics.ts ein drittes Bit und mach es fest:
/**
* Collision layers, one bit each. A shape is a member of some layers and collides with
* others, and so is every query: rays and shape casts that look for the world skip the
* player's own capsule.
*/
export const LAYER = {
world: 1,
player: 2,
climbable: 4,
};
/** What counts as solid: the ground and walls. */
export const SOLID = LAYER.world | LAYER.climbable;Kisten bekommen in BlockOptions eine Option climbable?: boolean, die sie in diese Ebene setzt. In block():
if (!options.ghost) {
// Mass 0 makes a static body: it never moves, and costs almost nothing.
const body = new PhysicsAggregate(mesh, PhysicsShapeType.BOX, { mass: 0 }, this.scene);
body.shape.filterMembershipMask = options.climbable ? LAYER.climbable : LAYER.world;
}Und weiter hinten rechts kommt in diesem Teil eine kletterbare Wand dazu, 6 m hoch und 8 m breit, in Bernstein. Ergänze climbable: '#ffc442' bei den Farben und die Wand in buildLevel():
// Further back on the right: a wall that can be climbed all the way up.
build.block('climbing-wall', { size: [8, 6, 2], at: [8, 3, -15], color: 'climbable', climbable: true });Hinauf
Die Spielfigur klettert mit 2 m/s: Ergänze climbSpeed: 2 in PLAYER in den Einstellungen. Ergänze 'climb' bei den Modi, ein Feld private wallNormal = new Vector3(); für die Wand, an der die Spielfigur hängt, und case 'climb': this.climbWall(support); break; im switch in update():
export type Mode = 'walk' | 'air' | 'ladder' | 'mantle' | 'hang' | 'climb';Ein Sprung vor einer kletterbaren Wand beginnt das Klettern, so wie ein Sprung vor einer Kante das Hinaufklettern beginnt. tryClimbing() probiert die Kante zuerst, so klettert man auf eine hüfthohe kletterbare Kiste hinauf, statt an ihr hochzuklettern:
/** From the ground, jump climbs onto a ledge in reach, or onto a climbable wall. */
private tryClimbing(): boolean {
if (this.input.axis('back', 'forward') <= 0) return false;
const center = this.controller.getPosition();
const ledge = this.findLedge(center, this.look.forward(), PLAYER.mantleMin, PLAYER.mantleMax);
if (ledge && this.startMantle(ledge)) return true;
return this.tryWall();
}In der Luft endet tryGrab() mit der Wand statt mit return false, so hält sich fest, wer eine kletterbare Wand anspringt:
this.startHang(high);
return true;
}
}
return this.tryWall();Ändere auch den Kommentar in walk() zu „A jump next to a ledge or a climbable wall climbs instead.“. Dann kommt der Abschnitt für Wände ans Ende der Klasse:
// Climbable walls -------------------------------------------------------------------
/** A climbable wall in front of `center`, within reach. */
private findWall(center: Vector3, direction: Vector3): { normal: Vector3; distance: number } | null {
const hit = raycast(center, center.add(direction.scale(PLAYER.radius + 0.4)), LAYER.climbable);
if (!hit || Math.abs(hit.hitNormalWorld.y) > 0.5) return null;
return {
normal: hit.hitNormalWorld.clone(),
distance: Math.hypot(hit.hitPointWorld.x - center.x, hit.hitPointWorld.z - center.z),
};
}
private tryWall(): boolean {
if (this.grabCooldown > 0) return false;
const forward = this.look.forward();
const wall = this.findWall(this.controller.getPosition(), forward);
if (!wall || Vector3.Dot(wall.normal, forward) > -0.5) return false;
if (this.crouching) this.setCrouching(false);
this.wallNormal.copyFrom(wall.normal);
this.mode = 'climb';
this.jumpTimer = 0;
return true;
}
private climbWall(support: CharacterSurfaceInfo): void {
if (this.input.pressedNow('crouch')) {
this.letGo(this.wallNormal, 1, 0);
return;
}
if (this.jumpTimer > 0) {
this.letGo(this.wallNormal, 4, 3);
return;
}
const center = this.controller.getPosition();
const wall = this.findWall(center, this.wallNormal.negate());
if (!wall) {
this.mode = 'air';
return;
}
this.wallNormal.copyFrom(wall.normal);
let climb = this.input.axis('back', 'forward');
let side = this.input.axis('left', 'right');
if (climb < 0 && support.supportedState === CharacterSupportedState.SUPPORTED) {
this.mode = 'walk';
return;
}
const into = this.wallNormal.negate();
// At the top the head rises above the wall: climb over the edge, or stop.
if (climb > 0 && !this.findWall(center.add(new Vector3(0, 0.6, 0)), into)) {
const ledge = this.findLedge(center, into, 0.3, PLAYER.grabMax);
if (ledge && this.startMantle(ledge)) return;
climb = 0;
}
// At the sides, stop where the climbable part ends.
const right = new Vector3(-this.wallNormal.z, 0, this.wallNormal.x);
if (side && !this.findWall(center.add(right.scale(side * PLAYER.radius)), into)) side = 0;
this.velocity.set(0, climb * PLAYER.climbSpeed, 0);
this.velocity.addInPlace(right.scale(side * PLAYER.climbSpeed));
// Keep the capsule just off the wall.
this.velocity.addInPlace(into.scale((wall.distance - PLAYER.radius - 0.05) * 10));
}An der Wand
findWall() schickt einen Strahl zur Wand, aber nur gegen die Kletterebene, und gibt die Normale der Wand und ihren Abstand zurück. Ein Boden oder eine Schräge ist keine Wand, deshalb zählen Treffer nicht, die nach oben oder unten zeigen.
Beim Klettern sucht die Spielfigur die Wand in jedem Frame neu, in der Richtung, in der sie zuletzt war, so folgt die Normale der Wand. Aus dieser Normale ergeben sich die Richtungen der Bewegung: an der Wand hinauf und an ihr entlang nach rechts, aus Sicht dessen, der vor ihr steht. W und S klettern hinauf und hinunter, A und D seitwärts, alles mit 2 m/s und ohne Schwerkraft.
Ein letzter Teil der Geschwindigkeit hält die Kapsel an der Wand: Er zeigt in die Wand hinein, das Zehnfache des Abstands über 5 cm hinaus. Treibt die Spielfigur weg, wird sie zurückgezogen, und der Controller hält sie an der Wand an.
Die Ränder
Oben. Ein zweiter Strahl, 60 cm über der Mitte der Kapsel, sucht die Wand ebenfalls. Findet er beim Hinaufklettern keine, ist der Kopf über der Oberkante der Wand, und
findLedge()sucht die Oberseite davor. Gibt es eine, bringt das Klettern aus Teil 8 die Spielfigur über die Kante. Wenn nicht, hält sie an.An den Seiten. Vor einer Bewegung zur Seite prüft ein Strahl, um einen Kapselradius in diese Richtung versetzt, dass die Wand weitergeht. Wo der kletterbare Teil endet, hält die Spielfigur an, auch wenn die Wand selbst weiterginge.
Unten. Wer auf den Boden hinunterklettert, beendet das Klettern, wie an einer Leiter.
C lässt los und die Spielfigur fallen, und die Leertaste springt rückwärts ab, mit demselben letGo() wie an der Leiter.
Ausprobieren
Geh zur bernsteinfarbenen Wand weiter hinten rechts, halte W und drück die Leertaste: Das HUD zeigt climb. Klettere hinauf, beweg dich seitwärts bis zum Ende der Wand, wo du anhältst, und klettere weiter nach oben, wo du über die Kante auf sie hinaufkletterst. Spring die Wand mit Anlauf an, und du hältst dich mitten in der Luft fest.
Im letzten Teil bekommt die Kamera die kleinen Bewegungen, durch die sich Gehen, Landen und Rennen lebendig anfühlen.