Can I please get some help with this code for moving along a wall?

Can I get some help on this? Coding isn’t my strong suit, I’m more of an artist, but I’m really trying to improve. I’ve done a ton of stuff in Blueprint, but I want to get better at creating things in c++. I’ve been working on a parkour system, and it works really great… aside from this bit for moving sideways along the wall. The character rotates, goes behind the wall, through the wall, etc., and I’m pretty sure it’s in my math. Getting the point I want to move to works , which is nice, but actually moving is a disaster.

I just have no idea and I’ve been trying to figure it out for a while.

Thank you so much!



        RightV = GetRootComponent()->GetRightVector();
        FVector ForwardV = GetRootComponent()->GetForwardVector();
        PlayerLocation = GetRootComponent()->GetComponentLocation();

        x = RightV * ReachMultiplier;
        x.Z = x.Z + AddStartHeight;

        if (Left)
        {
            x.X = x.X * -1.0;
            x.Y = x.Y * -1.0;
        }

        EndLocation = x + PlayerLocation;

        FVector v = FVector(0.0, 0.0, 120.0); //how high starting point is drawn above character

        FVector HeightStartLocation = (ForwardV * 10.0) + EndLocation + v; //default 10
        FVector HeightEndLocation = (ForwardV * 50.0) + PlayerLocation + x; // default 50


        const bool Hit2 = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), HeightStartLocation, HeightEndLocation, StrafeRadius, ETraceTypeQuery::TraceTypeQuery1, false, ActorsToIgnore, EDrawDebugTrace::Type::ForOneFrame, HitItem2, true);
        
        if (Hit2)
        {
            WallLocation = HitItem2.ImpactPoint;
            WallNormal = HitItem2.Normal;

            ClimbSidewaysLocation = WallNormal * 30.0 + WallLocation;
            ClimbSidewaysLocation.Z = GetRootComponent()->GetComponentLocation().Z;

            ClimbSidewaysRotation = UKismetMathLibrary::MakeRotFromX(WallNormal * -1.0);
            ClimbSidewaysRotation.Pitch = 0.0;
            bSuccess = true;
            return;

        }

I separated the wall detection with the movement in order to be able to add a montage or whatever between them (more flexibility).

void APlayerCharacter::FinishClimbSideways()
{
    FLatentActionInfo LatentInfo;
    LatentInfo.CallbackTarget = this;

    UKismetSystemLibrary::MoveComponentTo(RootComponent, ClimbSidewaysLocation, ClimbSidewaysRotation, true, true, 0.2, false, EMoveComponentAction::Type::Move, LatentInfo);


    CanClimb = false;
    InClimbingMode = true;
    IsClimbing = false;

    UE_LOG(LogTemp, Warning, TEXT("Finished climb sideways."));
}

There’s honestly too much going on and too little code visible to figure anything out.

        RightV = GetRootComponent()->GetRightVector();
        FVector ForwardV = GetRootComponent()->GetForwardVector();
        PlayerLocation = GetRootComponent()->GetComponentLocation();

Depending on how you rotate your character in animations, this rotation might not give you the rotation you want to work with? That would make a lot of sense if your rotation is glitching out. Instead of using the rotation vector of the root component you could try to use the rotation vectors of the detected wall to rotate to and move along.

If left you can invert the entire right vector.

After that do

 EndLocation = x + PlayerLocation + FVector(0.f, 0.f, AddStartHeight);

Assuming this normal is never giving ridiculous results, it should not.

Also make sure you are not mixing up rotations in local and absolute space.

Sorry, I didn’t want to dump a ton of code at once, I thought it’d be hard to go through.

basically, the function first checks if there is anything directly left or right, and if not, then it checks if it can move along the ledge the player is currently hanging on. That’s what the top screenshot is showing.

It’s probably ugly and messy as I am definitely still learning, so please bear with me. Having the help would really be great.

Right now, I’m not using any animation for the climb, since I didn’t want it to interfere with things. I just wanted to move the character to the correct spot and keep them there. They are using an anim montage (the climb/hang you see) but it’s just paused from the start of climbing.

I tried using the rotation of the wall first, but there are a ton of objects to climb and many have different rotations, so I thought that wouldn’t be a good idea.

The reason I didn’t invert the entire vector for right is because it was flipping the z value so that the sphere trace was going opposite of the z axis direction I wanted it to. The AddStartHeight is just a final tweak to ensure the hands of the character line up correctly with the top of the wall. So z value might be 200 or -200, and adding the AddStartHeight would make it 270 on the right and -130 on the left. Or at least that’s what was happening.

Here is the complete code for the ClimbSideWays function:

void APlayerCharacter::ClimbSideways(bool Left, float ReachMultiplier, float StrafeRadius, float AddEndHeight, float AddStartHeight, bool& bSuccess)
{
    bSuccess = false;

    if (IsClimbing)
    {
        return;
    }

    IsClimbing = true;

    FVector RightV = GetRootComponent()->GetRightVector();
    FVector PlayerLocation = GetRootComponent()->GetComponentLocation();
    FVector x = RightV * ReachMultiplier;
    x.Z = x.Z + AddStartHeight;

    if (Left)
    {
        x.X = x.X * -1.0;
        x.Y = x.Y * -1.0;
    }

    FVector EndLocation = x + PlayerLocation; 
    FVector StartLocation = PlayerLocation;
    StartLocation.Z = StartLocation.Z + AddEndHeight;
    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(this);

    FHitResult HitItem;
    FHitResult HitItem2;

    const bool Hit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), StartLocation, EndLocation, StrafeRadius, ETraceTypeQuery::TraceTypeQuery1, false, ActorsToIgnore, EDrawDebugTrace::Type::ForOneFrame, HitItem, true);
    
    if (Hit)
    {
        WallLocation = HitItem.ImpactPoint;
        WallNormal = HitItem.Normal;

        ClimbSidewaysLocation = WallNormal * 30.0 + WallLocation;
        ClimbSidewaysLocation.Z = HeightLocation.Z;

        ClimbSidewaysRotation = UKismetMathLibrary::MakeRotFromX(WallNormal * -1.0);
        ClimbSidewaysRotation.Pitch = 0.0;
        bSuccess = true;
        return;

    }

    else
    {
        RightV = GetRootComponent()->GetRightVector();
        FVector ForwardV = GetRootComponent()->GetForwardVector();
        PlayerLocation = GetRootComponent()->GetComponentLocation();

        x = RightV * ReachMultiplier;
        x.Z = x.Z + AddStartHeight;

        if (Left)
        {
            x.X = x.X * -1.0;
            x.Y = x.Y * -1.0;
        }

        EndLocation = x + PlayerLocation;

        FVector v = FVector(0.0, 0.0, 120.0); //how high starting point is drawn above character

        FVector HeightStartLocation = (ForwardV * 10.0) + EndLocation + v; //default 10
        FVector HeightEndLocation = (ForwardV * 50.0) + PlayerLocation + x; // default 50


        const bool Hit2 = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), HeightStartLocation, HeightEndLocation, StrafeRadius, ETraceTypeQuery::TraceTypeQuery1, false, ActorsToIgnore, EDrawDebugTrace::Type::ForOneFrame, HitItem2, true);
        
        if (Hit2)
        {
            WallLocation = HitItem2.ImpactPoint;
            WallNormal = HitItem2.Normal;

            ClimbSidewaysLocation = WallNormal * 30.0 + WallLocation;
            ClimbSidewaysLocation.Z = GetRootComponent()->GetComponentLocation().Z;

            ClimbSidewaysRotation = UKismetMathLibrary::MakeRotFromX(WallNormal * -1.0);
            ClimbSidewaysRotation.Pitch = 0.0;
            bSuccess = true;
            return;

        }
    
    }

    IsClimbing = false;
    return;
}

And then the other function is called if it finds a spot to move to:

void APlayerCharacter::FinishClimbSideways()
{
    FLatentActionInfo LatentInfo;
    LatentInfo.CallbackTarget = this;

    UKismetSystemLibrary::MoveComponentTo(RootComponent, ClimbSidewaysLocation, ClimbSidewaysRotation, true, true, 0.2, false, EMoveComponentAction::Type::Move, LatentInfo);


    CanClimb = false;
    InClimbingMode = true;
    IsClimbing = false;

    UE_LOG(LogTemp, Warning, TEXT("Finished climb sideways."));
}

I never used this method, but the last bool should be “shortest route” which you probably want to tick. I imagine that if you happen to pick the longest route the component might do a full spin to reach an angle.

When you pass ClimbSidewaysRotation to that method, does it point your character feet towards its side or feet toward the ground?

Could it be collision is stopping the rotation from arriving?

Usually it works just fine. If you have a separate collision / trace setup “Climbable” and make only certain edges climbable you have a ton of control . The Advanced Locomotion System plugin (free) does this. There’s a c++ repo of it on github.

Ohhh I didn’t think about that, but yeah, using the shortest route may help, I’ll give it a shot. I just didn’t want players sliding through walls or something if they should technically be unable to/blocked by a structure, lol.

The feet go all sorts of random directions. Sometimes the character stays upright, sometimes upside down, etc.

I do wonder if it’s an issue with collision… hrm. That may explain them spazzing out even though I had basically locked the pitch to 0?

Is there a way to sort of pull them back a bit rather than outright going into the wall?

I know there are some systems out there but I kinda really wanted to build my own for this, to be honest. I wanted it more based on physics and things like that without assigning spots on objects that are climbable.

I understand that, I also usually write my own code. Animation systems however are huge to maintain. UE has no LTS version and you are forced to constantly update the code to the latest versions, which is more maintenance than I hoped for. It’s good to take a look at ones available to at least check what challenges they faced and how they solved them. Came back to edit this post because I wanted to add that the character movement component has movement modes you can set. ALS sets the movement mode to Custom or None in some cases I believe so that it won’t interfere with the custom implementation at times. I believe that The CharacterMovementComponent initiates some kind of locking process which glues you to the “ground” when you are not walking around, such things are not desired when you are climbing and I think setting the movement mode to none might disable such interference.

You can measure character capsule width and add that as an offset to the wall’s normal vector to where the player is expected to be.
Best thing you can do is lock a player to a position but ignoring certain collision logic between player and climbable object. Otherwise many movement methods will simply halt or slow down on collision. Its not avoidable, some smaller details will always collide objects. The Pawn collision capsule will be the biggest issue. Before you start climbing something you should test if the capsule fits there or not, otherwise you cant move or glitch.

I believe I ported the ALS plugin to c++ long before or during the official port, so my ALS code differs from theirs (more compact) and contains some customizations of my own. It might be a readable example for you to study. Ill post a relevant bit of it, but the full thing is much bigger.

bool ACharacterALS::HasCapsuleComponentRoomForMantle(const FVector& InTargetLocation) const {
	if (!IsValid(GetCapsuleComponent())) {
		return false;
	}

	const FVector CapsuleOffset = FVector(0, 0, GetCapsuleComponent()->GetScaledCapsuleHalfHeight_WithoutHemisphere());
	const FVector Start = InTargetLocation + CapsuleOffset;
	const FVector End = InTargetLocation - CapsuleOffset;
	const float Radius = GetCapsuleComponent()->GetScaledCapsuleRadius();

	// Perform a trace to see if the capsule has room to be at the target location without colliding.
	FHitResult OutHitResult = FHitResult();
	UKismetSystemLibrary::SphereTraceSingleByProfile(this, Start, End, Radius, UCoreUtils::ECPPawn, false, TArray<AActor*>(), EDrawDebugTrace::None, OutHitResult, true);

	return !OutHitResult.bBlockingHit;
}
void ACharacterALS::Jump() {
	// No Super, we simply dont proceed if we are performing a movement action.
	if (GetMovementAction() != E_CharacterMovementAction::None) {
		return;
	}
	if (GetMovementState() != E_CharacterMovementState::Grounded) {
		return;
	}
	const FS_CharacterALSConfig* DTPtr = CharacterALSConfigDT.GetRow<FS_CharacterALSConfig>(CUR_LOG_CONTEXT);
	if (DTPtr != nullptr) {
		if (bHasInputAcceleration && TickCheckMantle(DTPtr->GroundedTraceSettings)) {
			return;
		}
	}

	if (GetMovementStance() == E_CharacterMovementStance::Crouching) {
		UnCrouch();
	}
	Super::Jump();
}
void ACharacterALS::StartMantle(float InMantleHeight, const FS_Movement_ComponentAndTransform& InMantleLedgeWS, E_CharacterMovementMantleType InMantleType) {
	// Validate
	if (!IsValid(MantleTimelineComponent)) {
		CUR_LOG(LogALSCustomPlugin, Error, "Invalid MantleTimelineComponent.");
		return;
	}
	if (!IsValid(GetMesh()) || !IsValid(GetMesh()->GetAnimInstance())) {
		CUR_LOG(LogALSCustomPlugin, Error, "Invalid GetMesh or GetAnimInstance.");
		return;
	}
	if (!IsValid(InMantleLedgeWS.Component)) {
		return;
	}

	// Convert the world space target to the mantle components local space for use in moving objects.
	MantleLedgeLS.Component = InMantleLedgeWS.Component;
	MantleLedgeLS.Transform = InMantleLedgeWS.Transform * (InMantleLedgeWS.Component->GetComponentTransform().Inverse());

	//Get the Mantle Asset and use it to set the new Mantle Params.
	const FS_Movement_Mantle_Asset MantleAsset = GetMovementMantleAsset(InMantleType);
	MantleParams.PositionCorrectionCurve = MantleAsset.PositionCorrectionCurve;
	MantleParams.AnimMontage = MantleAsset.AnimMontage;
	UAnimMontage* AnimMontage = MantleParams.AnimMontage.LoadSynchronous();
	if (!IsValid(AnimMontage)) {
		CUR_LOG(LogALSCustomPlugin, Error, "Invalid AnimMontage.");
		return;
	}

	// Validate mantle param pointers
	const UCurveVector* PositionCorrectionCurve = MantleParams.PositionCorrectionCurve.LoadSynchronous();
	if (!IsValid(PositionCorrectionCurve)) {
		CUR_LOG(LogALSCustomPlugin, Error, "Invalid MantleParams.");
		return;
	}

	// Update the remaining mantle params
	MantleParams.StartingPosition = FMath::GetMappedRangeValueClamped(FVector2D(MantleAsset.LowHeight, MantleAsset.HighHeight), FVector2D(MantleAsset.LowStartPosition, MantleAsset.HighStartPosition), InMantleHeight);

	MantleParams.PlayRate = FMath::GetMappedRangeValueClamped(FVector2D(MantleAsset.LowHeight, MantleAsset.HighHeight), FVector2D(MantleAsset.LowPlayRate, MantleAsset.HighPlayRate), InMantleHeight);
	MantleParams.StartingOffset = MantleAsset.StartingOffset;

	// Set the Mantle Target and calculate the Starting Offset (offset amount between the actor and target transform).
	MantleTarget = InMantleLedgeWS.Transform;

	FTransform AT = GetActorTransform();
	MantleActualStartOffset = FTransform(
		AT.GetRotation() - MantleTarget.GetRotation(),
		AT.GetLocation() - MantleTarget.GetLocation(),
		AT.GetScale3D() - MantleTarget.GetScale3D()
	);

	// Calculate the Animated Start Offset from the Target Location. 
	// This would be the location the actual animation starts at relative to the Target Transform. 
	FVector LocationOffset = MantleTarget.GetRotation().Vector() * MantleParams.StartingOffset.Y;
	LocationOffset.Z = MantleParams.StartingOffset.Z;
	LocationOffset = MantleTarget.GetLocation() - LocationOffset - MantleTarget.GetLocation();

	MantleAnimatedStartOffset = FTransform(FRotator(0.f, 0.f, 0.f), LocationOffset, FVector(1.f, 1.f, 1.f) - MantleTarget.GetScale3D());

	// Clear the Character Movement Mode and set the Movement State to Mantling
	if (IsValid(GetCharacterMovement())) {
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
	}
	SetMovementState(E_CharacterMovementState::Mantling, false);

	// Configure the Mantle Timeline so that it is the same length as the Lerp/Correction curve minus the starting position
	// , and plays at the same speed as the animation. Then start the timeline.
	float OutMinTime = 0.f;
	float OutMaxTime = 0.f;
	float TimelineLength = 0.f;

	PositionCorrectionCurve->GetTimeRange(OutMinTime, OutMaxTime);
	TimelineLength = OutMaxTime - MantleParams.StartingPosition;

	MantleTimelineComponent->SetTimelineLength(TimelineLength);
	MantleTimelineComponent->SetPlayRate(MantleParams.PlayRate);
	MantleTimelineComponent->PlayFromStart();

	GetMesh()->GetAnimInstance()->Montage_Play(AnimMontage, MantleParams.PlayRate, EMontagePlayReturnType::MontageLength, MantleParams.StartingPosition, false);
}

void ACharacterALS::EndMantle() {
	if (IsValid(GetCharacterMovement())) {
		GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	}
}

void ACharacterALS::TimelineUpdateMantle(float InBlendIn) {
	if (!IsValid(MantleTimelineComponent)) {
		return;
	}
	if (!IsValid(MantleLedgeLS.Component)) {
		MantleTimelineComponent->Stop();
	}

	// Continually update the mantle target from the stored local transform to follow along with moving objects.
	MantleTarget = MantleLedgeLS.Transform * MantleLedgeLS.Component->GetComponentTransform();

	// Update the Position and Correction Alphas using the Position/Correction curve set for each Mantle.
	const FVector CorrectionVector = MantleParams.PositionCorrectionCurve->GetVectorValue(MantleParams.StartingPosition + MantleTimelineComponent->GetPlaybackPosition());
	const float PositionAlpha = CorrectionVector.X;
	const float XYCorrectionAlpha = CorrectionVector.Y;
	const float ZCorrectionAlpha = CorrectionVector.Z;

	// Lerp multiple transforms together for independent control over the horizontal and vertical blend to the animated start position
	// , as well as the target position.

	// Blend into the animated horizontal and rotation offset using the Y value of the Position/Correction Curve.
	// Get the other offsets
	FTransform HTransform = FTransform::Identity;
	{
		FVector Location = MantleAnimatedStartOffset.GetLocation();
		Location.Z = MantleActualStartOffset.GetLocation().Z;
		const FTransform BTransform = FTransform(MantleActualStartOffset.GetRotation(), Location, FVector::OneVector);
		HTransform = UKismetMathLibrary::TLerp(MantleActualStartOffset, BTransform, XYCorrectionAlpha);
	}

	// Blend into the animated vertical offset using the Z value of the Position/Correction Curve.
	// Get the Z offset
	FTransform VTransform = FTransform::Identity;
	{
		FVector Location = MantleActualStartOffset.GetLocation();
		Location.Z = MantleAnimatedStartOffset.GetLocation().Z;
		const FTransform BTransform = FTransform(MantleActualStartOffset.GetRotation(), Location, FVector::OneVector);
		VTransform = UKismetMathLibrary::TLerp(MantleActualStartOffset, BTransform, ZCorrectionAlpha);
	}

	// Blend lerped transforms.
	FTransform BlendTransform = HTransform;
	FVector BlendLocation = BlendTransform.GetLocation();
	BlendLocation.Z = VTransform.GetLocation().Z;
	BlendTransform.SetLocation(BlendLocation);

	// Blend from the currently blending transforms into the final mantle target using the X value of the Position/Correction Curve.
	const FTransform OriginFinalMantleTarget = FTransform(
		MantleTarget.GetRotation().Rotator() + BlendTransform.GetRotation().Rotator()
		, MantleTarget.GetLocation() + BlendTransform.GetLocation()
		, MantleTarget.GetScale3D() + BlendTransform.GetScale3D()
	);
	const FTransform FinalMantleTarget = UKismetMathLibrary::TLerp(OriginFinalMantleTarget, MantleTarget, PositionAlpha);

	// Initial Blend In (controlled in the timeline curve) to allow the actor to blend into the Position/Correction curve at the midoint. 
	// This prevents pops when mantling an object lower than the animated mantle.
	const FTransform OriginLerpedTarget = FTransform(
		MantleTarget.GetRotation().Rotator() + MantleActualStartOffset.GetRotation().Rotator()
		, MantleTarget.GetLocation() + MantleActualStartOffset.GetLocation()
		, MantleTarget.GetScale3D() + MantleActualStartOffset.GetScale3D()
	);
	const FTransform LerpedTarget = UKismetMathLibrary::TLerp(OriginLerpedTarget, FinalMantleTarget, InBlendIn);

	// Set the actors location and rotation to the Lerped Target.
	TargetRotation = LerpedTarget.GetRotation().Rotator();
	SetActorLocationAndRotation(LerpedTarget.GetLocation(), LerpedTarget.GetRotation());
}

bool ACharacterALS::TickCheckMantle(const FS_Movement_Mantle_TraceSettings& InTraceSettings) {
	if (!IsValid(GetCapsuleComponent())) {
		return false;
	}
	if (!IsValid(GetCharacterMovement())) {
		return false;
	}

	// ? Does ZOffset need to be configurable? See ALS BP version.
	float ZOffset = 2.f;
	FVector CapsuleBaseLocation = GetCapsuleComponent()->GetComponentLocation() - (
		(GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + ZOffset)
		* GetCapsuleComponent()->GetUpVector()
	);

	// Trace in the direction of input to find a wall / object the character cannot walk on.

	FVector InitialTraceImpactPoint = FVector::ZeroVector;
	FVector InitialTraceNormal = FVector::ZeroVector;
	{
		FVector Direction = GetActorRotation().Vector();

		FVector Start = (
			CapsuleBaseLocation
			+ (Direction * -30.f)
			+ FVector(0.f, 0.f, (InTraceSettings.MinLedgeHeight + InTraceSettings.MaxLedgeHeight) / 2.f)
		);

		FVector End = Start + (Direction * InTraceSettings.ReachDistance);

		float Radius = ((InTraceSettings.MaxLedgeHeight - InTraceSettings.MinLedgeHeight) / 2.f) + 1.f;

		const UALSCustomPluginSettings* ALSCustomPluginSettings = GetDefault<UALSCustomPluginSettings>();
		ECollisionChannel ClimbableCollisionChannel = ALSCustomPluginSettings->ClimbableCollisionChannel;

		FHitResult OutHitResult = FHitResult();
		UKismetSystemLibrary::CapsuleTraceSingle(this, Start, End, Radius, InTraceSettings.ForwardTraceRadius, UEngineTypes::ConvertToTraceType(ClimbableCollisionChannel), false, TArray<AActor*>(), EDrawDebugTrace::None, OutHitResult, true);

		if (OutHitResult.bBlockingHit
			&& !OutHitResult.bStartPenetrating
			&& !GetCharacterMovement()->IsWalkable(OutHitResult)
			) {
			InitialTraceImpactPoint = OutHitResult.ImpactPoint;
			InitialTraceNormal = OutHitResult.ImpactNormal;
		}
		else {
			return false;
		}
	}

	// Trace downward from the first traces Impact Point and determine if the hit location is walkable.

	FVector DownTraceLocation = FVector::ZeroVector;
	UPrimitiveComponent* HitComponent = nullptr;
	{
		FVector End = FVector(InitialTraceImpactPoint.X, InitialTraceImpactPoint.Y, CapsuleBaseLocation.Z) + (InitialTraceNormal * -15.f);
		FVector Start = End + FVector(0.f, 0.f, (InTraceSettings.MaxLedgeHeight + InTraceSettings.DownwardTraceRadius + 1.f));
		FHitResult OutHitResult = FHitResult();

		const UALSCustomPluginSettings* ALSCustomPluginSettings = GetDefault<UALSCustomPluginSettings>();
		ECollisionChannel ClimbableCollisionChannel = ALSCustomPluginSettings->ClimbableCollisionChannel;

		UKismetSystemLibrary::SphereTraceSingle(this, Start, End, InTraceSettings.DownwardTraceRadius, UEngineTypes::ConvertToTraceType(ClimbableCollisionChannel), false, TArray<AActor*>(), EDrawDebugTrace::None, OutHitResult, true);

		if (OutHitResult.bBlockingHit
			&& GetCharacterMovement()->IsWalkable(OutHitResult)
			) {
			DownTraceLocation.X = OutHitResult.Location.X;
			DownTraceLocation.Y = OutHitResult.Location.Y;
			DownTraceLocation.Z = OutHitResult.ImpactPoint.Z;
			HitComponent = OutHitResult.GetComponent();
		}
		else {
			return false;
		}
	}

	// Check if the capsule has room to stand at the downward traces location. If so, set that location as the Target Transform and calculate the mantle height.

	FTransform TargetTransform = FTransform::Identity;
	float MantleHeight = 0.f;
	E_CharacterMovementMantleType MantleType = E_CharacterMovementMantleType::LowMantle;
	{
		FVector CapsuleLocationFromBase = DownTraceLocation + FVector(0.f, 0.f, GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + 2.f);
		if (HasCapsuleComponentRoomForMantle(CapsuleLocationFromBase)) {
			TargetTransform.SetLocation(CapsuleLocationFromBase);
			TargetTransform.SetRotation((InitialTraceNormal * FVector(-1.f, -1.f, 0.f)).ToOrientationQuat());
			MantleHeight = (TargetTransform.GetLocation() - GetActorLocation()).Z;
		}
		else {
			return false;
		}
	}

	// Determine the Mantle Type by checking the movement mode and Mantle Height.
	
	switch(GetMovementState()) {
	case(E_CharacterMovementState::Air):
		MantleType = E_CharacterMovementMantleType::FallingCatch;
		break;
	default:
		if (MantleHeight > 125.f) {
			MantleType = E_CharacterMovementMantleType::HighMantle;
		}
		else {
			MantleType = E_CharacterMovementMantleType::LowMantle;
		}
	}

	// If everything checks out, start the Mantle
	
	StartMantle(MantleHeight, FS_Movement_ComponentAndTransform(TargetTransform, HitComponent), MantleType);
	return true;
}

Posted code shows how to check for space of a pawn collision capsule at a position, if it’s suitable for a “mantle” action which means climbing onto an edge, which is done when pressing jump while accellerating to a wall. “mantling” is a climbing animation montage which initiates, and the player is moved and rotated towards the edge while it plays. For smooth interpolation of movement and rotation curves, offsets and various animations are used based on conditions. After the mantling “ends” normal movement is restored. The character is no longer locked to a position or rotation.

I have a lot of that in my edge climb function, funny enough. I was going between the two (the edge climb and the sideways climb) because I figured they wouldn’t be that different, lol.

But this is my ‘climb up’ for ledge climbing, and it works great:


        USkeletalMeshComponent* mesh = GetMesh();
        FVector Location = GetActorLocation();
        FVector temploc;
        float WallDistance = temploc.Distance(WallLocation, Location);

        if (WallDistance < 200.0)
        {
            UE_LOG(LogTemp, Warning, TEXT("Starting climb..."));

            FVector NewLoc = WallNormal * LocationMultiplier + WallLocation;
            NewLoc.Z = HeightLocation.Z - ZAdjust;

            FRotator NewRot = UKismetMathLibrary::MakeRotFromX(WallNormal * -1.0);
            NewRot.Pitch = 0.0;

            FLatentActionInfo LatentInfo;
            LatentInfo.CallbackTarget = this;


            UCharacterMovementComponent* movement = GetCharacterMovement();
            movement->SetMovementMode(MOVE_Flying);
            movement->StopMovementImmediately();
            
            UKismetSystemLibrary::MoveComponentTo(RootComponent, NewLoc, NewRot, true, true, 0.2, false, EMoveComponentAction::Type::Move, LatentInfo);

And to check if you can climb:

void APlayerCharacter::CheckCanClimb(float inHeightExtend, float inForwardMultiplier, float inHeightMultipler, float ForwardRadius, float HeightRadius, bool &bCanClimb_out)
{
    bCanClimb_out = false;

    if (IsClimbing || InClimbingMode || !CanClimb)
    {
        return;
    }

    FVector ForwardV = GetActorForwardVector();
    FVector Location = GetActorLocation();
    FVector x = ForwardV * inForwardMultiplier;

    FVector EndLocation = Location;
    FVector StartLocation = x + Location;

    TArray<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(this);

    FHitResult HitItem;
    FHitResult HitItem2;

    const bool Hit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), EndLocation, StartLocation, ForwardRadius, ETraceTypeQuery::TraceTypeQuery1, false, ActorsToIgnore, EDrawDebugTrace::Type::ForOneFrame, HitItem, true);
    if (Hit)
    {
        WallLocation = HitItem.ImpactPoint;
        WallNormal = HitItem.Normal;


        FVector v = FVector(0.0, 0.0, inHeightMultipler);
        FVector HeightEndLocation = (ForwardV * 50) + Location;
        FVector HeightStartLocation = (ForwardV * inHeightExtend) + Location + v;

        const bool Hit2 = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), HeightStartLocation, HeightEndLocation, HeightRadius, ETraceTypeQuery::TraceTypeQuery1, false, ActorsToIgnore, EDrawDebugTrace::Type::ForOneFrame, HitItem2, true);
        if (Hit2)
        {
            HeightLocation = HitItem2.Location;
            USkeletalMeshComponent* mesh = GetMesh();  //GetMesh();

            FVector socketlocation = mesh->GetSocketLocation("pelvisSocket");
            float RangeValue = socketlocation.Z - HeightLocation.Z;

            if ((RangeValue >= -50.0) && (RangeValue <= 20.0))
            {
                bCanClimb_out = true;
            }
            
        }
        
    }

    return;
}

This is fragile, you are setting the reference bool to false, before determining if it should be true. This can cause a “false true false true false true” behavior if the bool is read by another process. Your method “CheckCanClimb” should return a bool, not set one by reference.

USomeClass::SomeTick() {
  if (!CheckCanClimb(...)) {
    StopClimbing();
  }
}

Is it? I think I might have left that in there while I was testing, because I realized it’s not actually being used at all. Probably for the same reason you mentioned, honestly. I’ll remove it since it has no purpose.

Also! I figured out my issue, it was indeed that the character was having a collision issue with the wall, haha. They were too close and it was causing the weird freak out. I got it working now!

        if (Hit2)
        {
            WallLocation.Z = PlayerLocation.Z;

            WallLocation.X = HitItem2.Location.X;
            WallLocation.Y = HitItem2.Location.Y;

            ClimbSidewaysLocation = WallLocation + (WallNormal * 30.0);

            ClimbSidewaysRotation = UKismetMathLibrary::MakeRotFromX(WallNormal * -1.0);
            ClimbSidewaysRotation.Pitch = 0.0;
            bSuccess = true;
            return;

        }
2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.