A First-Person Controller in Babylon.js · Part 9 of 11 in the series
Part 9: Hanging From Ledges
Grab ledges that are too high to climb onto at once, hang just under the edge, move along it as far as it goes, and pull yourself up or let go.

Quest objectives
- Grab ledges above the reach of a climb, on the way down from a jump
- Hang just under the edge, and keep walls in the way
- Move along the ledge as far as it goes on at the same height
- Pull up onto the top, or let go
The climb from part 8 reaches ledges up to 1.3 m above the feet. Higher ones the hands can still reach: at the top of a jump, a ledge 2 m above the feet is at about the height of an outstretched arm. Games let the player grab it, hang and then pull up, or move along it hand over hand.
A wall with a ledge
Further back on the left, this part adds a wall 3.2 m high with a ledge along its top. In the middle the ledge has a gap, where a lower block of 2.4 m stands between two parts of the wall. Add it to buildLevel(), and ledge: '#c9c2f2' to the colors:
// 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' });The hands reach up to 2.1 m above the feet, and the player moves along a ledge at 1.5 m/s. Add shimmySpeed after ladderSpeed in PLAYER in src/settings.ts, and grabMax after the range for climbing:
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,Grabbing
Add 'hang' to the modes:
export type Mode = 'walk' | 'air' | 'ladder' | 'mantle' | 'hang';While hanging, the player keeps the ledge and the point the capsule hangs at. Add the fields next to the others:
private ledge: Ledge | null = null;
private hangPoint = new Vector3();tryGrab() looks for a high ledge when there is no low one, with the same findLedge() and a higher range. Replace it:
/** 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;
}A high ledge is only grabbed on the way down, or near the top of a jump, when the player rises slower than 1 m/s. On the way up, the feet get closer to the top every frame, and once they are within 1.3 m, a climb is better than a hang.
Hanging
Add case 'hang': this.hang(dt); break; to the switch in update(), and the hanging itself to the ledges section:
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);
}The capsule hangs just off the wall, 5 cm away, with its top 5 cm under the edge. That puts the eyes 20 cm under the top, and the player looks at the edge right above them.
The player does not jump to that point. Each frame, the velocity points at it, 15 times the distance away: a hand's width away it is fast, close to it slow, and the player settles on it in a fraction of a second. The controller still moves the capsule, so walls still block it, which a setPosition() would not respect.
Along the ledge
A and D move the hang point along the wall. Before every step, findLedge() looks at the wall a capsule's width further on. If the ledge goes on there at the same height, give or take 15 cm, the hang point moves and takes on the ledge found there, so the hands follow a wall that bends a little. If not, the player stops: at the end of the wall, and at the gap, where the top is 80 cm lower.
Up or down
Space pulls up. The player looks for the ledge again from where they hang now and starts the climb from part 8, with all its checks: if there is no room on top, nothing happens.
C or S lets go, with a small push away from the wall, and the cooldown from part 7 keeps the player from grabbing the same ledge again on the way down.
Try it
Walk to the high wall behind the start on the left, and jump at it: the HUD shows hang, and the feet are 1.35 m above the floor, 1.85 m under the top. Move along the ledge with A and D, and you stop where the ledge ends at the gap. Pull up with Space, or let go with C. Jump at the gap itself, and you climb straight onto the lower block.
In the next part the player climbs walls that have holds all over: up, down and sideways.