#Abilities with Charges

28 messages ยท Page 1 of 1 (latest)

gentle furnace
#

Hey guys! I have implemented a Charges economy system for using abilities and wanted to share here if anyone wants to check this out.
I first looked up for something that GAS would provide by default for this, but didn't find anything so I made this based on an infinite stackable charge gameplay effect and overriding a few cooldown functions from UGameplayAbility.

The ability charges will be represented by how many stacks of the charge GE are active and the ability will be blocked from activating if there is no active stack. The cooldown starting will consume a charge and when ending will handle recharging stacks.

#

First I created the variables in the base ability class (I think in the course it is called AuraAbility), a bool to check if the ability will use charges or not, a TSubclassOf<UGameplayEffect> to select the GE that will represent the charges, an int32 that will determine how many charges the ability can have and a recharge mode, that will determine if the cooldown recharges one charge at a time or all charges at once (I created a enum for that).

One important step is to have the InstancingPolicy set to Instanced Per Actor, so the ability don't lose track of the active effects involved (this also means that if your ability is dependant on changing variables values, you may need to reset them before activating the ability again as it is not instanced per execution anymore). Because of this I used the meta specifier EditCondition in bHasCharges to ensure this condition and in the other charge parameters to depend on bHasCharges being true

#

To grant the ability charge effect stacks we can override the OnGiveAbility function. There we can check if the ability will use charges and apply the initial charges stacks. We need to add a gameplay tag to the charge GE Granted Tags so we can also add them to the ability's ActivationRequiredTags in this function, as this will block the ability from activating if don't have at least one active stack of the charge GE.

#

Now, by default, if the character has the ability cooldown tag active, the ability will be blocked from activating, so we have to override some cooldown functions and actually use the cooldown for consuming a charge stack when it starts and for recharging stacks when it ends. So we have to override CommitAbility, CommitAbilityCooldown and CheckCooldown.

#

In CheckCooldown we can just check if the ability is using charges and return true in this case.

#

CommitAbility and CommitAbilityCooldown will do almost the same thing, the difference is in CommitAbility we have to call CommitAbilityCost when handling the charge logic, as that function commits both cost and cooldown.

#

Now our main logic is in HandleCooldownRecharge, here we apply the cooldown effect and will have to bind lambdas to it's OnGameplayEffectRemoved_InfoDelegate and OnGameplayEffectStackChangeDelegate (we have to bind both, as removing the last stack of a gameplay effect doesn't trigger the stack change delegate). In the lambdas we take care of applying stacks of the charge GE.

#

With the AllCharges recharge mode the cooldown GE doesn't need to be stackable and will only bind to the effect removed delegate, so we can return early before binding to the stack change delegate. And in this case, if the cooldown is already active, we don't even need to apply another cooldown effect.

With the OneCharge recharge mode the cooldown GE has to be stackable, as each time a cooldown stack is removed it will apply one stack of the charge GE.

#

Now in the editor, for example my Fireball ability, its GE_Fireball_Cooldown, we can set the stacking props like this if we want the OneCharge recharge mode

#

I just set the stacking limit to 5, because I won't use a higher charge count in my project, but it can be any number you like

#

Then create a charge GE, add a granted tag, set the duration policy to infinite and stacking type to Aggregate By Target. I used the same naming convention for the other effects, so GE_Fireball_Charges

#

And in the GA_Fireball ability, set instancing policy to Instanced Per Actor and set the charge props

#

using the Category="Cooldowns" in the .h file will keep the Cooldown Gameplay Effect Class together with the charges props

gentle furnace
#

Hey! I made the charges show in the UI. It was harder than the system itself ๐Ÿ˜‚

#

For the UI to show the charges, first we have to change one little thing in the ability. Where we check if the delegates are already bound, we just need to change the function IsBound() to IsBoundToObject(this), as we will do one more binding to them in an async task and the IsBound() returns true if anything is bound. It was ok before because we only bound the ability.

#

Now mostly we have to do things in the WBP_SpellGlobe, WaitCooldownChange async task and create a new WaitChargesChange async task.
In the WaitForCooldown async task, we have to add a lambda to handle its stack changes. We need to to this, because currently this task is only listening for removing and adding events. In the lambda we can broadcast the CooldownStart again everytime a stack is removed (we don't need to do anything when a stack is added), as this means that a new ability charge was applied and there is still a stack of cooldown that will start again.

#

WaitCooldownChange.cpp

    UAbilitySystemComponent* TargetASC,
    const FGameplayEffectSpec& SpecApplied,
    FActiveGameplayEffectHandle ActiveCooldownHandle
)
{
    FGameplayTagContainer AssetTags;
    SpecApplied.GetAllAssetTags(AssetTags);

    FGameplayTagContainer GrantedTags;
    SpecApplied.GetAllGrantedTags(GrantedTags);

    if (AssetTags.HasTagExact(CooldownTag) || GrantedTags.HasTagExact(CooldownTag))
    {
        const FGameplayEffectQuery GameplayEffectQuery =
            FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTag.GetSingleTagContainer());
        
        const TArray<float> TimesRemaining = ASC->GetActiveEffectsTimeRemaining(GameplayEffectQuery);
        
        const float TimeRemaining = FMath::Max(TimesRemaining);
        
        CooldownStart.Broadcast(TimeRemaining);

        const bool bAlreadyBound = ASC
        ->OnGameplayEffectStackChangeDelegate(ActiveCooldownHandle)
        ->IsBoundToObject(this);

        if (bAlreadyBound) return;
    
        ASC
            ->OnGameplayEffectStackChangeDelegate(ActiveCooldownHandle)
            ->AddWeakLambda(this,[this](
                    FActiveGameplayEffectHandle EffectHandle,
                    int32 NewStack,
                    int32 PrevStack
                    )
                    {
                        if (NewStack > PrevStack) return;
                
                        const FGameplayEffectQuery CooldownEffectQuery =
                            FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTag.GetSingleTagContainer());
                        
                        const TArray<float> RemainingTimes = ASC->GetActiveEffectsDuration(CooldownEffectQuery);
                        
                        const float Remaining = FMath::Max(RemainingTimes);
                        
                        CooldownStart.Broadcast(Remaining);
                    });
    }
}```
#

This is very similar to the cooldown one, with one additional step: we also need to bind the stack change delegate when initializing the async task. This is important because we already grant the charges stacks when the ability is given and the player may already have a active charge GE.

#

And we broadcast the new stack value directly, as we want to update the UI when applying or removing any stack.

Before going to the blueprints, we can add a ChargeTag variable to the FAuraAbilityInfo in AbilityInfo.h as we're gonna use it in the SpellGlobe widget (this will be the same we use in the charge GE).

#

In the WBP_SpellGlobe we can create the visuals for the charge indicator the way we like the best. I just added a smaller globe attached to it like this

#

The important thing here is to mark the indicator wrapper and the text as variables as we need to do things with them in the Event Graph (and if you want to show a recharge indicator like the cooldown it has to be marked as variable too)

In the WBP_SpellGlobe we have this part in the Event Graph that handles receiving ability info.

#

In the Receive Ability Info function, we now have to set the received charges tag to a variable.

(if you don't see the ChargesTag pin, check the down arrow at the bottom)

#

In this function, we can check if the ability uses charges or normal cooldown. I just checked if the ChargesTag is valid here as I won't set it in the AbilityInfo if the ability doesn't use charges. And here is important to get the current number of charges to update the text correctly. We can create a function in the OverlayWidgetController to handle that for us or do it directly here.

#

Now in the sequence where we are handling the cooldown we can add 2 extra nodes, one for ending the charge change task if it already exists and one for handling the charge change task and updating the UI.

One thing I wanted was to only show the cooldown in the main globe if the last charge was used, so it doesn't seem to the player that the ability can't be used when they used the ability but there is still charges left, so I hide the cooldown in this situation and only show the recharge.

#

One last thing is when clearing the spell globe you have to clear those charge widgets and the tag variable too