Introduction
The Legend of Zelda: Breath of the Wild needs no introduction. It’s a game that redefined the modern open-world formula. It’s all about exploration, discovery, and freedom. As such, movement plays a fundamental role in making the game loop fun. There is no shortage of great movement mechanics, and the climbing system is no exception. See a mountain on the horizon? You can climb it. Need to scout the area? How about climbing the big tree over there.
In this tutorial, we’re going to recreate this mechanic while exploring how the character movement component works. I really hope you enjoy it and have fun!
Getting Started
To get started, create a C++ Third Person template. I’ll be using Unreal Engine 5 during this project, but aside from a few things we’ll talk about in the animation section, everything works the same on recent UE4 versions.
Warning
This is an intermediate-level tutorial. I assume basic C++ and Unreal knowledge from the reader. However, if you don’t understand some concept or need help, please comment down below!
The code for this tutorial is available here!
CMC Setup
To create the climbing physics, we will extend the default CharacterMovementComponent
(CMC) by inheriting it. As the name implies, this class dictates how the character should move, handling the acceleration, velocity, replication, and even collision. This component is tightly coupled with the Character class and cannot exist without it.
Inheriting
To start, let’s create the header file for our custom character movement component:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "CustomCharacterMovementComponent.generated.h"
UCLASS()
class CS_API UCustomCharacterMovementComponent : public UCharacterMovementComponent
{
GENERATED_BODY()
};
We also need to make sure the Character class uses our newly created component instead of the base one. To do that, let’s head to the YourCharacter class. Add a FObjectInitializer
parameter to the constructor signature. In the implementation, call SetDefaultSubobjectClass
of the initializer using the UCustomCharacterMovementComponent
type:
// Replace default constructor:
/* YourCharacter.h */
ACSCharacter(const FObjectInitializer& ObjectInitializer);
/* YourCharacter.cpp */
ACSCharacter::ACSCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UCustomCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
Warning
A bug in Unreal causes the blueprint to become corrupt after changing a component’s type. To avoid the editor from crashing later on, inside the Content Browser, right-click the character blueprint -> Asset Actions -> Reload.
It’s important to note that even though we changed the default component class, the ACharacter
still holds a reference to the UCharacterMovementComponent
base type. Consequently, we would need to cast it every time we use it. It’s a good idea to cache the cast and retrieve it with a function:
/* YourCharacter.h */
public:
UFUNCTION(BlueprintPure)
FORCEINLINE UCustomCharacterMovementComponent* GetCustomCharacterMovement() const { return MovementComponent; }
protected:
UPROPERTY(Category=Character, VisibleAnywhere, BlueprintReadOnly)
UCustomCharacterMovementComponent* MovementComponent;
/* YourCharacter.cpp */
ACSCharacter::ACSCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UCustomCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
// ...
MovementComponent = Cast<UCustomCharacterMovementComponent>(GetCharacterMovement());
}
That’s it. We now have our new component that inherits from the CharacterMovementComponent
. In the next section, we’ll discuss how this component works on a high level.
Overview
To learn more about it, we’ll dive into the source code of the base CharacterMovementComponent
class.
Tip
If you don’t have the Unreal source code in your project, you can download it by checking the option inside the installation options. An alternative is to read it on Github.
At first glance, we see ourselves in the middle of a gigantic implementation file with more than 12k lines! A lot is going on here, even network replication and server-client correction. We won’t waste time talking about design issues and instead focus on understanding the core parts of this class.
The idea behind the CharacterMovementComponent
is to provide a movement system based on Movement Modes. There are 7 predefined modes, including: Walking, Falling, Swimming, and Custom. These are mostly self-explanatory. When moving around with your character, you are in the Walking mode. To implement our climb movement, Unreal provides us with the Custom mode. We’ll come back to it in a bit.
Below is a simple diagram with the core functions and their execution order.
The PerformMovement
function is the heart of the class, handling external physics, like impulses, forces and gravity, as well as computing movement from animation root motion. In a non-networked game, the PerformMovement
is called on every tick (TickComponent
). This is the only function in the diagram we won’t override.
The StartNewPhysics
function selects a Phys function based on the current movement mode. For example, if the character is currently in Walking mode, the StartNewPhysics
calls the PhysWalking function that handles the physics of ground movement, calculating the corresponding velocity and acceleration.
The PhysCustom
is a virtual function that doesn’t do anything on its own, so developers can write their implementation. We’ll code the climbing system in it. Note that it’s possible for a Phys function to call StartNewPhysics
, thus immediately changing to another movement physics. An example would be the character losing grip while climbing and switching to PhysFalling
.
After the character movement computation ends, the OnMovementUpdated
function is called. If the player wants to climb, we can use this function to change the movement mode by calling SetMovementMode
. When the SetMovementMode
is called, it triggers the OnMovementModeChanged
function. Useful to handle the transition between modes.
The Climbing
Detecting Surfaces
Our first step is to detect climbable surfaces. We also need to store the collision information to handle the character movement appropriately. The question is: how to retrieve such information?
There are many answers to this question. The simplest solution that comes to mind is to use a single line-trace that goes forward from the character’s location. However, it has many shortcomings. As an example, take a look at the left image below. Imagine that your character is climbing the leftmost wall and want to go to the rightmost one. This scenario is impossible as the trace is always pointing forward, and the character doesn’t have enough information to rotate towards the next wall.
We can solve this by using multiple line-traces instead. This includes traces for corners, low, mid, and high body checks, or by tracing in the direction the character is moving. By leveraging all the information from these traces, we have all the information we need. However, we’ll do things differently and solve it using Shape Sweeps.
While using multiple line traces is perfectly fine, we can treat all the hits from the shape sweep uniformly, so it’s easier to reason about and cleaner to write. We can cover many edge cases like the ones mentioned above using a larger shape than the Character Capsule Collision. The SweepMultiByChannel
returns an array of hits which we’ll store to determine the correct way to move later on.
Let’s dive into the code. First, let’s declare the SweepAndStoreWallHits
. Also declare CurrentWallHits
, an array of FHitResult
. For the shape of the sweep, I will be using a Capsule to match the player capsule. So, to fine-tune the collision detection, let’s declare two variables: CollisionCapsuleRadius
and CollisionCapsuleHalfHeight
.
/* CustomCharacterMovementComponent.h */
private:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
void SweepAndStoreWallHits();
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere)
int CollisionCapsuleRadius = 50;
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere)
int CollisionCapsuleHalfHeight = 72;
TArray<FHitResult> CurrentWallHits;
FCollisionQueryParams ClimbQueryParams;
We want the SweepAndStoreWallHits
function to run on every Tick, so call it inside TickComponent
. The job of the SweepAndStoreWallHits
function is to call SweepMultiByChannel
with the appropriate parameters and store the hits it retrieves. Even though we are using a Sweep Trace, we don’t need it to sweep across two locations. Relying on the start position is enough, which might tempt us to use the same start and end location. However, doing so cause bugs like Landscapes being ignored. For that reason, I added a small offset on the end location. As for the rotation of the sweep, we don’t need it to follow the character’s rotation, so use the quaternion identity.
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::BeginPlay()
{
Super::BeginPlay();
ClimbQueryParams.AddIgnoredActor(GetOwner());
}
void UCustomCharacterMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
SweepAndStoreWallHits();
}
void UCustomCharacterMovementComponent::SweepAndStoreWallHits()
{
const FCollisionShape CollisionShape = FCollisionShape::MakeCapsule(CollisionCapsuleRadius, CollisionCapsuleHalfHeight);
const FVector StartOffset = UpdatedComponent->GetForwardVector() * 20;
// Avoid using the same Start/End location for a Sweep, as it doesn't trigger hits on Landscapes.
const FVector Start = UpdatedComponent->GetComponentLocation() + StartOffset;
const FVector End = Start + UpdatedComponent->GetForwardVector();
TArray<FHitResult> Hits;
const bool HitWall = GetWorld()->SweepMultiByChannel(Hits, Start, End, FQuat::Identity,
ECC_WorldStatic, CollisionShape, ClimbQueryParams);
HitWall ? CurrentWallHits = Hits : CurrentWallHits.Reset();
}
Silver Capsule represents the Capsule Sweep, while blue spheres represent the Hit Results.
Entering Climbing Mode
We now have a way to detect climbable surfaces. The next step is to check if we can start climbing, and if we can, change the movement mode to Climbing.
In Zelda BOTW, Link automatically starts climbing when running towards a wall. But that only occurs if the angle between the wall and his movement is small enough. Let’s do the same. Create a CanStartClimbing
function that will iterate through the CurrentWallHits
and return True if any hit angle is within range.
Imagine our character wants to start climbing a steep wall, like the one depicted below. Let’s say that in order for that to be successful, it needs to be looking at a maximum of 25 degrees from the wall. To know where the character is heading, we can simply use its Forward Vector F. We can then find the angle between F and the wall normal wN.
However, doing so won’t give us the correct answer, as we don’t care how steep the wall is (or how much the normal points up or down). One solution is to project wN into a horizontal plane p and normalize it. We can construct p by creating a plane whose normal equals to UpVector
. The result is a vector R with its Z component equal to zero. In other words, we can simply ignore the Z component.
Note
The theory above works because we assume the Up direction in our game is always UpVector
. Be aware of this if your game has unusual gravity! If that’s the case, project the vector into a plane whose normal is the opposite of gravity.
Let’s create a variable to control the minimum angle (MinHorizontalDegreesToStartClimbing
). To find the angle between the character’s forward vector and the HorizontalProjectedNormal
, we need to calculate the Arccosine of the Dot product. The result is in radians, so convert it to degrees.
In the original game, the character can climb everywhere but 90 degrees ceilings. To do the same, calculate the Dot product between the wall normal and the HorizontalProjectedNormal
. If the result is zero, this means that the vectors are perpendicular, indicating a flat ceiling.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="1.0", ClampMax="75.0"))
float MinHorizontalDegreesToStartClimbing = 25;
bool CanStartClimbing();
/* CustomCharacterMovementComponent.cpp */
bool UCustomCharacterMovementComponent::CanStartClimbing()
{
for (FHitResult& Hit : CurrentWallHits)
{
const FVector HorizontalNormal = Hit.Normal.GetSafeNormal2D();
const float HorizontalDot = FVector::DotProduct(UpdatedComponent->GetForwardVector(), -HorizontalNormal);
const float VerticalDot = FVector::DotProduct(Hit.Normal, HorizontalNormal);
const float HorizontalDegrees = FMath::RadiansToDegrees(FMath::Acos(HorizontalDot));
const bool bIsCeiling = FMath::IsNearlyZero(VerticalDot);
if (HorizontalDegrees <= MinHorizontalDegreesToStartClimbing && !bIsCeiling)
{
return true;
}
}
return false;
}
The
UpdatedComponent
is the component we move and update. We will use it throughout the project to retrieve information like the current location and rotation.
Unfortunately, we are not done yet. There are cases where the character can climb but shouldn’t. The image below is one such case. We are detecting a climbable surface, but it is too low.
We shouldn’t be able to climb here!
One solution is to line trace forward at the character’s eye height. If we hit something, that means there’s actually a surface where the character can hang on. We will use the same idea later, so let’s make a function for it:
/* CustomCharacterMovementComponent.h */
private:
bool EyeHeightTrace(const float TraceDistance) const;
/* CustomCharacterMovementComponent.cpp */
bool UCustomCharacterMovementComponent::EyeHeightTrace(const float TraceDistance) const
{
FHitResult UpperEdgeHit;
const FVector Start = UpdatedComponent->GetComponentLocation() +
(UpdatedComponent->GetUpVector() * GetCharacterOwner()->BaseEyeHeight);
const FVector End = Start + (UpdatedComponent->GetForwardVector() * TraceDistance);
return GetWorld()->LineTraceSingleByChannel(UpperEdgeHit, Start, End, ECC_WorldStatic, ClimbQueryParams);
}
We could check if the EyeHeightTrace
returns true at the start of the CanStartClimbing
function. However, there are still some edge cases. If the trace length is too small, it won’t detect steep surfaces. If it is too big, it can wrongly detect another far away surface. This problem is illustrated below.
The character should be able to climb the steep surface on the left but shouldn’t on the right.
To handle that, we can make the trace length dependent on how steep the surface is. VerticalDot
represents exactly that. Create another function, IsFacingSurface
, that calculates the line trace and calls the EyeHeightTrace
. As we used normalized vectors to calculate the VerticalDot
, the result is a scalar between 0 and 1 (assuming they always face the same direction). Finally, add the IsFacingSurface
check inside CanStartClimbing
.
/* CustomCharacterMovementComponent.h */
private:
bool IsFacingSurface() const;
/* CustomCharacterMovementComponent.cpp */
bool UCustomCharacterMovementComponent::CanStartClimbing()
{
// ...
if (HorizontalDegrees <= MinHorizontalDegreesToStartClimbing &&
!bIsCeiling && IsFacingSurface(VerticalDot)) // Add IsFacingSurface
{
return true;
}
// ...
}
bool UCustomCharacterMovementComponent::IsFacingSurface(const float SurfaceVerticalDot) const
{
constexpr float BaseLength = 80;
const float SteepnessMultiplier = 1 + (1 - Steepness) * 5;
return EyeHeightTrace(BaseLength * SteepnessMultiplier);
}
Different trace lengths based on steepness.
When ready to climb, we now can call SetMomeventMode
passing EMovementMode::MOVE
Custom as the parameter. However, this function also expects a second parameter CustomMode. This is great, as we can have many submodes, like climbing and running.
Let’s create a new enum to represent custom modes:
#pragma once
#include "UObject/ObjectMacros.h"
UENUM(BlueprintType)
enum ECustomMovementMode
{
CMOVE_Climbing UMETA(DisplayName = "Climbing"),
CMOVE_MAX UMETA(Hidden),
};
When the movement updates, change the movement mode if we can start climbing:
/* CustomCharacterMovementComponent.h */
private:
virtual void OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity) override;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity)
{
if (CanStartClimbing())
{
SetMovementMode(EMovementMode::MOVE_Custom, ECustomMovementMode::CMOVE_Climbing);
}
Super::OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);
}
Great. Now, whenever we are in the range of a valid climbable surface, the character changes to the Climbing Mode. As we didn’t specify the Climbing physics yet, there’s no movement. Before we head into coding the climbing physics, let’s set up the inputs needed to test our code.
Input
Unlike in BOTW, where Link automatically starts climbing while pressing forward, we will set up a dedicated climbing action.
Creating actions in the Input menu for climbing and cancelling.
In order to begin and stop climbing, let’s create a boolean bWantsToClimb
to store the player intention alongisde two functions: TryClimbing
and CancelClimbing
. We can also replace the code on OnMovementUpdated
by checking if bWantsToClimb
is true:
/* CustomCharacterMovementComponent.h */
public:
void TryClimbing();
void CancelClimbing();
private:
bool bWantsToClimb = false;
void UCustomCharacterMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation,
const FVector& OldVelocity)
{
// if (CanStartClimbing())
if (bWantsToClimb)
// ...
}
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::TryClimbing()
{
if (CanStartClimbing())
{
bWantsToClimb = true;
}
}
void UCustomCharacterMovementComponent::CancelClimbing()
{
bWantsToClimb = false;
}
Finally, head to the YourCharacter
class. Bind the new actions inside SetupPlayerInputComponent
and create the appropriate functions:
/* CustomCharacter.h */
protected:
void Climb();
void CancelClimb();
/* CustomCharacter.cpp */
void ACustomCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// ...
PlayerInputComponent->BindAction("Climb", IE_Pressed, this, &ACustomCharacter::Climb);
PlayerInputComponent->BindAction("Cancel Climb", IE_Pressed, this, &ACustomCharacter::CancelClimb);
}
void ACustomCharacter::Climb()
{
MovementComponent->TryClimbing();
}
void ACustomCharacter::CancelClimb()
{
MovementComponent->CancelClimb();
}
Next, let’s update both MoveForward
and MoveRight
functions. The way they work by default is to get the forward and right directions based on the camera orientation. When climbing, however, we want the direction relative to the climbing surface instead. Moving forward, then, is to move up.
First, we need a way to tell if we are in the climbing state or not. Let’s head back to the CMC. Create the IsClimbing
function that checks if the movement component mode is Custom and if the custom mode is Climbing (which we recently created). Then, we need a function to retrieve the climbing surface normal GetClimbSurfaceNormal
. However, even though we have a list of hits, we still haven’t decided what to do with it. For now, let’s return the first hit if possible. We’ll talk more about it in an upcoming section.
/* CustomCharacterMovementComponent.h */
public:
UFUNCTION(BlueprintPure)
bool IsClimbing() const;
UFUNCTION(BlueprintPure)
FVector GetClimbSurfaceNormal() const;
/* CustomCharacterMovementComponent.cpp */
bool UCustomCharacterMovementComponent::IsClimbing() const
{
return MovementMode == EMovementMode::MOVE_Custom && CustomMovementMode == ECustomMovementMode::CMOVE_Climbing;
}
FVector UCustomCharacterMovementComponent::GetClimbSurfaceNormal() const
{
// Temporary code!!
return CurrentWallHits.Num() > 0 ? CurrentWallHits[0] : FVector::Zero();
}
Head back to the character class. We can now find the up direction (for the move forward function) by calculating the cross product of the surface normal and the character’s left vector. As for the move right function, we can follow the same idea but use the character’s up vector instead of the left vector.
Info
In case the math above didn’t make sense to you, remember about cross-product left-hand rule.
Point your index finger along the surface normal and your middle finger along the character’s left vector. Your thumb is the resultant vector that, in this example, indicates the forward (up) direction.
/* CustomCharacter.cpp */
void ACSCharacter::MoveForward(float Value)
{
// ...
FVector Direction;
if (MovementComponent->IsClimbing())
{
Direction = FVector::CrossProduct(MovementComponent->GetClimbSurfaceNormal(), -GetActorRightVector());
}
else
{
// Previous direction computation code ...
}
AddMovementInput(Direction, Value);
}
void ACSCharacter::MoveRight(float Value)
{
// ...
if (MovementComponent->IsClimbing())
{
Direction = FVector::CrossProduct(MovementComponent->GetClimbSurfaceNormal(), GetActorUpVector());
}
// else ...
}
Climbing Physics
Let’s finally talk about climbing physics! As discussed in the CMC overview, we’ll override the PhysCustom
function to implement our custom climbing physics. The custom movement mode can have many submodes, so create a dedicated function PhysClimbing
to make our intentions explicit.
/* CustomCharacterMovementComponent.h */
private:
virtual void PhysCustom(float deltaTime, int32 Iterations) override;
void PhysClimbing(float deltaTime, int32 Iterations);
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::PhysCustom(float deltaTime, int32 Iterations)
{
if (CustomMovementMode == ECustomMovementMode::CMOVE_Climbing)
{
PhysClimbing(deltaTime, Iterations);
}
Super::PhysCustom(deltaTime, Iterations);
}
If you have played around with the third-person template, you might have noticed that the character rotates in the direction you move. This behavior is dictated by the bOrientRotationToMovement
variable. However, we want the character to face the surface, so set it to false while climbing. In addition, don’t forget to realign the character rotation with the floor when exiting the climbing mode.
Lastly, let’s adjust the character’s capsule collision while in climbing mode. As you can see in the image below, when using proper climbing animation, it becomes clear that the capsule is too tall. By decreasing its height, we can make the character climb more intricate surfaces, as we’ll see later on.
Default capsule half-height on the left (90 cm). Reduced height on the right to better fit the character while climbing (60 cm).
/* CustomCharacterMovementComponent.h */
private:
virtual void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) override;
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="0.0", ClampMax="80.0"))
float ClimbingCollisionShrinkAmount = 30;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode)
{
if (IsClimbing())
{
bOrientRotationToMovement = false;
UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
Capsule->SetCapsuleHalfHeight(Capsule->GetUnscaledCapsuleHalfHeight() - ClimbingCollisionShrinkAmount);
}
const bool bWasClimbing = PreviousMovementMode == MOVE_Custom && PreviousCustomMode == CMOVE_Climbing;
if (bWasClimbing)
{
bOrientRotationToMovement = true;
const FRotator StandRotation = FRotator(0, UpdatedComponent->GetComponentRotation().Yaw, 0);
UpdatedComponent->SetRelativeRotation(StandRotation);
UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
Capsule->SetCapsuleHalfHeight(Capsule->GetUnscaledCapsuleHalfHeight() + ClimbingCollisionShrinkAmount);
// After exiting climbing mode, reset velocity and acceleration
StopMovementImmediately();
}
Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);
}
We’re now ready to build the PhysClimbing
. Yet, it might feel overwhelming thinking about all the possible ways to handle velocity, acceleration, and collision handling. The bad news is there is no documentation in this regard. The good news is that we can inspect how the default Phys
functions operate to know where to start. A good candidate is PhysFlying
, a small and easy-to-understand function.
void UCharacterMovementComponent::PhysFlying(float deltaTime, int32 Iterations) | |
{ | |
if (deltaTime < MIN_TICK_TIME) | |
{ | |
return; | |
} | |
RestorePreAdditiveRootMotionVelocity(); | |
// Calculates velocity if not being controlled by root motion. | |
if( !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() ) | |
{ | |
if( bCheatFlying && Acceleration.IsZero() ) | |
{ | |
Velocity = FVector::ZeroVector; | |
} | |
const float Friction = 0.5f * GetPhysicsVolume()->FluidFriction; | |
// Important!! - Updates velocity and acceleration with given friction and deceleration. | |
CalcVelocity(deltaTime, Friction, true, GetMaxBrakingDeceleration()); | |
} | |
ApplyRootMotionToVelocity(deltaTime); | |
Iterations++; | |
bJustTeleported = false; | |
FVector OldLocation = UpdatedComponent->GetComponentLocation(); | |
// Location Change | |
const FVector Adjusted = Velocity * deltaTime; | |
FHitResult Hit(1.f); | |
// Important!! Moves component by given Location Change and rotates by given rotation. | |
// Handles penetrations. | |
SafeMoveUpdatedComponent(Adjusted, UpdatedComponent->GetComponentQuat(), true, Hit); | |
// If Hit.Time >= 1.f, didn't hit anything. | |
if (Hit.Time < 1.f) | |
{ | |
/* Ignore - We are not interested in Stepping Up. */ | |
const FVector GravDir = FVector(0.f, 0.f, -1.f); | |
const FVector VelDir = Velocity.GetSafeNormal(); | |
const float UpDown = GravDir | VelDir; | |
bool bSteppedUp = false; | |
if ((FMath::Abs(Hit.ImpactNormal.Z) < 0.2f) && (UpDown < 0.5f) && (UpDown > -0.2f) && CanStepUp(Hit)) | |
{ | |
float stepZ = UpdatedComponent->GetComponentLocation().Z; | |
bSteppedUp = StepUp(GravDir, Adjusted * (1.f - Hit.Time), Hit); | |
if (bSteppedUp) | |
{ | |
OldLocation.Z = UpdatedComponent->GetComponentLocation().Z + (OldLocation.Z - stepZ); | |
} | |
} | |
/* Ignore */ | |
if (!bSteppedUp) | |
{ | |
// Handles blocking/physics interaction. | |
HandleImpact(Hit, deltaTime, Adjusted); | |
// Slides along collision. Specially important for climbing to feel good. | |
SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true); | |
} | |
} | |
// Velocity based on distance traveled. | |
if( !bJustTeleported && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() ) | |
{ | |
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / deltaTime; | |
} | |
} |
The code flow is quite straightforward. First, it calculates the velocity and acceleration, considering the friction and deceleration (CalcVelocity
). Then, it moves the component by the new velocity and delta time (SafeMoveUpdatedComponent
). After that, it handles the impact and slides along the surface (HandleImpact
and SlideAlongSurface
). In the end, the velocity is calculated as the distance between the previous location and the new one.
Our PhysClimbing
function will be pretty similar. I refactored the PhysFlying
by extracting the code into functions to make it more readable. I’ve also removed unrelated code and made changes relevant to the climbing physics, so let’s get into it.
/* CustomCharacterMovementComponent.h */
private:
void ComputeSurfaceInfo();
void ComputeClimbingVelocity(float deltaTime);
bool ShouldStopClimbing();
void StopClimbing(float deltaTime, int32 Iterations);
void MoveAlongClimbingSurface(float deltaTime);
void SnapToClimbingSurface(float deltaTime) const;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::PhysClimbing(float deltaTime, int32 Iterations)
{
if (deltaTime < MIN_TICK_TIME)
{
return;
}
ComputeSurfaceInfo();
if (ShouldStopClimbing())
{
StopClimbing(deltaTime, Iterations);
return;
}
ComputeClimbingVelocity(deltaTime);
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
MoveAlongClimbingSurface(deltaTime);
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / deltaTime;
}
SnapToClimbingSurface(deltaTime);
}
The first change is the call to a new function ComputeSurfaceInfo
. This function is responsible for calculating the appropriate CurrentClimbingNormal
and CurrentClimbingPosition
from the hits we stored previously. For now, let’s get this information from the first Hit, as we’re going to explore a more robust approach in the next section. As it is, this information is sufficient to test the climbing physics in simple collisions, like a flat wall.
/* CustomCharacterMovementComponent.h */
private:
FVector CurrentClimbingNormal;
FVector CurrentClimbingPosition;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::ComputeSurfaceInfo()
{
// Temporary code!
if (CurrentWallHits.Num() > 0)
{
CurrentClimbingNormal = CurrentWallHits[0].Normal;
CurrentClimbingPosition = CurrentWallHits[0].ImpactPoint;
}
}
// Update temporary code to return the new variable instead!
FVector UCustomCharacterMovementComponent::GetClimbSurfaceNormal() const
{
return CurrentClimbingNormal;
}
Next, we need to check if we ShouldStopClimbing
before doing any computation. The function returns true if the bWantsToClimb
is false, if there is no climbing normal, or if the character is on a 90 degree ceiling. If that is the case, we should call StopClimbing
which sets the movement mode to Falling, and start a new physics.
bool UCustomCharacterMovementComponent::ShouldStopClimbing() const
{
const bool bIsOnCeiling = FVector::Parallel(CurrentClimbingNormal, FVector::UpVector);
return !bWantsToClimb || CurrentClimbingNormal.IsZero() || bIsOnCeiling;
}
void UCustomCharacterMovementComponent::StopClimbing(float deltaTime, int32 Iterations)
{
bWantsToClimb = false;
SetMovementMode(EMovementMode::MOVE_Falling);
StartNewPhysics(deltaTime, Iterations);
}
Moving on, we have the ComputeClimbVelocity
function, responsible for calculating the velocity and crucial to the movement feel and how the player perceives the climbing action. As we’re mimicking Zelda BOTW, Link appears to put lots of effort into climbing - I imagine climbing a mountain with your bare hands isn’t easy - as the movement feels heavy. The animation plays a big part here, but it wouldn’t feel right if Link starts or stops moving very fast or suddenly changes direction effortlessly. In other words, we are talking about his Acceleration and Deceleration (Braking).
The original PhysFlying
code checks if there’s no Acceleration
and changes the velocity to zero if that’s the case. We don’t want that. Let’s also set the Friction
to 0
, implying that we don’t want any resistance to sliding along a surface. This provides a smoother movement when dealing with corners, for example. Instead of calling GetMaxBrakingDeceleration
, which selects a value based on the current movement mode (returns 0
if in custom mode), let’s use a new field BrakingDecelerationClimbing
.
Lastly, by inspecting CalcVelocity
, we can see that the max speed and acceleration values are set by calling GetMaxSpeed
and GetMaxAcceleration
. It works the same way as GetMaxBrakingDeceleration
, returning a value based on the current movement mode. Luckily, both functions are virtual, so we can override them to return new values.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="10.0", ClampMax="500.0"))
float MaxClimbingSpeed = 120.f;
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="10.0", ClampMax="2000.0"))
float MaxClimbingAcceleration = 380.f;
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="0.0", ClampMax="3000.0"))
float BrakingDecelerationClimbing = 550.f;
virtual float GetMaxSpeed() const override;
virtual float GetMaxAcceleration() const override;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::ComputeClimbVelocity(float deltaTime)
{
RestorePreAdditiveRootMotionVelocity();
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
constexpr float Friction = 0.0f;
constexpr bool bFluid = false;
CalcVelocity(deltaTime, Friction, bFluid, BrakingDecelerationClimbing);
}
ApplyRootMotionToVelocity(deltaTime);
}
float UCustomCharacterMovementComponent::GetMaxSpeed() const
{
return IsClimbing() ? MaxClimbingSpeed : Super::GetMaxSpeed();
}
float UCustomCharacterMovementComponent::GetMaxAcceleration() const
{
return IsClimbing() ? MaxClimbingAcceleration : Super::GetMaxAcceleration();
}
Low Acceleration (
380 cm/s
) and Breaking (550 cm/s
) on the left. High Acceleration (2000 cm/s
) and Braking (2000 cm/s
) on the right. Both with the same Max Speed (120 cm/s
).
Then, we have the MoveAlongClimbingSurface
function that handles the actual component movement. The only difference from the PhysFlying
is the call to a new function GetClimbingRotation
. As the name suggests, it returns the correct orientation from the wall.
void UCustomCharacterMovementComponent::MoveAlongClimbingSurface(float deltaTime)
{
const FVector Adjusted = Velocity * deltaTime;
FHitResult Hit(1.f);
SafeMoveUpdatedComponent(Adjusted, GetClimbingRotation(deltaTime), true, Hit);
if (Hit.Time < 1.f)
{
HandleImpact(Hit, deltaTime, Adjusted);
SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true);
}
}
For the climbing rotation, we can make the character look towards the opposite of the wall normal. To achieve that in code, we can create a rotation matrix with an X-axis (forward direction) equal to -CurrentClimbingNormal
. However, an instantaneous rotation change would be rough. Instead, let’s interpolate from the current rotation to the targeted one, with ClimbingRotationSpeed
controlling the speed.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="1.0", ClampMax="12.0"))
int ClimbingRotationSpeed = 6;
FQuat GetClimbingRotation(float deltaTime) const;
FVector CurrentClimbingNormal;
/* CustomCharacterMovementComponent.cpp */
FQuat UCustomCharacterMovementComponent::GetClimbingRotation(float deltaTime) const
{
const FQuat Current = UpdatedComponent->GetComponentQuat();
const FQuat Target = FRotationMatrix::MakeFromX(-CurrentClimbingNormal).ToQuat();
return FMath::QInterpTo(Current, Target, deltaTime, ClimbingRotationSpeed);
}
Alright, we’re almost done. We still need a way to keep the character from falling out of the surface. The SnapToClimbingSurface
function moves the component towards the surface while taking into consideration the desired distance from it (DistanceFromSurface
). By making the component stay a bit further away from the surface, we can reduce the collision and thus increase the movement smoothness. Multiplying it by deltatime and ClimbingSnapSpeed
give us a smooth and controllable snap movement.
Projecting the
CurrentClimbingPosition
(p) into the forward vector (x) returns a consistent distance.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="0.0", ClampMax="60.0"))
float ClimbingSnapSpeed = 4.f;
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="0.0", ClampMax="80.0"))
float DistanceFromSurface = 45.f;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::SnapToClimbingSurface(float deltaTime) const
{
const FVector Forward = UpdatedComponent->GetForwardVector();
const FVector Location = UpdatedComponent->GetComponentLocation();
const FQuat Rotation = UpdatedComponent->GetComponentQuat();
const FVector ForwardDifference = (CurrentClimbingPosition - Location).ProjectOnTo(Forward);
const FVector Offset = -CurrentClimbingNormal * (ForwardDifference.Length() - DistanceFromSurface);
constexpr bool bSweep = true;
UpdatedComponent->MoveComponent(Offset * ClimbingSnapSpeed * deltaTime, Rotation, bSweep);
}
Tip
You can improve this method by adding a distance threshold. If you are within the threshold, move the component smoothly as we did. If not, you can instantly teleport it to the correct distance. Doing so decreases the chance that the character loses grip with high velocities.
Computing Surface Info
We now have a working climbing physics, but we haven’t agreed on what to do with all the possible surface hits inside the ComputeSurfaceInfo
function. We may have one hit in a left corner, one directly in front of us, and one in the ceiling. How do we choose one?
One way to solve this is to choose all of them! We can calculate CurrentClimbingPosition
as an average (or the centroid) of every hit. In other words, it’s the sum divided by the number of hits. As for the CurrentClimbingNormal
, we can get an average by normalizing the sum of the hits. Lastly, we need to reset both fields at the start of the function.
// Replace the temporary code
void UCustomCharacterMovementComponent::ComputeSurfaceInfo()
{
CurrentClimbingNormal = FVector::ZeroVector;
CurrentClimbingPosition = FVector::ZeroVector;
if (CurrentWallHits.IsEmpty())
{
return;
}
for (const FHitResult& WallHit : CurrentWallHits)
{
CurrentClimbingPosition += WallHit.ImpactPoint;
CurrentClimbingNormal += WallHit.Normal;
}
CurrentClimbingPosition /= CurrentWallHits.Num();
CurrentClimbingNormal = CurrentClimbingNormal.GetSafeNormal();
}
Surface hits in cyan. Average (
CurrentClimbingPosition
) in blue.
Our climbing system is finally working! We can climb over many obstacles and complex terrain smoothly. But there’s still one thing that is not working as expected. When dealing with different close or overlapping geometries, the CurrentClimbingNormal
might behave incorrectly, as exemplified in the image below. This happens because we are relying on an overlap detection (SweepAndStoreWallHits
). The sweep is already inside the collision, causing the normal to point toward its center.
The current method causes the character to rotate incorrectly in some cases.
In reality, the lowermost hit normal should have been the same as the uppermost one. Even though an overlap is perfect for detecting multiple collisions, we end up losing some control. If the sweep came from the character location to the obstacles instead of just existing inside of things, it would detect the surface as expected.
The system already knows where the obstacles are by storing them as an array of hits. In that case, we can fire traces from the character location to each hit. This gives us a much more precise surface detection. To do this, we’ll be using a small sphere as the shape for the sweeps. The end of each sweep will be the direction from the character to each hit location with a big enough length. Finally, we calculate the CurrentClimbingPosition
and CurrentClimbingNormal
with the new sweep information.
void UCustomCharacterMovementComponent::ComputeSurfaceInfo()
{
// ...
const FVector Start = UpdatedComponent->GetComponentLocation();
const FCollisionShape CollisionSphere = FCollisionShape::MakeSphere(6);
for (const FHitResult& WallHit : CurrentWallHits)
{
const FVector End = Start + (WallHit.ImpactPoint - Start).GetSafeNormal() * 120;
FHitResult AssistHit;
GetWorld()->SweepSingleByChannel(AssistHit, Start, End, FQuat::Identity,
ECC_WorldStatic, CollisionSphere, ClimbQueryParams);
CurrentClimbingPosition += AssistHit.ImpactPoint;
CurrentClimbingNormal += AssistHit.Normal;
}
// ...
}
Visual debug of tracing to each hit.
Animation
Enough of this weird walking animation while climbing. Let’s finally work on proper animation! I’ll be using this Climbing Animation Set. However, if you want to get something free, Mixamo has some good animations (search for keywords like climb and hang).
Note
The third-person project animation blueprint makes use of State Alias that isn’t available on UE4. Nevertheless, the logic is the same. You just need to make the direct transitions.
Also, you might have noticed that the character model sometimes goes down while climbing. This happens because the
Node for the Foot IK. Either remove it or disable if the character is climbing.
We’ll be modifying the default Animation Blueprint. First, we need to change the references to our custom character class and movement component:
Change both the character and the movement component to their custom version.
Next, in order to change to the correct animation, we must keep track of whether we are in the climbing state or not. In the animation blueprint, create a variable IsClimbing
and set it by getting the value of the Is Climbing
function of our CMC.
Set the newly created
IsClimbing
variable in the animation blueprint.
We can now create a new Climb state and add a transition to it from the Locomotion (if you are on UE4, the walk state has a different name) with the IsClimbing
variable as its rule:
The animation set I’m using comes with eight directional animations (Up, Down, Left, Right, and diagonals) and an idle animation. To create a smooth transition, we’ll use a 2D blendspace asset. The Horizontal Axis will represent the left/right movement, while the Vertical Axis will represent the up/down movement. For their min and max value, I’ll use 120, the same as the maximum climbing speed.
Climb Movement Blendspace parameters
Climb Movement Blendspace parameters
We still need a way to feed the blendspace with an X/Y value. We can’t simply use the velocity as it is because it’s rotated along with the character’s rotation. To solve this, we can multiply the velocity by the inverse of the character rotation, essentially unrotating it. Inside the animation blueprint, let’s create a Velocity to Blendspace 2D
function that receives the velocity as an input and outputs a Vector2D
. With the unrotate velocity, we can map the Y
component (right vector) to the blendspace X
component, and the Z
component (up vector) to the blendspace Y
component.
We can now add the blendspace to the Climb state. For the parameters, use the result of the Velocity to Blendspace 2D
function in a Vector2D
variable.
Note
Depending on the DistanceFromSurface
field, the character’s mesh can be quite far from touching the surface. We’ll solve this using inverse kinematics. However, if you don’t want to use IK, you calculate the distance and offset the mesh forward.
Only having a transition from walking to climbing isn’t good enough. Let’s create intermediate states with transitional animations. Like grabbing the wall from the ground and while falling.
Example of two
Climbing from ground animation.
Whew, finally. We have a working climbing system with pretty animations! Even so, there’s still a long way to go if you want to add this to your game. I hope the following sections help you achieve your vision.
Extra Mechanics
In this section, we’ll be extending our climbing system with some extra mechanics alongside their animations.
Climbing Down
Our character can already climb almost any surface, but aside from falling off by canceling the climbing, it can’t stand on the floor when climbing down. And what about climbing into a walkable surface. Shouldn’t it transition to walking state?
To handle this, let’s create the ClimbDownToFloor
function. As the character will change state if this function is successful, we can call it after the ShouldStopClimbing
fails.
/* CustomCharacterMovementComponent.h */
private:
bool ClimbDownToFloor() const;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::PhysClimbing(float deltaTime, int32 Iterations)
{
// ...
if (ShouldStopClimbing() || ClimbDownToFloor()) // <--
{
StopClimbing(deltaTime, Iterations);
return;
}
// ...
First, we need to check if there is a floor beneath the character. To do this, create the CheckFloor
function that line traces downward from the character’s position. The FloorCheckDistance
controls the trace distance. As we still need to retrieve information from the hit, pass a reference to an FHitResult
. If CheckFloor
returns false, there’s no need to do further computation. Otherwise, there are two ways that this function can succeed: if we are climbing toward a walkable floor or if we are already climbing one.
In the first scenario, we can find if the floor is walkable by comparing its normal Z component to the WalkableFloorZ
(GetWalkableFloorZ
). If the Z component of the floor normal is at least greater than the WalkableFloorZ
, the character can walk on it. To find out if we are descending toward the floor, calculate the dot product between the velocity and the negative floor normal. In this case, the dot product represents the speed at which we are moving down. I’ve used an arbitrary minimum speed to count as moving toward the floor, as we don’t want players to accidentally climb down.
The latter case works similarly, but in this case, we compare it with the CurrentClimbingNormal
instead. If the CurrentClimbingNormal
is walkable, the character can stand up. In some cases, only checking on the climbing location can make the character fall, considering the rotation offset. One way to solve this is to check if the hit is registering a walkable floor (bIsClimbingFloor && bOnWalkableFloor)
.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditAnywhere, meta=(ClampMin="1.0", ClampMax="500.0"))
float FloorCheckDistance = 100.f;
bool CheckFloor(FHitResult& FloorHit) const;
/* CustomCharacterMovementComponent.cpp */
bool UCustomCharacterMovementComponent::ClimbDownToFloor() const
{
FHitResult FloorHit;
if (!CheckFloor(FloorHit))
{
return false;
}
const bool bOnWalkableFloor = FloorHit.Normal.Z > GetWalkableFloorZ();
const float DownSpeed = FVector::DotProduct(Velocity, -FloorHit.Normal);
const bool bIsMovingTowardsFloor = DownSpeed >= MaxClimbingSpeed / 3 && bOnWalkableFloor;
const bool bIsClimbingFloor = CurrentClimbingNormal.Z > GetWalkableFloorZ();
return bIsMovingTowardsFloor || (bIsClimbingFloor && bOnWalkableFloor);
}
bool UCustomCharacterMovementComponent::CheckFloor(FHitResult& FloorHit) const
{
const FVector Start = UpdatedComponent->GetComponentLocation();
const FVector End = Start + FVector::DownVector * FloorCheckDistance;
return GetWorld()->LineTraceSingleByChannel(FloorHit, Start, End, ECC_WorldStatic, ClimbQueryParams);
}
Tip
A capsule trace at the location is a better way to make sure that the character can stand up. If it registers a hit, that means something will block it, and we should abort the function to avoid collision issues.
For the animation, we only need to focus on the transition between the Climb state and Land state on the Animation Blueprint. Again, if you are using Unreal Engine 4, the default states are a bit different. Add the Climb alias to the To Land state alias. Next, update the transition between To Land and Land so that it only occurs if the character is neither falling nor climbing.
“To Land” State Alias (UE 5 only).
Climb Up Ledges
When Link climbs up a ledge, he performs a fast and satisfactory animation, quickly moving forward and transitioning to a locomotion state.
One way to do this is by playing an animation while moving the character to the correct location and blocking the user input until it finishes. This has the upside of giving us lots of control over the physics. However, it’s quite hard to move the character in a way that matches the animation. For this reason, we’ll use the root motion technique, letting the animation control the movement physics.
Ledge climbing animation (Climbing_ClimbUpAtTop_RM for those using the same animation set as me). The red line represents the actual root motion.
Like in BOTW, we’ll make the character start climbing up the ledge whenever possible. For this reason, we’ll call the new TryClimbUpLedge
function after the movement is done.
/* CustomCharacterMovementComponent.h */
private:
bool TryClimbUpLedge() const;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::PhysClimbing(float deltaTime, int32 Iterations)
{
// ...
MoveAlongClimbingSurface(deltaTime);
TryClimbUpLedge(); // <--
// ...
}
Before building the new function, we need a way to store and play the root motion animation. Animation Montages are perfect for this, as they can replicate the root motion in network games. Create an animation montage and add the proper animation into the default slot track. Make sure the animation has Root Motion enabled!
In case you have the same animation as I: there’s a long pause after the character climbs up, resulting in the player waiting a bit too much before resuming control. For this reason, I start blending out the animation 0.5 seconds before it finishes.
We’ll then need a reference to the montage (LedgeClimbMontage
), which we can feed through the editor, and an animation instance to play it (AnimInstance
), that we can get at the BeginPlay
. If the montage is already playing, abort the function.
Otherwise, there are three requisites for the function to be successful: 1- We are moving up. 2- We are on a ledge. 3- We can stand at the new location after climbing up. If all of them are true, we can play the LedgeClimbMontage
. However, before doing it, reset the character rotation leaving only the Yaw unaffected. This improves the motion when the character is climbing steep surfaces.
For the first check, as we did in the previous section, we can find the up speed by calculating the dot product between the velocity and its up vector.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditDefaultsOnly)
UAnimMontage* LedgeClimbMontage;
UPROPERTY()
UAnimInstance* AnimInstance;
bool HasReachedEdge() const;
bool IsLocationWalkable(const FVector& CheckLocation) const;
bool CanMoveToLedgeClimbLocation() const;
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::BeginPlay()
{
Super::BeginPlay();
AnimInstance = GetCharacterOwner()->GetMesh()->GetAnimInstance();
// ...
}
bool UCustomCharacterMovementComponent::TryClimbUpLedge()
{
if (AnimInstance && LedgeClimbMontage && AnimInstance->Montage_IsPlaying(LedgeClimbMontage))
{
return false;
}
const float UpSpeed = FVector::DotProduct(Velocity, UpdatedComponent->GetUpVector());
const bool bIsMovingUp = UpSpeed >= MaxClimbingSpeed / 3;
if (bIsMovingUp && HasReachedEdge() && CanMoveToLedgeClimbLocation())
{
const FRotator StandRotation = FRotator(0, UpdatedComponent->GetComponentRotation().Yaw, 0);
UpdatedComponent->SetRelativeRotation(StandRotation);
AnimInstance->Montage_Play(LedgeClimbMontage);
return true;
}
return false;
}
For the second one, the HasReachedEdge
returns true if it didn’t hit anything by tracing at the eye height. A good trace distance, in this case, should be big enough so that the trace ends close to where the character collision would be after climbing up. That’s why I calculated it by multiplying the capsule radius with a value greater than two. You could use a property instead.
bool UCustomCharacterMovementComponent::HasReachedEdge() const
{
const UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
const float TraceDistance = Capsule->GetUnscaledCapsuleRadius() * 2.5f;
return !EyeHeightTrace(TraceDistance);
}
Initially, we wrote the EyeHeightTrace
to check if the character could start climbing. However, remember that we now change the capsule height while climbing. This lowers the eye height, so we need to add back the ClimbingCollisionShrinkAmount
:
bool UCustomCharacterMovementComponent::EyeHeightTrace(const float TraceDistance) const
{
// ...
const float BaseEyeHeight = GetCharacterOwner()->BaseEyeHeight;
const float EyeHeightOffset = IsClimbing() ? BaseEyeHeight + ClimbingCollisionShrinkAmount : BaseEyeHeight;
const FVector Start = UpdatedComponent->GetComponentLocation() + UpdatedComponent->GetUpVector() * EyeHeightOffset;
// ...
}
The character won’t climb. The second check (
HasReachedEdge
) failed as the trace hit a close object.
Finally, for the third check, we need to find if the location is walkable. If it is, we also need to know whether something would block the character from moving into it.
But first, we need to calculate the location where the character will roughly be standing. I’ll use some arbitrary magic numbers that I found by playtesting. The important thing is to make the location a bit higher since it can be on inclined or irregular terrain.
Then, the IsLocationWalkable
traces down from the CheckLocation
, returning true if it hits a walkable surface. Next, we trace the location with a capsule instead, imitating the character. It starts directly above the character and moves horizontally. If it hits something, it means that it’s blocked, so the function returns false.
bool UCustomCharacterMovementComponent::CanMoveToLedgeClimbLocation() const
{
// Could use a property instead for fine-tuning.
const FVector VerticalOffset = FVector::UpVector * 160.f;
const FVector HorizontalOffset = UpdatedComponent->GetForwardVector() * 120.f;
const FVector CheckLocation = UpdatedComponent->GetComponentLocation() + HorizontalOffset + VerticalOffset;
if (!IsLocationWalkable(CheckLocation))
{
return false;
}
FHitResult CapsuleHit;
const FVector CapsuleStartCheck = CheckLocation - HorizontalOffset;
const UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
const bool bBlocked = GetWorld()->SweepSingleByChannel(CapsuleHit, CapsuleStartCheck,CheckLocation,
FQuat::Identity, ECC_WorldStatic, Capsule->GetCollisionShape(), ClimbQueryParams);
return !bBlocked;
}
bool UCustomCharacterMovementComponent::IsLocationWalkable(const FVector& CheckLocation) const
{
const FVector CheckEnd = CheckLocation + (FVector::DownVector * 250.f);
FHitResult LedgeHit;
const bool bHitLedgeGround = GetWorld()->LineTraceSingleByChannel(LedgeHit, CheckLocation, CheckEnd,
ECC_WorldStatic, ClimbQueryParams);
return bHitLedgeGround && LedgeHit.Normal.Z >= GetWalkableFloorZ();
}
On the left, the
IsLocationWalkable
fails as it encounters a 60 degrees slope. On the right, the location is walkable, but the capsule trace fails as it hits an obstacle.
One last thing. While root motion is active, we apply its velocity instead of calculating it based on input. We have to do the same with rotation. Inside GetClimbingRotation
, return the current rotation if root motion is active. Otherwise, proceed with the previous logic.
FQuat UCustomCharacterMovementComponent::GetClimbingRotation(float deltaTime) const
{
const FQuat Current = UpdatedComponent->GetComponentQuat();
if (HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocity())
{
return Current;
}
// ...
}
That’s it! We can now climb up ledges just like Link. Even though this new animation isn’t on the animation blueprint and has no direct transitions, the montage takes responsibility for blending the animation using the In and Out blending controls.
Climb Dash
In BOTW, Link can dash while climbing to move quickly in one direction. Even though it may look weird, Link doesn’t lose grip or speed when dashing through corners or small surface variations. Just imagine how frustrating it would be if it wasn’t true.
Fortunately, we have almost everything we need to perform this movement. We can think of it as climbing really fast with very high acceleration. In theory, we only need to adjust some properties like ClimbingSnapSpeed
and ClimbingRotationSpeed
and override the initial velocity.
Sadly, it won’t be this easy, mainly because of the animation and how it would feel. To convey a satisfactory dash animation, it needs a strong anticipation phase, where the character is idle, and a follow-through stage. Simply overriding the velocity as soon as the player does the action won’t look good.
You might be thinking of solving this issue using root motion, which would be perfectly fine. The motion would be controlled by the animation, after all. However, there are some downsides to this approach. First, it wouldn’t be 100% controlled by animation, as the character needs to rotate around corners, which implies the velocity changes direction. Another drawback is that we need to update the animation whenever we want to adjust the dash speed. What about the character picking up equipment that increases the dash speed?
To solve this, we’ll be using a CurveFloat
to dictate both the character speed and the duration of the climb dash.
Create a FloatCurve asset.
The curve asset works like any other curve editor. Right-click to create keyframes and right-click the keyframes to edit their tangents. The crucial part here is to match the speed with the animation. On my side, the launch animation ends at 0.29s
, so the speed quickly rises from 0cm/s to 950cm/s at 0.4s
. As the animation land phase starts at 0.93s
, I made a smooth transition to it from the 0.4s
mark. The full animation I’m using is 1.58 seconds long, but as I want the player to resume control quicker, I have made the last keyframe at 1.15s
. Later, I’ll use a longer blend to let the rest of the animation play out a bit.
Dash speed curve.
All good, so now we need a reference to the CurveFloat
we just created. We also need to store the dash direction, a bool to determine if we’re dashing or not, and a float to keep track of the current dash time.
Let’s create a public function so the character class can call it when the player desires to dash. If we are not already dashing, set the bWantsToClimbDash
to true, reset the CurrentClimbDashTime
and store the ClimbDashDirection
. If the player presses the dash while the character is not moving, let’s assume they want to go up. Otherwise, if there is acceleration, use it as the desired direction.
/* CustomCharacterMovementComponent.h */
private:
UPROPERTY(Category="Character Movement: Climbing", EditDefaultsOnly)
UCurveFloat* ClimbDashCurve;
FVector ClimbDashDirection;
bool bWantsToClimbDash = false;
float CurrentClimbDashTime;
public:
UFUNCTION(BlueprintCallable)
void TryClimbDashing();
UFUNCTION(BlueprintPure)
bool IsClimbDashing() const
{
return IsClimbing() && bIsClimbDashing;
}
UFUNCTION(BlueprintPure)
FVector GetClimbDashDirection() const;
{
return ClimbDashDirection;
}
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::TryClimbDashing()
{
if (ClimbDashCurve && bWantsToClimbDash == false)
{
bWantsToClimbDash = true;
CurrentClimbDashTime = 0.f;
StoreClimbDashDirection();
}
}
void UCustomCharacterMovementComponent::StoreClimbDashDirection()
{
ClimbDashDirection = UpdatedComponent->GetUpVector();
const float AccelerationThreshold = MaxClimbingAcceleration / 10;
if (Acceleration.Length() > AccelerationThreshold)
{
ClimbDashDirection = Acceleration.GetSafeNormal();
}
}
After the player successfully starts dashing, we need to update its state. Let’s do it in the PhysClimbing
function just before the ComputeClimbingVelocity
. As this gets called every tick, we can increment the CurrentClimbDashTime
by deltaTime
. We can stop the climb dash if the CurrentClimbDashTime
is greater than the last keyframe time. To get this maximum time, call the GetTimeRange
on the curve. The parameters are passed by reference and modified in place.
Oh, and don’t forget to call StopClimbDashing
inside StopClimbing
.
/* CustomCharacterMovementComponent.h */
private:
void UpdateClimbDashState(float deltaTime);
void StopClimbDashing();
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::PhysClimbing(float deltaTime, int32 Iterations)
{
// ...
UpdateClimbDashState(deltaTime); // <--
ComputeClimbingVelocity(deltaTime);
// ...
}
void UCustomCharacterMovementComponent::UpdateClimbDashState(float deltaTime)
{
if (!bWantsToClimbDash)
{
return;
}
CurrentClimbDashTime += deltaTime;
// Better to cache it when dash starts
float MinTime, MaxTime;
ClimbDashCurve->GetTimeRange(MinTime, MaxTime);
if (CurrentClimbDashTime >= MaxTime)
{
StopClimbDash();
}
}
void UCustomCharacterMovementComponent::StopClimbDashing()
{
bWantsToClimbDash = false;
CurrentClimbDashTime = 0.f;
}
void UCustomCharacterMovementComponent::StopClimbing(float deltaTime, int32 Iterations)
{
StopClimbDash();
// ...
}
Let’s now override the velocity inside ComputeClimbingVelocity
to make it follow the curve. To get the current value on the curve, we call GetFloatValue
with the CurrentClimbDashTime
as the parameter. The velocity is then the float value multiplied by the dash direction.
However, just using the dash direction as it is won’t allow us to rotate around corners. We need a way to align the direction with the surface. A solution is to create a plane defined by the surface normal and project the direction into it. But we don’t want to take into consideration the Z component of the surface normal, so get the 2D part of it.
/* CustomCharacterMovementComponent.h */
private:
void AlignClimbDashDirection();
/* CustomCharacterMovementComponent.cpp */
void UCustomCharacterMovementComponent::ComputeClimbingVelocity(float deltaTime)
{
// ...
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
if (bWantsToClimbDash)
{
AlignClimbDashDirection();
const float CurrentCurveSpeed = ClimbDashCurve->GetFloatValue(CurrentClimbDashTime);
Velocity = ClimbDashDirection * CurrentCurveSpeed;
}
else
{
constexpr float Friction = 0.0f;
constexpr bool bFluid = false;
CalcVelocity(deltaTime, Friction, bFluid, BrakingDecelerationClimbing);
}
}
// ...
}
void UCustomCharacterMovementComponent::AlignClimbDashDirection()
{
const FVector HorizontalSurfaceNormal = GetClimbSurfaceNormal().GetSafeNormal2D();
ClimbDashDirection = FVector::VectorPlaneProject(ClimbDashDirection, HorizontalSurfaceNormal);
}
As mentioned earlier, we need to update some variables to handle the character moving at a higher speed. Let’s increase both the ClimbingRotationSpeed
and the ClimbingSnapSpeed
, multiplying them by the difference between the current speed and the MaxClimbingSpeed
.
FQuat UCustomCharacterMovementComponent::GetClimbingRotation(float deltaTime) const
{
// ...
const float RotationSpeed = ClimbingRotationSpeed * FMath::Max(1, Velocity.Length() / MaxClimbingSpeed);
return FMath::QInterpTo(Current, Target, deltaTime, RotationSpeed);
}
void UCustomCharacterMovementComponent::SnapToClimbingSurface(float deltaTime) const
{
// ...
const float SnapSpeed = ClimbingSnapSpeed * FMath::Max(1, Velocity.Length() / MaxClimbingSpeed);
UpdatedComponent->MoveComponent(Offset * SnapSpeed * deltaTime, Rotation, bSweep);
}
We’re done with the logic. For the animation, the setup will be pretty similar to what we have already done in the Animation section, so I’ll keep it as short as possible. Note that I’ll be using a 2D blendspace, but a 1D Blendspace is sufficient, as there’s no need to account for the speed.
Create a new blendspace and setup the animation. As the for the axis values I used a (-1, 1) range, since we are only interested in the direction.
Create a new bool
IsClimbingDash
in the animation blueprint.
Create a new Vector2D
ClimbDashVelocity
. Computed in the same way as theClimbVelocity
.
Add the transitions between Climb and Climb Dash based on the blueprint variable
IsClimbDashing
.
Hook up the
ClimbDashVelocity
to the Climb Dash blendspace.
That’s it! We can climb dash in any direction, playtest the dash speed in runtime by editing a curve, and transverse rough terrain, including sharp corners and bumps. In the end, as we are just overriding the velocity, all the other mechanics like climbing up ledges and climbing down to the floor work perfectly fine while dashing!
Climb dashing into a ledge.
Inverse Kinematics
When exploring the world, climbing mountains and ruins, the character will run into a vast number of terrain types. An issue then arises. The animation fails to adapt. Sometimes the leg hangs around in the air, while the hands touch an invisible wall. We don’t want that, but how do we fix these issues?
Using Inverse Kinematics (IK), we can procedurally adjust the animation to adapt it to the terrain. We can think of this process as tracing from the hands and feet and moving each part to their respective hits. Of course, the problem of solving IK is not that simple, and an entire post could be written just for this subject. We won’t be discussing this in-depth, but I’ll point you in the right direction.
Comparison between without IK (on the left) and with IK (on the right).
The good news is that Unreal has recently released Control Rig, a toolset to animate and rig animations in real-time that bundles with powerful IK systems. If you are using UE5, the default third-person project already comes with foot IK. Although it is meant to correct the foot’s position vertically, I used it as a template for my solution. It’s far from perfect or complete, but it’s a simple solution that adds a lot to the animation.
Below you can find the screenshots. The idea is to modify the hands and feet additively. To do this, we trace and find the offset from where it hit, which will be the target. Then, we lerp the target vector by deltatime to make it a smooth motion and wire the result to the Modify Transforms node with additive global mode. After doing this for each bone, plug the transforms into the Full Body IK node. Depending on your skeleton, you may need to exclude and add additional configuration to some bones. By increasing the Pull Chain Strength and Alpha, both hands and feet can force the mesh to stay close to them if you change the Root Behaviour to Pre Pull.
This is it for this tutorial! We covered quite a few things, so I hope it is of great value to you! If you have questions, need help figuring something out, or have a suggestion, please feel free to comment below 😃.
Simply awesome! As a 3D artist, I need five years to understand and implement your content, thanks a lot, Vitor!
Thank you! xD I'm sure you can do it
Thank you for this detailed tutorial! This came at the perfect timing as I've just started implementing my own custom movement modes.
Having a problem where all the code compiles and the game starts but as soon as i attempt to move the editor freezes and crashes, saying it was from the IsClimbing() method
*fixed
i have the same issue how did you fix it?
Absolutely amazing! The tutorial is very straightforward and helped me figuring out a lot. The format is perfect too. Thanks a lot!
Great tutorial! Could you also post a screenshot for the HandTrace/FootTrace logic in the control rig? I created my own but I'm curious how my implementation might differ from yours.
Hey, thanks for the tutorial - really great resource.
How did you generate the images for the 'Detecting Surfaces' and 'Entering Climbing Mode' sections with the capsule and blue spheres? Was there a debugging toggle you created in the code or from ue?
Thanks! I used the DrawDebugCapsule and
DrawDebugSphere
UE functions.Awesome tutorial, thank you for sharing Vitor!
A guy built a procedural climbing system on Unity, with animations based on 2 key frames only! Here is a youtube link and the dev log going with it: https://www.youtube.com/watch?v=BzyxhuG7aaM https://www.uproomgames.com/dev-log/procedural-climbing
This looks pretty awesome, and not being dependent on animations for transitions/friction is pretty awesome. Have you ever considered exploring this in UE5?
Thanks Neo!
It's pretty awesome indeed, thanks for sharing. Procedural animation is a huge interest of mine, and the GDC video the guy pointed out was the basis for my undergraduate thesis. I'll definitely write something about it in the future!
Oh great! I want to try to build a procedural climbing system, but being quite new to Unreal Engine, this is going to be... Hard and dirty :D Can't wait to read you! Happy coding!
Hello Victor! Thank you very much for such amazing tutorial!
Right now I'm trying to properly implement IK bones while climbing. Is it possible, to share a screanshot for example, show how you doing sphere traces and find Hit Location for Hands and legs targets? Thanks!
Could you post pics of your IK trace functions? The whole tutorial went smooth until I had to do that. Getting all sorts of strange results.
Hey, is there any link to a demo project, would like to just try it out without copying ^^ ? Also do you have a blueprint version instead of c++ or can/could you use both in UE4 ?
Hey Julian, unfortunately I can't share the demo project as it is since the animations are from a paid asset. The source code is on GitHub though! Sorry, I don't have a blueprint version, but I believe it is possible to recreate it in blueprint!
You could try this tutorial: https://www.youtube.com/watch?v=DY6Wx67A_3o
Hi Vitor! Great job, the results are amazing, and very well explained tutorial, but I followed step by step, and notice some things would not work if I just followed here(I compared the code here and from github), if you want I can share with you my annotations on those issues.
Thank you anyway for sharing!
Of course! Please do. If you can, please add me on discord
PurpleSkyD#3701
. Thanks a lot, I appreciate it!This is a very interesting tutorial. Good homework for me. I'm trying to merge this with existing character (of course)... The main problem I'm having is the animBP in the editor cannot get the 'IsClimbing' / 'IsClimbDashing' variables from the character's updated movement component, even though the code compiles just fine. Starting out, I got some errors that the UProperty additions needed to have a Category defined, but I added those: UFUNCTION(Category = "Character Movement: Climbing", BlueprintPure) bool IsClimbing() const;
[Update: Works now.]
Minor points: In the tutorial, there's a part where you skip out early from the animation montage by 0.5s (I couldn't find how to do this, but instead I set an Anim End Time .5s shorter in the Animation Segment panel, shown in link: [https://imgur.com/a/FVnIjKV]
An easy step to miss: Assign the Montage and Curve assets in the blueprint of character's movement component details: [https://imgur.com/a/vk6XG8v]
For the blendspaces in the Output Animation Pose part of the state machine, I get warnings: "Node Blendspace Player 'BS_ClimbMoving' uses Blueprint to update its values, access member variables directly or use a constant value for better performance."
Hi, thanks for checking the tutorial!
1- Did you make sure you are storing the custom CMC with the correct variable type in the Anim blueprint? Does the
GetCustomCharacterMovement
return the new custom CMC? Maybe try to close the editor and rebuild the project.2- Sorry for that, I need to make it more clear. The setting Blend Out Trigger Time is inside the Anim Montage (Asset Details), on the Blend Option section.
3- It seems like you are doing some computation when passing the values to the blendspace node. If you want to read more about it: Animation Fast Path.
We could probably chat on Discord or something than clogging up your tutorial page with confusion.
Sure!
PurpleSkyD#3701
on discord.Could you plase also upload the character to github? i can't get it to work in character.h i always get : Unrecognized type 'UCustomCharacterMovementComponent' - type must be a UCLASS, USTRUCT, UENUM, or global delegate.
Sure! I'll upload it in a minute. The code on GitHub uses the prefix My instead of Custom. Maybe this is why it doesn't recognize the type?
yeah the error is gone now and complies everything but i am getting a nullptr error from UMyCharacterMovementComponent as soon as i try to move my character. someone here in the comment section had a similar issue but the comment on this one only says *fixed
actually the GetCustomCharacterMovement function returns nullptr.
Do you mind sharing the transition rules from air/ground state aliases please ?
Of course! Here it is
Would you be able to make a video tutorial on this? It is a very good tutorial but some people learn better with a video (no matter how many images.)
Thanks for the feedback! Yeah, I understand that, but I haven't done a video tutorial before and I'm already working on another project. I would be grateful if someone made a video tutorial out of this. Maybe even blueprint focused!
Do you think i should manually code it? Or just download your source files?
Well, I really hope that people can learn from this tutorial and use that knowledge to create stuff on their own. I would recommend you follow it while taking your time to understand (and even research further) how things work and naturally code along.
Ok, thank you. Also one more thing, what enumeration c++ class did you use? I know there are a few.
Sorry, I don't think I understood your question. If you are referring to the
ECustomMovementMode
, it's an enum created by us.i will explain better, when i am creating enum c++ class, it asks me for a type, like user defined enum and normal enum.
Oh I see. It's just a normal enum. I created an empty .h file in Visual Studio.
Okay thanks!
Hello and thank you very much for the great tutorial. When I am trying to compile the CustomCharacterMovementComponent overriding BeginPlay and TickFunction I get this weird error: This is only in the beginning, the first few rows declaring the SweepAndStoreHitResults and other capsule variables.
Creating library C:\Users\User\Documents\Unreal Projects\UdemyCPP\Binaries\Win64\UnrealEditor-UdemyCPP.patch4.lib and object C:\Users\User\Documents\Unreal Projects\UdemyCPP\Binaries\Win64\UnrealEditor-UdemyCPP.patch4.exp CustomCharacterMovementComponent.gen.cpp.obj : error LNK2001: unresolved external symbol "private: virtual void __cdecl UCustomCharacterMovementComponent::TickComponent(float,enum ELevelTick,struct FActorComponentTickFunction *)" (?TickComponent@UCustomCharacterMovementComponent@@EEAAXMW4ELevelTick@@PEAUFActorComponentTickFunction@@@Z) CustomCharacterMovementComponent.cpp.obj : error LNK2001: unresolved external symbol "private: virtual void __cdecl UCustomCharacterMovementComponent::TickComponent(float,enum ELevelTick,struct FActorComponentTickFunction *)" (?TickComponent@UCustomCharacterMovementComponent@@EEAAXMW4ELevelTick@@PEAUFActorComponentTickFunction@@@Z) MainCharacter.cpp.obj : error LNK2001: unresolved external symbol "private: virtual void _cdecl UCustomCharacterMovementComponent::TickComponent(float,enum ELevelTick,struct FActorComponentTickFunction *)" (?TickComponent@UCustomCharacterMovementComponent@@EEAAXMW4ELevelTick@@PEAUFActorComponentTickFunction@@@Z) C:\Users\User\Documents\Unreal Projects\UdemyCPP\Binaries\Win64\UnrealEditor-UdemyCPP.patch4.exe : fatal error LNK1120: 1 unresolved externals
I've fixed it :) Since I am not starting on a new project it turns out that I was overriding the wrong Tick in my Actor :)
I'm also getting issues with the game crashing due to the isClimbing() method failing. I think it's because MovementComponent in CSCharacter.cpp is returning null.
I read through the comments and saw that there were some others with similar issues - is there a solution to the problem?
Reloading the character blueprint seemed to do it for me
Hi, sorry about that!
Could you show me a few lines of the error so I can help you figure it out?
Hey Vitor!
Little bit late but I was trying to implement it and I'm receiving the same error as well.
Tried reloading the blueprint and everything but with no avail, unfortunately. Any idea?
This is the full trace error reporter returns:
` Unhandled Exception: EXCEPTIONACCESSVIOLATION reading address 0x00000000000001c4
UnrealEditorDwarf!UClimbComponent::IsClimbing() [D:\Unreal Projects\DWARF\Source\Dwarf\CustomComponents\ClimbComponent.cpp:471] UnrealEditorDwarf!ADwarfCharacter::MoveForward() [D:\Unreal Projects\DWARF\Source\Dwarf\DwarfCharacter.cpp:111] UnrealEditor_Dwarf!TBaseUObjectMethodDelegateInstance::Execute() [D:\UE5.0\Engine\Source\Runtime\Core\Public\Delegates\DelegateInstancesImpl.h:594] UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorCore UnrealEditorCore UnrealEditorCore UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorEngine UnrealEditorUnrealEd UnrealEditorUnrealEd UnrealEditor UnrealEditor UnrealEditor UnrealEditor UnrealEditor UnrealEditor kernel32 ntdll ` Thanks!
Hey Ruben! Could you try creating an entirely new blueprint character that derives from the new character class? Let me know if this helps!
Hi again!
I sorted it out just adding a check in the MoveForward & MoveRight functions.
Something like this:
FVector Direction; if (ClimbComponent && ClimbComponent->IsClimbing()) { Direction = FVector::CrossProduct(ClimbComponent->GetClimbSurfaceNormal(), GetActorUpVector()); } else { Direction = GetControlOrientationMatrix().GetUnitAxis(EAxis::Y); } AddMovementInput(Direction, Value);
However, it seems now I'm struggling with the animation blueprint.
When character is in the air, and I press the climb key (E), transition From Air -> Air Grab -> Climb seem to not be working as expected. Seems to be stucked in the fall loop.
In From Air I have the following checks: Locomotion, Jump, Fall Loop, Land.
From Ground: Locomotion, Land. (This one works).
Weird :D
heyo!
thanks for making this tutorial on zelda climbing, I've tried a few different tutorials like it but yours has come out the best so far, good job!
I was curious which animation you used for the ledge vault. I'm using the same animation package you referenced in your tutorial but I couldn't find that one, and I scoured mixamo and none of those looked quite right either, but I could have just missed it or it looked different than your gif.
Let me know, thanks! (DM'd you on reddit, but I figured I'd try here too)
Hi, thank you very much!
It's the Climbing_ClimbUpAtTop_RM animation from the animation package we're using. Sorry about that, I'll update the tutorial with this information.
thank you!
Any idea why my character wont fully climb up the ledge and is always falling down after the animation is beeing played. i am using the same pack and rm is enabled. anyway big thanks
I alse meet the same problem ,have got solution to fix it?
Excuse me, but here should be: (changed function parameter). Correct me if I'm wrong.
/* CustomCharacterMovementComponent.h */ private: bool IsFacingSurface(const float Steepness) const;
/* CustomCharacterMovementComponent.cpp */ bool UCustomCharacterMovementComponent::CanStartClimbing() { // ... if (HorizontalDegrees <= MinHorizontalDegreesToStartClimbing && !bIsCeiling && IsFacingSurface(VerticalDot)) // Add IsFacingSurface { return true; } // ... }
bool UCustomCharacterMovementComponent::IsFacingSurface(const float Steepness) const { constexpr float BaseLength = 80.0f; const float SteepnessMultiplier = 1 + (1 - Steepness) * 5;
}
Perfect!! Thanks for your contribution :D.
Really cool work. Not only did you write a tutorial that let's someone make a very cool implementation of something, but it also meaningfully educational on the topic it covers. Really well done.
Can I get the project? I owned the animation used from the video project. I want to test out the climbing project you made. Thank you :)
Is there a way to control movement using the camera while climbing? For example you're holding down W to go up and then you turn the camera right, it would then move you upright if that makes sense.
Sure! It all comes down to how you handle the
AddMovementInput(Direction, Value)
method inside the character class. In your case, the Direction computation takes into account where the camera is pointing relative to your character.Thanks for this amazing tutorial! Having an issue with the climbing animation. Wondering if you could help. When my character is climbing, the arms animate fine, but the legs seem to be just dangling. When I remove the Control Rig in the AnimGraph, this problem goes away. I have set the ShouldDoIKTrace bool to NOR on IsFalling and IsClimbing, so I am not sure why is would still do the IK trace.
Thank you Aaron! Have you take a look inside the Control Rig? Maybe the
ShouldDoIKTrace
bool is not being used properly.i have same issue.. I dont know what to do. can you show that?
Hi, thanks for this tutorial. Let me know if this whole climbing system is activated by a key? How can I make it start scaling automatically=?
Hey! Yes, but it can be easily modified. You just need to call
TryClimbing
in the character class with your arbitrary logic. One such logic is to check if the player is pressing forward and the character is within a certain angle from the wall.Hey, I love this and I have it set up so my character starts climbing automatically when you walk up to a wall. I do have one problem, possibly related to animation. I'm using the climb up ledge animation from the tutorial linked below, but it seems the root motion doesn't get high enough onto the wall or the character goes into the falling state too early. Something weird is happening. Any idea what might be the cause, or how to fix it without using a different animation (can't afford paid animations)?
The tutorial: https://www.youtube.com/watch?v=ki6o3mdu0T8
Hi Vaani! Unfortunately, this a downside of relying on the root motion. The animation become responsible for moving the actor. In this case, the animation tries to go up and forward, but the character's capsule is still colliding with the wall.
There are many ways to solve this issue. One thing you can try is to use Motion Warping.
I somewhat figured out what is really happening. Towards the end of the animation, the character leaves the climbing state and scales the capsule back up, but it's before the animation gets on top of the ledge.
Well no, scratch that. It was just a coincidence that not scaling the capsule back up fixed it. Somehow, if I just return from the "StopClimbing" function if the montage is playing, that doesn't work. Guess I'll have a go at motion warping lol
Thanks for the tutorial/Github source code. Much appreciated. I am currently trying to get it to work in multiplayer. We will see how it goes. Currently some things replicate and other things don't. So I am slowly trying to figure out what to replicate.
I'm adding climbing to a c++ version of ALS. It turned out that I only needed to replicate 3 functions to get it to work on multiplayer. TryClimb, CancelClimb, and PhysClimb.
Hi Bert, did you need to just implement a Run on Server for these functions or did you implement Multicast? I did the Run on Server and it works as expected on the server (both server and client see correct climb movement) but on the client, the character grabs the wall but input movement is not working - i.e. character is just stuck. I can stop climbing. I think it's the PhysClimb function not working as required in this setup. Thanks :-)
This was probably the best wall climbing tutorial I've ever came across. I just have one issue with the trace. When I am moving a long a wall with a concave edge (around 90 degrees or less), it will only update the hit location result when I am moving in one direction. The trace will not register the edge or the face on the other side of the mesh, preventing me from moving any further if I try to move in the other direction. I'm not sure how to fix that.
Having a similar issue - if I move to certain sides (or start on those sides) and move down the trace doesn't register a hit - this is most apparent when moving down. Anyone have any ideas?
I downloaded your source files & I replaced the names with my own & for some reason nothing is being recognized by the engine.
Excellent tutorial, thx a lot! Do u mind sharing the HandTrace function code? Thx in advance!
Many thanks for the tutorial. I have never found ANYTHING like this in the public domain. Could you additionally share screenshots of inverse kinematics? Available screenshots are incomplete
I just want to use the downloaded files to save myself hours, how do I do this.
Great tutorial! I'm just starting learning UE5 and I really needed something like this! However, I have a question: if I'm handling the binding input action in the controller, how should I handle it?
This is a really good tutorial, thank you for making it. It has been a big help with my project, I have been doing something very similar except in blueprints and it seems our approaches were almost identical.
But there are a few things about the climbing geometry I can't quite figure out. I made my climbing gyms out of brushes that I then converted to static meshes, and when I do my capsule trace I only ever get one hit even if I am on a corner or something. I have made sure to configure the trace channels properly, the geometry does not block the trace, so it's not for that reason. I tried making some level geometry out of the shape actors instead, and I get multiple hits with those, even on the same shape. So I guess some of your climbing geometry, the blocky stuff, is made of those shape actors. But what about the bumpy surface at the start of the 'Computing Surface Info' chapter? Or the buildings you use in your video? Are they not single static meshes?
Basically my question is: how did you manage to get multiple hits from a single static mesh?
If the code from git hub is used there is a build error.
SweepAndStoreWallHits should not be void! We need to return the CurrentHits. Do you have a fix?
void UMyCharacterMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
}
void UMyCharacterMovementComponent::SweepAndStoreWallHits() { const FCollisionShape CollisionShape = FCollisionShape::MakeCapsule(CollisionCapsuleRadius, CollisionCapsuleHalfHeight);
}
Try this Brent!
void URFCharacterMovementComponent::SweepAndStoreWallHits() { // Create a capsule shape for the sweep const FCollisionShape CollisionShape = FCollisionShape::MakeCapsule(CollisionCapsuleRadius, CollisionCapsuleHalfHeight);
}
Hi Vitor! This is really Awesome Tutorials! But I got a problem That is character still get weird rotation issues when climbing event I followed all of your code! It seems like because you use Multi Capsule Channel to get the HitResults and some time it will return weird location ( even character climbing on a flat wall ,here is the pic https://file.notion.so/f/f/574ea1a1-ae45-4886-8c77-b90d4b0a3dd5/b7915b9e-7eaf-4884-8ed4-9ce80da9dc70/image.png?table=block&id=1338ff17-6228-8063-aa63-f2250c24cb13&spaceId=574ea1a1-ae45-4886-8c77-b90d4b0a3dd5&expirationTimestamp=1730764800000&signature=BIQtL6QAIoI2jaOZ2yOdaBz9UfKuN-hNuYLei49QABw&downloadName=image.png)
when character climbing to the lower part of this transparent wall, the impact point is not in front of character! and character start to rotate to weird direction. Could you help me figure this out?
https://mjj.today/i/jwKbNp