Zum Inhalt springen
DasPeTe Tutorials

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

Teil 8: Auf Kanten klettern

Finde mit zwei Strahlen eine Kante vor der Spielfigur, prüfe mit Shape-Casts, dass die Kapsel auf dem Weg hinauf passt, und klettere auf einem festen Weg über die Kante, vom Boden und aus einem Sprung.

Fortgeschritten25 Min.

Quest-Ziele

  • Eine Kante mit einem Strahl nach unten und einem nach vorne finden
  • Mit Shape-Casts prüfen, dass die Kapsel hinauf und darüber passt
  • Auf einem festen Weg über die Kante klettern
  • Vom Boden mit einem Sprung klettern und aus der Luft, indem man auf eine Kante zuhält
  • Geduckt ankommen, wo kein Platz zum Stehen ist

Ein Sprung von 1,2 m bringt die Spielfigur auf eine 1-m-Kiste und nicht höher. Spiele lassen einen auf Höheres klettern: Geh auf eine hüfthohe Mauer zu, drück Springen, und die Figur zieht sich hoch. Diese Bewegung heißt Mantle, und sie klappt mit jeder Kiste im Level, ohne sie zu markieren.

Kanten im Level

Hinter dem Start kommen in diesem Teil vier Kisten dazu: 1,2 m, 1,6 m und 2,2 m hoch und eine von 1,2 m mit einem Dach 1,3 m darüber, das nur Platz zum Ducken lässt. Sie kommen in buildLevel() und mantle: '#ff5caa' zu den Farben:

src/level.ts
// Behind the start: ledges to climb onto. The last one leaves room only to crouch.
  build.block('mantle-1', { size: [2, 1.2, 2], at: [-4.5, 0.6, -9], color: 'mantle' });
  build.block('mantle-2', { size: [2, 1.6, 2], at: [-1.5, 0.8, -9], color: 'mantle' });
  build.block('mantle-3', { size: [2, 2.2, 2], at: [1.5, 1.1, -9], color: 'mantle' });
  build.block('mantle-4', { size: [2, 1.2, 2], at: [4.5, 0.6, -9], color: 'mantle' });
  build.block('mantle-4-roof', { size: [2, 0.3, 2], at: [4.5, 2.65, -9], color: 'mantle' });

Die Spielfigur klettert auf Kanten, die zwischen 50 cm und 1,3 m über den Füßen liegen. Auf niedrigere kann sie steigen oder springen, höhere erreicht sie noch nicht. Der Bereich kommt in PLAYER in src/settings.ts:

src/settings.ts
/** Climbing onto a ledge: from this high above the feet... */
  mantleMin: 0.5,
  /** ...up to this high. */
  mantleMax: 1.3,

Eine Kante finden

Eine Kante ist eine begehbare Oberseite mit einer Wand darunter. Zwei Strahlen finden eine:

  1. Ein Strahl nach unten, 80 cm vor der Mitte der Spielfigur, von der höchsten Kantenhöhe bis zur niedrigsten. Er findet die Oberseite, und seine Normale sagt, ob man darauf stehen kann.

  2. Ein Strahl nach vorne, 5 cm unter der Oberseite. Er findet die Wand, in welche Richtung sie zeigt und wie weit sie weg ist. Ohne Wand war die Oberseite eine Schräge und keine Kante.

Die Spielfigur merkt sich eine Kante als ihre Höhe, die Normale der Wand und den Abstand dazu. Ergänze oben in src/player.ts 'mantle' bei den Modi und die Typen für eine Kante und einen Klettervorgang:

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

/** The top of something the player can climb onto, and the wall below it. */
interface Ledge {
  top: number;
  /** The wall's normal, pointing at the player. */
  normal: Vector3;
  /** Horizontal distance from the player's center to the wall. */
  distance: number;
}

/** A scripted climb onto a ledge: straight up to `via`, then over the edge to `to`. */
interface Mantle {
  from: Vector3;
  via: Vector3;
  to: Vector3;
  time: number;
  duration: number;
}

Ergänze in der Klasse ein Feld private mantle: Mantle | null = null; und an ihrem Ende einen Abschnitt für Kanten, beginnend mit der Suche:

src/player.ts
/**
   * Looks for a ledge in `forward` from `center`, whose top is between `min` and `max`
   * above the feet: a ray down just past the wall finds the top, a ray forward just under
   * the top finds the wall.
   */
  private findLedge(center: Vector3, forward: Vector3, min: number, max: number): Ledge | null {
    const feet = center.y - this.controller.footOffset;
    const reach = PLAYER.radius + 0.45;
    const above = new Vector3(center.x + forward.x * reach, feet + max, center.z + forward.z * reach);
    const below = new Vector3(above.x, feet + min, above.z);
    const down = raycast(above, below);
    if (!down || down.hitNormalWorld.y < this.controller.maxSlopeCosine) return null;
    const top = down.hitPointWorld.y;
    if (top > feet + max - 0.01) return null;

    const from = new Vector3(center.x, top - 0.05, center.z);
    const face = raycast(from, from.add(forward.scale(reach)));
    if (!face || Math.abs(face.hitNormalWorld.y) > 0.3) return null;
    const normal = face.hitNormalWorld.clone();
    normal.y = 0;
    normal.normalize();
    if (Vector3.Dot(normal, forward) > -0.5) return null;
    const distance = Math.hypot(face.hitPointWorld.x - center.x, face.hitPointWorld.z - center.z);
    return { top, normal, distance };
  }

Ein Havok-Strahl, der in einer Kiste beginnt, trifft diese Kiste nicht. Ist eine Kiste höher als max, beginnt der Strahl nach unten also in ihr und findet nichts, und die Kiste ist keine Kante, was stimmt. Die Prüfung gegen max - 0.01 fängt eine Oberseite genau am Anfang des Strahls ab. Die Wand muss zur Spielfigur zeigen, auf 60 Grad genau, damit niemand eine Kante von der Seite greift.

Wann geklettert wird

Zwei Momente starten das Klettern:

src/player.ts
/** From the ground, jump climbs onto a ledge in reach. */
  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);
    return ledge !== null && this.startMantle(ledge);
  }

  /** In the air, moving at a ledge climbs onto it. */
  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 low = this.findLedge(center, this.look.forward(), PLAYER.mantleMin, PLAYER.mantleMax);
    return low !== null && this.startMantle(low);
  }
  • Am Boden klettert die Sprungtaste, statt zu springen, wenn eine Kante vor der Spielfigur ist und sie vorwärts geht. walk() fragt zuerst. Es löscht den Sprung, damit das Klettern keinen Sprung für später übrig lässt:

src/player.ts
if (this.jumpTimer > 0) {
      // A jump next to a ledge climbs onto it instead.
      this.jumpTimer = 0;
      if (this.tryClimbing()) return;
      this.jump();
      return;
    }
  • In der Luft klettert, wer vorwärts auf eine Kante zuhält. Ein Sprung an die 1,6-m-Kiste bringt die Füße auf dem Weg nach oben auf 1,3 m an ihre Oberseite heran, und dort beginnt das Klettern. In air(), hinter der Leiter: if (this.tryLadder() || this.tryGrab()) return;

Wer in der Luft C hält, greift nicht zu: Ducken heißt „lass mich fallen“.

Platz zum Klettern

Das Klettern geht senkrecht nach oben, bis die Füße über der Oberseite sind, dann nach vorne über die Kante, bis die Kapsel kurz dahinter auf der Oberseite steht. Beide Abschnitte dieses Wegs werden mit Shape-Casts geprüft, bevor sich etwas bewegt: Passt die stehende Kapsel nicht, wird die geduckte probiert, so endet ein Klettern in eine niedrige Lücke geduckt. Das kommt in den Kantenabschnitt:

src/player.ts
/**
   * Checks that the capsule fits on the way up and over the edge, standing or else
   * crouching, and starts the climb.
   */
  private startMantle(ledge: Ledge): boolean {
    const center = this.controller.getPosition();
    const feet = center.y - this.controller.footOffset;
    const over = ledge.normal.scale(-(ledge.distance + PLAYER.radius + 0.1));
    for (const crouch of [false, true]) {
      const shape = crouch ? this.crouchShape : this.standShape;
      const half = (crouch ? PLAYER.crouchHeight : PLAYER.standHeight) / 2;
      const from = new Vector3(center.x, feet + half, center.z);
      const via = new Vector3(center.x, ledge.top + half + 0.05, center.z);
      const to = via.add(over);
      if (sweepHits(shape, from, via) || sweepHits(shape, via, to)) continue;

      if (this.crouching !== crouch) this.setCrouching(crouch);
      const height = ledge.top - feet;
      this.mantle = { from, via, to, time: 0, duration: 0.2 + 0.2 * height };
      this.mode = 'mantle';
      this.velocity.setAll(0);
      this.controller.setVelocity(this.velocity);
      return true;
    }
    return false;
  }

  private climbOver(dt: number): void {
    const mantle = this.mantle!;
    mantle.time += dt;
    const t = Math.min(mantle.time / mantle.duration, 1);
    // The first 60 percent of the time goes up, the rest over the edge.
    const position =
      t < 0.6
        ? Vector3.Lerp(mantle.from, mantle.via, smooth(t / 0.6))
        : Vector3.Lerp(mantle.via, mantle.to, smooth((t - 0.6) / 0.4));
    this.controller.setPosition(position);
    if (t === 1) {
      this.mode = 'walk';
      this.mantle = null;
    }
  }

Das Klettern selbst läuft nach Plan: climbOver() setzt die Kapsel mit setPosition() auf den Weg, die ersten 60 Prozent der Zeit nach oben und den Rest darüber, und der Controller bewegt sie nicht von selbst. Das ist sicher, weil die Shape-Casts den Weg schon geprüft haben. Eine höhere Kante dauert länger: 0,2 s plus 0,2 s pro Meter. smooth() lässt jeden Abschnitt weich anfangen und enden. Es kommt hinter die anderen Hilfsfunktionen am Ende der Datei:

src/player.ts
function smooth(t: number): number {
  return t * t * (3 - 2 * t);
}

In update() bekommt das Klettern seinen Fall, und solange es läuft, bekommt der Controller weder eine Geschwindigkeit, noch wird er integriert:

src/player.ts
update(dt: number): void {
    if (this.controller.getPosition().y < -20) this.respawn();
    const support = this.controller.checkSupport(dt, DOWN);
    this.velocity.copyFrom(this.controller.getVelocity());

    this.coyoteTimer = Math.max(this.coyoteTimer - dt, 0);
    this.jumpTimer = Math.max(this.jumpTimer - dt, 0);
    this.grabCooldown = Math.max(this.grabCooldown - dt, 0);
    if (this.input.pressedNow('jump')) this.jumpTimer = PLAYER.jumpBuffer;
    this.running = !this.crouching && this.input.down('run') && this.input.axis('back', 'forward') > 0;

    if (this.mode === 'walk' || this.mode === 'air') this.updateStance();

    switch (this.mode) {
      case 'walk':
        this.walk(dt, support);
        break;
      case 'air':
        this.air(dt, support);
        break;
      case 'ladder':
        this.climbLadder(support);
        break;
      case 'mantle':
        this.climbOver(dt);
        break;
    }

    if (this.mode !== 'mantle') {
      this.controller.setVelocity(this.velocity);
      this.controller.integrate(dt, support, GRAVITY);
    }

    const eye = this.crouching ? PLAYER.crouchEye : PLAYER.standEye;
    this.eyeHeight += (eye - this.eyeHeight) * (1 - Math.exp(-12 * dt));
  }

Ausprobieren

Geh zur 1,2-m-Kiste hinter dem Start, halte W und drück die Leertaste: Das HUD zeigt kurz mantle, und du stehst oben. Spring an die 1,6-m- und die 2,2-m-Kiste, und du kletterst auch auf sie. Kletter auf die letzte unter dem Dach, und du kommst geduckt an:

Die Kiste mit dem Dach darüber: Wer hinaufklettert, hat nur Platz zum Ducken.
Das Projekt am Ende von Teil 8: Geh auf die pinken Kisten hinter dem Start zu und spring, um hinaufzuklettern.
In neuem Tab öffnen

Im nächsten Teil greift die Spielfigur Kanten, die dafür zu hoch sind, hängt daran und hangelt sich entlang.

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.