Move Physics
One of the best ways to learn is stand on the shoulders of giants and look at how they did something. So lets crack open one of the ::Phys() functions inside UE to see what they do.
Using PhysFlying as an example
PhysFlying() is the simplest out of the three (walking, swimming, flying) giving us the barebones needed to perform our physics
github link (requires EpicGames' approval) here
// redacted and commented
void UCharacterMovementComponent::PhysFlying(float deltaTime, int32 Iterations)
{
// 1) Reset any changes made to Velocity last frame by Root Motion
// so we aren't adding more velocity than intended
// 2) Calculate Velocity this frame as long as Root Motion isn't enabled
// 3) Add any Root Motion Velocity this frame
// 4) Scale Velocity for framerate independence
// 5) Save the old component location so we can update after adjusting it
// for collisions
// 6) Move component
// 7) Handle collision and slide if necesssary (makes it so we don't get
// stuck on a wall if it makes sense that our velocity would instead
// slide along the wall)
// 8) Update Velocity based off actual distance travelled (collision or
//slide could have adjusted it)
}
Most of the above is going to be nicely handled by functions already existing within the UCharacterMovementComponent
Just copying the comments into our PhysClimbing function so we can visually group them you can see there's really 4 steps
compute velocity
cache location
move
update velocity based off new location
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
// Note: Taken from UCharacterMovementComponent::PhysFlying
if (DeltaTime < MIN_TICK_TIME)
return;
ComputeSurfaceInfo();
if (ShouldStopClimbing())
{
StopClimbing(DeltaTime, Iterations);
return;
}
// Compute Velocity
{
// 1) Reset any changes made to Velocity last frame by Root Motion
// 2) Calculate Velocity this frame as long as Root Motion isn't enabled
// 3) Add any Root Motion Velocity this frame
// 4) Scale Velocity for framerate independence
}
// 5) Save the old component location
// Move
{
// 6) Move component
// 7) Handle collision and slide if necesssary
}
// 8) Update Velocity based off actual distance travelled
}
Lets stub out those functions so we can clean up PhysClimbing()
.h
// ZCCharacterMovementComponent.h
private:
//...
void ComputeClimbingVelocity(float DeltaTime);
void MoveAlongClimbingSurface(float DeltaTime);
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::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;
}
}
Computing Climbing Velocity
Our Velocity while climbing plays a huge part into the feel of the game for this mechanic. We're going to mimic Genshin and BOTW which make the character quite slow and feel heavy while climbing as I imagine it's difficult.
If the feel of your game was that climbing was super easy, maybe the character could speedily traverse the wall with ease.
Acceleration Considerations
What we're really talking about is acceleration and deceleration.
High Acceleration and Deceleration will make it feel more responsive and snappy, like your character has full control over their actions. When they press right they go right.

Low Acceleration and Deceleration will make it feel more weighted, like the character has to put a lot of effort to perform the action. This will create moments of drag in opposite directions as their acceleration changes..

This "more weighted" feel is what we want. Lets make some properties to let us control the Speed, Acceleration, and Deceleration (i.e. Braking) while climbing.
.h
// ZCCharacterMovementComponent.h
private:
// ...
virtual float GetMaxSpeed() const override;
virtual float GetMaxAcceleration() const override;
// Clamping to similar max values in the base class (which we'll be overriding)
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;
The reason for overriding the two accessors is because we're going to leverage the built in function CharacterMovementComponent::CalcVelocity() which is used for all current physics already instead of writing ours from complete scratch.
.cpp
// ZCCharacterMovementComponent.cpp
float UZCCharacterMovementComponent::GetMaxSpeed() const
{
return IsClimbing() ? MaxClimbingSpeed : Super::GetMaxSpeed();
}
float UZCCharacterMovementComponent::GetMaxAcceleration() const
{
return IsClimbing() ? MaxClimbingAcceleration : Super::GetMaxAcceleration();
}
And finally our velocity computation
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::ComputeClimbingVelocity(float DeltaTime)
{
RestorePreAdditiveRootMotionVelocity();
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
constexpr float Friction = 0.f;
constexpr bool bFluid = false;
// Will use our overridden functions when climbing
CalcVelocity(DeltaTime, Friction, bFluid, BrakingDecelerationClimbing);
}
ApplyRootMotionToVelocity(DeltaTime);
}
Move, Collision, and Slide
Next up is to actually move the character.
We'll be utilizing CharacterMovementComponent::SafeMoveUpdatedComponent() which takes in a rotation so first we need to figure out how we should be rotated.
Climbing Rotation
When you're climbing something you're always facing the surface, so it would make sense that the characters forward should always be pointing opposite of the Surface Normal.
Additionally we don't want to snap immediately to the rotation, but instead want to smoothly transition as we climb over various surfaces.
.h
// ZCCharacterMovementComponent.h
private:
// ...
FQuat GetSmoothClimbingRotation(float DeltaTime) const;
UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "1.0", ClampMax = "12.0"))
float ClimbingRotationSpeed = 6.f;
Our character's forward is the X-axis so we want to create a rotation based off the Surface Normal but only caring about the X-axis, and then reverse it.
.cpp
// ZCCharacterMovementComponent.cpp
FQuat UZCCharacterMovementComponent::GetSmoothClimbingRotation(float DeltaTime) const
{
// Smoothly rotate towards the surface
const FQuat Current = UpdatedComponent->GetComponentQuat();
const FQuat Target = FRotationMatrix::MakeFromX(-CurrentClimbingNormal).ToQuat();
return FMath::QInterpTo(Current, Target, DeltaTime, ClimbingRotationSpeed);
}
Move & Collision/Sliding
Now we can finish the movement logic
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::MoveAlongClimbingSurface(float DeltaTime)
{
// Aside from getting our own rotation this logic is exactly from PhysFlying()
const FVector Adjusted = Velocity * DeltaTime;
FHitResult Hit(1.f);
SafeMoveUpdatedComponent(Adjusted, GetSmoothClimbingRotation(DeltaTime), true, Hit);
if (Hit.Time < 1.f)
{
HandleImpact(Hit, DeltaTime, Adjusted);
SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true);
}
}
This on its own will get us most of the way there however there is one issue, and thats sometimes we fall off the wall of odd shapes or sharp corners.

And this is because we aren't actually doing anything to keep us stuck to the wall.
Right now we're just moving you with respect the surface plane, so sometimes our Velocity will be shooting right off the edge.
You might remember we calculated the CurrentClimbingPosition and CurrentClimbingNormal for surface orientation, and now we're going to use those to keep the character stuck on the surface.
Sticking to Surface
After we move the character based off their Velocity and Collisions we're actually going to move them one more time, but this time towards the surface (sticking them to it).
We want to move the character to where the surface collision is, but we can't simply move towards the inverse Surface Normal because it could be at an angle depending on where the Shape Sweep hits.
So, we need to
Take the vector from player to surface collision (yellow)
and Project it onto the character's Forward vector (red)
which will move the character only as far as the collision point on the surface is, in the forward direcetion


Let's create a function to handle this surface snapping
.h
// ZCCharacterMovementComponent.h
private:
// ...
void SnapToClimbingSurface(float DeltaTime) const;
UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "60.0"))
float ClimbingSnapSpeed = 4.f;
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::SnapToClimbingSurface(float DeltaTime) const
{
const FVector Forward = UpdatedComponent->GetForwardVector();
const FVector Location = UpdatedComponent->GetComponentLocation();
const FVector ForwardDifference = (CurrentClimbingPosition - Location).ProjectOnTo(Forward);
constexpr bool bSweep = true;
const FQuat Rotation = UpdatedComponent->GetComponentQuat();
UpdatedComponent->MoveComponent(ForwardDifference * ClimbingSnapSpeed * DeltaTime, Rotation, bSweep);
}
void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
// ...
// ...
// Add to the very end to snap to the surface
SnapToClimbingSurface(DeltaTime);
}
This works! however if notice our capsule collider is smashed up against the surface

When traversing surfaces of different shapes and edges we might be twitching and bumping into collisions which will make the movement seem a little jittery and studder.
Instead if we offset the final snap position by some amount we could make a much smoother climbing movement.
.h
// ZCCharacterMovementComponent.h
private:
// ...
UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "90.0"))
float ClimbingDistanceFromSurface = 45.f;
Now update SnapToClimbingSurface to offset the final forward position
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::SnapToClimbingSurface(float DeltaTime) const
{
const FVector Forward = UpdatedComponent->GetForwardVector();
const FVector Location = UpdatedComponent->GetComponentLocation();
const FVector ForwardDifference = (CurrentClimbingPosition - Location).ProjectOnTo(Forward);
// Larger the ClimbingDistanceFromSurface, the further back we offset
const FVector Offset = -CurrentClimbingNormal * (ForwardDifference.Length() - ClimbingDistanceFromSurface);
constexpr bool bSweep = true;
const FQuat Rotation = UpdatedComponent->GetComponentQuat();
// use the Offset vector for the move instead of ForwardDifference
UpdatedComponent->MoveComponent(Offset * ClimbingSnapSpeed * DeltaTime, Rotation, bSweep);
}

Now we stick to walls without falling off!

Last updated