How to implement Climbing in a 3d environement ?


I'm trying to recreate a climbing mechanic similar to the game "The Legends of Zelda : Breath of the Wild" but encounter some difficulty :

  • Sometime the character teleport to the origin of the map

  • When facing a certain direction and initializing a transition (ex : turning around a corner) the character do a 360 turn

  • On some occasion the character won't turn around a corner (i'm not certain but i think it's because the angle is superior to 90°)

  • When transitioning from a wall to another if they aren't straight the rotation doesn't go well

And that's only the problem that i found, here is how i proceed so far when the character is climbing if it encounter a corner i initialize a transition with some parameters and then apply the transition during the physics function has follow :

func physics_process(delta: float) -> void:
    if transition:

    var input_direction: = get_input_direction()

    var up: Vector3 = player.global_transform.basis.y * input_direction.y
    var right: Vector3 = player.global_transform.basis.x * input_direction.x
    var move_direction: = up + right
    if move_direction.length() > 1.0:
        move_direction = move_direction.normalized()

    if _can_move(move_direction):
        velocity = move_direction * climb_speed
        velocity = player.move_and_slide(velocity, Vector3.UP,true)

    if player.is_on_floor() or _is_floor(player.knee_cast.get_collision_normal()):
    if player.is_on_floor.is_colliding():

    if player.climb_cast.is_colliding():
        player.transform = _look_at(player.translation, player.climb_cast.get_collision_normal())
        #distance from wall
        var collision: Vector3 = player.climb_cast.get_collision_point()
        var length_from_wall = (player.translation - collision).length()
        if length_from_wall > offset_from_wall:
            var vz = -player.global_transform.basis.z
            # warning-ignore:return_value_discarded
            player.translation -= vz.normalized() * offset_from_wall

    if input_direction.y > 0 and !player.head_cast.is_colliding():
        var speed: float = 4.0
        if _is_floor(player.ledge_cast.get_collision_normal()):
            speed = 2.0
            start_climb = true
            _initialise_transition(speed, player.ledge_cast, player.translation)
            _initialise_transition(speed, player.ledge_cast, player.ledge_cast.get_collision_point())

    if input_direction.x < 0:
        if player.left_corner_cast.is_colliding():
            _initialise_transition(4.0, player.left_corner_cast, player.translation)
        if !player.climb_cast.is_colliding():
            _initialise_transition(4.0, player.left_corner_cast_2, player.left_corner_cast_2.get_collision_point(),true)
            abs_normal = false

    if input_direction.x > 0:
        if player.right_corner_cast.is_colliding():
            _initialise_transition(4.0, player.right_corner_cast, player.translation)
        if !player.climb_cast.is_colliding():
            _initialise_transition(4.0, player.right_corner_cast_2, player.right_corner_cast_2.get_collision_point())
            abs_normal = false

func _initialise_transition(speed : float, cast, rot_center : Vector3, invert_angle : bool = false) -> void:
    transition_percentage = 0.0
    var col = cast.get_collision_point()
    var normal: Vector3 = cast.get_collision_normal()
    transition_speed = speed
    start_position = player.translation
    target_position = col
    target_position += normal * offset_from_wall
    start_rotation = player.rotation
    helper.transform = _look_at(target_position, normal)
    target_rotation = helper.rotation
    center_of_rotation = rot_center
    if start_climb:
        start_climb = false
        target_position -= helper.transform.basis.y * 0.5
    if center_of_rotation != player.translation:
        if !start_climb:
            center_of_rotation -= helper.transform.basis.y * 0.5
        var vect : Vector3 = player.translation - rot_center
        rotation_angle = vect.angle_to(normal)
        if rad2deg(rotation_angle) > 90 or rad2deg(rotation_angle) < -90:
            supl_rot_angle = 2 * rotation_angle
            supl_rot_angle = 0.0
        if invert_angle:
            rotation_angle = -rotation_angle
        previous_rotation_angle = 0.0
        previous_supl_rot_angle = 0.0
        rotation_normal = helper.transform.basis.y
        helper.translation = target_position
        helper.look_at(helper.translation - player.translation, rotation_normal)
    transition = true

func _make_transition(delta : float):
    transition_percentage += transition_speed * delta
    if transition_percentage > 1:
        transition_percentage = 1
        transition = false
    if center_of_rotation == player.translation:
        var tp: Vector3 = lerp(start_position, target_position, transition_percentage)
        player.translation = tp
        var rp: Vector3 = lerp(start_rotation, target_rotation, transition_percentage)
        player.rotation = rp
        center_of_rotation = player.translation
        var angle: float = lerp(0.0, rotation_angle, transition_percentage)
        var current_angle = angle
        angle -= previous_rotation_angle
        previous_rotation_angle = current_angle
        var supl_angle: float
        var current_supl_rot: float
        if supl_rot_angle != 0.0:
            supl_angle = lerp(0.0, supl_rot_angle, transition_percentage)
            current_supl_rot = supl_angle
            supl_angle -= previous_supl_rot_angle
            previous_supl_rot_angle = current_supl_rot
            helper.rotate(rotation_normal, supl_angle)
            helper.rotate(rotation_normal, angle)
        player.translation = center_of_rotation + helper.transform.basis.z - helper.transform.basis.z * offset_from_wall

Note that the detection of a corner is made with raycast

I'm probably forgetting something but i have join a minimal project here

If you need some precision don't hesitate to ask

PS : You can find the climbing logic under the folder "src/player/states/climbing" or in the player scene "StateMachine/Climbing"

PS : I place some position3d on the main map to place the player in the same starting position as the gif


Leave a Comment

BoldItalicStrikethroughOrdered listUnordered list
Align leftAlign centerAlign rightToggle HTML viewToggle full pageToggle lights
Drop image/file