Zum Inhalt springen
DasPeTe Tutorials

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.

Fortgeschritten20 Min.

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:

src/physics.ts
/**
 * 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():

src/level.ts
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():

src/level.ts
// 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():

src/player.ts
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:

src/player.ts
/** 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:

src/player.ts
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:

src/player.ts
// 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.

Das Projekt am Ende von Teil 10: Spring die bernsteinfarbene Wand an und klettere in alle Richtungen.
In neuem Tab öffnen

Im letzten Teil bekommt die Kamera die kleinen Bewegungen, durch die sich Gehen, Landen und Rennen lebendig anfühlen.

Kommentare

Die Kommentare sind GitHub Discussions: Zum Schreiben mit GitHub anmelden.

Die Kommentare kommen über giscus von GitHub. Beim Laden gehen deine IP-Adresse an giscus.app und GitHub.