Combat Abilities System — Расширение Gameplay Ability System в Unreal Engine, Часть 2

от автора

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

В этой статье пойдёт речь о расширении компонента 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.

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


ссылка на оригинал статьи https://habr.com/ru/articles/835190/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *