Всем здравствуйте! После успеха первой части статьи пора приступить к написанию следующей.

В этой статье пойдёт речь о расширении компонента AbilitySysystemComponent, создании способности атаки c комбинацией и добавление этой способности с помощью GameFeatures.

Расширение AbilitySystemComponent

Для начала создадим класс компонента UDataAbilitySystemComponent, который будет создавать и инициализировать атрибуты и их значения:

UDataAbilitySystemComponent
DataAbilitySystemComponent.h

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "DataAbilitySystemComponent.generated.h"

class UCombatAbilityBase;

USTRUCT()
struct FCombatAttributeData
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere)
	TSubclassOf<UAttributeSet> AttributeSetType{nullptr};

	UPROPERTY(EditAnywhere)
	TObjectPtr<UDataTable> DataTable{nullptr};
	
};

UCLASS(meta=(BlueprintSpawnableComponent))
class COMBATABILITIESSYSTEMRUNTIME_API UDataAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:

	virtual void InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor) override;

	virtual void BeginDestroy() override;

	UFUNCTION(BlueprintCallable, Category="CombatAbilities")
	FGameplayAbilitySpecHandle GrantAbilityOfType(TSubclassOf<UGameplayAbility> InAbilityType, const bool bRemoveAfterActivation);

	void SetupAbilities();
	void SetupAttributes();
	void RemoveAllAbilitiesAndAttributes();

	bool IsUsingAbilityByClass(const TSubclassOf<UGameplayAbility> InAbilityClass) const;

	TArray<UGameplayAbility*> GetActiveAbilitiesByClass(TSubclassOf<UGameplayAbility> InAbilitySearch) const;

private:
	void GrantAbilitiesAndAttributes();
	
	UFUNCTION()
	void OnPawnControllerChanged(APawn* InPawn, AController* InNewController);

private:
	UPROPERTY(EditDefaultsOnly, Category="Abilities")
	TArray<TSubclassOf<UCombatAbilityBase>> DefaultAbilities;

	UPROPERTY(EditDefaultsOnly, Category="Abilities")
	TArray<FCombatAttributeData> DefaultAttributes;
	
	TArray<FGameplayAbilitySpecHandle> DefaultAbilityHandle;

	UPROPERTY(Transient)
	TArray<UAttributeSet*> AddedAttributes;
	
};
DataAbilitySystemComponent.cpp

#include "Components/DataAbilitySystemComponent.h"

#include "CombatAbilitiesSystemRuntimeModule.h"
#include "Abilities/CombatAbilityBase.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(DataAbilitySystemComponent)

void UDataAbilitySystemComponent::InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor)
{
	Super::InitAbilityActorInfo(InOwnerActor, InAvatarActor);

	if(AbilityActorInfo)
	{
		if(UGameInstance* GameInstance{InOwnerActor->GetGameInstance()}; GameInstance)
		{
			GameInstance->GetOnPawnControllerChanged().AddDynamic(this, &UDataAbilitySystemComponent::OnPawnControllerChanged);
		}
	}

	GrantAbilitiesAndAttributes();
}

void UDataAbilitySystemComponent::BeginDestroy()
{
	if(AbilityActorInfo && AbilityActorInfo->OwnerActor.IsValid())
	{
		if(UGameInstance* GameInstance{AbilityActorInfo->OwnerActor->GetGameInstance()}; GameInstance)
		{
			GameInstance->GetOnPawnControllerChanged().RemoveAll(this);
		}
	}
	Super::BeginDestroy();
}

FGameplayAbilitySpecHandle UDataAbilitySystemComponent::GrantAbilityOfType(TSubclassOf<UGameplayAbility> InAbilityType,
	const bool bRemoveAfterActivation)
{
	FGameplayAbilitySpecHandle AbilitySpecHandle;
	if(InAbilityType)
	{
		FGameplayAbilitySpec AbilitySpec(InAbilityType);
		AbilitySpec.RemoveAfterActivation = bRemoveAfterActivation;

		AbilitySpecHandle = GiveAbility(AbilitySpec);
	}

	return AbilitySpecHandle;
}

void UDataAbilitySystemComponent::GrantAbilitiesAndAttributes()
{
	RemoveAllAbilitiesAndAttributes();

	SetupAbilities();

	SetupAttributes();
}

void UDataAbilitySystemComponent::OnPawnControllerChanged(APawn* InPawn, AController* InNewController)
{
	if(AbilityActorInfo && AbilityActorInfo->OwnerActor == InPawn && AbilityActorInfo->PlayerController != InNewController)
	{
		AbilityActorInfo->InitFromActor(AbilityActorInfo->OwnerActor.Get(), AbilityActorInfo->AvatarActor.Get(), this);
	}
}

void UDataAbilitySystemComponent::RemoveAllAbilitiesAndAttributes()
{
	for(UAttributeSet* AttributeSet : AddedAttributes)
	{
		RemoveSpawnedAttribute(AttributeSet);
	}
	for(FGameplayAbilitySpecHandle AbilitySpecHandle : DefaultAbilityHandle)
	{
		SetRemoveAbilityOnEnd(AbilitySpecHandle);
	}
	AddedAttributes.Empty(DefaultAttributes.Num());
	DefaultAbilityHandle.Empty(DefaultAbilities.Num());
}

bool UDataAbilitySystemComponent::IsUsingAbilityByClass(const TSubclassOf<UGameplayAbility> InAbilityClass) const
{
	if(!InAbilityClass)
	{
		UE_LOG(LogCombatAbilitySystem, Error, TEXT("IsUsingAbilityByClass() provided AbilityClass is null"))
		return false;
	}

	return GetActiveAbilitiesByClass(InAbilityClass).Num() > 0;
}

TArray<UGameplayAbility*> UDataAbilitySystemComponent::GetActiveAbilitiesByClass(TSubclassOf<UGameplayAbility> InAbilitySearch) const
{
	TArray<FGameplayAbilitySpec> Specs = GetActivatableAbilities();
	TArray<FGameplayAbilitySpec*> MatchingGameplayAbilities;
	TArray<UGameplayAbility*> ActiveAbilities;

	for(const FGameplayAbilitySpec& Spec : Specs)
	{
		if(Spec.Ability && Spec.Ability.GetClass()->IsChildOf(InAbilitySearch))
		{
			MatchingGameplayAbilities.Add(const_cast<FGameplayAbilitySpec*>(&Spec));
		}
	}

	for(const FGameplayAbilitySpec* Spec : MatchingGameplayAbilities)
	{
		TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();
		for(UGameplayAbility* ActiveAbility : AbilityInstances)
		{
			if(ActiveAbility->IsActive())
			{
				ActiveAbilities.Add(ActiveAbility);
			}
		}
	}

	return ActiveAbilities;
}


void UDataAbilitySystemComponent::SetupAbilities()
{
	DefaultAbilityHandle.Reserve(DefaultAbilities.Num());
	for(const TSubclassOf<UCombatAbilityBase>& Ability : DefaultAbilities)
	{
		if(*Ability)
		{
			DefaultAbilityHandle.Add(GiveAbility(FGameplayAbilitySpec(Ability)));
		}
	}
}

void UDataAbilitySystemComponent::SetupAttributes()
{
	for(const FCombatAttributeData& AttributeData : DefaultAttributes)
	{
		if(AttributeData.AttributeSetType)
		{
			UAttributeSet* NewAttributeSet {NewObject<UAttributeSet>(this, AttributeData.AttributeSetType)};
			if(AttributeData.DataTable)
			{
				NewAttributeSet->InitFromMetaDataTable(AttributeData.DataTable);
			}
			AddedAttributes.Add(NewAttributeSet);
			AddAttributeSetSubobject(NewAttributeSet);
		}
	}
}

Далее от предыдущего класса создаём UCombatSystemComponent, отвечающий за боевые составляющие:

  • CombatActionTable — Таблица, от которой способности будут брать анимационные монтажи

  • bWindowComboAttack, bRequestTriggerCombo, bNextComboAbilityActivated, bShouldTriggerCombo — отвечают за реализацию последовательности комбинировании действий (можно ещё через GameplayTag'и — пишите в комментариях что думаете на этот счёт, и какие ещё есть варианты). Будут применяться в AnimNotifyState при анимациях.

UCombatSystemComponent
CombatSystemComponent.h

#pragma once

#include "CoreMinimal.h"
#include "DataAbilitySystemComponent.h"
#include "CombatComponentInterface.h"
#include "CombatSystemComponent.generated.h"


UCLASS(meta=(BlueprintSpawnableComponent))
class COMBATABILITIESSYSTEMRUNTIME_API UCombatSystemComponent : public UDataAbilitySystemComponent, public ICombatComponentInterface
{
	GENERATED_BODY()

public:

	explicit UCombatSystemComponent(const FObjectInitializer& InInitializer = FObjectInitializer::Get());

	virtual void AbilityLocalInputPressed(int32 InputID) override;
	
	// Begin ICombatComponentInterface
	virtual TArray<FCombatAnimationInfo> GetMontageAction_Implementation(const FGameplayTag& InTagName) const override;

	virtual FCombatAnimationInfo GetComboMontageAction_Implementation(const FGameplayTag& InTagName) override;

	virtual UGameplayAbility* GetCurrentActiveComboAbility_Implementation() const override;

	virtual void IncrementComboIndex_Implementation() override;

	virtual void RequestTriggerCombo_Implementation() override;

	virtual void ActivateNextCombo_Implementation() override;
	
	virtual void ResetCombo_Implementation() override;

	virtual bool IsOpenComboWindow_Implementation() const override;
	
	virtual bool IsActiveNextCombo_Implementation() const override;

	virtual bool IsShouldTriggerCombo_Implementation() const override;

	virtual bool IsRequestTriggerCombo_Implementation() const override;
	
	virtual void OpenComboWindow_Implementation() override;

	virtual void CloseComboWindow_Implementation() override;
	// End ICombatComponentInterface

private:
	void ActivateComboAbility(const TSubclassOf<UGameplayAbility> InAbilityClass);

private:
	UPROPERTY(EditDefaultsOnly, Category="Abilities|Anims")
	TObjectPtr<UDataTable> CombatActionTable;

	UPROPERTY(EditDefaultsOnly, Category="Abilities|Anims")
	FCombatAnimationInfo DodgeMontage;

	UPROPERTY()
	int32 ComboIndex;

	UPROPERTY()
	bool bWindowComboAttack;
	UPROPERTY()
	bool bRequestTriggerCombo;
	UPROPERTY()
	bool bNextComboAbilityActivated;
	UPROPERTY()
	bool bShouldTriggerCombo;
	
};
CombatSystemComponent.cpp

#include "Components/CombatSystemComponent.h"

#include "CombatAbilitiesSystemRuntimeModule.h"
#include "Abilities/CombatAttackAbility.h"
#include "Data/CombatActionData.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(CombatSystemComponent)

UCombatSystemComponent::UCombatSystemComponent(const FObjectInitializer& InInitializer) :
	Super(InInitializer), ComboIndex(0), bWindowComboAttack(false), bRequestTriggerCombo(false),
	bNextComboAbilityActivated(false),
	bShouldTriggerCombo(false)
{
}

void UCombatSystemComponent::AbilityLocalInputPressed(const int32 InputID)
{
	if(IsGenericConfirmInputBound(InputID))
	{
		LocalInputConfirm();
		return;
	}
	if(IsGenericCancelInputBound(InputID))
	{
		LocalInputCancel();
		return;
	}
	
	for(FGameplayAbilitySpec Spec : ActivatableAbilities.Items)
	{
		if(Spec.InputID == InputID && Spec.Ability)
		{
			Spec.InputPressed = true;

			if(Spec.Ability->IsA(UCombatAttackAbility::StaticClass()))
			{
				ActivateComboAbility(Spec.Ability.GetClass());
			}
			else
			{
				if(Spec.IsActive())
				{
					AbilitySpecInputPressed(Spec);
				}
				else
				{
					TryActivateAbility(Spec.Handle);
				}
			}
		}
	}
}

TArray<FCombatAnimationInfo> UCombatSystemComponent::GetMontageAction_Implementation(const FGameplayTag& InTagName) const
{
	if(!CombatActionTable)
	{
		UE_LOG(LogCombatAbilitySystem, Warning, TEXT("CombatActionTable is nullptr"));
		return {};
	}
	if(!InTagName.IsValid())
	{
		UE_LOG(LogCombatAbilitySystem, Warning, TEXT("Parramenter InTagName is no valid"));
		return {};
	}
	
	FCombatActionData ActionData = *CombatActionTable->FindRow<FCombatActionData>(InTagName.GetTagName(), "CombatContext");
	
	return ActionData.Animations;	
}

FCombatAnimationInfo UCombatSystemComponent::GetComboMontageAction_Implementation(const FGameplayTag& InTagName)
{
	TArray<FCombatAnimationInfo> Animations = GetMontageAction_Implementation(InTagName);
	
	if(ComboIndex >= Animations.Num())
	{
		ComboIndex = 0;
	}

	return Animations[ComboIndex];
}

UGameplayAbility* UCombatSystemComponent::GetCurrentActiveComboAbility_Implementation() const
{
	TArray<UGameplayAbility*> Abilities = GetActiveAbilitiesByClass(UComboAbility::StaticClass());

	return Abilities.IsValidIndex(0) ? Abilities[0] : nullptr;
}

void UCombatSystemComponent::IncrementComboIndex_Implementation()
{
	if(bWindowComboAttack)
	{
		ComboIndex += 1;
	}
	
}

void UCombatSystemComponent::RequestTriggerCombo_Implementation()
{
	bRequestTriggerCombo = true;
}

void UCombatSystemComponent::ActivateNextCombo_Implementation()
{
	bNextComboAbilityActivated = true;

}

bool UCombatSystemComponent::IsActiveNextCombo_Implementation() const
{
	return bNextComboAbilityActivated;
}

void UCombatSystemComponent::ResetCombo_Implementation()
{
	ComboIndex = 0;
}

bool UCombatSystemComponent::IsShouldTriggerCombo_Implementation() const
{
	return bShouldTriggerCombo;
}

bool UCombatSystemComponent::IsRequestTriggerCombo_Implementation() const
{
	return bRequestTriggerCombo;
}

void UCombatSystemComponent::OpenComboWindow_Implementation()
{
	bWindowComboAttack = true;
}

bool UCombatSystemComponent::IsOpenComboWindow_Implementation() const
{
	return bWindowComboAttack;
}

void UCombatSystemComponent::CloseComboWindow_Implementation()
{
	bWindowComboAttack = false;
	bRequestTriggerCombo = false;
	bShouldTriggerCombo = false;
	bNextComboAbilityActivated = false;
}

void UCombatSystemComponent::ActivateComboAbility(const TSubclassOf<UGameplayAbility> InAbilityClass)
{
	bShouldTriggerCombo = false;

	if(IsUsingAbilityByClass(InAbilityClass))
	{
		bShouldTriggerCombo = bWindowComboAttack;
	}
	else
	{
		TryActivateAbilityByClass(InAbilityClass);
	}
	
}

Расширение GameplayAbility

Теперь создадим абстрактный класс UCombatAbilityBase, который управляет выполнением анимационных монтажей и обработкой события.

UCombatAbilityBase
CombatAbilityBase.h

#pragma once

#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "CombatAbilityBase.generated.h"


UCLASS(Abstract)
class COMBATABILITIESSYSTEMRUNTIME_API UCombatAbilityBase : public UGameplayAbility
{
	GENERATED_BODY()

public:
	explicit UCombatAbilityBase(const FObjectInitializer& InInitializer = FObjectInitializer::Get());

protected:

	UFUNCTION()
	virtual void OnMontageCompleted(FGameplayTag EventTag, FGameplayEventData EventData);
	
	UFUNCTION()
	virtual void OnMontageCancelled(FGameplayTag EventTag, FGameplayEventData EventData);
	
	UFUNCTION()
	virtual void OnEventReceived(FGameplayTag EventTag, FGameplayEventData EventData);

	UFUNCTION(BlueprintCallable, Category="CombatAbility")
	void PlayMontageWaitEvent(UAnimMontage* InMontage, const float InRateMontage = 1.f,
		const FName& InStartSection = NAME_None, const bool InbStopWhenAbilityEnds = true);

private:
	UPROPERTY(EditDefaultsOnly, Category="Combat|Events")
	FGameplayTagContainer WaitMontageEvents;
	
};
CombatAbilityBase.cpp

#include "Abilities/CombatAbilityBase.h"

#include "Abilities/Tasks/PlayMontageAndWaitForEvent.h"

UCombatAbilityBase::UCombatAbilityBase(const FObjectInitializer& InInitializer)
	: Super(InInitializer)
{
}

void UCombatAbilityBase::OnMontageCompleted(FGameplayTag EventTag, FGameplayEventData EventData)
{
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

void UCombatAbilityBase::OnMontageCancelled(FGameplayTag EventTag, FGameplayEventData EventData)
{
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}

void UCombatAbilityBase::OnEventReceived(FGameplayTag EventTag, FGameplayEventData EventData)
{
}

void UCombatAbilityBase::PlayMontageWaitEvent(UAnimMontage* InMontage, const float InRateMontage,
	const FName& InStartSection, const bool InbStopWhenAbilityEnds)
{
	auto* MontageTask {UPlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, InMontage, WaitMontageEvents,
			InRateMontage, InStartSection, InbStopWhenAbilityEnds)};
	MontageTask->OnCompleted.AddDynamic(this, &UCombatAbilityBase::OnMontageCompleted);
	MontageTask->OnBlendOut.AddDynamic(this, &UCombatAbilityBase::OnMontageCompleted);
	MontageTask->OnCancelled.AddDynamic(this, &UCombatAbilityBase::OnMontageCancelled);
	MontageTask->OnInterrupted.AddDynamic(this, &UCombatAbilityBase::OnMontageCancelled);
	MontageTask->EventReceived.AddDynamic(this, &UCombatAbilityBase::OnEventReceived);
	MontageTask->ReadyForActivation();

}

Создадим дочерний класс UCombatAttackAbility, который будет запускать анимацию и наносить урон в виде применения GameplayEffect

UCombatAttackAbility
UCombatAttackAbility.h

#pragma once

#include "CoreMinimal.h"
#include "ComboAbility.h"
#include "Abilities/CombatAbilityBase.h"
#include "Data/CombatActionData.h"
#include "CombatAttackAbility.generated.h"


UCLASS(Abstract)
class COMBATABILITIESSYSTEMRUNTIME_API UCombatAttackAbility : public UComboAbility
{
	GENERATED_BODY()

protected:
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
		const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

	virtual void OnEventReceived(FGameplayTag EventTag, FGameplayEventData EventData) override;


private:
	UPROPERTY(EditDefaultsOnly, Category="Combat")
	float PauseHitMontage{0.05f};

	UPROPERTY(EditDefaultsOnly, Category="Combat")
	TSubclassOf<UGameplayEffect> DamageEffectClass;

	UPROPERTY()
	FCombatAnimationInfo AttackAnimation;

	UPROPERTY()
	TArray<AActor*> HitActors;

	void ResetMontage() const;

	
};
CombatAttackAbility.cpp

#include "Abilities/CombatAttackAbility.h"

#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "CombatComponentInterface.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(CombatAttackAbility)

void UCombatAttackAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
                                           const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
                                           const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	if(!CommitAbility(Handle, ActorInfo, ActivationInfo))
	{
		EndAbility(Handle, ActorInfo, ActivationInfo, false, true);
		return;
	}


	if(auto* AbilityComponent{ActorInfo->AvatarActor.Get()->FindComponentByInterface<UCombatComponentInterface>()}; AbilityComponent)
	{
		ICombatComponentInterface::Execute_IncrementComboIndex(AbilityComponent);

		AttackAnimation = ICombatComponentInterface::Execute_GetComboMontageAction(AbilityComponent, AbilityTags.First());

		PlayMontageWaitEvent(AttackAnimation.Montage, AttackAnimation.Speed);
	}

}

void UCombatAttackAbility::OnEventReceived(FGameplayTag EventTag, FGameplayEventData EventData)
{
	AActor* HitActor{EventData.Target};
	if(HitActors.Contains(HitActor)) return;
	
	HitActors.AddUnique(HitActor);
	
	CurrentActorInfo->AnimInstance.Get()->Montage_Pause(AttackAnimation.Montage.Get());
	FTimerHandle TimerHandle;
	GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UCombatAttackAbility::ResetMontage, PauseHitMontage);
	(void)ApplyGameplayEffectToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EventData.TargetData, DamageEffectClass, 1);

}

void UCombatAttackAbility::ResetMontage() const
{
	CurrentActorInfo->AnimInstance.Get()->Montage_Resume(AttackAnimation.Montage.Get());

}

Поле PauseHitMontage и метод ResetMontage() используются для замораживания анимации при попадании, чтобы создать импакт попадания, не создавая дополнительные визуальные эффекты или анимации.

Добавление AnimNotify и AnimNotifyState

Далее создадим NotifyState для управления окном, в анимации котором можно будет применять комбинированный удар:

UComboWindowAnimNotifyState
ComboWindowAnimNotifyState.h

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "ComboWindowAnimNotifyState.generated.h"

UCLASS()
class COMBATABILITIESSYSTEMRUNTIME_API UComboWindowAnimNotifyState : public UAnimNotifyState
{
	GENERATED_BODY()

public:

	explicit UComboWindowAnimNotifyState(const FObjectInitializer& InInitializer = FObjectInitializer::Get());

	virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override;
	virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
	virtual void NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference) override;

private:
	UPROPERTY(EditAnywhere, Category="Combo")
	bool bEndCombo;
	
	UPROPERTY()
	TObjectPtr<UObject> CombatComponent;

};
ComboWindowAnimNotifyState.cpp

#include "Animation/ComboWindowAnimNotifyState.h"

#include "AbilitySystemComponent.h"
#include "CombatAbilitiesSystemRuntimeModule.h"
#include "CombatComponentInterface.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(ComboWindowAnimNotifyState)

UComboWindowAnimNotifyState::UComboWindowAnimNotifyState(const FObjectInitializer& InInitializer) :
	Super(InInitializer), bEndCombo(false)
{
}

void UComboWindowAnimNotifyState::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                                              float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
	Super::NotifyBegin(MeshComp, Animation, TotalDuration, EventReference);

	if(!MeshComp) return;
	if(!MeshComp->GetOwner()) return;;
	
	CombatComponent = MeshComp->GetOwner()->FindComponentByInterface<UCombatComponentInterface>();
	if(CombatComponent)
	{
		ICombatComponentInterface::Execute_OpenComboWindow(CombatComponent);
	}
}

void UComboWindowAnimNotifyState::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
	const FAnimNotifyEventReference& EventReference)
{
	Super::NotifyEnd(MeshComp, Animation, EventReference);

	if(CombatComponent)
	{
		if(!ICombatComponentInterface::Execute_IsActiveNextCombo(CombatComponent) && bEndCombo)
		{
			UE_LOG(LogAbilitySystemComponent, Display, TEXT("RESET COMBO"));

			ICombatComponentInterface::Execute_ResetCombo(CombatComponent);
		}
		ICombatComponentInterface::Execute_CloseComboWindow(CombatComponent);
	}
	
}

void UComboWindowAnimNotifyState::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
	float FrameDeltaTime, const FAnimNotifyEventReference& EventReference)
{
	if(!CombatComponent)
	{
		UE_LOG(LogAbilitySystemComponent, Warning, TEXT("CombatComponent is Null"));
		return;
	}
	
	const bool bOpenWindowCombo {ICombatComponentInterface::Execute_IsOpenComboWindow(CombatComponent)};
	const bool bShouldTriggerCombo {ICombatComponentInterface::Execute_IsShouldTriggerCombo(CombatComponent)};
	const bool bRequestTriggerCombo{ICombatComponentInterface::Execute_IsRequestTriggerCombo(CombatComponent)};
	
	if(bOpenWindowCombo && bShouldTriggerCombo && bRequestTriggerCombo && !bEndCombo)
	{

		if(ICombatComponentInterface::Execute_IsActiveNextCombo(CombatComponent))
		{
			return;
		}

		const UGameplayAbility* ComboAbility {ICombatComponentInterface::Execute_GetCurrentActiveComboAbility(CombatComponent)};
		if(!ComboAbility)
		{
			UE_LOG(LogAbilitySystemComponent, Warning, TEXT("ComboAbility is Null"));
			return;
		}

		auto* AbilityComponent {MeshComp->GetOwner()->GetComponentByClass<UAbilitySystemComponent>()};
		if(const bool bSuccess {AbilityComponent->TryActivateAbilityByClass(ComboAbility->GetClass())}; bSuccess)
		{
			ICombatComponentInterface::Execute_ActivateNextCombo(CombatComponent);
		}
		else
		{
			UE_LOG(LogCombatAbilitySystem, Verbose, TEXT("CombotNotifyTick Ability %s didn't activate"), *ComboAbility->GetClass()->GetName());
		}
	}
}

UTriggerComboAnimNotify — отправляет запрос на следующую анимацию в последовательности, в моменте где будет работать Tick у UComboWindowAnimNotifyState:

UTriggerComboAnimNotify
TriggerComboAnimNotify.сpp

#include "Animation/TriggerComboAnimNotify.h"

#include "CombatComponentInterface.h"

void UTriggerComboAnimNotify::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                                     const FAnimNotifyEventReference& EventReference)
{
	if(auto* CombatComponent{MeshComp->GetOwner()->FindComponentByInterface<UCombatComponentInterface>()}; CombatComponent)
	{
		ICombatComponentInterface::Execute_RequestTriggerCombo(CombatComponent);
	}
	
}

Пример применения UComboWindowAnimNotifyState и UTriggerComboAnimNotify
Пример применения UComboWindowAnimNotifyState и UTriggerComboAnimNotify

Создание GameFeatureAction

Пришло время создать AddAbilities_GameFeatureAction, который будет добавлять способность с привязкой InputAction, инициализировать AttributeSet и добавлять компонент CombatSystemComponent, если у выбранного актора не будет этого компонента:

AddAbilities_GameFeatureAction
AddAbilities_GameFeatureAction.h

#pragma once

#include "CoreMinimal.h"
#include "GameFeature_WorldActionBase.h"
#include "GameplayAbilitySpec.h"
#include "Components/GameFrameworkComponentManager.h"
#include "AddAbilities_GameFeatureAction.generated.h"

class UInputAction;

USTRUCT(BlueprintType)
struct FCombatAbilityMapping
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSoftClassPtr<UGameplayAbility> Ability;
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSoftObjectPtr<UInputAction> InputAction;

};

USTRUCT(BlueprintType)
struct FCombatAttributesMapping
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSoftClassPtr<UAttributeSet> Attribute;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSoftObjectPtr<UDataTable> AttributeData;
	
};

USTRUCT()
struct FGameFeatureAbilitiesEntry
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category="Abilities")
	TSoftClassPtr<AActor> ActorClass;

	UPROPERTY(EditAnywhere, Category="Abilities")
	TArray<FCombatAbilityMapping> GrantedAbilities;
	
	UPROPERTY(EditAnywhere, Category="Abilities")
	TArray<FCombatAttributesMapping> GrantedAttributes;
};

UCLASS(DisplayName="Add Combat Abilities")
class COMBATABILITIESSYSTEMRUNTIME_API UAddAbilities_GameFeatureAction : public UGameFeature_WorldActionBase
{
	GENERATED_BODY()

public:
	virtual void OnGameFeatureActivating() override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
#if WITH_EDITORONLY_DATA
	virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) override;
#endif

#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(TArray<FText>& ValidationErrors) override;
#endif
	
private:
	virtual void AddToWorld(const FWorldContext& InWorldContext) override;

	void RemoveActorAbilities(AActor* InActor);
	void Reset();
	void HandleActorExtension(AActor* InActor, FName InEventName, const int32 EntryIndex);
	void AddActorAbilities(AActor* InActor, const FGameFeatureAbilitiesEntry& AbilitiesEntry);

	template<class ComponentType>
	ComponentType* FindOrAddComponentForActor(const AActor* InActor, const FGameFeatureAbilitiesEntry& InAbilitiesEntry)
	{
		return Cast<ComponentType>(FindOrAddComponentForActor(ComponentType::StaticClass(), InActor, InAbilitiesEntry));
	}

	UActorComponent* FindOrAddComponentForActor(UClass* InComponentType, const AActor* InActor, const FGameFeatureAbilitiesEntry& InAbilitiesEntry);

private:
	UPROPERTY(EditAnywhere, Category="Abilities", meta=(AllowPrivateAccess="true", TitleProperty="ActorClass", ShowOnlyInnerProperties))
	TArray<FGameFeatureAbilitiesEntry> AbilitiesList;
	
	struct FActorExtensions
	{
		TArray<FGameplayAbilitySpecHandle> Abilities;
		TArray<UAttributeSet*> Attributes; 
	};
	
	TMap<TObjectPtr<AActor>, FActorExtensions> ActiveExtensions;
	
	TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequests;

};
AddAbilities_GameFeatureAction.cpp

#include "GameFeature/AddAbilities_GameFeatureAction.h"

#include "AbilitySystemComponent.h"
#include "CombatAbilitiesSystemRuntimeModule.h"
#include "Components/GameFrameworkComponentManager.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Components/CombatSystemComponent.h"
#include "Engine/AssetManager.h"
#include "Input/AbilityInputBindingComponent.h"


#define LOCTEXT_NAMESPACE "CombatAbilitiesSystemFeatures"

#include UE_INLINE_GENERATED_CPP_BY_NAME(AddAbilities_GameFeatureAction)


void UAddAbilities_GameFeatureAction::OnGameFeatureActivating()
{
	if (!ensureAlways(ActiveExtensions.IsEmpty()) ||
		!ensureAlways(ComponentRequests.IsEmpty()))
	{
		Reset();
	}
	Super::OnGameFeatureActivating();
}

void UAddAbilities_GameFeatureAction::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
	Super::OnGameFeatureDeactivating(Context);
	Reset();
}

#if WITH_EDITORONLY_DATA
void UAddAbilities_GameFeatureAction::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
	if (!UAssetManager::IsValid()) return;
	
	auto AddBundleAsset = [&AssetBundleData](const FTopLevelAssetPath& SoftObjectPath)
	{
		AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, SoftObjectPath);
		AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, SoftObjectPath);
	};

	for (const FGameFeatureAbilitiesEntry& Entry : AbilitiesList)
	{
		for (const FCombatAbilityMapping& Ability : Entry.GrantedAbilities)
		{
			AddBundleAsset(FTopLevelAssetPath(Ability.Ability->GetPathName()));
			if (!Ability.InputAction.IsNull())
			{
				AddBundleAsset(FTopLevelAssetPath{Ability.InputAction->GetPathName()});
			}
		}

		for (const FCombatAttributesMapping& Attributes : Entry.GrantedAttributes)
		{
			AddBundleAsset(FTopLevelAssetPath(Attributes.Attribute->GetPathName()));
			if (!Attributes.AttributeData.IsNull())
			{
				AddBundleAsset(FTopLevelAssetPath(Attributes.AttributeData->GetPathName()));
			}
		}
	}
}
#endif

#if WITH_EDITOR
EDataValidationResult UAddAbilities_GameFeatureAction::IsDataValid(TArray<FText>& ValidationErrors)
{
	EDataValidationResult Result {CombineDataValidationResults(Super::IsDataValid(ValidationErrors), EDataValidationResult::Valid)};

	int32 EntryIndex {0};
	for (const FGameFeatureAbilitiesEntry& Entry : AbilitiesList)
	{
		if (Entry.ActorClass.IsNull())
		{
			Result = EDataValidationResult::Invalid;
			ValidationErrors.Add(FText::Format(LOCTEXT("EntryHasNullActor", "Null ActorClass at index {0} in AbilitiesList"), FText::AsNumber(EntryIndex)));
		}

		if (Entry.GrantedAbilities.IsEmpty() && Entry.GrantedAttributes.IsEmpty())
		{
			Result = EDataValidationResult::Invalid;
			ValidationErrors.Add(FText::Format(LOCTEXT("EntryHasNoAddOns", "Empty GrantedAbilities and GrantedAttributes at index {0} in AbilitiesList"), FText::AsNumber(EntryIndex)));
		}

		int32 AbilityIndex {0};
		for (const FCombatAbilityMapping& Ability : Entry.GrantedAbilities)
		{
			if (Ability.Ability.IsNull())
			{
				Result = EDataValidationResult::Invalid;
				ValidationErrors.Add(FText::Format(LOCTEXT("EntryHasNullAbility", "Null AbilityType at index {0} in AbilitiesList[{1}].GrantedAbilities"), FText::AsNumber(AbilityIndex), FText::AsNumber(EntryIndex)));
			}
			++AbilityIndex;
		}

		int32 AttributesIndex {0};
		for (const FCombatAttributesMapping& Attributes : Entry.GrantedAttributes)
		{
			if (Attributes.Attribute.IsNull())
			{
				Result = EDataValidationResult::Invalid;
				ValidationErrors.Add(FText::Format(LOCTEXT("EntryHasNullAttributeSet", "Null AttributeSetType at index {0} in AbilitiesList[{1}].GrantedAttributes"), FText::AsNumber(AttributesIndex), FText::AsNumber(EntryIndex)));
			}
			++AttributesIndex;
		}

		++EntryIndex;
	}

	return Result;

	return EDataValidationResult::NotValidated;
}
#endif

void UAddAbilities_GameFeatureAction::AddToWorld(const FWorldContext& WorldContext)
{
	const UWorld* World {WorldContext.World()};
	const UGameInstance* GameInstance {WorldContext.OwningGameInstance};
	if(!GameInstance && !World && !World->IsGameWorld()) return;

	UGameFrameworkComponentManager* ComponentManager {UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance)};
	if(!ComponentManager)
	{
		UE_LOG(LogCombatAbilitySystem, Warning, TEXT("Failed to get UGameFrameworkComponentManager from %s"), *GameInstance->GetName())
		return;
	}

	int32 EntryIndex {0};
	for (const FGameFeatureAbilitiesEntry& Entry : AbilitiesList)
	{
		if (!Entry.ActorClass.IsNull())
		{
			UGameFrameworkComponentManager::FExtensionHandlerDelegate AddAbilitiesDelegate {UGameFrameworkComponentManager::FExtensionHandlerDelegate::CreateUObject(
				this, &UAddAbilities_GameFeatureAction::HandleActorExtension, EntryIndex)};
			TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle {ComponentManager->AddExtensionHandler(Entry.ActorClass, AddAbilitiesDelegate)};

			ComponentRequests.Add(ExtensionRequestHandle);
			EntryIndex++;
		}
	}
	
}

void UAddAbilities_GameFeatureAction::Reset()
{
	while (!ActiveExtensions.IsEmpty())
	{
		const auto ExtensionIt = ActiveExtensions.CreateIterator();
		RemoveActorAbilities(ExtensionIt->Key);
	}

	ComponentRequests.Empty();
}

void UAddAbilities_GameFeatureAction::HandleActorExtension(AActor* Actor, FName EventName, int32 EntryIndex)
{
	if (AbilitiesList.IsValidIndex(EntryIndex))
	{
		const FGameFeatureAbilitiesEntry& Entry {AbilitiesList[EntryIndex]};
		if (EventName == UGameFrameworkComponentManager::NAME_ExtensionRemoved || EventName == UGameFrameworkComponentManager::NAME_ReceiverRemoved)
		{
			RemoveActorAbilities(Actor);
		}
		else if (EventName == UGameFrameworkComponentManager::NAME_ExtensionAdded || EventName == UGameFrameworkComponentManager::NAME_GameActorReady)
		{
			AddActorAbilities(Actor, Entry);
		}
	}
}

void UAddAbilities_GameFeatureAction::AddActorAbilities(AActor* Actor, const FGameFeatureAbilitiesEntry& AbilitiesEntry)
{
	auto* CombatAbilitySystemComponent {FindOrAddComponentForActor<UCombatSystemComponent>(Actor, AbilitiesEntry)};
	if(!CombatAbilitySystemComponent)
	{
		UE_LOG(LogCombatAbilitySystem, Error, TEXT("Failed to find/add an ability component to '%s'. Abilities will not be granted."), *Actor->GetPathName());
		return;
	}

	FActorExtensions AddedExtensions;
	AddedExtensions.Abilities.Reserve(AbilitiesEntry.GrantedAbilities.Num());
	AddedExtensions.Attributes.Reserve(AbilitiesEntry.GrantedAttributes.Num());

	for (const FCombatAbilityMapping& Ability : AbilitiesEntry.GrantedAbilities)
	{
		if (Ability.Ability.IsNull()) continue;

		FGameplayAbilitySpec NewAbilitySpec(Ability.Ability.LoadSynchronous());
		FGameplayAbilitySpecHandle AbilityHandle = CombatAbilitySystemComponent->GiveAbility(NewAbilitySpec);

		if (!Ability.InputAction.IsNull())
		{
			if (auto* InputComponent = FindOrAddComponentForActor<UAbilityInputBindingComponent>(Actor, AbilitiesEntry); InputComponent)
			{
				InputComponent->SetInputBinding(Ability.InputAction.LoadSynchronous(), AbilityHandle);
			}
			else
			{
				UE_LOG(LogCombatAbilitySystem, Error, TEXT("Failed to find/add an ability input binding component to '%s' -- are you sure it's a pawn class?"), *Actor->GetPathName());
			}
		}

		AddedExtensions.Abilities.Add(AbilityHandle);
	}

	for (const FCombatAttributesMapping& Attributes : AbilitiesEntry.GrantedAttributes)
	{
		if (Attributes.Attribute.IsNull()) continue;

		if (TSubclassOf<UAttributeSet> SetType = Attributes.Attribute.LoadSynchronous(); SetType)
		{
			UAttributeSet* NewSet = NewObject<UAttributeSet>(CombatAbilitySystemComponent, SetType);
			if (!Attributes.AttributeData.IsNull())
			{
				if (UDataTable* InitData = Attributes.AttributeData.LoadSynchronous(); InitData)
				{
					NewSet->InitFromMetaDataTable(InitData);
				}
			}

			AddedExtensions.Attributes.Add(NewSet);
			CombatAbilitySystemComponent->AddAttributeSetSubobject(NewSet);
		}
	}

	ActiveExtensions.Add(Actor, AddedExtensions);
	
}

UActorComponent* UAddAbilities_GameFeatureAction::FindOrAddComponentForActor(UClass* InComponentType,
	const AActor* InActor, const FGameFeatureAbilitiesEntry& InAbilitiesEntry)
{
	UActorComponent* Component {InActor->FindComponentByClass(InComponentType)};
	
	bool bMakeComponentRequest {Component == nullptr};
	if (Component)
	{
		if (Component->CreationMethod == EComponentCreationMethod::Native)
		{
			const UObject* ComponentArchetype = Component->GetArchetype();
			bMakeComponentRequest = ComponentArchetype->HasAnyFlags(RF_ClassDefaultObject);
		}
	}

	if (bMakeComponentRequest)
	{
		const UWorld* World = InActor->GetWorld();
		const UGameInstance* GameInstance = World->GetGameInstance();

		if (UGameFrameworkComponentManager* ComponentMan = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance))
		{
			TSharedPtr<FComponentRequestHandle> RequestHandle = ComponentMan->AddComponentRequest(InAbilitiesEntry.ActorClass, InComponentType);
			ComponentRequests.Add(RequestHandle);
		}

		if (!Component)
		{
			Component = InActor->FindComponentByClass(InComponentType);
			ensureAlways(Component);
		}
	}

	return Component;
}

void UAddAbilities_GameFeatureAction::RemoveActorAbilities(AActor* Actor)
{
	FActorExtensions* ActorExtensions {ActiveExtensions.Find(Actor)};
	if(!ActorExtensions) return;

	if (auto* CombatAbilitySystemComponent {Actor->FindComponentByClass<UCombatSystemComponent>()})
	{
		for (UAttributeSet* AttribSetInstance : ActorExtensions->Attributes)
		{
			CombatAbilitySystemComponent->GetSpawnedAttributes_Mutable().Remove(AttribSetInstance);
		}

		auto* InputComponent {Actor->FindComponentByClass<UAbilityInputBindingComponent>()};
		for (FGameplayAbilitySpecHandle AbilityHandle : ActorExtensions->Abilities)
		{
			if (InputComponent)
			{
				InputComponent->ClearInputBinding(AbilityHandle);
			}
			CombatAbilitySystemComponent->SetRemoveAbilityOnEnd(AbilityHandle);
		}
	}

	ActiveExtensions.Remove(Actor);
	
}

#undef LOCTEXT_NAMESPACE

Заключение

На этом и закончу свой рассказ про разработку расширения плагинов AbilitySystem и применение GameFeatures.

Спасибо за прочтение! Ставьте лайки, подписывайтесь и оставляйте комментарии.

Комментарии (0)