Ein First-Person-Controller in Babylon.js · Teil 6 von 11 der Serie
Teil 6: Ducken
Tausche die Kapsel gegen eine kürzere, ohne die Füße zu bewegen, prüfe mit einem Shape-Cast auf eine Decke, bevor du wieder aufstehst, und senke den Blick weich ab.

Quest-Ziele
- Eine Ducktaste wählen, die der Browser dir lässt
- Die Kapsel gegen eine kürzere tauschen und die Füße an Ort und Stelle lassen
- Vor dem Aufstehen mit einem Shape-Cast auf eine Decke prüfen
- Die Augen weich senken und heben, bei jeder Bildrate
- Geduckt langsamer gehen
Hinter den Rampen bekommt der Parcours in diesem Teil einen Tunnel. Seine Decke ist 1,4 m hoch, zu niedrig, um aufrecht hindurchzugehen. Die Spielfigur bekommt eine zweite, kürzere Kapsel, um hindurchzukriechen.
// Left, behind the ramps: a tunnel too low to stand up in.
build.block('tunnel-wall-a', { size: [6, 2.5, 0.4], at: [-9, 1.25, -3.2], color: 'tunnel' });
build.block('tunnel-wall-b', { size: [6, 2.5, 0.4], at: [-9, 1.25, -0.8], color: 'tunnel' });
build.block('tunnel-roof', { size: [6, 1.1, 2.8], at: [-9, 1.95, -2], color: 'tunnel' });Der Tunnel kommt in buildLevel() hinter die Kisten und seine Farbe in COLORS: tunnel: '#8f7ae6'.
Eine Taste zum Ducken
Viele PC-Spiele ducken mit Strg. Im Browser ist das eine Falle: Strg+W schließt den Tab, und eine Seite kann das nicht verhindern, außer mit der Keyboard Lock API, die nur im Vollbild und nur in Chromium-Browsern funktioniert. Wer beim Vorwärtsgehen duckt, würde das Spiel schließen. Deshalb liegt Ducken auf C. Ergänze es in den Belegungen in src/input.ts:
/**
* Which keys do what. `event.code` names the physical key, so WASD stays where it is on
* any keyboard layout. Control is not used: Ctrl+W closes the tab, and no page can stop it.
*/
const BINDINGS = {
forward: ['KeyW', 'ArrowUp'],
back: ['KeyS', 'ArrowDown'],
left: ['KeyA', 'ArrowLeft'],
right: ['KeyD', 'ArrowRight'],
run: ['ShiftLeft', 'ShiftRight'],
jump: ['Space'],
crouch: ['KeyC'],
};Ergänze · C crouch auch im Hinweis des Overlays in index.html.
Eine kürzere Kapsel
Geduckt ist die Kapsel 1,1 m hoch, die Augen sind auf 0,95 m, und die Spielfigur bewegt sich mit 2 m/s. Das kommt in PLAYER in src/settings.ts:
export const PLAYER = {
radius: 0.35,
standHeight: 1.8,
crouchHeight: 1.1,
/** Eye height above the feet. */
standEye: 1.65,
crouchEye: 0.95,
walkSpeed: 4,
runSpeed: 7,
crouchSpeed: 2,Die Spielfigur legt beide Kapseln einmal am Anfang an und behält sie. Neue Felder kommen in die Klasse Player:
mode: Mode = 'air';
crouching = false;
running = false;
/** Eye height above the feet, eased when the stance changes. */
eyeHeight = PLAYER.standEye;
private readonly controller: PhysicsCharacterController;
private readonly standShape: PhysicsShapeCapsule;
private readonly crouchShape: PhysicsShapeCapsule;Der Konstruktor legt die Kapseln an und beginnt mit der stehenden:
constructor(scene: Scene, level: Level, input: Input, look: Look) {
this.level = level;
this.input = input;
this.look = look;
this.standShape = capsule(PLAYER.standHeight, scene);
this.crouchShape = capsule(PLAYER.crouchHeight, scene);
this.controller = new PhysicsCharacterController(
level.spawn.add(new Vector3(0, PLAYER.standHeight / 2, 0)),
{ shape: this.standShape, capsuleHeight: PLAYER.standHeight, capsuleRadius: PLAYER.radius },
scene,
);
this.controller.maxSlopeCosine = Math.cos((PLAYER.maxSlopeDegrees * Math.PI) / 180);
}Der Controller von Babylon.js tauscht seine Form mit setShapeOptions(). Das zweite Argument hält die Füße an Ort und Stelle: Die Position des Controllers ist die Mitte der Kapsel, ohne es hinge die kürzere Kapsel also 35 cm in der Luft, mit ihrer Mitte dort, wo die der alten war, und fiele. Ohne shape würde er jedes Mal eine neue Kapsel aus capsuleHeight und capsuleRadius bauen, und eine neue Kapsel ist in keiner Kollisionsebene, sodass Strahlen die Spielfigur wieder träfen. Mit einer braucht es die Höhe trotzdem: Der Controller rechnet damit aus, wo die Füße sind.
Platz zum Aufstehen
Wer im Tunnel C loslässt, darf nicht in die Decke hinein aufstehen. Vor dem Aufstehen prüft die Spielfigur mit einem Shape-Cast, ob Platz ist: Die geduckte Kapsel wird um den Höhenunterschied nach oben geschwenkt, und trifft sie unterwegs etwas, ist da eine Decke. Ein einzelner Strahl vom Kopf nach oben würde eine Decke übersehen, die nur einen Teil der Kapsel überdeckt.
Der Shape-Cast kommt in src/physics.ts, hinter die Strahlen:
const castInput = new ShapeCastResult();
const castHit = new ShapeCastResult();
/**
* Sweeps a shape from one point to another and says whether anything solid is in the way.
* Only the layers in the shape's collide mask count.
*/
export function sweepHits(shape: PhysicsShape, from: Vector3, to: Vector3): boolean {
plugin.shapeCast(
{ shape, rotation: Quaternion.Identity(), startPosition: from, endPosition: to, shouldHitTriggers: false },
castInput,
castHit,
);
return castHit.hasHit;
}Ein Shape-Cast nutzt den Kollisionsfilter der Form selbst, und die Kapseln der Spielfigur kollidieren nur mit Festem, also findet der Schwenk die Spielfigur nicht. Die Importe oben in der Datei wachsen entsprechend:
import {
HavokPlugin,
PhysicsRaycastResult,
Quaternion,
RegisterJoinedPhysicsEngineComponent,
ShapeCastResult,
Vector3,
type PhysicsShape,
type Scene,
} from '@babylonjs/core/pure';Jetzt das Ducken selbst, als Abschnitt am Ende der Klasse Player. Importiere sweepHits aus ./physics.ts:
// Crouching -------------------------------------------------------------------------
private updateStance(): void {
const wants = this.input.down('crouch');
if (wants && !this.crouching) this.setCrouching(true);
else if (!wants && this.crouching && this.roomToStand()) this.setCrouching(false);
}
private setCrouching(crouching: boolean): void {
this.crouching = crouching;
// Swap the capsule and keep the feet where they are.
this.controller.setShapeOptions(
{
shape: crouching ? this.crouchShape : this.standShape,
capsuleHeight: crouching ? PLAYER.crouchHeight : PLAYER.standHeight,
capsuleRadius: PLAYER.radius,
},
true,
);
}
/** Sweeps the crouching capsule up to standing height: anything in the way is a ceiling. */
private roomToStand(): boolean {
const center = this.controller.getPosition();
const raised = center.add(new Vector3(0, PLAYER.standHeight - PLAYER.crouchHeight, 0));
return !sweepHits(this.crouchShape, center, raised);
}C halten duckt sofort. Loslassen steht auf, sobald Platz ist, so bleibt die Spielfigur im Tunnel geduckt, bis sie ihn verlässt, und steht draußen von selbst auf.
Die Augen
Die Kapsel wechselt ihre Höhe in einem Frame, aber die Augen sollen nicht 70 cm springen. Die Spielfigur führt eine eigene Augenhöhe und lässt sie zum Ziel gleiten. Ersetze eyePosition():
/** The point between the eyes, in world space. */
eyePosition(result = new Vector3()): Vector3 {
return result.copyFrom(this.controller.getPosition()).addInPlaceFromFloats(0, this.eyeHeight - this.controller.footOffset, 0);
}Und update(), das vor den Modi die Haltung aktualisiert und danach die Augen gleiten lässt:
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);
if (this.input.pressedNow('jump')) this.jumpTimer = PLAYER.jumpBuffer;
this.running = !this.crouching && this.input.down('run') && this.input.axis('back', 'forward') > 0;
this.updateStance();
switch (this.mode) {
case 'walk':
this.walk(dt, support);
break;
case 'air':
this.air(dt, support);
break;
}
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));
}1 - Math.exp(-12 * dt) ist der Anteil der restlichen Strecke, der in diesem Frame zurückgelegt wird. Pro Sekunde ist das bei jeder Bildrate derselbe Anteil, und die halbe Strecke ist nach etwa 60 Millisekunden geschafft. Das verbreitete value += (target - value) * 0.2 wäre bei 120 Bildern pro Sekunde doppelt so schnell wie bei 60.
Wer geduckt ist, kann nicht rennen und bewegt sich im Ducktempo:
private groundSpeed(): number {
if (this.crouching) return PLAYER.crouchSpeed;
return this.running ? PLAYER.runSpeed : PLAYER.walkSpeed;
}Zum Schluss zeigt das HUD die Haltung. In src/hud.ts:
const stance = player.crouching ? 'crouching' : player.running ? 'running' : 'standing';Ausprobieren
Halte C: Der Blick sinkt weich ab, das HUD zeigt crouching und 2,0 m/s. Geh in den Tunnel hinter den Rampen, lass in der Mitte C los, und du bleibst unten. Geh am anderen Ende hinaus, und du stehst von selbst auf. Aufrecht hält dich der Tunnel am Eingang auf.
Im nächsten Teil klettert die Spielfigur zum ersten Mal: eine Leiter hoch, mit einem Zustandsautomaten, der für mehr bereit ist.