Ledge Up

We need a way for us to get on top of the ledge once we get close enough to a flat (walkable) surface while climbing.

BOTW ledge up example

Root Motion Montage

One way to accomplish this is by playing the animation, moving the character, and blocking user input till it finishes. However this create a dependency on timing the animation correctly, and if either side changes it requires the other to change as well. Instead we'll use an animation with Root Motion.

Root Motion animations move the physical character with the animation (as opposed to in place animations)

red line showing the root bone of the animation physically moving

Similar to those games, we'll automatically ledge up whenever we're close to a ledge. .h

// ZCCharacterMovementComponent.h

private:
// ...
    bool TryClimbUpLedge();

Let's add our call to check for TryClimbUpLedge() after we move (since we might have come into range of a ledge this frame)

.cpp

// ZCCharacterMovementComponent.cpp


void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
	// ...

	MoveAlongClimbingSurface(DeltaTime);
	TryClimbUpLedge();

	if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
		Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / DeltaTime;

	SnapToClimbingSurface(DeltaTime);
}

Before filling out TryClimbUpLedge() we need a way to access the Root Motion animation to play.

We're going to use a Montage for this which thankfully supports replicating root motion in network games (not that we are one but just good to know).

Create a new animation montage and add the proper Root Motion animation to the default slot track

(green) animation, (yellow) optional values you might need to tweak to get the animation looking right speed wise

You might find that you have to play around with some of the settings like End Time, Play Scale, and Blend In/Out to get it looking right

Calling Montage from C++

As long as we have a reference to the montage via an AnimationInstance we can run it from c++.

There are 3 prerequisites needed before we can actually play the ledge climb.

  • We're moving upwards

  • We're near a ledge (i.e. there is a gap in the wall with a stand-able surface above)

  • We can actually fit on the ledge (it isn't too small, or obstacles in the way)

.h

// ZCCharacterMovementComponent.h
private:
// ...
    	bool HasReachedLedge() const;
	bool IsLedgeWalkable(const FVector& LocationToCheck) const;
	bool CanMoveToLedgeClimbLocation() const;
	
	UPROPERTY(Category = "Character Movement: Climbing", EditDefaultsOnly)
	UAnimMontage* LedgeClimbMontage;
	
	UPROPERTY()
	UAnimInstance* AnimInstance;

First lets store the animation montage in BeginPlay()

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::BeginPlay()
{
	Super::BeginPlay();

	AnimInstance = GetCharacterOwner()->GetMesh()->GetAnimInstance();
	// ...
}

Upwards Movment check

First we check if we're moving up by comparing the angle of movement (Velocity) and what we know to be the Component's up

Need to use the Component's Up versus global Up because the wall might be at an angle like an over or underhang

Then we reset the rotation and play the montage (which will move us the rest of the way).

// ZCCharacterMovementComponent.cpp

bool UZCCharacterMovementComponent::TryClimbUpLedge()
{
	if (!AnimInstance || !LedgeClimbMontage)
	{
		return false;
	}
	if (AnimInstance->Montage_IsPlaying(LedgeClimbMontage))
	{
		return false;
	}
	
	const float UpSpeed = FVector::DotProduct(Velocity.GetSafeNormal(), UpdatedComponent->GetUpVector());
	const bool bIsMovingUp = UpSpeed > 0;

	if (bIsMovingUp /*&& HasReachedLedge() && CanMoveToLedgeClimbLocation()*/)
	{
		const FRotator StandRotation = FRotator(0, UpdatedComponent->GetComponentRotation().Yaw, 0);
		UpdatedComponent->SetRelativeRotation(StandRotation);
		AnimInstance->Montage_Play(LedgeClimbMontage);
		bIsInLedgeClimb = true;

		return true;
	}

	return false;
}

Reached Ledge check

The purpose of HasReachedLedge() is to make sure we're within range of a ledge, of which there are two parts

  • Check at some height above the character to see if we don't collide with a wall

  • The length of the check should be at least equal to the shape sweep

The reason for the length check is to protect against false positives like this, where there technically is a ledge but not a traversable one

We can use the existing EyeHeightTrace() for the height to fire the ray from, but need to remember that when climbing we shrunk the capsul collider, so now instead of using just the Component's BaseEyeHeight, we need to add back on the height proportional to the capsul collider we shrunk

.cpp

// ZCCharacterMovementComponent.cpp

bool UZCCharacterMovementComponent::EyeHeightTrace(const float TraceDistance) const
{
	FHitResult UpperEdgeHit;
	
	// UpdatedComponent is at the location of the MovementComponent (middle of the character)
	const FVector ComponentOrigin = UpdatedComponent->GetComponentLocation();
	// ...
	
	// old
	//const FVector EyeHeight = ComponentOrigin + (UpdatedComponent->GetUpVector() * GetCharacterOwner()->BaseEyeHeight);
	
	// new, keeping the eye height equal to the un-shrunk capsul
	const ACharacter* Owner = GetCharacterOwner();
	// Feel free to add/remove height from BaseEyeHeight to fit your root motion animation best. 
	const float BaseEyeHeight = Owner ? Owner->BaseEyeHeight : 1;
	const float EyeHeightOffset = IsClimbing() ? BaseEyeHeight + CollisionCapsulClimbingShinkAmount : BaseEyeHeight;
	
	// ...
	// extend it out
	const FVector End = EyeHeight + (UpdatedComponent->GetForwardVector() * TraceDistance);

	return GetWorld()->LineTraceSingleByChannel(UpperEdgeHit, EyeHeight, End, ECC_WorldStatic, ClimbQueryParams);
}

then add the new check to HasReachedLedge()

.cpp

// ZCCharacterMovementComponent.cpp

bool UZCCharacterMovementComponent::HasReachedLedge() const
{
	const float ShapeSweepEdge = CollisionCapsulRadius + CollisionCapsulForwardOffset;
	
	return !EyeHeightTrace(ShapeSweepEdge);
}

bool UZCCharacterMovementComponent::TryClimbUpLedge()
{
	// ...
	if (bIsMovingUp && HasReachedLedge() /*&& CanMoveToLedgeClimbLocation()*/)
	{
		// ...
	}

	// ...
}

Ledge Traversability check

Lastly, we need to account for situations where the ledge above is large enough to stand on, but either the ledge isn't flat enough, or there isn't enough space on the ledge (i.e. obstacles exist blocking)

the raycast doesn't hit anything indicating we have enough ledge space, but obstacles prevent us from ledging up

To do this we'll sweep at the expected ledge up with a shape similar to our character's capsul collider.

If it hits anything we know there is something in the way, otherwise if not then we know there is at least enough space for our character!

(left) enough space for the capsule collider, (right) not enough space for the capsule collider

.cpp

// Some code

bool UZCCharacterMovementComponent::CanMoveToLedgeClimbLocation() const
{
	// Change scalars into values that fit your root motion animation's distance best. 
	// These work for mine so that the animation ends at the ledge edge
	const FVector VerticalOffset = FVector::UpVector * 200.f;
	const FVector HorizontalOffset = UpdatedComponent->GetForwardVector() * 120.f;

	const FVector LocationToCheck = UpdatedComponent->GetComponentLocation() + HorizontalOffset + VerticalOffset;

	if (!IsLedgeWalkable(LocationToCheck))
	{
		return false;
	}

	FHitResult CapsulHit;
	const FVector CapsulStartCheck = LocationToCheck - HorizontalOffset;
	const UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();

	const bool bBlocked = GetWorld()->SweepSingleByChannel(CapsulHit, CapsulStartCheck, LocationToCheck, FQuat::Identity, ECC_WorldStatic, Capsule->GetCollisionShape(), ClimbQueryParams);
	return !bBlocked;
}

bool UZCCharacterMovementComponent::IsLedgeWalkable(const FVector& LocationToCheck) const
{
	// magic number just makes it so we raycast reasonable far enough to hit something
	const FVector CheckEnd = LocationToCheck + (FVector::DownVector * 250.f);

	FHitResult LedgeHit;
	const bool bHitLedgeGround = GetWorld()->LineTraceSingleByChannel(LedgeHit, LocationToCheck, CheckEnd, ECC_WorldStatic, ClimbQueryParams);

	return bHitLedgeGround && LedgeHit.Normal.Z >= GetWalkableFloorZ();
}

Last thing to do is make sure we don't change the rotation while in root motion which happens during our physics check GetSmoothClimbingRotation.

.cpp

// ZCCharacterMovementComponent.cpp

FQuat UZCCharacterMovementComponent::GetSmoothClimbingRotation(float DeltaTime) const
{
	const FQuat Current = UpdatedComponent->GetComponentQuat();
	
	// new, bail early if we're in root motion
	if (HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocity())
	{
		return Current;
	}
	
	const FQuat Target = FRotationMatrix::MakeFromX(-CurrentClimbingNormal).ToQuat();

	return FMath::QInterpTo(Current, Target, DeltaTime, ClimbingRotationSpeed);
}

Now we will ledge up when reaching the surface!

Last updated