
Всем здравствуйте! После успеха первой части статьи пора приступить к написанию следующей.
В этой статье пойдёт речь о расширении компонента 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.
Спасибо за прочтение! Ставьте лайки, подписывайтесь и оставляйте комментарии.
ссылка на оригинал статьи https://habr.com/ru/articles/835190/
Добавить комментарий