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.

multiple surface collisions

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

Green sphere is CurrentClimbingPosition with CurrentClimbingNormal facing the walls

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);
}
a little hard to see but here the capsule collider can be seen changing size when attached and detached

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