A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Climb System provides a complete solution for vertical traversal, ledge hanging, wall running, and parkour mechanics. It allows players to detect climbable surfaces via raycasting, transition seamlessly between movement states, and interact with the environment in a fluid, “Assassin’s Creed”-like manner.
ClimbStateClimbLedgeSystemClimbStateTrackerLedgeZoneThe Climb System separates “movement” from “climbing”. When a climbable edge is detected, the standard character controller logic (gravity, friction, walking speed) is suspended, and the ClimbState takes over.
LedgeZones are supported).The heart of the system is the ClimbState enum. This finite state machine determines allowed inputs and movement vectors.
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
pub enum ClimbState {
None, // Standard character movement
Approaching, // Moving towards a detected ledge (auto-adjust)
Hanging, // Idle on the wall
ClimbingUp, // Ascending
ClimbingDown, // Descending
ClimbingLeft, // Shimmying left
ClimbingRight, // Shimmying right
Vaulting, // Quick hop over low obstacles
Falling, // Special fall state (detached from wall)
}
ClimbState::None.Approaching.Hanging. Gravity is disabled.ClimbingUp/Down/Left/Right.ForceMode::Impulse).Falling or None.ClimbLedgeSystemThis is the “Brain” of the climb system. It contains typically static configuration settings for how climbing feels. It is attached to the Player entity.
| Field | Type | Description |
| :— | :— | :— |
| climb_ledge_active | bool | Master switch for the system. |
| climb_ledge_ray_forward_distance | f32 | How far forward to check for walls (default: 1.0). |
| climb_ledge_ray_down_distance | f32 | How far down to check for the lip of the ledge (default: 1.0). |
| layer_mask_to_check | u32 | Physics layer mask for climbable geometry. |
| Field | Type | Description |
| :— | :— | :— |
| adjust_to_hold_on_ledge_position_speed | f32 | Lerp speed when snapping to the wall (default: 3.0). |
| adjust_to_hold_on_ledge_rotation_speed | f32 | Slerp speed for aligning rotation to wall normal (default: 10.0). |
| hand_offset | f32 | Vertical offset to align visual hands with the ledge edge (default: 0.2). |
| hold_on_ledge_offset | Vec3 | Fine-tuning offset for the hang position. |
| climb_ledge_speed_first_person | f32 | climbing speed when in FP mode. |
| Field | Type | Description |
| :— | :— | :— |
| only_grab_ledge_if_moving_forward | bool | Prevents accidental grabs when backing up. |
| can_jump_when_hold_ledge | bool | Allows wall jumping. |
| jump_force_when_hold_ledge | f32 | Ejection force for wall jumps. |
| auto_climb_in_third_person | bool | If true, automatically mounts ledges without jump button. |
These fields are useful for debugging in the inspector but shouldn’t be set manually:
ledge_zone_found: Is the raycast hitting a valid LedgeZone?climbing_ledge: Are we currently attached?surface_to_hang_on_ground_found: Did the low-raycast find a drop-down ledge?ClimbStateTrackerTracks dynamic runtime data.
#[derive(Component, Debug, Reflect)]
pub struct ClimbStateTracker {
pub current_state: ClimbState,
pub previous_state: ClimbState,
pub state_timer: f32, // Time in current state (useful for anims)
pub stamina: f32, // Current energy (0-100)
pub max_stamina: f32,
pub stamina_drain_rate: f32,
pub is_stamina_depleted: bool,
}
LedgeZoneA marker component for Trigger volumes or specific collider entities that overrides climb behavior.
LedgeZone override the global raycast settings while the player is inside the zone.#[derive(Component, Debug, Reflect)]
pub struct LedgeZone {
pub ledge_zone_active: bool,
pub ledge_zone_can_be_climbed: bool, // Set false to make "unclimbable" paint
pub custom_climb_speed: Option<f32>, // (Future Feature)
}
LedgeDetectionA transient component updated every frame by the raycasting system.
#[derive(Component, Debug, Reflect)]
pub struct LedgeDetection {
pub ledge_found: bool,
pub ledge_position: Vec3, // World space point of the edge
pub ledge_normal: Vec3, // Normal of the wall face
pub surface_type: SurfaceType, // Material (Wood, Stone, Metal)
}
To enable climbing on a character:
ClimbLedgeSystem::default().ClimbStateTracker::default().LedgeDetection::default() (required for the system to write data).CharacterController is present (the systems read character.is_dead etc).If your character isn’t grabbing ledges:
climb_ledge_ray_forward_distance. The character’s collider radius might keep them too far from the wall.climb_ledge_ray_down_distance must be long enough to reach from “Forward Ray Hit” down to the actual ledge lip.hand_offset to fix “floating hands” or “hands inside wall” visual issues.The system supports both modes via flags in ClimbLedgeSystem:
auto_climb_in_first_person: specialized logic for FPS feels (often faster, less animation-heavy).has_to_look_at_ledge_position_on_first_person: Requires the player to aim at the ledge to grab it, adding skill/realism.The detection system runs in the FixedUpdate loop to ensure physics consistency. It primarily uses the detect_ledge system.
To robustly detect a climbable edge (a “cliff” or “wall top”) without relying on manual triggers, the system uses two raycasts:
PlayerTransform.forward.climb_ledge_ray_forward_distance.Vec3::DOWN.climb_ledge_ray_down_distance.Success Condition:
If successful, LedgeDetection.ledge_position is set to the hit point of Ray 2, which represents the exact edge coordinates for the hands to grab.
detect_ledge_below)This secondary system (detect_ledge_below) allows players to walk off a roof and automatically turn around to grab the ledge, or “drop down” onto a ledge below them.
DOWN and BACK.AutoHang component, which smoothly interpolates the player’s position to the ledge, rotating them 180 degrees to face the wall.When in ClimbState::ClimbingUp/Left/Right, standard physics forces are modified.
LedgeNormal and Vec3::UP.Ledge. If the wall ends, shimmying stops to prevent moving into thin air.If the player climbs up and there is no wall above (it’s a floor), the state transitions to Vaulting.
None (Grounded) via the character controller’s ground detection.When jumping from a ledge:
Impulse (instant velocity change).UP + BACK (away from wall).UP (reach higher).base_force * stamina_multiplier * surface_multiplier.Not all walls are equal. The SurfaceType enum allows different gameplay properties based on the material detected by the raycast (e.g., via Collider material or tag).
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Default)]
pub enum SurfaceType {
#[default] Default,
Stone, // Default speed
Wood, // 0.9x speed (rough)
Metal, // 0.8x speed (slippery)
Ice, // 1.2x speed (fast slide) / High stamina drain
Rope, // 0.7x speed
Custom(f32),
}
calculate_climb_speed applies the multiplier to movement.calculate_stamina_cost scales effort (e.g., Ice drains stamina faster).The system includes helper components for syncing movement with animations, particularly useful for Target Matching (aligning the hand bone exactly with the ledge).
ClimbAnimation ComponentUsed to provide data to your Animation Graph (e.g., bevy_animation_graph or standard Bevy animator).
match_start_value / match_end_value: Normalized time ranges (0.0 to 1.0) within an animation clip where physical position correction should occur.match_mask_value: Vector mask defining which axes to match (e.g., Vec3::Y for vertical alignment).ClimbState switches to Vaulting, trigger the “Vault” animation.LedgeDetection.ledge_position as the Target Match point.match_end_value, the hand/foot is exactly at the ledge_position.This prevents “clipping” where the character’s hands go inside the mesh or hover above it.
Climbing is physically demanding. The ClimbStateTracker manages a stamina pool to prevent infinite climbing (unless configured otherwise).
pub struct ClimbStateTracker {
pub stamina: f32, // Current: 0.0 - 100.0
pub max_stamina: f32, // Cap: 100.0
pub stamina_drain_rate: f32,// Per second cost
pub stamina_regen_rate: f32,// Per second recovery (Grounded only)
}
ClimbState is not None.ClimbState == None (Grounded).stamina <= 0.0: is_stamina_depleted becomes true.update_climb_state system forces a transition to ClimbState::Falling, causing the player to let go of the wall.climb_ledge_ray_forward_distance. It must be > Collider Radius + Skin Width.ClimbLedgeSystem.hold_on_ledge_offset is zero or incorrect.hold_on_ledge_offset to push the model back from the wall.ClimbLedgeSystem.hand_offset incorrect.hand_offset value. This value represents the distance from the “Ray Hit (Ledge Lip)” to the “Hand Bone”.layer_mask_to_check mismatch.RigidBody::Kinematic or disables Gravity during ClimbState::Hanging. The update_climb_movement system should take full control of the Transform/Velocity.