After a couple of weeks of break spent in the south west of France — great sights, food and drinks, highly recommended! — let’s get back to work.

I’ve started implementing a basic dash ability that can be used to quickly escape from dangerous situations. The ability will be shared by all mechs in The Bug Squad. At some point I’d like to make it upgradable to make it deal damage too — we’ll see. This is how things currently look:

Implementing the dash movement

The first step to implement the ability was creating the actual dash movement. When dashing, I want mechs to be driven at full speed for some time, with players being able to steer, but not stop before dashing is over.

Mechs in The Bug Squad use the standard UCharacterMovementComponent (CMC) provided by the engine. This is a powerful (and complex) component that supports movement prediction on the local owning client to reduce perceived lag, as well as reconciliation with the server, interpolated location updates on remote clients, etc.

CMC network prediction model doesn’t integrate with the one in GAS at all though, and finding the right balance between what needs to be implemented with GAS vs. CMC proved challenging. The fact that it’s been a while since I’ve worked with CMC probably didn’t help either. 😄

void UBSMechMovementComponent::PerformMovement(const float DeltaTime)
{
    if (!HasValidData())
    {
        return;
    }

    // Dash
    if (bDashing && !bWantsToDash)
    {
        EndDash();
    }
    else if (!bDashing && bWantsToDash)
    {
        StartDash();
    }

    // Always force maximum acceleration when dashing, regardless of player's input
    bForceMaxAccel = bDashing;

    Super::PerformMovement(DeltaTime);
}

In my current setup dashing is controlled by a bWantsToDash flag on my custom movement component. The flag is set on the locally owning client and replicates to the server via the SavedMove mechanism, similarly to what happens with other input flags such as bPressedJump or bWantsToCrouch. This ensures movement is consistent between the owning client and the server. While dashing is active, the movement component drives the mech at full acceleration/speed via bForceMaxAccel.

The StartDash() and EndDash() functions are responsible for updating acceleration and velocity accordingly:

bool UBSMechMovementComponent::StartDash()
{
    if (!HasValidData())
    {
        return false;
    }

    // Start dashing
    // Done as soon as possible so calls to GetMaxAcceleration and GetMaxSpeed return expected values
    bDashing = true;

    // Determine dash direction
    // We want to dash even if currently standing still, so try (in order) acceleration,
    // velocity and rotation to get a valid dash direction
    FVector DashDirection;
    if (Acceleration.SizeSquared() > SMALL_NUMBER)
    {
        DashDirection = Acceleration.GetSafeNormal();
    }
    else
    {
        DashDirection = (Velocity.SizeSquared() < SMALL_NUMBER) ? UpdatedComponent->GetForwardVector() : Velocity.GetSafeNormal();
    }

    // Update movement
    Acceleration = DashDirection * GetMaxAcceleration();
    Velocity = FMath::Min(GetMaxSpeed(), CharacterOwner->GetVelocity().Size2D()) * DashDirection;

    return true;
}

void UBSMechMovementComponent::EndDash()
{
    bDashing = false;
    Acceleration = Acceleration.GetSafeNormal() * GetMaxAcceleration();
    Velocity *= DashEndSpeedFactor;
}

Probably not the cleanest implementation, but seems to work well enough even in simulated high lag / packet loss scenarios.

Creating the ability

For this ability I wanted to play around with the built-in cooldown system in GAS. Cooldown is handled via an effect that can be configured on the ability:

A screenshot of the configurable CooldownGameplayEffectClass on the ability
A screenshot of the cooldown effect class, set to last four seconds

When activated, the ability applies a dashing effect to the mech and triggers the dash movement in the character movement component. The duration of the effect controls for how long the ability remains active:


void UBSPlayerAbility_Dash::ActivateAbilityInternal(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
    const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
    {
        if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
        {
            CancelAbility(Handle, ActorInfo, ActivationInfo, true);
            return;
        }

        // Get dash duration from effect
        const FGameplayEffectSpecHandle DashEffectSpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo,
            ActivationInfo, DashingEffect, GetAbilityLevel());
        if (!DashEffectSpecHandle.IsValid()
            || DashEffectSpecHandle.Data->GetDuration() <= KINDA_SMALL_NUMBER)
        {
            CancelAbility(Handle, ActorInfo, ActivationInfo, true);
            return;
        }

        // Predictively apply "dashing" effect
        ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, DashEffectSpecHandle);

        // Request dash
        // Will replicate to server via movement component flags
        if (IsLocallyControlled())
        {
            if (UBSMechMovementComponent* MovementComp = Cast<UBSMechMovementComponent>(ActorInfo->MovementComponent.Get()))
            {
                MovementComp->bWantsToDash = true;
            }
        }

        // Wait for dash to end, then end ability
        const float DashDuration = DashEffectSpecHandle.Data->GetDuration();
        UAbilityTask_WaitDelay* WaitDelayTask = UAbilityTask_WaitDelay::WaitDelay(this, DashDuration);
        WaitDelayTask->OnFinish.AddDynamic(this, &ThisClass::K2_EndAbility);
        WaitDelayTask->ReadyForActivation();
    }

    Super::ActivateAbilityInternal(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}

I’m not 100% positive about handling the duration of the ability this way, but if I understand things correctly using a duration effect should allow to eventually increase the duration of the dash (and maybe change other properties of the dash as well) depending on the player’s level. When the ability ends, the bWantsToDash flag is reset — effectively ending the dash movement.

A minimal UI

At this point the mech was able dash, but there was no clear indication of when the cooldown would elapse and dash could be triggered again. For this I’ve added a simple Progress Bar to my current minimal HUD:

A screenshot of the blueprint graph that displays the cooldown UI

The GetCooldownRemainingForTag function was adapted from GAS Documentation. Let’s take a look at the mind blowing result one more time:

Next up: adding visual and sound effects when dashing via Gameplay Cues!