[MovementComponent] Pawn Capsule Collision bounces randomly when sliding off a 90 degree ledge

Hello. I’ve been trying to fix a problem with my custom movement component for my pawn.

My problem is that when the player slides off a 90 degree ledge, the slide adjustment from ComputeSlideVector and TwoWallAdjust seems to freeze and jitter before launching the pawn up in the air.

Here’s a gif I made to demonstrate: (Let it play once to play smoothly)

Keep in mind I’m not pressing jump here. I only pressed the key to move left once to start to slide off the edge.

The blue arrow/line on top of the player is set to point the direction of the impact normal every time the capsule collision calls the hit event. I will talk more about this later in the post.

My movement component right now is currently only setup to move the player based on it’s velocity. Calculating the velocity such as gravity, running, jumping, friction is done in Blueprints temporarily by setting the velocity every tick.
I have checked my blueprint code thoroughly, but nothing in the code would make my player jump suddenly. You can see the variables on the top right says CurrentWalkRun and CurrentJumpVelocity is 0 and the only thing moving the player is the gravity velocity which is at -1500 (multiplied by DeltaTime of course). The same thing also happens with no friction applied, so that’s not the problem either,

My current C++ code for sliding the player looks like this:


void UMatoranMovementComponent::SlidePlayer(const FVector& Delta, float Time, const FVector& Normal, FHitResult& Hit)
{
	const FVector OldHitNormal = Normal;

	FVector SlideDelta = ComputeSlideVector(Delta, Time, OldHitNormal, Hit);
	const FQuat Rotation = UpdatedComponent->GetComponentQuat();

	//slide along first collision
	SafeMoveUpdatedComponent(SlideDelta, Rotation, true, Hit);

	//if we hit another surface, loop to adjust to each new surface (up to 5 in a single tick)
	for (int NumOfSurfaces = 0; NumOfSurfaces < 5; NumOfSurfaces++)
	{
		if (Hit.IsValidBlockingHit())
		{
			TwoWallAdjust(SlideDelta, Hit, OldHitNormal);
			SafeMoveUpdatedComponent(SlideDelta, Rotation, true, Hit);
		}

		else
		{
			NumOfSurfaces = 5;
		}
	}
};

SlidePlayer is called like this:


void UMatoranMovementComponent::PreformMove(const FVector2D& MovementInput, const FVector2D& CameraInput, bool IsJumpPressed, bool IsCrouchPressed, float DeltaTime)
{
	FHitResult Hit(1.f);

	const FVector OldVelocity = Velocity;
	const FVector OldLocation = UpdatedComponent->GetComponentLocation();
	const FQuat Rotation = UpdatedComponent->GetComponentQuat();

	//Calculate player move
	const FVector MoveDelta = ComputeMoveDelta(OldVelocity, DeltaTime, MovementInput, CameraInput, IsJumpPressed, IsCrouchPressed);


	// Move actor
	SafeMoveUpdatedComponent(MoveDelta, Rotation, true, Hit);

	//If actor hits an obstacle, adjust move to slide along it
	if (Hit.IsValidBlockingHit())
	{
		SlidePlayer(MoveDelta, 1.f - Hit.Time, Hit.Normal, Hit);
	}

	// Update velocity
	const FVector NewLocation = UpdatedComponent->GetComponentLocation();
	Velocity = ((NewLocation - OldLocation) / DeltaTime);

	// Finalize
	UpdateComponentVelocity();
};

PreformMove is called every tick.

ComputeMoveDelta is currently a modified function I ripped from ProjectileMovementComponent.cpp, which returns the Pawn’s current velocity (which I set in Blueprints) and adds acceleration, which right now is a function returning an empty FVector.


FVector UMatoranMovementComponent::ComputeMoveDelta(const FVector& InVelocity, float DeltaTime, const FVector2D& MovementInput,const FVector2D& CameraInput, bool IsJumpPressed, bool IsCrouchPressed) const
{
	// Velocity Verlet integration (http://en.wikipedia.org/wiki/Verlet_integration#Velocity_Verlet)
	// The addition of p0 is done outside this method, we are just computing the delta.
	// p = p0 + v0*t + 1/2*a*t^2

	// We use ComputeVelocity() here to infer the acceleration, to make it easier to apply custom velocities.
	// p = p0 + v0*t + 1/2*((v1-v0)/t)*t^2
	// p = p0 + v0*t + 1/2*((v1-v0))*t

	const FVector NewVelocity = ComputeVelocity(InVelocity, DeltaTime, MovementInput, CameraInput, IsJumpPressed, IsCrouchPressed);
	const FVector Delta = (InVelocity * DeltaTime) + (NewVelocity - InVelocity) * (0.5f * DeltaTime);
	return Delta;
}

I tried enabling “Trace Complex On Move” in the Capsule Collision, which at first strangely seemed to fix it:

But after testing on other cases, it seems to freeze and unfreeze the player rapidly, slowing down the slide, but does not launch the player in the air.

The blue arrow now moves smoothly from one surface to the other instead of instantly snapping 90 degrees like it would sometimes do without complex collision, which I find odd since the triangular mesh and the collision models are 1:1 on the walls.

I also don’t want to use complex collision due to the way I set up stairs which is by having them act like a ramp collision wise, but look like stairs visually:

(Player Collision)

(Visibility Collision)

Using complex collision would make the player collide with the steps of the stairs anyway

Now, do you guys have any idea what the problem here could be? Could it be the fact that I set the velocity in Blueprints instead of in C++? Personally I think the main cause for these issues is ComputeSlideVector and TwoWallAdjust. The movement works perfectly fine in all cases except 90 degree ledges. I do not know if it’s because I’m using these functions wrong or of there is something wrong with the way they calculate the sliding angle to adjust the velocity. I looked inside them and saw that they use the crossproduct for 90 degree collisions and bellow, and use dot product for any cases above that. I do not know enough about trigonometry to be able to create my own functions to handle this. But I think Epic’s functions should work correctly here since I guess they would have fixed something like this if they had the same issues and that I’m just using the functions incorrectly or something.

Any help would be much appreciated!

Thanks for your time.

-Headstub

–Post Removed–

Okay so I ended up removing both my “answers” since they still do not work. I will post an update if I manage to fix it. For now, the problem is still the same:/

Okay I managed to fix it. The problem was this line of code:


Velocity = ((NewLocation - OldLocation) / DeltaTime);

The cause for the problem was that NewLocation could be moved by ResolvePenetrationImpl which could cause the velocity to drastically change direction, which it did for me in form of the sudden jump when sliding against the ledge.

Reading through the FloatingPawnMovement.cpp I saw that they had implemented a way to remove this problem by not updating velocity if the collision was position corrected by ResolvePenetrationImpl.

The code looks like this:


			// Update velocity
			// We don't want position changes to vastly reverse our direction (which can happen due to penetration fixups etc)
			if (!bPositionCorrected)
			{
				const FVector NewLocation = UpdatedComponent->GetComponentLocation();
				Velocity = ((NewLocation - OldLocation) / DeltaTime);
			}

bPositionCorrected is decided by calling this function:


bool UFloatingPawnMovement::ResolvePenetrationImpl(const FVector& Adjustment, const FHitResult& Hit, const FQuat& NewRotationQuat)
{
	bPositionCorrected |= Super::ResolvePenetrationImpl(Adjustment, Hit, NewRotationQuat);
	return bPositionCorrected;
}

So by not updating the velocity when penetration fixups are happening, the random bounce is removed and the player slides normally down the ledge.

-Headstub

Thanks so much for this! Was running into the exact same issue and this helped.