Всем здравствуйте! После успеха первой части статьи пора приступить к написанию следующей.
В этой статье пойдёт речь о расширении компонента 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);
}
}
Создание 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.
Спасибо за прочтение! Ставьте лайки, подписывайтесь и оставляйте комментарии.