Altogether with teamtwentythree’s answer I’ve found a neat little way of implementing it using GAS events on the tags. (so no ticks, + it looks sexy af)
Here’s what the little BP brick looks like.
Mine takes an ability class, but you can easilly adapt the code from there for anything else.
LIMITATIONS:
- Requires setting up pretty much unique ‘AbilityTags’ on the abilities since that’s what we’re using. One should be enough. ie.: Combat.Spell.Fireball.
- IMPORTANT: Tags added in ‘AbilityTags’ need to also be in ‘ActivationOwnedTags’.
Why: I noticed this after saddly, ‘AbilityTags’ arent subbed to the event that listent for tag changes.
Debate: Tho, you can easilly override some functionality in UGameplayAbility to automatically add these tags and not have to write them at both places. I wouldnt consider just using ‘ActivationOwnedTags’ instead of ‘AbilityTags’, these could have other tags like ‘FreezeWhileCasting’ that could be general and thus be used by other abilities and be a big fu.
Here’s the code.
.h
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AbilitySystemComponent.h"
#include "AsyncTaskGameplayAbilityEnded.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAsyncTaskGameplayAbilityEndedEv);
UCLASS(BlueprintType, meta = (ExposedAsyncProxy = AsyncTask))
class TPSMPPG_API UAsyncTaskGameplayAbilityEnded : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
UPROPERTY(BlueprintAssignable)
FAsyncTaskGameplayAbilityEndedEv OnEnded;
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskGameplayAbilityEnded* ListenForGameplayAbilityEnd(UAbilitySystemComponent* abilitySystemComponent, TSubclassOf<UGameplayAbility> abilityClass);
// You must call this function manually when you want the AsyncTask to end.
// For UMG Widgets, you would call it in the Widget's Destruct event.
UFUNCTION(BlueprintCallable)
void EndTask();
protected:
UAbilitySystemComponent* ASC;
FGameplayTagContainer TagsStillApplied;
TMap<FGameplayTag, FDelegateHandle> HandlesMap;
UFUNCTION()
virtual void OnCallback(const FGameplayTag CallbackTag, int32 NewCount);
};
.cpp
UAsyncTaskGameplayAbilityEnded* UAsyncTaskGameplayAbilityEnded::ListenForGameplayAbilityEnd(UAbilitySystemComponent* abilitySystemComponent, TSubclassOf<UGameplayAbility> abilityClass)
{
if (!IsValid(abilitySystemComponent))
{
Print::Say("Couldnt create Task, missing ASC");
return nullptr;
}
const UGameplayAbility* const abilityDef = abilityClass.GetDefaultObject();
if (abilityDef == nullptr)
{
Print::Say("Couldnt create Task, Ability " + abilityClass->GetName() + " CDO is invalid");
return nullptr;
}
if (abilityDef->AbilityTags.IsValid() == false)
{
Print::Say("Couldnt create Task, Ability " + abilityClass->GetName() + " requires at least one AbilityTags");
return nullptr;
}
// TODO: check that 'AbilityTags' are contained within 'ActivationOwnedTags'. This requires to make another class that has a public getter to this protected field.
UAsyncTaskGameplayAbilityEnded* r = NewObject<UAsyncTaskGameplayAbilityEnded>();
r->ASC = abilitySystemComponent;
r->TagsStillApplied = abilityDef->AbilityTags;
for (const auto tag : r->TagsStillApplied)
{
auto h = abilitySystemComponent->RegisterGameplayTagEvent(tag, EGameplayTagEventType::NewOrRemoved).AddUObject(r, &UAsyncTaskGameplayAbilityEnded::OnCallback);
r->HandlesMap.Add(tag, h);
}
return r;
}
void UAsyncTaskGameplayAbilityEnded::EndTask()
{
if (IsValid(ASC))
{
for (const auto tag : TagsStillApplied)
{
ASC->UnregisterGameplayTagEvent(*HandlesMap.Find(tag), tag, EGameplayTagEventType::NewOrRemoved);
}
}
SetReadyToDestroy();
MarkAsGarbage();
}
void UAsyncTaskGameplayAbilityEnded::OnCallback(const FGameplayTag CallbackTag, int32 NewCount)
{
if (NewCount == 0) {
if (TagsStillApplied.HasTagExact(CallbackTag))
{
TagsStillApplied.RemoveTag(CallbackTag);
ASC->UnregisterGameplayTagEvent(*HandlesMap.Find(CallbackTag), CallbackTag, EGameplayTagEventType::NewOrRemoved);
if (TagsStillApplied.IsEmpty())
{
OnEnded.Broadcast();
}
}
}
}