Hazel

Parkour & Traversal

Vaulting a rail, mantling onto a roof, hanging off a ledge and shimmying along it, climbing a ladder, running along a wall and kicking off it, swinging from a rope — provided by the renzora_parkour plugin.

Almost none of it needs authoring. Ledges, walls and their heights are found by casting rays at whatever collision geometry is already in the scene, so ordinary level meshes are vaultable and climbable the moment a character with a Parkour Controller runs at them. Only two things need a marker, because no cast can infer them: a ladder (a ladder and a fence are the same shape) and a swing anchor (a point in space with nothing to touch).

The controller owns movement

This is the part to read before wiring anything up.

The engine has no built-in character controller: a kinematic body moves because a script calls move_controller(), which is collide-and-slide and nothing else. Parkour Controller replaces that entirely — it runs gravity, ground contact, jumping, collision response and the traversals.

It has to. Hanging off a lip, or being pinned to a sphere around a rope anchor, or being halfway through an arc over a fence, are positions, not forces; no velocity fed to collide-and-slide lands on them exactly, and "nearly right" in a traversal system reads as the character clipping through the ledge they were supposed to catch.

So a parkour character is driven with parkour_move() instead of move_controller(). Calling both on the same entity means two systems writing the same Transform each frame, and whichever ran last wins.

Setting up a character

Add Parkour Controller to the entity that should move — usually the same entity that has the animator.

  • No physics component is required. The controller sweeps a capsule built from its own radius / height and writes Transform directly, so it needs neither a body nor a collider to move, fall, or traverse.
  • Add a Collision Shape if other things need to hit the character — bullets, triggers, enemies. A collider on its own is the safest pairing: the physics engine reads its transform to keep it in place and never writes back, so there is nothing to fight over.
  • If you add a Physics Body it must be a Kinematic Body. The default type is Rigid Body, which is dynamic: the solver applies gravity, integrates a position and writes that back over the controller every frame. The two then fight, and the character sinks, drifts or jitters. Kinematic is written to but never moved on its own, so it round-trips harmlessly — and it lets the character push dynamic objects around, which a bare collider cannot.
  • Set foot_offset if the model's origin isn't at its feet. 0 means origin at the soles, which is how most imported characters sit; a model with its origin at the hips wants roughly half its height.

The character's own colliders are excluded from every probe and sweep, including any on its children — an imported model normally keeps its collider on a child mesh rather than on the entity the controller sits on, and a capsule that collides with its own body cannot move at all.

Then drive it from a script:

-- player.lua
function on_update()
    local x, y = input_axis_2d("move")

    -- World-relative: -Z is forward. `input_axis_2d` returns +Y for "up" on
    -- the stick, so it is negated to point down -Z.
    parkour_move(x, 0, -y)

    parkour_sprint(input_button_pressed("sprint"))

    if input_button_just_pressed("jump") then parkour_jump() end
    if input_button_just_pressed("interact") then parkour_action() end
    if input_button_just_pressed("crouch") then parkour_release() end
end

For a third-person game you want this rotated into camera space before it is passed in — the controller takes a world direction and does not know where the camera is looking. Rotating by the camera's yaw t gives:

local sin, cos = math.sin(t), math.cos(t)
parkour_move(-sin * -y + cos * x, 0, -cos * -y - sin * x)

parkour_move() is consumed each frame, not latched — the same contract as move_controller(). Call it every frame you want the character to move; stop calling it and they stop.

Only in Play or Simulate. Every parkour system is gated on the simulation running, so a character sits exactly where you placed them while editing. Use Play, or Simulate to watch them move with the editor still live.

The states

The controller is in exactly one state at a time, readable as get("ParkourReadState.state"):

StateWhat it isHow you leave it
groundedStanding or running on walkable groundJump, walk off an edge, or start a traversal
airborneFalling or rising under gravityLand, grab a ledge, start a wall run, grab a rope
vaultingPlaying a vault over a railFinishes on its own, into airborne
mantlingClimbing up onto a ledgeFinishes on its own, into airborne above the lip
hangingHanging from a lip by the handsClimb up, drop, or shimmy sideways along it
climbingOn a ladderTop out, step off at the bottom, jump off, or let go
wall_runningRunning along a wall, on the clockTime out, run out of wall, or wall-jump
swingingOn a rope, as a pendulumLet go, jump off, or swing into something

Vault vs mantle

Both start from the same probe; what separates them is what the ground does on the far side of the lip.

  • The ground drops away within vault_max_depth → it is a rail, and the character vaults over it and lands beyond, keeping their momentum.
  • The ground is level with the top → it is a platform, and the character mantles onto it and stands there.

Height decides whether either is possible at all: up to vault_max_height (1.2 m) can be vaulted, up to mantle_max_height (2.3 m) can be mantled, and anything below step_height (0.4 m) is just walked over without a traversal — otherwise the character would vault every kerb.

Traversals are authored motion. Once one starts, the character follows a fixed curve with gravity and collision switched off for its duration, because the probe that started it already proved the whole path was clear — re-testing it every frame can only make the character collide with the very obstacle they are deliberately passing over.

With auto_traverse on (the default) vaults and mantles trigger on contact. Turn it off and they need parkour_action(), which is what a game with a "press E to climb" prompt wants — ParkourReadState.can_vault / can_mantle tell you when to show it.

Ledge hanging

While airborne, a ledge between 55% and 115% of the character's height catches them (ledge_grab, on by default). From a hang:

  • Left/right input shimmies along the lip — and only onto lip that is really there, so the character can't slide off the end of a balcony still in the hang pose.
  • parkour_jump(), parkour_action(), or up input climbs up (a mantle).
  • parkour_release() or down input drops off.

Ladders

Add Parkour Ladder to the ladder object — or to any ancestor of its collider, since the lookup walks up the hierarchy, so a collider buried inside an imported model still counts.

FieldDefaultEffect
climb_speed_scale1.0Multiplies the controller's climb_speed, so a rope ladder can be slower than a steel one without retuning the character
auto_attachtrueLatch on by walking into it. Off requires parkour_action()
exit_at_toptrueMantle onto the top when the climb runs out of ladder. Off leaves the character on the top rung until they jump or let go — right for a ladder up a wall, wrong for one into a hatch

While climbing, y in parkour_move() is the climb axis and the horizontal part is ignored: a ladder is a rail, and letting the stick push the character sideways off it mid-climb is the most common way ladder controllers feel broken.

Wall running and wall jumps

Both come from wall_run. A wall run needs a near-vertical surface within about a third of a metre of the character, a horizontal speed above 80% of walk_speed, and movement input; it lasts wall_run_duration seconds under the much weaker wall_run_gravity, which is what makes it read as running rather than sliding. Starting one sheds most of the jump’s climb, so the run holds its height instead of carrying the character metres up the wall.

One run per wall, per ground touch. Once a wall has been run, that same wall will not catch the character again until they land — otherwise the run simply restarts the moment it expires, and wall_run_duration limits nothing. A wall facing a different way is a fresh opportunity straight away, so a corridor can still be zig-zagged up.

A wall jump works off any wall in reach — beside or in front — so running into a wall and pressing jump kicks off it. It takes priority over every other use of the button while airborne, because that is the one the player pressed at a wall.

Swinging

Add Parkour Swing Anchor to an empty entity at the pivot point — a rope's fixing, a bar, a hook, a vine. It needs no collider; anchors are found by proximity and a line-of-sight check.

FieldDefaultEffect
rope_length0.0Rope length in metres. 0 uses however far away the character was when they grabbed it — right for a vine, wrong for a trapeze bar
max_grab_distance6.0How far away it can be grabbed from
damping0.35Energy lost per second, as a fraction of speed. 0 swings forever

Grab with parkour_action() while airborne. Movement input pumps the swing along its travel; it is deliberately weak, because a swing the player can drive like a walk stops reading as a rope. parkour_release() lets go; parkour_jump() lets go with swing_release_boost extra height on top — often the difference between clearing the gap and not.

Swinging through a wall is prevented by sweeping each step: hitting something lets go rather than clipping.

Blocking geometry

Ray-probed traversal is occasionally right about geometry a designer wanted left alone — the lip of a bottomless pit, a decorative railing that should read as impassable, a collision proxy standing in for something soft. Add Parkour Blocker to it (or to an ancestor) and every probe ignores it.

Tuning

Parkour Controller defaults describe an adult human: 1.8 m tall, runs at 7 m/s, jumps 1.15 m, vaults a 1.2 m rail, mantles a 2.3 m wall.

FieldDefaultEffect
radius / height0.35 / 1.8The swept capsule. Ledge-grab reach is derived from height, so a shorter character grabs lower ledges
foot_offset0.0Distance from the entity origin down to the soles
walk_speed / run_speed4.0 / 7.0Ground speed, with and without parkour_sprint(true)
acceleration40.0How fast the target speed is reached, m/s²
air_control0.35Fraction of acceleration that still applies mid-air
gravity-22.0Downward acceleration. Jump height is independent of it — the launch speed is derived — so this can be tuned for feel without changing how high the character jumps
terminal_velocity-55.0Fastest the character may fall
jump_height1.15Apex of a standing jump, in metres
max_slope55.0Steepest surface that counts as ground, in degrees
step_height0.4Walked over without a traversal; also how far the controller snaps up or down to follow stairs and slopes without a traversal
coyote_time0.12Grace period after walking off an edge during which jump still works
jump_buffer_time0.15How long a jump or action press is remembered
face_movement / turn_speedtrue / 12.0Turn the character toward where they are going
facing_offset0.0Degrees to turn the model by, on top of the direction the character is facing. Purely cosmetic. Bevy treats -Z as forward, but glTF characters — anything out of Mixamo especially — are usually authored facing +Z, and imported as-is they travel correctly while appearing to run backwards. Set 180 and they face the way they are going
auto_traversetrueVault and mantle on contact, without parkour_action()
forward_reach0.55How far ahead obstacles are looked for
vault_max_height / vault_max_depth / vault_duration1.2 / 1.1 / 0.45Vault limits and how long the move plays
mantle_max_height / mantle_duration2.3 / 0.7Mantle limits and how long the move plays
ledge_grab / hang_shimmy_speedtrue / 1.3Catch ledges when airborne; sideways speed while hanging
climb_speed2.2Ladder speed, scaled per-ladder
wall_runtrueEnables wall running and wall jumping
wall_run_speed / wall_run_duration / wall_run_gravity6.5 / 1.5 / -3.0How fast, how long, and how much it sags
wall_jump_up / wall_jump_away6.0 / 5.0Wall-jump speed, up and outward
swing / swing_release_boosttrue / 2.0Enables swinging; extra upward speed on letting go

Seeing what it is doing

Turn on Gizmos → Physics in the viewport and a parkour character draws its own diagnostics. It shares that dropdown with collider wireframes, including the Selected only and Always settings.

The important thing it shows is the swept capsule — built from radius, height and foot_offset, and not the collision shape you authored. That capsule is what decides where the character can go, and nothing else in the viewport shows it, so a capsule that does not match its model looks exactly like one that does.

It is drawn upright and unscaled, because that is how the controller casts it. If your character is leaning but the capsule is not, something else is rotating the entity.

DrawnMeans
Capsule, greenGrounded
Capsule, amberAirborne — if it never turns green, foot_offset is probably wrong
Capsule, violetPlaying a vault or mantle (gravity and collision off)
Capsule, blueHolding a ledge, ladder or rope
Capsule, pinkWall running
Cross at the soles, and a line up to the originWhere the controller thinks the feet are. The line appears only when foot_offset is non-zero
Disc + normal at the feetGround contact and its surface normal
Grey line ahead at knee heightThe direction and reach everything is probed along
Cross at a lip, with a drop line to foot heightA ledge, and its height above the feet — the number the whole decision turns on
Lip green / amber / blue / greyIt would vault it / mantle it / could grab it in mid-air / found it but will not act
Second green cross beyond a lipWhere a vault would land
Pink line from the chestA wall in reach, at the distance sensed
Violet arcThe path a traversal is following, and where it ends

Everything after the capsule comes from what the controller recorded on its last frame, not from fresh casts — so it shows what the state machine actually decided from, and it only appears while the simulation is running.

Collider colours

Collider wireframes are coloured by body type: green static, violet kinematic, orange dynamic, blue sensor. Violet versus orange is worth knowing here — a character left on the default Rigid Body (dynamic) looks fine until the solver starts fighting the controller for its transform, and the colour is the fastest way to spot it.

Animation

Add Parkour Animations and the controller crossfades one clip per state for you. The defaults are the names an imported character most often already has (idle, walk, run, jump, fall, vault, mantle, hang, shimmy, climb, wall_run, swing), plus a blend duration.

An empty name means "don't drive this state", so a project can let the controller handle locomotion and keep the traversals for itself. Leave the component off entirely and the controller drives no animation at all — read ParkourReadState.state and run your own state machine instead.

jump plays while rising and fall while descending, so a character who walked off a ledge falls rather than playing a jump they never made.

Scripting

FunctionEffect
parkour_move(x, y, z)Movement intent in world space. x/z steer, y climbs on ladders and hangs. Call every frame — it is consumed, not latched
parkour_sprint(on)Move at run_speed instead of walk_speed
parkour_jump()Jump, wall-jump, climb up from a hang, or let go of a swing with a boost
parkour_action()Context traversal: vault, mantle, grab a ledge, mount a ladder, grab a rope
parkour_release()Let go of whatever is being held — ledge, ladder or swing

Reads go through reflection on ParkourReadState, like every other component:

FieldTypeMeaning
statestringOne of the state names in the table above
eventstringThe event that fired this frame, else empty (see below)
groundedboolStanding on walkable ground
velocity / speedVec3 / floatCurrent velocity and its magnitude
traversingboolA vault or mantle is playing — stop feeding input and hold the camera
hanging, climbing, wall_running, swingingboolThe individual states, for convenience
can_vault, can_mantle, can_grabboolThere is an obstacle in front that could be traversed right now — what to hang a button prompt on
near_ladderboolFacing a Parkour Ladder
ledge_heightfloatHeight of the ledge ahead above the feet, 0 if none

event is set for exactly one frame, so polling it once per on_update catches each event once. The values are jump, land, vault_start, vault_end, mantle_start, mantle_end, grab, release, ladder_mount, ladder_dismount, wall_run_start, wall_run_end, wall_jump, swing_grab, swing_release.

function on_update()
    -- Don't fight the controller while it is playing a move.
    if get("ParkourReadState.traversing") then return end

    local ev = get("ParkourReadState.event")
    if ev == "land" then play_sound("footstep_land")
    elseif ev == "grab" then play_sound("ledge_grab") end

    if get("ParkourReadState.can_vault") then
        ui_show("VaultPrompt")
    else
        ui_hide("VaultPrompt")
    end
end

See also

  • Physics — bodies, colliders, and the move_controller this replaces
  • Ragdoll Physics — handing the same skeleton over to the solver
  • Animation — clips, state machines and event markers
  • Scripting API — the full function list