Zum Inhalt springen
DasPeTe Tutorials

Ein First-Person-Controller in Babylon.js · Teil 9 von 11 der Serie

Teil 9: An Kanten hängen

Greif Kanten, die zu hoch sind, um gleich hinaufzuklettern, häng knapp unter der Kante, hangel dich an ihr entlang, so weit sie reicht, und zieh dich hoch oder lass los.

Fortgeschritten20 Min.

Quest-Ziele

  • Kanten über der Reichweite des Kletterns greifen, auf dem Weg nach unten aus einem Sprung
  • Knapp unter der Kante hängen, und Wände dabei im Weg lassen
  • An der Kante entlanghangeln, so weit sie auf gleicher Höhe weitergeht
  • Sich hochziehen oder loslassen

Das Klettern aus Teil 8 erreicht Kanten bis 1,3 m über den Füßen. Höhere erreichen die Hände trotzdem: Oben im Sprung ist eine Kante 2 m über den Füßen etwa auf der Höhe eines ausgestreckten Arms. Spiele lassen einen sie greifen, daran hängen und sich dann hochziehen oder Hand über Hand an ihr entlanghangeln.

Eine Wand mit Kante

Weiter hinten links kommt in diesem Teil eine 3,2 m hohe Wand mit einer Kante an ihrer Oberseite dazu. In der Mitte hat die Kante eine Lücke, wo eine niedrigere Kiste von 2,4 m zwischen zwei Teilen der Wand steht. Sie kommt in buildLevel() und ledge: '#c9c2f2' zu den Farben:

src/level.ts
// Further back on the left: a wall too high to climb onto, with a gap in its ledge.
  build.block('ledge-a', { size: [4, 3.2, 2], at: [-11, 1.6, -15], color: 'ledge' });
  build.block('ledge-b', { size: [3, 3.2, 2], at: [-5.5, 1.6, -15], color: 'ledge' });
  build.block('ledge-gap', { size: [1, 2.4, 2], at: [-8.5, 1.2, -15], color: 'ledge' });

Die Hände reichen bis 2,1 m über die Füße, und an einer Kante hangelt sich die Spielfigur mit 1,5 m/s entlang. shimmySpeed kommt in PLAYER in src/settings.ts hinter ladderSpeed und grabMax hinter den Bereich fürs Klettern:

src/settings.ts
shimmySpeed: 1.5,
  /** Climbing onto a ledge: from this high above the feet... */
  mantleMin: 0.5,
  /** ...up to this high. Higher ledges can be grabbed from the air. */
  mantleMax: 1.3,
  /** The highest ledge the hands can reach, above the feet. */
  grabMax: 2.1,

Greifen

Ergänze 'hang' bei den Modi:

src/player.ts
export type Mode = 'walk' | 'air' | 'ladder' | 'mantle' | 'hang';

Beim Hängen merkt sich die Spielfigur die Kante und den Punkt, an dem die Kapsel hängt. Die Felder kommen zu den anderen:

src/player.ts
private ledge: Ledge | null = null;
  private hangPoint = new Vector3();

tryGrab() sucht eine hohe Kante, wenn es keine niedrige gibt, mit demselben findLedge() und einem höheren Bereich. Ersetze es:

src/player.ts
/** In the air, moving at a ledge climbs onto it, or grabs it if it is higher. */
  private tryGrab(): boolean {
    if (this.grabCooldown > 0 || this.input.down('crouch') || this.input.axis('back', 'forward') <= 0) return false;
    const center = this.controller.getPosition();
    const forward = this.look.forward();
    const low = this.findLedge(center, forward, PLAYER.mantleMin, PLAYER.mantleMax);
    if (low && this.startMantle(low)) return true;
    // Grab only on the way down or near the top of a jump, not while still flying upwards.
    if (this.velocity.y < 1) {
      const high = this.findLedge(center, forward, PLAYER.mantleMax, PLAYER.grabMax);
      if (high) {
        this.startHang(high);
        return true;
      }
    }
    return false;
  }

Eine hohe Kante wird nur auf dem Weg nach unten gegriffen oder oben im Sprung, wenn die Spielfigur langsamer als 1 m/s steigt. Auf dem Weg nach oben kommen die Füße der Oberseite mit jedem Frame näher, und sobald sie weniger als 1,3 m entfernt sind, ist Hinaufklettern besser als Hängen.

Hängen

Ergänze case 'hang': this.hang(dt); break; im switch in update() und das Hängen selbst im Kantenabschnitt:

src/player.ts
private startHang(ledge: Ledge): void {
    this.ledge = ledge;
    this.hangPoint.copyFrom(this.hangCenter(this.controller.getPosition(), ledge));
    this.mode = 'hang';
    this.jumpTimer = 0;
  }

  /** Where the center of the capsule hangs: just off the wall, the head under the top. */
  private hangCenter(center: Vector3, ledge: Ledge): Vector3 {
    const offset = ledge.distance - PLAYER.radius - 0.05;
    return new Vector3(
      center.x - ledge.normal.x * offset,
      ledge.top - PLAYER.standHeight / 2 - 0.05,
      center.z - ledge.normal.z * offset,
    );
  }

  private hang(dt: number): void {
    const ledge = this.ledge!;
    if (this.input.pressedNow('crouch') || this.input.pressedNow('back')) {
      this.letGo(ledge.normal, 1, 0);
      return;
    }
    if (this.jumpTimer > 0) {
      this.jumpTimer = 0;
      const center = this.controller.getPosition();
      const fresh = this.findLedge(center, ledge.normal.negate(), PLAYER.mantleMax, PLAYER.grabMax + 0.2);
      if (fresh && this.startMantle(fresh)) return;
    }

    const side = this.input.axis('left', 'right');
    if (side) {
      // Move along the wall only as far as the ledge goes on at the same height.
      const right = new Vector3(-ledge.normal.z, 0, ledge.normal.x);
      const ahead = this.hangPoint.add(right.scale(side * (PLAYER.radius + 0.1)));
      ahead.y = this.controller.getPosition().y;
      const next = this.findLedge(ahead, ledge.normal.negate(), PLAYER.mantleMax, PLAYER.grabMax + 0.2);
      if (next && Math.abs(next.top - ledge.top) < 0.15) {
        this.hangPoint.addInPlace(right.scale(side * PLAYER.shimmySpeed * dt));
        this.ledge = next;
      }
    }
    // Steer towards the hang point instead of jumping there, so walls still block.
    this.hangPoint.subtractToRef(this.controller.getPosition(), this.velocity);
    this.velocity.scaleInPlace(15);
  }

Die Kapsel hängt knapp vor der Wand, 5 cm entfernt, mit ihrer Oberkante 5 cm unter der Kante. Damit sind die Augen 20 cm unter der Oberseite, und die Spielfigur schaut auf die Kante direkt über sich.

Die Spielfigur springt nicht an diesen Punkt. In jedem Frame zeigt die Geschwindigkeit auf ihn, das 15-Fache des Abstands: eine Handbreit entfernt ist das schnell, nahe dran langsam, und in Sekundenbruchteilen kommt die Spielfigur dort zur Ruhe. Der Controller bewegt die Kapsel weiterhin, also halten Wände sie weiter auf, was ein setPosition() nicht beachten würde.

An der Kante entlang

A und D verschieben den Hängepunkt an der Wand entlang. Vor jedem Schritt schaut findLedge() eine Kapselbreite weiter auf die Wand. Geht die Kante dort auf gleicher Höhe weiter, auf 15 cm genau, rückt der Hängepunkt weiter und übernimmt die Kante, die dort gefunden wurde, so folgen die Hände auch einer Wand, die sich leicht biegt. Wenn nicht, bleibt die Spielfigur stehen: am Ende der Wand und an der Lücke, wo die Oberseite 80 cm tiefer liegt.

Hinauf oder hinunter

  • Die Leertaste zieht hoch. Die Spielfigur sucht die Kante von dort aus, wo sie jetzt hängt, noch einmal und startet das Klettern aus Teil 8, mit allen seinen Prüfungen: Ist oben kein Platz, passiert nichts.

  • C oder S lässt los, mit einem kleinen Stoß von der Wand weg, und die Abklingzeit aus Teil 7 verhindert, dass die Spielfigur auf dem Weg nach unten dieselbe Kante wieder greift.

Ausprobieren

Geh zur hohen Wand links hinter dem Start und spring sie an: Das HUD zeigt hang, und die Füße sind 1,35 m über dem Boden, 1,85 m unter der Oberseite. Hangel dich mit A und D an der Kante entlang, und du hältst an, wo sie an der Lücke endet. Zieh dich mit der Leertaste hoch oder lass mit C los. Spring direkt an die Lücke, und du kletterst gleich auf die niedrigere Kiste.

Das Projekt am Ende von Teil 9: Spring an die hohe Wand, um an ihrer Kante zu hängen, und hangel dich mit A und D entlang.
In neuem Tab öffnen

Im nächsten Teil klettert die Spielfigur an Wänden, die überall Griffe haben: hinauf, hinunter und seitwärts.

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.