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.