Wall Attachment
The idea is when the player presses Climb, as long as there is a surface to climb on, we'll attach the player to the wall.
We've already been setting the Movement Mode to MOVE_Custom, and _Climbing, but haven't actually done anything with that yet. Let's hook into the CharacterMovementComponent::PhysCustom by overriding it and adding our own logic.
.h
// ZCCharacterMovementComponent.h
private:
virtual void PhysCustom(float deltaTime, int32 Iterations) override;
void PhysClimbing(float DeltaTime, int32 Iterations);
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::PhysCustom(float deltaTime, int32 Iterations)
{
if (CustomMovementMode == ECustomMovementMode::CMOVE_Climbing)
{
PhysClimbing(deltaTime, Iterations);
}
Super::PhysCustom(deltaTime, Iterations);
}
void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
// Note: Taken from UCharacterMovementComponent::PhysFlying
if (DeltaTime < MIN_TICK_TIME)
return;
}
Where to attach
Since we're using a shape sweep we will most likely have a few collision points, ones on corners of one wall, and the crest of another and even a third.

To determine where we should attach, we can just average all the hits to find our final location.
Likewise for our orientation we can add up all the surface normals then normalize the aggregate vector to get an average normal.
Let's store these in some new members and make a function to calculate them.
.h
// ZCCharacterMovementComponent.h
private:
void ComputeSurfaceInfo()
FVector CurrentClimbingNormal;
FVector CurrentClimbingPosition;
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::ComputeSurfaceInfo()
{
CurrentClimbingNormal = FVector::ZeroVector;
CurrentClimbingPosition = FVector::ZeroVector;
if (CurrentWallHits.IsEmpty())
return;
for (const FHitResult& WallHit : CurrentWallHits)
{
CurrentClimbingPosition += WallHit.ImpactPoint;
CurrentClimbingNormal += WallHit.Normal;
}
// Store position as the mean of all the surface impacts
CurrentClimbingPosition /= CurrentWallHits.Num();
CurrentClimbingNormal = CurrentClimbingNormal.GetSafeNormal();
}
Then add a call to our new function in PhysClimbing
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
// Note: Taken from UCharacterMovementComponent::PhysFlying
if (DeltaTime < MIN_TICK_TIME)
return;
ComputeSurfaceInfo();
}
Now when we press Climb next to multiple surfaces, the position to attach is an average of all the hits


When to Detach
The player should detach from the wall when any of the following occurs
Input press to detatch
Climbing on a flat surface
Or if the climbing normal is zero (meaning the plane doesn't exist) since it will be used to determine other aspects of the climb
.h
// ZCCharacterMovementComponent.h
private:
bool ShouldStopClimbing();
void StopClimbing(float DeltaTime, int32 Iterations);
Implement the functions and add it to PhysClimbing()
.cpp
// ZCCharacterMovementComponent.cpp
bool UZCCharacterMovementComponent::ShouldStopClimbing()
{
const bool bIsOnCeiling = FVector::Parallel(CurrentClimbingNormal, FVector::UpVector);
return !bWantsToClimb || CurrentClimbingNormal.IsZero() || bIsOnCeiling;
}
void UZCCharacterMovementComponent::StopClimbing(float DeltaTime, int32 Iterations)
{
bWantsToClimb = false;
SetMovementMode(EMovementMode::MOVE_Falling);
StartNewPhysics(DeltaTime, Iterations);
}
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;
}
}

The last thing we need to do is make sure we don't accidentally collide with anything outside of where it makes sense while we're on the wall.
Essentially we want our capsule collider to only collide with things in our more "scrunched up" climbing pose ( like the image below)

Capsule Size
The idea is when the movement changes, if we're in the climbing mode, shrink the capsule colider.
If it changes and we're not climbing but we were we'll reset the size, make sure you character is standing upright (because who knows what position they released from the wall), and stop their movement (CharacterMovementComponent gives us this function nicely)
.h
// ZCCharacterMovementComponent.h
public:
//...
UFUNCTION(BlueprintPure)
bool IsClimbing() const;
UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta=(ClampMin="0.0", ClampMax = "72.0"))
int CollisionCapsulClimbingShinkAmount = 30;
private:
virtual void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) override;
We want to change the capsule size while on the wall.
Additionally once we're attached to the surface we want to always stay facing the surface versus what the Third Person Template is currently having us do which is rotate with the input.
While climbing we want to turn bOrientRotationToMovement off, and then re-enable it when we're not climbing
.cpp
// ZCCharacterMovementComponent.cpp
bool UZCCharacterMovementComponent::IsClimbing() const
{
return MovementMode == EMovementMode::MOVE_Custom && CustomMovementMode == ECustomMovementMode::CMOVE_Climbing;
}
void UZCCharacterMovementComponent::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode)
{
if (IsClimbing())
{
bOrientRotationToMovement = false;
// Shrink down
UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
if (Capsule)
{
Capsule->SetCapsuleHalfHeight(Capsule->GetUnscaledCapsuleHalfHeight() - CollisionCapsulClimbingShinkAmount);
}
}
const bool bWasClimbing = (PreviousMovementMode == EMovementMode::MOVE_Custom && PreviousCustomMode == ECustomMovementMode::CMOVE_Climbing);
if (bWasClimbing)
{
bOrientRotationToMovement = true;
// Reset pitch so we end standing straight up
const FRotator StandRotation = FRotator(0, UpdatedComponent->GetComponentRotation().Yaw, 0);
UpdatedComponent->SetRelativeRotation(StandRotation);
UCapsuleComponent* Capsule = CharacterOwner->GetCapsuleComponent();
if (Capsule)
{
Capsule->SetCapsuleHalfHeight(Capsule->GetUnscaledCapsuleHalfHeight() + CollisionCapsulClimbingShinkAmount);
}
// After exiting climbing mode, resets velocity and acceleration
StopMovementImmediately();
}
Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);
}

If you're trying to figure out how to see the capsule component, I find it useful to just type in 'hidden' in the BP's details

Last updated