Climb Dash
Lastly we want to be able to "dash" while climbing to give our players a choice to speed up traversal (usually at the cost of increased stamina consumption, not that we have stamina atm)

Defining Dash Velocity
A key part of the dash visually is the feeling of anticipation for the dash just before the dash
The character doesn't immediately dash when you press the button, they wait for a split second as if the character is channeling their energy to lurch in a direction
Additionally their Velocity changes similar to an easing function like so (left to right) such that the Velocity increases rapidly at the start, then slows down gradually at the end

So what we'll need is another Animation Blendspace (the account for dashing in all 8 directions) and a Velocity function representing the above example.
For the Velocity function we'll an Float Curve which maps a speed (in cm/s) to seconds
Here I want my animation to only play for 0.6s, have a pause of no Velocity for the first 0.2s, then a rapid increase for the next 0.1s, then slow down for the rest of the 0.3s.

Now lets create a reference to store our curve as well as some necessary metrics we need to perform our dash like dash direction, a bool indicating we're in the dash state, and a float to keep track of our dash time.
Dash Physics
.h
// ZCCharacterMovementComponent.h
public:
// ...
UFUNCTION(BlueprintPure)
bool IsClimbDashing() const { return IsClimbing() && bWantsToClimbDash; }
UFUNCTION(BlueprintPure)
FVector GetClimbDashDirection() const { return ClimbDashDirection; }
private:
// ...
UPROPERTY(Category = "Character Movement: Climbing", EditDefaultsOnly)
UCurveFloat* ClimbDashCurve;
FVector ClimbDashDirection;
bool bWantsToClimbDash = false;
float CurrentClimbDashTime;
Additionally lets create a function to handle dashing.
When the character presses the dash button we'll set bWantsToClimbDash to true, and use the characters acceleration as our bases for ClimbDashDirection (no input defaults to character's up) and reset the CurrentClimbDashTime (which we use to map from a time to speed in ClimbDashCurve)
.h
// ZCCharacterMovementComponent.h
public:
// ...
UFUNCTION(BlueprintCallable)
void TryClimbDashing();
private:
// ...
void CacheClimbDashDirection();
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::TryClimbDashing()
{
if (!IsClimbing())
{
return;
}
if (ClimbDashCurve && !bWantsToClimbDash)
{
bWantsToClimbDash = true;
CurrentClimbDashTime = 0.f;
CacheClimbDashDirection();
}
}
void UZCCharacterMovementComponent::CacheClimbDashDirection()
{
ClimbDashDirection = UpdatedComponent->GetUpVector();
// magic number here should be tweaked to your liking and put into a property
const float AccelerationThreshold = MaxClimbingAcceleration / 10.f;
if (Acceleration.Length() > AccelerationThreshold)
{
ClimbDashDirection = Acceleration.GetSafeNormal();
}
}
We still need to hook dashing into our physics so lets do that now.
Before calcualting the velocity, we want to update CurrentClimbDashTime so that if we are dashing we only dash as long as the curve is.
.h
// ZCCharacterMovementComponent.h
private:
// ...
void UpdateClimbDashState(float DeltaTime);
void StopClimbDashing();
Add the call to UpdateClimbDashState after we compute the surface info but before we compute the velocity
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::PhysClimbing(float DeltaTime, int32 Iterations)
{
// ...
ComputeSurfaceInfo();
UpdateClimbDashState(DeltaTime);
ComputeClimbingVelocity(DeltaTime);
// ...
}
void UZCCharacterMovementComponent::UpdateClimbDashState(float DeltaTime)
{
if (!bWantsToClimbDash)
{
return;
}
CurrentClimbDashTime += DeltaTime;
float MinTime, MaxTime;
ClimbDashCurve->GetTimeRange(MinTime, MaxTime);
if (CurrentClimbDashTime >= MaxTime)
{
StopClimbDashing();
}
}
void UZCCharacterMovementComponent::StopClimbDashing()
{
bWantsToClimbDash = false;
CurrentClimbDashTime = 0.f;
}
void UZCCharacterMovementComponent::StopClimbing(float DeltaTime, int32 Iterations)
{
StopClimbDashing();
// ...
}
Now we need to use our climb dash speed taken from the float curve if we're in the bWantsToClimbDash state when computing velocity.
We do this by extracting the value of the curve at the given time.
However we want to support dashing around corners so we'll need to align our CurrentClimbDirection with the surface.
.h
// ZCCharacterMovementComponent.h
private:
// ...
void AlignClimbDashDirection();
.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::ComputeClimbingVelocity(float DeltaTime)
{
// ...
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
if (bWantsToClimbDash)
{
AlignClimbDashDirection();
const float CurrentCurveSpeed = ClimbDashCurve->GetFloatValue(CurrentClimbDashTime);
Velocity = ClimbDashDirection * CurrentCurveSpeed;
}
else
{
constexpr float Friction = 0.f;
constexpr bool bFluid = false;
CalcVelocity(DeltaTime, Friction, bFluid, BrakingDecelerationClimbing);
}
}
// ...
}
Aligning Dash Direction with Surface
The idea here is that we only want to dash along the surface (horizontally and vertically).
So that means we can ignore the Z component of the surface

Then we can align the dash direction (which was previously defined by the character's acceleration) by projecting it onto the plane which is defined by the surface normal

.cpp
// ZCCharacterMovementComponent.cpp
void UZCCharacterMovementComponent::AlignClimbDashDirection()
{
const FVector HorizontalSurfaceNormal = GetClimbSurfaceNormal().GetSafeNormal2D();
ClimbDashDirection = FVector::VectorPlaneProject(ClimbDashDirection, HorizontalSurfaceNormal);
}
Updating Physics Speeds
One more thing we should do is take the Velocity into account when defining our GetClimbingRotation() and ClimbingSnapSpeed since our Velocity could be rapidly changing if we're dashing.
.cpp
// ZCCharacterMovementComponent.cpp
FQuat UZCCharacterMovementComponent::GetSmoothClimbingRotation(float DeltaTime) const
{
// ...
const FQuat Target = FRotationMatrix::MakeFromX(-CurrentClimbingNormal).ToQuat();
// new, RotationSpeed is now determined by how fast we're moving (so that we rotate quicker if we're dashing around a corner)
const float RotationSpeed = ClimbingRotationSpeed * FMath::Max(1, Velocity.Length() / MaxClimbingSpeed);
return FMath::QInterpTo(Current, Target, DeltaTime, RotationSpeed);
}
void UZCCharacterMovementComponent::SnapToClimbingSurface(float DeltaTime) const
{
// ...
const bool bSweep = true;
// new, SnapSpeed is now determined by how fast we're moving (so we don't disconnect with the surface if we're dashing around a corner)
const float SnapSpeed = ClimbingSnapSpeed * FMath::Max(1, Velocity.Length() / MaxClimbingSpeed);
UpdatedComponent->MoveComponent(Offset * SnapSpeed * DeltaTime, Rotation, bSweep);
}
Dash Animations
Lastly we need some animations.
Create a new Blendspace similar to our climbing Blendspace, except for dashing we can use a 1D Blendspace since speed isn't something it needs to account for.

Then in the Anim Graph create two new variables, a bool to store IsClimbDashing and a 2D Vector to store the ClimbDashVelocity

And set them in the Event Graph (using our handy Velocity to 2DBlendspace function)

Create a new State in the Locomotion State graph with the appropriate transition rules for climb dashing

And finally add in the blendspace

Now we can climb dash in any direction, and it handles corners and edges!

Last updated