Hi, I’m not a programmer. I’m learning GAS, and I need an ability that can run some code, almost on tick. AI says using TimerByEvent in a Gameplay Ability is a No No. It suggested Ability Task, so I requested it. It works, but should I trust it? Here it is:
header
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "AbilityTask_OnInterval.generated.h"
/**
* Delegate fired every interval tick.
* @param TimeElapsed - Total time elapsed since task start.
* @param TickCount - Number of elapsed ticks.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnIntervalTick, float, TimeElapsed, int32, TickCount);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnIntervalFinished);
/**
* Ability Task that triggers repeatedly at a given interval.
*/
UCLASS()
class MYPROJECT_API UAbilityTask_OnInterval : public UAbilityTask
{
GENERATED_BODY()
public:
UAbilityTask_OnInterval(const FObjectInitializer& ObjectInitializer);
/** Creates and starts the task */
UFUNCTION(BlueprintCallable, meta = (DisplayName = "AbilityTask_OnInterval", DefaultToSelf = "OwningAbility", HidePin = "OwningAbility", BlueprintInternalUseOnly = "TRUE"), Category = "Ability|Tasks")
static UAbilityTask_OnInterval* OnInterval(UGameplayAbility* OwningAbility, float Interval, int32 MaxTicks = 0);
/** Called every interval tick */
UPROPERTY(BlueprintAssignable)
FOnIntervalTick OnTick;
UPROPERTY(BlueprintAssignable)
FOnIntervalFinished OnFinished;
protected:
virtual void Activate() override;
virtual void OnDestroy(bool bInOwnerFinished) override;
private:
FTimerHandle IntervalTimerHandle;
float Interval;
int32 MaxTicks;
int32 TickCount;
float TimeElapsed;
void IntervalTick();
void FinishTask();
};
cpp
#include "AbilityTask_OnInterval.h"
#include "AbilitySystemComponent.h"
#include "TimerManager.h"
UAbilityTask_OnInterval::UAbilityTask_OnInterval(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, Interval(0.f)
, MaxTicks(0)
, TickCount(0)
, TimeElapsed(0.f)
{
}
UAbilityTask_OnInterval* UAbilityTask_OnInterval::OnInterval(UGameplayAbility* OwningAbility, float InInterval, int32 InMaxTicks)
{
UAbilityTask_OnInterval* Task = NewAbilityTask<UAbilityTask_OnInterval>(OwningAbility);
Task->Interval = FMath::Max(InInterval, KINDA_SMALL_NUMBER); // avoid zero or negative intervals
Task->MaxTicks = InMaxTicks;
return Task;
}
void UAbilityTask_OnInterval::Activate()
{
TickCount = 0;
TimeElapsed = 0.f;
if (!Ability)
{
EndTask();
return;
}
if (Ability->GetCurrentActorInfo() && Ability->GetCurrentActorInfo()->AvatarActor.IsValid())
{
UWorld* World = Ability->GetCurrentActorInfo()->AvatarActor->GetWorld();
if (World)
{
World->GetTimerManager().SetTimer(IntervalTimerHandle, this, &UAbilityTask_OnInterval::IntervalTick, Interval, true);
return; // timer set successfully
}
}
// Fallback: if failed to set timer, end task immediately
EndTask();
}
void UAbilityTask_OnInterval::IntervalTick()
{
++TickCount;
TimeElapsed += Interval;
OnTick.Broadcast(TimeElapsed, TickCount);
if (MaxTicks > 0 && TickCount >= MaxTicks)
{
FinishTask();
}
}
void UAbilityTask_OnInterval::FinishTask()
{
if (Ability && Ability->GetCurrentActorInfo() && Ability->GetCurrentActorInfo()->AvatarActor.IsValid())
{
UWorld* World = Ability->GetCurrentActorInfo()->AvatarActor->GetWorld();
if (World)
{
World->GetTimerManager().ClearTimer(IntervalTimerHandle);
}
}
OnFinished.Broadcast();
EndTask();
}
void UAbilityTask_OnInterval::OnDestroy(bool bInOwnerFinished)
{
if (Ability && Ability->GetCurrentActorInfo() && Ability->GetCurrentActorInfo()->AvatarActor.IsValid())
{
UWorld* World = Ability->GetCurrentActorInfo()->AvatarActor->GetWorld();
if (World)
{
World->GetTimerManager().ClearTimer(IntervalTimerHandle);
}
}
Super::OnDestroy(bInOwnerFinished);
}
Any feedback, much appreciate it.