Move Physics

One of the best ways to learn is stand on the shoulders of giants and look at how they did something. So lets crack open one of the ::Phys() functions inside UE to see what they do.

Using PhysFlying as an example

PhysFlying() is the simplest out of the three (walking, swimming, flying) giving us the barebones needed to perform our physics

github link (requires EpicGames' approval) here

In the interests of not getting into any legality trouble by exposing by approval only engine code , I'll post my own interpretation of the function

// redacted and commented

void UCharacterMovementComponent::PhysFlying(float deltaTime, int32 Iterations)
{
	// 1) Reset any changes made to Velocity last frame by Root Motion
	// so we aren't adding more velocity than intended

	// 2) Calculate Velocity this frame as long as Root Motion isn't enabled
	
	// 3) Add any Root Motion Velocity this frame 
	
	// 4) Scale Velocity for framerate independence

	// 5) Save the old component location so we can update after adjusting it 
	// for collisions
	
	// 6) Move component
	
	// 7) Handle collision and slide if necesssary (makes it so we don't get 
	// stuck on a wall if it makes sense that our velocity would instead
	// slide along the wall)
	
	// 8) Update Velocity based off actual distance travelled (collision or
	//slide could have adjusted it)
}

Most of the above is going to be nicely handled by functions already existing within the UCharacterMovementComponent

Just copying the comments into our PhysClimbing function so we can visually group them you can see there's really 4 steps

  • compute velocity

  • cache location

  • move

  • update velocity based off new location

.cpp

// ZCCharacterMovementComponent.cpp

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;
	}

	// Compute Velocity
	{
		// 1) Reset any changes made to Velocity last frame by Root Motion	
		// 2) Calculate Velocity this frame as long as Root Motion isn't enabled
		// 3) Add any Root Motion Velocity this frame 
		// 4) Scale Velocity for framerate independence
	}
	
	// 5) Save the old component location
	
	// Move
	{
		// 6) Move component
		// 7) Handle collision and slide if necesssary
	}
	
	// 8) Update Velocity based off actual distance travelled
}

Lets stub out those functions so we can clean up PhysClimbing()

.h

// ZCCharacterMovementComponent.h

private:
//...
	void ComputeClimbingVelocity(float DeltaTime);
	void MoveAlongClimbingSurface(float DeltaTime);

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
	if (DeltaTime < MIN_TICK_TIME)
		return;

	ComputeSurfaceInfo();

	if (ShouldStopClimbing())
	{
		StopClimbing(DeltaTime, Iterations);
		return;
	}

	ComputeClimbingVelocity(DeltaTime)
	
	const FVector OldLocation = UpdatedComponent->GetComponentLocation();

	MoveAlongClimbingSurface(DeltaTime);

	if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
	{
		Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / DeltaTime;
	}
}

Computing Climbing Velocity

Our Velocity while climbing plays a huge part into the feel of the game for this mechanic. We're going to mimic Genshin and BOTW which make the character quite slow and feel heavy while climbing as I imagine it's difficult.

If the feel of your game was that climbing was super easy, maybe the character could speedily traverse the wall with ease.

Acceleration Considerations

What we're really talking about is acceleration and deceleration.

High Acceleration and Deceleration will make it feel more responsive and snappy, like your character has full control over their actions. When they press right they go right.

high acceleration and deceleration

Low Acceleration and Deceleration will make it feel more weighted, like the character has to put a lot of effort to perform the action. This will create moments of drag in opposite directions as their acceleration changes..

low acceleration and deceleration

This "more weighted" feel is what we want. Lets make some properties to let us control the Speed, Acceleration, and Deceleration (i.e. Braking) while climbing.

.h

// ZCCharacterMovementComponent.h

private:
// ...
	virtual float GetMaxSpeed() const override;
	virtual float GetMaxAcceleration() const override;

// Clamping to similar max values in the base class (which we'll be overriding)
	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "10.0", ClampMax = "500.0"))
	float MaxClimbingSpeed = 120.f;
	
	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "10.0", ClampMax = "2000.0"))
	float MaxClimbingAcceleration = 380.f;
	
	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "3000.0"))
	float BrakingDecelerationClimbing = 550.f;

The reason for overriding the two accessors is because we're going to leverage the built in function CharacterMovementComponent::CalcVelocity() which is used for all current physics already instead of writing ours from complete scratch.

.cpp

// ZCCharacterMovementComponent.cpp

float UZCCharacterMovementComponent::GetMaxSpeed() const
{
	return IsClimbing() ? MaxClimbingSpeed : Super::GetMaxSpeed();
}

float UZCCharacterMovementComponent::GetMaxAcceleration() const
{
	return IsClimbing() ? MaxClimbingAcceleration : Super::GetMaxAcceleration();
}

And finally our velocity computation

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::ComputeClimbingVelocity(float DeltaTime)
{
	RestorePreAdditiveRootMotionVelocity();

	if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
	{
		constexpr float Friction = 0.f;
		constexpr bool bFluid = false;
		// Will use our overridden functions when climbing
		CalcVelocity(DeltaTime, Friction, bFluid, BrakingDecelerationClimbing);
	}

	ApplyRootMotionToVelocity(DeltaTime);
}

Move, Collision, and Slide

Next up is to actually move the character.

We'll be utilizing CharacterMovementComponent::SafeMoveUpdatedComponent() which takes in a rotation so first we need to figure out how we should be rotated.

Climbing Rotation

When you're climbing something you're always facing the surface, so it would make sense that the characters forward should always be pointing opposite of the Surface Normal.

Additionally we don't want to snap immediately to the rotation, but instead want to smoothly transition as we climb over various surfaces.

.h

// ZCCharacterMovementComponent.h

private:
// ... 
	FQuat GetSmoothClimbingRotation(float DeltaTime) const;
	
	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "1.0", ClampMax = "12.0"))
	float ClimbingRotationSpeed = 6.f;

Our character's forward is the X-axis so we want to create a rotation based off the Surface Normal but only caring about the X-axis, and then reverse it.

.cpp

// ZCCharacterMovementComponent.cpp

FQuat UZCCharacterMovementComponent::GetSmoothClimbingRotation(float DeltaTime) const
{
	// Smoothly rotate towards the surface
	const FQuat Current = UpdatedComponent->GetComponentQuat();
	const FQuat Target = FRotationMatrix::MakeFromX(-CurrentClimbingNormal).ToQuat();

	return FMath::QInterpTo(Current, Target, DeltaTime, ClimbingRotationSpeed);
}

Move & Collision/Sliding

Now we can finish the movement logic

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::MoveAlongClimbingSurface(float DeltaTime)
{
	// Aside from getting our own rotation this logic is exactly from PhysFlying()
	
	const FVector Adjusted = Velocity * DeltaTime;

	FHitResult Hit(1.f);
	SafeMoveUpdatedComponent(Adjusted, GetSmoothClimbingRotation(DeltaTime), true, Hit);

	if (Hit.Time < 1.f)
	{
		HandleImpact(Hit, DeltaTime, Adjusted);
		SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true);
	}
}

This on its own will get us most of the way there however there is one issue, and thats sometimes we fall off the wall of odd shapes or sharp corners.

And this is because we aren't actually doing anything to keep us stuck to the wall.

Right now we're just moving you with respect the surface plane, so sometimes our Velocity will be shooting right off the edge.

You might remember we calculated the CurrentClimbingPosition and CurrentClimbingNormal for surface orientation, and now we're going to use those to keep the character stuck on the surface.

Sticking to Surface

After we move the character based off their Velocity and Collisions we're actually going to move them one more time, but this time towards the surface (sticking them to it).

We want to move the character to where the surface collision is, but we can't simply move towards the inverse Surface Normal because it could be at an angle depending on where the Shape Sweep hits.

So, we need to

  • Take the vector from player to surface collision (yellow)

  • and Project it onto the character's Forward vector (red)

which will move the character only as far as the collision point on the surface is, in the forward direcetion

vector to surface (yellow) projected onto character forward (red)
real example

Let's create a function to handle this surface snapping

.h

// ZCCharacterMovementComponent.h

private:
// ...
	void SnapToClimbingSurface(float DeltaTime) const;
	
	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "60.0"))
	float ClimbingSnapSpeed = 4.f;

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::SnapToClimbingSurface(float DeltaTime) const
{
	const FVector Forward = UpdatedComponent->GetForwardVector();
	const FVector Location = UpdatedComponent->GetComponentLocation();
	const FVector ForwardDifference = (CurrentClimbingPosition - Location).ProjectOnTo(Forward);

	constexpr bool bSweep = true;
	const FQuat Rotation = UpdatedComponent->GetComponentQuat();
	UpdatedComponent->MoveComponent(ForwardDifference * ClimbingSnapSpeed * DeltaTime, Rotation, bSweep);
}

void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
	// ...
	// ...
	
	// Add to the very end to snap to the surface
	SnapToClimbingSurface(DeltaTime);
}

This works! however if notice our capsule collider is smashed up against the surface

When traversing surfaces of different shapes and edges we might be twitching and bumping into collisions which will make the movement seem a little jittery and studder.

Instead if we offset the final snap position by some amount we could make a much smoother climbing movement.

.h

// ZCCharacterMovementComponent.h

private:
// ...

	UPROPERTY(Category = "Character Movement: Climbing", EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "90.0"))
	float ClimbingDistanceFromSurface = 45.f;

Now update SnapToClimbingSurface to offset the final forward position

.cpp

// ZCCharacterMovementComponent.cpp

void UZCCharacterMovementComponent::SnapToClimbingSurface(float DeltaTime) const
{
	const FVector Forward = UpdatedComponent->GetForwardVector();
	const FVector Location = UpdatedComponent->GetComponentLocation();
	const FVector ForwardDifference = (CurrentClimbingPosition - Location).ProjectOnTo(Forward);
	
	// Larger the ClimbingDistanceFromSurface, the further back we offset
	const FVector Offset = -CurrentClimbingNormal * (ForwardDifference.Length() - ClimbingDistanceFromSurface);

	constexpr bool bSweep = true;
	const FQuat Rotation = UpdatedComponent->GetComponentQuat();
	// use the Offset vector for the move instead of ForwardDifference 
	UpdatedComponent->MoveComponent(Offset  * ClimbingSnapSpeed * DeltaTime, Rotation, bSweep);
}
ClimbingDistanceFromSurface value examples (left) 70, (right) 45

Now we stick to walls without falling off!

Last updated