This post is a summary based on Lee Deukwoo’s lecture and documentation prepared by another developer.
For detailed and accurate information, please refer to the above links.
Series
Basics of GAS
Basic Character Creation with GAS
Attributes and Gameplay Effects
- Unreal GAS Character Attributes
- Unreal GAS Gameplay Effects
- Interfacing Attributes and UI in Unreal GAS
Utilizing GAS
- Implementing Item Crate in Unreal GAS
- Implementing Area of Effect Skill in Unreal GAS
In this post, we will explore how animation sections registered in animation montages can be played in GAS and create a custom Task while learning how to use Ability Tasks in blueprints.
Implementing Continuous Attack
In the previous post, we represented the attack motion by playing the first section of the animation montage using UAbilityTask_PlayMontageAndWait.
The Gameplay Ability class in GAS already defines a method that allows changing the animation to be played based on the section’s name.
1
2
3
4
5
6
7
8
9
10
|
void UGameplayAbility::MontageJumpToSection(FName SectionName)
{
check(CurrentActorInfo);
UAbilitySystemComponent* const AbilitySystemComponent = GetAbilitySystemComponentFromActorInfo_Checked();
if (AbilitySystemComponent->IsAnimatingAbility(this))
{
AbilitySystemComponent->CurrentMontageJumpToSection(SectionName);
}
}
|
Let’s implement continuous attacks using this method.
Naive Approach
The most basic mechanism to implement continuous attacks is as follows:
- Register the animation montage in the animation instance
- Set an AnimNotify (NextComboCheck) to verify at the end of each animation montage section if it can transition to the next section
- When an attack request is received before the attack ends, set a flag to proceed to the next section
- Check the flag in AnimNotify and transition to the next section using the Montage_JumpToSection method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// Invoked upon attack input
void AMyCharacter::AttackNonEquip_Multicast_Implementation()
{
auto animInstance = Cast<UMSBAnimInstance>(GetMesh()->GetAnimInstance());
if(CharacterState->IsAttacking())
{
animInstance->SetNextComboInputOn(true);
}
else
{
CharacterState->SetAttacking(true);
animInstance->PlayComboAnim();
}
}
// Play continuous attack animation montage
void UMyAnimInstance::PlayComboAnim()
{
CurrentCombo = 1;
NextComboInputOn = false;
Montage_Play(ComboMontage);
}
void UMyAnimInstance::AnimNotify_NextComboCheck()
{
if(NextComboInputOn)
{
JumpToNextSection();
NextComboInputOn = false;
}
}
FName UMyAnimInstance::GetNextComboSectionName()
{
CurrentCombo = FMath::Clamp(CurrentCombo+1, 1, MaxComboCount);
auto NextSection = FName(*FString::Printf(TEXT("Combo%d"), CurrentCombo));
return NextSection;
}
void UMSBAnimInstance::JumpToNextSection()
{
auto text = GetNextComboSectionName();
Montage_JumpToSection(text, ComboMontage);
}
|
Using GAS
We will now implement the equivalent code keeping in mind the following:
- Use of GAS
- Separate and manage Montage Section data as assets (enables rapid development without needing to recompile code even if the total number of attacks, playback speed, etc., change)
- Use of Timer instead of AnimNotify
Primary Data Asset class used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
auto Character = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
Character->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
CurrentComboData = Character->GetComboActionData(); // Retrieved ABA_ComboAttack defined earlier
UAbilityTask_PlayMontageAndWait* PlayerAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this,
FName("PlayAttack"),
Character->GetComboActionMontage(),
1.0f,
GetNextSection()
);
PlayerAttackTask->OnCompleted.AddDynamic(this, &ThisClass::OnActionComplete);
PlayerAttackTask->OnCompleted.AddDynamic(this, &ThisClass::OnActionInterrupted);
PlayerAttackTask->ReadyForActivation();
StartComboTimer();
}
void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo)
{
//Super::InputPressed(Handle, ActorInfo, ActivationInfo);
if(!ComboTimerHandle.IsValid())
{
HasNextComboInputOn = false;
}
else
{
HasNextComboInputOn = true;
}
}
FName UABGA_Attack::GetNextSection()
{
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, CurrentComboData->MaxComboCount);
auto NextSection = FString::Printf(TEXT("%s%d"), *CurrentComboData->MontageSectionNamePrefix, CurrentCombo);
return FName(NextSection);
}
void UABGA_Attack::StartComboTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(CurrentComboData->EffectiveFrameCount.IsValidIndex(ComboIndex));
float ComboEffectiveTime = CurrentComboData->EffectiveFrameCount[ComboIndex] / CurrentComboData->FrameRate;
if(ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager()
.SetTimer(ComboTimerHandle, this, &ThisClass::CheckComboInput, ComboEffectiveTime, false);
}
}
void UABGA_Attack::CheckComboInput()
{
ComboTimerHandle.Invalidate();
if(HasNextComboInputOn)
{
MontageJumpToSection(GetNextSection()); // pre-defined from super class
StartComboTimer();
HasNextComboInputOn = false;
}
}
|
Custom Ability Task
In the previous post, we implemented jumping using the provided Jump ability, but handling the state transitions as needed was not straightforward, and directly jumping the character from the ability made it challenging to adapt to additional requirements.
Let’s create a new Jump ability and task to learn how to restrict the activation of different abilities based on the character’s state.
Implementation (C++)
The custom Task implementation follows the Custom Ability Task Creation Pattern we mentioned earlier.
- Declare a delegate that broadcasts when the Task has completed
- Declare a callback function to be bound to this delegate in the Ability
- Use the ReadyForActivation method to make the Task executable
- End the Ability in the method called by the delegate
In this case, the Jump Task ends when the character lands, meaning when the character’s feet touch the ground.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
// Task Implementation
void UABAT_JumpAndWaitForLanding::Activate()
{
Super::Activate();
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.AddDynamic(this, &ThisClass::OnLanded);
Character->Jump();
SetWaitingOnAvatar();
}
void UABAT_JumpAndWaitForLanding::OnDestroy(bool bInOwnerFinished)
{
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.RemoveDynamic(this, &ThisClass::OnLanded);
Super::OnDestroy(bInOwnerFinished);
}
void UABAT_JumpAndWaitForLanding::OnLanded(const FHitResult& Hit)
{
if(ShouldBroadcastAbilityTaskDelegates())
{
OnComplete.Broadcast();
}
}
// Ability Implementation
UABGA_Jump::UABGA_Jump()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// Implement jump using custom ability task
auto JumpAndWaitForLandingTask = UAbilityTask::NewAbilityTask<UABAT_JumpAndWaitForLanding>(this);
JumpAndWaitForLandingTask->OnComplete.AddDynamic(this, &ThisClass::OnLanded);
// JumpAndWaitForLandingTask->ReadyForActivation();
}
void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo)
{
auto Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
Character->StopJumping();
Super::InputReleased(Handle, ActorInfo, ActivationInfo);
}
bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const
{
bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags);
if(!bResult) return false;
const auto Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
return (Character && Character->CanJump());
}
void UABGA_Jump::OnLanded()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
|
To make tags on whether to block activation for jumping or attacking, these were specified in blueprints.
For jumping, it isn’t allowed to attack, and for attacking, it isn’t allowed to jump. This was achieved by setting the Activation Blocked Tags
.

Using custom task in Blueprints
The method UAbilityTask::NewAbilityTask
cannot be called from blueprints. While modifying engine code is an option, here, we will create a separate static method for task creation and declare the OnComplete delegate as BlueprintAssignable
to let blueprints know when the task has completed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FJumpAndWaitForLandingDelegate);
/**
*
*/
UCLASS()
class ARENABATTLEGAS_API UABAT_JumpAndWaitForLanding : public UAbilityTask
{
GENERATED_BODY()
public:
UABAT_JumpAndWaitForLanding();
UFUNCTION(BlueprintCallable, Category="Abiltiy|Task", meta=(
DisplayName = "JumpAndWaitForLanding", HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly=true))
static UABAT_JumpAndWaitForLanding* CreateTask(UGameplayAbility* OwningAbility);
virtual void Activate() override;
virtual void OnDestroy(bool bInOwnerFinished) override;
UPROPERTY(BlueprintAssignable)
FJumpAndWaitForLandingDelegate OnComplete;
protected:
UFUNCTION()
void OnLanded(const FHitResult& Hit);
};
UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
{
UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
return NewTask;
}
|
After modifying the task code as above, the part in UABGA_Jump::ActivateAbility
where the task was created and ReadyForActivation
was called should be commented out.
Then, in the blueprint of the Jump ability, bind the delegates.
