#1. 개념
1. 정의
'Game Ability System'은 액터가 수행할 수 있는 다양한 Ability들을 관리하고 제어하는 프레임워크입니다.
2. Gameplay Attribute(UAttributeSet)
[정의] : GAS에서 'Gameplay Attribute'는 게임 플레이와 관련된 수치 값들을 저장/계산/수정하기 위해 사용됩니다. 이러한 수치 값들은 Gameplay Attribute 소유자의 현재 HP 수치 등을 나타내며, GAS에서 액터들은 Gameplay Attribute 수치 값들을 Attribute Set(UAttributeSet) 내부에 저장합니다. Attribute Set은 Gameplay Attribute와 다른 시스템 간의 상호작용을 관리하며, 해당 Gameplay Attribute를 소유한 액터의 Ability System Component에 등록됩니다. 따라서, 기본 메커니즘은 Attribute Set 생성 -> Attribute Set에 Gameplay Attribute 등록 -> GA 사용자의 Ability System Component에 해당 Attribute Set을 등록.
3. Ability System Component(UAbilitySystemComponent)
[정의] : GAS에서 ASC는 액터의 다양한 Ability들을 관리하고 제어하는데 사용됩니다. 이 컴포넌트는 액터가 수행할 수 있는 Ability 목록을 모듈화 하여 관리하고, 이들을 효율적으로 제어하고 실행할 수 있는 기능을 제공합니다. 결과적으로, ASC는 액터와 GAS 사이의 가교 역할을 수행합니다.
4. Gameplay Ability(UGameplayAbility)
[정의] : GAS에서 'Gameplay Ability'는 액터가 소유하고, 특정 조건을 만족하면 언제든 트리거 할 수 있는 인게임 액션입니다.
[특징]
(1) Ability's Owner(Ability의 소유권을 가진 액터)를 트래킹합니다.- Gameplay Effec의 Calculation 작업 시 Ability 사용자의 Attribute에 접근하는 등의 작업은 Ability 사용자의 정보가 필요합니다.
(2) Ability's State(Ability의 상태)를 트래킹 합니다.- Ability의 활성화 여부 등의 상태를 확인하여 활성화 도중 Ability를 멈추거나, 특정 조건에 의해 다른 동작을 중간에 수행하는 작업 등을 위해 Ability의 현재 상태를 파악해야 합니다.
(3) 다양한 시스템과의 상호작용. - Ability가 활성화 되어 있는 동안 특정 시점에 다른 시스템과의 상호작용이 발생할 수 있기 때문입니다.
5. Ability Task(UAbilityTask)
[정의] : GAS에서 'Ability Task'는 Gameplay Ability 실행 중 '비동기 작업(Asynchronous Task)'을 수행합니다. 이를 통해, Gameplay Ability가 동일 프레임에서 여러 개의 서로 다른 기능을 수행할 수 있습니다. 예를 들면, 'Meteor' Gameplay Ability가 활성화되어, 하나의 Ability Task가 Spell Casting 애니메이션을 재생하는 동안, 다른 Ability Task는 플레이어의 조준 지점에 타깃 조준선을 배치하고 있을 수 있습니다. 'Meteor' Gameplay Ability는 사용자가 주문 시전 확인 입력을 히트하는 경우 종료되며, Ability Task는 언제든 자체적으로 종료될 수 있지만, 아무리 늦어도 관련 Ability가 종료될 때 반드시 종료됩니다.
6. Gameplay Effect(UGameplayEffect)
[정의] : GAS에서 'Gameplay Effect'는 GAS 내 Gameplay Ability의 Target이 된 액터들의 Attribute 수치 값의 변경을 수행합니다. 따라서, Gameplay Effect는 Target 액터에게 데미지를 적용하거나, Buff 혹은 Debuff를 일정 시간 동안 적용하는 등의 작업들을 수행할 수 있습니다.
7. Gameplay Cue(UGameplayCueNotify_Actor/Static)
[정의] : GAS에서 'Gameplay Cue'는 GAS에서 특정 Gameplay Effect와 협력하여 Gameplay Event에 대해 시각적, 청각적, 혹은 촉각적인 피드백을 제공하는 시스템입니다. 따라서, GC는 캐릭터의 능력이나 이펙트를 게임 내에서 어떻게 나타낼지 정의합니다.
#2. Attribute Set(UAttributeSet)
1. Attribute Set 정의 : 사용자 정의 Attribute Set의 선언과 정의
1. AttributeSetBase.h : Attribute Set 정의
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "GDAttributeSetBase.generated.h"
/*
* @목적 : Gameplay Attribute 값들의 Getter&Setter를 위한 매크로 정의
* @설명 : Get(Attribute_Name)을 통해 간단히 Attirbute 수치를 가져올 수 있도록합니다.
* @주의 : 직접적으로 호출하는 것 보단, GameplayEffectSystem을 통해 Attribute 수치를 변경하는 것을 권장합니다.
*/
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
*
*/
UCLASS()
class GASDOCUMENTATION_API UGDAttributeSetBase : public UAttributeSet
{
GENERATED_BODY()
public:
UGDAttributeSetBase();
/*
* @목적 : Attribute의 변화 이후에 항상 호출되는 함수
* @설명 : Health, Mana, Stamina 등 Max 값 변경이 가능한 Attribute 수치들에 대한 현재 값 조정에 사용
*/
// AttributeSet Overrides
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
/*
* @목적 : Attribute의 변화 이후에 호출되는 Handling Logic
* @설명 : Current Health의 Clmap 작업 등 Attribute 값의 변화가 일어난 이후에 발생하는 핸들링 로직을 처리합니다.
*/
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// Current Health, when 0 we expect owner to die unless prevented by an ability. Capped by MaxHealth.
// Positive changes can directly use this.
// Negative changes to Health should go through Damage meta attribute.
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)
// MaxHealth is its own attribute since GameplayEffects may modify it
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, MaxHealth)
// Health regen rate will passively increase Health every second
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_HealthRegenRate)
FGameplayAttributeData HealthRegenRate;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, HealthRegenRate)
// Current Mana, used to execute special abilities. Capped by MaxMana.
UPROPERTY(BlueprintReadOnly, Category = "Mana", ReplicatedUsing = OnRep_Mana)
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Mana)
// MaxMana is its own attribute since GameplayEffects may modify it
UPROPERTY(BlueprintReadOnly, Category = "Mana", ReplicatedUsing = OnRep_MaxMana)
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, MaxMana)
// Mana regen rate will passively increase Mana every second
UPROPERTY(BlueprintReadOnly, Category = "Mana", ReplicatedUsing = OnRep_ManaRegenRate)
FGameplayAttributeData ManaRegenRate;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, ManaRegenRate)
// Current stamina, used to execute special abilities. Capped by MaxStamina.
UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_Stamina)
FGameplayAttributeData Stamina;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Stamina)
// MaxStamina is its own attribute since GameplayEffects may modify it
UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_MaxStamina)
FGameplayAttributeData MaxStamina;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, MaxStamina)
// Stamina regen rate will passively increase Stamina every second
UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_StaminaRegenRate)
FGameplayAttributeData StaminaRegenRate;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, StaminaRegenRate)
// Armor reduces the amount of damage done by attackers
UPROPERTY(BlueprintReadOnly, Category = "Armor", ReplicatedUsing = OnRep_Armor)
FGameplayAttributeData Armor;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Armor)
// Damage is a meta attribute used by the DamageExecution to calculate final damage, which then turns into -Health
// Temporary value that only exists on the Server. Not replicated.
UPROPERTY(BlueprintReadOnly, Category = "Damage")
FGameplayAttributeData Damage;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Damage)
// MoveSpeed affects how fast characters can move.
UPROPERTY(BlueprintReadOnly, Category = "MoveSpeed", ReplicatedUsing = OnRep_MoveSpeed)
FGameplayAttributeData MoveSpeed;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, MoveSpeed)
UPROPERTY(BlueprintReadOnly, Category = "Character Level", ReplicatedUsing = OnRep_CharacterLevel)
FGameplayAttributeData CharacterLevel;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, CharacterLevel)
// Experience points gained from killing enemies. Used to level up (not implemented in this project).
UPROPERTY(BlueprintReadOnly, Category = "XP", ReplicatedUsing = OnRep_XP)
FGameplayAttributeData XP;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, XP)
// Experience points awarded to the character's killers. Used to level up (not implemented in this project).
UPROPERTY(BlueprintReadOnly, Category = "XP", ReplicatedUsing = OnRep_XPBounty)
FGameplayAttributeData XPBounty;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, XPBounty)
// Gold gained from killing enemies. Used to purchase items (not implemented in this project).
UPROPERTY(BlueprintReadOnly, Category = "Gold", ReplicatedUsing = OnRep_Gold)
FGameplayAttributeData Gold;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Gold)
// Gold awarded to the character's killer. Used to purchase items (not implemented in this project).
UPROPERTY(BlueprintReadOnly, Category = "Gold", ReplicatedUsing = OnRep_GoldBounty)
FGameplayAttributeData GoldBounty;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, GoldBounty)
protected:
// Helper function to proportionally adjust the value of an attribute when it's associated max attribute changes.
// (i.e. When MaxHealth increases, Health increases by an amount that maintains the same percentage as before)
void AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty);
/**
* These OnRep functions exist to make sure that the ability system internal representations are synchronized properly during replication
**/
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
UFUNCTION()
virtual void OnRep_HealthRegenRate(const FGameplayAttributeData& OldHealthRegenRate);
UFUNCTION()
virtual void OnRep_Mana(const FGameplayAttributeData& OldMana);
UFUNCTION()
virtual void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana);
UFUNCTION()
virtual void OnRep_ManaRegenRate(const FGameplayAttributeData& OldManaRegenRate);
UFUNCTION()
virtual void OnRep_Stamina(const FGameplayAttributeData& OldStamina);
UFUNCTION()
virtual void OnRep_MaxStamina(const FGameplayAttributeData& OldMaxStamina);
UFUNCTION()
virtual void OnRep_StaminaRegenRate(const FGameplayAttributeData& OldStaminaRegenRate);
UFUNCTION()
virtual void OnRep_Armor(const FGameplayAttributeData& OldArmor);
UFUNCTION()
virtual void OnRep_MoveSpeed(const FGameplayAttributeData& OldMoveSpeed);
UFUNCTION()
virtual void OnRep_CharacterLevel(const FGameplayAttributeData& OldCharacterLevel);
UFUNCTION()
virtual void OnRep_XP(const FGameplayAttributeData& OldXP);
UFUNCTION()
virtual void OnRep_XPBounty(const FGameplayAttributeData& OldXPBounty);
UFUNCTION()
virtual void OnRep_Gold(const FGameplayAttributeData& OldGold);
UFUNCTION()
virtual void OnRep_GoldBounty(const FGameplayAttributeData& OldGoldBounty);
private:
FGameplayTag HitDirectionFrontTag;
FGameplayTag HitDirectionBackTag;
FGameplayTag HitDirectionRightTag;
FGameplayTag HitDirectionLeftTag;
};
앞서 살펴본 바와 같이, Attribute Set은 게임 플레이 내 액터의 속성 수치 값들을 저장, 계산, 그리고 수정하는 작업들을 관리합니다. 각각의 캐릭터 속성 값들의 Initter, Setter, Getter 등을 매크로를 통해 정의했으며, 이들의 Getter&Setter는 하드 코딩을 통해 직접적인 접근은 지양하고, 'GE'를 통한 계산작업을 통해 간접적으로 호출하는 것이 좋습니다. - 캡슐화, 타입 안정성 등. 예상한 대로, GE와 AttributeSet은 GAS 내에서 밀접한 관계를 유지하며, 지속적으로 상호작용 합니다.
2. UAttributeSet::PreAttributeChange()/PostGameplayEffectExectue()
void UGDAttributeSetBase::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
// This is called whenever attributes change, so for max health/mana we want to scale the current totals to match
Super::PreAttributeChange(Attribute, NewValue);
// If a Max value changes, adjust current to keep Current % of Current to Max
if (Attribute == GetMaxHealthAttribute()) // GetMaxHealthAttribute comes from the Macros defined at the top of the header
{
AdjustAttributeForMaxChange(Health, MaxHealth, NewValue, GetHealthAttribute());
}
else if (Attribute == GetMaxManaAttribute())
{
AdjustAttributeForMaxChange(Mana, MaxMana, NewValue, GetManaAttribute());
}
else if (Attribute == GetMaxStaminaAttribute())
{
AdjustAttributeForMaxChange(Stamina, MaxStamina, NewValue, GetStaminaAttribute());
}
else if (Attribute == GetMoveSpeedAttribute())
{
// Cannot slow less than 150 units/s and cannot boost more than 1000 units/s
NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}
}
void UGDAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
{
Super::PostGameplayEffectExecute(Data);
FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();
UAbilitySystemComponent* Source = Context.GetOriginalInstigatorAbilitySystemComponent();
const FGameplayTagContainer& SourceTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags();
FGameplayTagContainer SpecAssetTags;
Data.EffectSpec.GetAllAssetTags(SpecAssetTags);
// Get the Target actor, which should be our owner
AActor* TargetActor = nullptr;
AController* TargetController = nullptr;
AGDCharacterBase* TargetCharacter = nullptr;
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
TargetCharacter = Cast<AGDCharacterBase>(TargetActor);
}
// Get the Source actor
AActor* SourceActor = nullptr;
AController* SourceController = nullptr;
AGDCharacterBase* SourceCharacter = nullptr;
if (Source && Source->AbilityActorInfo.IsValid() && Source->AbilityActorInfo->AvatarActor.IsValid())
{
SourceActor = Source->AbilityActorInfo->AvatarActor.Get();
SourceController = Source->AbilityActorInfo->PlayerController.Get();
if (SourceController == nullptr && SourceActor != nullptr)
{
if (APawn* Pawn = Cast<APawn>(SourceActor))
{
SourceController = Pawn->GetController();
}
}
// Use the controller to find the source pawn
if (SourceController)
{
SourceCharacter = Cast<AGDCharacterBase>(SourceController->GetPawn());
}
else
{
SourceCharacter = Cast<AGDCharacterBase>(SourceActor);
}
// Set the causer actor based on context if it's set
if (Context.GetEffectCauser())
{
SourceActor = Context.GetEffectCauser();
}
}
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
// Try to extract a hit result
FHitResult HitResult;
if (Context.GetHitResult())
{
HitResult = *Context.GetHitResult();
}
// Store a local copy of the amount of damage done and clear the damage attribute
const float LocalDamageDone = GetDamage();
SetDamage(0.f);
if (LocalDamageDone > 0.0f)
{
// If character was alive before damage is added, handle damage
// This prevents damage being added to dead things and replaying death animations
bool WasAlive = true;
if (TargetCharacter)
{
WasAlive = TargetCharacter->IsAlive();
}
if (!TargetCharacter->IsAlive())
{
//UE_LOG(LogTemp, Warning, TEXT("%s() %s is NOT alive when receiving damage"), TEXT(__FUNCTION__), *TargetCharacter->GetName());
}
// Apply the health change and then clamp it
const float NewHealth = GetHealth() - LocalDamageDone;
SetHealth(FMath::Clamp(NewHealth, 0.0f, GetMaxHealth()));
if (TargetCharacter && WasAlive)
{
// This is the log statement for damage received. Turned off for live games.
//UE_LOG(LogTemp, Log, TEXT("%s() %s Damage Received: %f"), TEXT(__FUNCTION__), *GetOwningActor()->GetName(), LocalDamageDone);
// Play HitReact animation and sound with a multicast RPC.
const FHitResult* Hit = Data.EffectSpec.GetContext().GetHitResult();
if (Hit)
{
EGDHitReactDirection HitDirection = TargetCharacter->GetHitReactDirection(Data.EffectSpec.GetContext().GetHitResult()->Location);
switch (HitDirection)
{
case EGDHitReactDirection::Left:
TargetCharacter->PlayHitReact(HitDirectionLeftTag, SourceCharacter);
break;
case EGDHitReactDirection::Front:
TargetCharacter->PlayHitReact(HitDirectionFrontTag, SourceCharacter);
break;
case EGDHitReactDirection::Right:
TargetCharacter->PlayHitReact(HitDirectionRightTag, SourceCharacter);
break;
case EGDHitReactDirection::Back:
TargetCharacter->PlayHitReact(HitDirectionBackTag, SourceCharacter);
break;
}
}
else
{
// No hit result. Default to front.
TargetCharacter->PlayHitReact(HitDirectionFrontTag, SourceCharacter);
}
// Show damage number for the Source player unless it was self damage
if (SourceActor != TargetActor)
{
AGDPlayerController* PC = Cast<AGDPlayerController>(SourceController);
if (PC)
{
PC->ShowDamageNumber(LocalDamageDone, TargetCharacter);
}
}
if (!TargetCharacter->IsAlive())
{
// TargetCharacter was alive before this damage and now is not alive, give XP and Gold bounties to Source.
// Don't give bounty to self.
if (SourceController != TargetController)
{
// Create a dynamic instant Gameplay Effect to give the bounties
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);
FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();
FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();
Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());
}
}
}
}
}// Damage
else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// Handle other health changes.
// Health loss should go through Damage.
SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
} // Health
else if (Data.EvaluatedData.Attribute == GetManaAttribute())
{
// Handle mana changes.
SetMana(FMath::Clamp(GetMana(), 0.0f, GetMaxMana()));
} // Mana
else if (Data.EvaluatedData.Attribute == GetStaminaAttribute())
{
// Handle stamina changes.
SetStamina(FMath::Clamp(GetStamina(), 0.0f, GetMaxStamina()));
}
}
1. PreAttributeChange 함수 : AttributeBase의 PreAttributeChange 함수는 Attribute 수치의 변경이 이루어지기 전에 호출되는 함수입니다. 현재 Attribute 수치와 해당 Attirubte 수치의 상한 간의 비율 조정 등의 작업을 수행합니다.
2. PostGameplayEffectExectue 함수 : GE에 의한 Attribute 수치 값에 변경이 이루어진 이후에 호출되는 함수로, GE에 의한 수치 값 변경 관련 핸들링 로직을 정의합니다.
3. GE_Attributes : Attribute 초기화를 수행하는 GE 구현
다음으로, 캐릭터의 Attribute Set의 초기 설정 값을 적용해주기 위해 별도의 Gameplay Effect를 정의합니다. 앞서 살펴본 바와 같이, Attribute Set에 직접적으로 접근하는 것보다, GE를 통해 각 Attribute의 수치 값을 조정해 주는 것이 좋습니다. 따라서, GE 유형의 블루프린트 클래스를 생성하고, Modifiers 항목에서 각 Attribute 항목에 대한 Modifier 설정을 작성합니다. 그리고, 캐릭터의 블루프린트 클래스에 "DefaultAttributes(GameplayEffect 유형)" 멤버를 해당 GE로 설정해 줍니다.
4. AGDCharacterBase::InitializeAttributes : GE를 ASC에 전달하여, 기본 Attribute 초기화
void AGDCharacterBase::InitializeAttributes()
{
if (!AbilitySystemComponent.IsValid())
{
return;
}
/*
* @목적 : 캐릭터의 ASC에 AttributeSet을 등록하는 GE입니다.
* @설명 : 네이밍을 보고 UAttributeSet 유형의 클래스로 헷갈리면 안됩니다!
*/
if (!DefaultAttributes)
{
UE_LOG(LogTemp, Error, TEXT("%s() Missing DefaultAttributes for %s. Please fill in the character's Blueprint."), *FString(__FUNCTION__), *GetName());
return;
}
/*
* @목적 : GE를 통해 캐릭터의 기본 AttributeSet을 초기화합니다.
* @설명 : GE의 Modifiers 항목에서 기본 AttributeSet 수치 값들을 설정하고, ASC의 ApplyGameplayEffectSpecToTarget을 호출합니다.
*/
// Can run on Server and Client
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle NewHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributes, GetCharacterLevel(), EffectContext);
if (NewHandle.IsValid())
{
FActiveGameplayEffectHandle ActiveGEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*NewHandle.Data.Get(), AbilitySystemComponent.Get());
}
}
마지막으로, 캐릭터의 초기 Attribute Set의 각 항목의 초기 값을 변경하는 GE를 ASC에 전달합니다. 캐릭터의 Default Attribute 수치 값을 GE를 통해 ASC에 전달하고, ASC는 "ApplyGameplayEffectSpecToTarget"을 호출해 줍니다.
#3. Ability System Component
1. ASC 선언
1. PlayerState.h : ASC가 저장되어 관리되는 곳
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h"
#include "GameplayEffectTypes.h"
#include "GDPlayerState.generated.h"
/*
* @목적 : APlayerState를 상속하는 커스텀 PlayerState 클래스를 정의합니다.
* @설명 : APawn::GetPlayerState() 함수를 통해 해당 PlayerState를 가져옴
*/
UCLASS()
class GASDOCUMENTATION_API AGDPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AGDPlayerState();
// Implement IAbilitySystemInterface
class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
//...
protected:
UPROPERTY()
class UGDAbilitySystemComponent* AbilitySystemComponent;
};
*2. PlayerState::PlayerState() : ASC와 Attribute Set의 저장과 관리하는 장소
AGDPlayerState::AGDPlayerState()
{
// Create ability system component, and set it to be explicitly replicated
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
// Mixed mode means we only are replicated the GEs to ourself, not the GEs to simulated proxies. If another GDPlayerState (Hero) receives a GE,
// we won't be told about it by the Server. Attributes, GameplayTags, and GameplayCues will still replicate to us.
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
// Create the attribute set, this replicates by default
// Adding it as a subobject of the owning actor of an AbilitySystemComponent
// automatically registers the AttributeSet with the AbilitySystemComponent
AttributeSetBase = CreateDefaultSubobject<UGDAttributeSetBase>(TEXT("AttributeSetBase"));
// Set PlayerState's NetUpdateFrequency to the same as the Character.
// Default is very low for PlayerStates and introduces perceived lag in the ability system.
// 100 is probably way too high for a shipping game, you can adjust to fit your needs.
NetUpdateFrequency = 100.0f;
// Cache tags
DeadTag = FGameplayTag::RequestGameplayTag(FName("State.Dead"));
}
이제 UAttributeSet을 상속받은 사용자 정의 Attribute Set 목록을 ASC에 등록해 줍니다. 먼저, Player State 클래스로 돌아가서 사용자 정의 Attribute Set 클래스를 컴포넌트로 등록해줍니다. 이때, ASC에 별도의 Attribute Set 등록 함수 없이도, 한 객체 내부에서 위와 같이 코드를 작성해도 문제없이 ASC에 Attribute Set이 등록이 됩니다. 신기하네요?
3. CharacterBase.h
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h"
#include "GameplayTagContainer.h"
#include "GASDocumentation/GASDocumentation.h"
#include "GDCharacterBase.generated.h"
UCLASS()
class GASDOCUMENTATION_API AGDCharacterBase : public ACharacter, public IAbilitySystemInterface
{
/*
* [목적] : AbilitySytemComponent를 가져오는 Getter를 정의합니다.
* [설명] : 해당 함수는 인터페이스 함수로, IAbilitySytemInterface 클래스에서 선언되어, 해당 함수 호출 시 세부적인 액터 클래스의 이름을 알 필요 없이,
* IAbilitySystemInterface를 가져와 호출할 수 있습니다. 따라서, GetAbilitySystemComponent() 인터페이스 함수는 인터페이스 클래슬르 통해
* 호출하는 것이 권장됩니다.
*/
// Implement IAbilitySystemInterface
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override
{
return AbilitySystemComponent.Get();
}
//...
protected:
TWeakObjectPtr<class UGDAbilitySystemComponent> AbilitySystemComponent;
};
먼저, 액터 내부에 ASC를 Attach 하는 방법입니다. 먼저, ASC를 Attach 할 액터가 IAbilitySystemInterface 클래스를 상속하도록 합니다. IAbilitySystemInterface 클래스는 인터페이스 함수로 "GetAbilitySystemComponent"만을 갖고 있으며, 액터 클래스에서 해당 인터페이스 함수를 오버라이딩 합니다. 일반적으로, ASC는 Player State에서 관리하는 것이 좋습니다. 따라서, ASC에 대한 'TWeakObjectPtr'처럼 비교적 느슨한 참조 형식의 포인터만 저장하고 관리해 줍니다.
4. CharacterBase::PossessedBy() : PS의 ASC와 Attribute Set 가져오기
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}
//...
}
ASC에 대한 참조를 가져오기 위해선 APawn 클래스의 "GetPlayerState" 함수를 활용합니다.
#4. Gameplay Ability & Ability Task
1. Gameplay Ability 기본 메커니즘
1. Input Comp <-> ASC Binding 수행
2. ASC::TryActivateAbility 호출
3. 2번 함수 본문에서 Ability::CanActivateAbility 호출
4. 2번 함수 본문에서 3번 함수의 결과 값이 참이라면, Ability::ActivateAbility 호출
5. 4번 함수 본문에서 Ability::CommitAbility 호출 *(GE로 정의된 해당 Gameplay Ability의 Cost 혹은 Cooldown 계산)
6. 5번 함수의 결과 값이 참이라면, 해당 Gameplay Ability의 AbilityTask::ReadyforActivate 호출
7. AbilityTask의 동작 완료 및 Ability 활성화 종료
2. Gameplay Ability 생성 및 정의
* 주의 : UE 5.2 ver 업데이트 이후 축 및 액션 매핑 대신 Enhanced Input 사용이 권장되어, 해당 부분에 대한 별도의 포스팅 업로드 예정. ASC와 Input을 Binding 하는 부분은 가볍게 이런 느낌이구나 생각하면 될 듯.
1. CustomGameplayAbility.h
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "GASDocumentation/GASDocumentation.h"
#include "GDGameplayAbility.generated.h"
UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
// 0 None
None UMETA(DisplayName = "None"),
// 1 Confirm
Confirm UMETA(DisplayName = "Confirm"),
// 2 Cancel
Cancel UMETA(DisplayName = "Cancel"),
// 3 LMB
Ability1 UMETA(DisplayName = "Ability1"),
// 4 RMB
Ability2 UMETA(DisplayName = "Ability2"),
// 5 Q
Ability3 UMETA(DisplayName = "Ability3"),
// 6 E
Ability4 UMETA(DisplayName = "Ability4"),
// 7 R
Ability5 UMETA(DisplayName = "Ability5"),
// 8 Sprint
Sprint UMETA(DisplayName = "Sprint"),
// 9 Jump
Jump UMETA(DisplayName = "Jump")
};
/**
*
*/
UCLASS()
class GASDOCUMENTATION_API UGDGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UGDGameplayAbility();
// Abilities with this set will automatically activate when the input is pressed
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
EGDAbilityInputID AbilityInputID = EGDAbilityInputID::None;
// Value to associate an ability with an slot without tying it to an automatically activated input.
// Passive abilities won't be tied to an input so we need a way to generically associate abilities with slots.
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
EGDAbilityInputID AbilityID = EGDAbilityInputID::None;
// Tells an ability to activate immediately when its granted. Used for passive abilities and abilities forced on others.
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Ability")
bool ActivateAbilityOnGranted = false;
// If an ability is marked as 'ActivateAbilityOnGranted', activate them immediately when given here
// Epic's comment: Projects may want to initiate passives or do other "BeginPlay" type of logic here.
virtual void OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) override;
};
먼저, 'UGameplayBility' 클래스를 상속하는 사용자 정의 게임플레이 어빌리티 클래스를 정의합니다. 사용자 정의 게임플레이 어빌리티 클래스는 Ability의 ID, 그리고 이에 대응되는 Input ID를 가집니다.
2. CustomCharacterBase::BindASCInput() : ASC와 Input의 바인딩
/*
* @목적 : 사용자 Input과 ASC를 Binding 합니다.
*/
void AGDHeroCharacter::BindASCInput()
{
if (!ASCInputBound && AbilitySystemComponent.IsValid() && IsValid(InputComponent))
{
FTopLevelAssetPath AbilityEnumAssetPath = FTopLevelAssetPath(FName("/Script/GASDocumentation"), FName("EGDAbilityInputID"));
AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
FString("CancelTarget"), AbilityEnumAssetPath, static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));
ASCInputBound = true;
}
}
캐릭터 클래스의 Input Component와 ASC의 바인딩을 수행하는 함수를 정의합니다.
3. CustomCharacterBase::AddCharacterAbilities() : ASC에 Ability 등록
void AGDCharacterBase::AddCharacterAbilities()
{
// Grant abilities, but only on the server
if (GetLocalRole() != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->bCharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}
해당 캐릭터가 사용 가능한 Ability 목록을 입력받습니다. 그리고, 각 Ability를 캐릭터의 ASC에 등록해 줍니다. 앞서, 각 Gameplay Ability 객체는 고유의 Ability ID와 더불어, Input ID를 가집니다. 이러한 고유 ID는 캐릭터의 ASC에 해당 Gameplay Ability를 등록하는 시점에 필요한 정보들로, 사용자 Input과 ASC에 등록된 Ability의 바인딩에 활용되며, 서로를 구분하기 위함입니다. 이후, 특정 사용자 입력이 히트되면, 캐릭터의 ASC 내부에서 입력에 대응되는 Ability를 찾아
3. Gameplay Ability 활용 예제(GE, 그리고 AT 등의 자세한 설명은 다음 항목)
1. GA_FireGun.h : 캐릭터의 총 발사 동작을 수행하는 Gameplay Ability
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "Characters/Abilities/GDGameplayAbility.h"
#include "Characters/Abilities/AbilityTasks/GDAT_PlayMontageAndWaitForEvent.h"
#include "Characters/GDProjectile.h"
#include "GDGA_FireGun.generated.h"
/**
*
*/
UCLASS()
class GASDOCUMENTATION_API UGDGA_FireGun : public UGDGameplayAbility
{
GENERATED_BODY()
public:
UGDGA_FireGun();
UPROPERTY(BlueprintReadOnly, EditAnywhere)
UAnimMontage* FireHipMontage;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
UAnimMontage* FireIronsightsMontage;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TSubclassOf<AGDProjectile> ProjectileClass;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TSubclassOf<UGameplayEffect> DamageGameplayEffect;
/** Actually activate ability, do not call this directly. We'll call it from APAHeroCharacter::ActivateAbilitiesWithTags(). */
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
protected:
UPROPERTY(BlueprintReadOnly, EditAnywhere)
float Range;
UPROPERTY(BlueprintReadOnly, EditAnywhere)
float Damage;
UFUNCTION()
void OnCancelled(FGameplayTag EventTag, FGameplayEventData EventData);
UFUNCTION()
void OnCompleted(FGameplayTag EventTag, FGameplayEventData EventData);
UFUNCTION()
void EventReceived(FGameplayTag EventTag, FGameplayEventData EventData);
};
- GA의 활성화 시점에 필요한 정보들을 데이터 멤버로 선언합니다.( 애님 몽타주, 발사체 객체... 등)
- GA의 활성화 시점에 피격된 적 혹은 GA가 전달하고자 하는 GE를 데이터 멤버로 선언합니다.(GE_Damage... etc)
- ActivateAbility 멤버 함수 오버라이딩 및 정의 (해당 함수는 직접적으로 호출하지 않는 것이 권장됩니다.)
2. BP_GA_FireGun : Gameplay Ability 유형의 블루프린트 클래스
1. 사용자 정의 Gamgeplay Ability에 필요한 정보들 입력
2. Ability ID + Input ID 설정
3. Gameplay Tag : 해당 Ability의 활성화 여부를 결정하는 필요/불필요 Ability 목록을 Gameplay Tag를 통해 확인합니다.
3. GA_FireGun::ActivateAbility : Gameplay Ability의 활성화
void UGDGA_FireGun::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData * TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
UAnimMontage* MontageToPlay = FireHipMontage;
if (GetAbilitySystemComponentFromActorInfo()->HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag(FName("State.AimDownSights"))) &&
!GetAbilitySystemComponentFromActorInfo()->HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag(FName("State.AimDownSights.Removal"))))
{
MontageToPlay = FireIronsightsMontage;
}
// Play fire montage and wait for event telling us to spawn the projectile
UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->OnCancelled.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->EventReceived.AddDynamic(this, &UGDGA_FireGun::EventReceived);
// ReadyForActivation() is how you activate the AbilityTask in C++. Blueprint has magic from K2Node_LatentGameplayTaskCall that will automatically call ReadyForActivation().
Task->ReadyForActivation();
}
1. ASC는 UAbilitySystemComponent::TryActivateAbility 함수 호출
2. 해당 Gameplay Ability 객체의 UGameplayAbility::CanActivateAbility 함수 호출
3. 2번 함수의 결과 값이 참이라면, UAbility::ActivateAbility 함수 호출
4-1. 3번 함수의 본문에서 UAbility::CommitAbility를 통해 Cost와 Cooldown등의 조건 합치 여부 확인
4-2. 3번 함수의 본문에서 Ability Task의 생성과 이벤트에 콜백 함수들 바인딩
4-3. 3번 함수의 본문에서 Ability Task의 ReadyForActivation 함수 호출
5. Ability Task의 종료, Ability의 종료
4. CustomAbilityTask::CustomAbilityTask() : 사용자 정의 AT의 생성
UGDAT_PlayMontageAndWaitForEvent* UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility,
FName TaskInstanceName, UAnimMontage* MontageToPlay, FGameplayTagContainer EventTags, float Rate, FName StartSection, bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale)
{
UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate);
/*
* @목적 : NewTaks 템플릿 함수를 통해 새로운 Ability Task 객체를 생성합니다.
* @설명
* 1. Ability의 ActivateAbility 함수 본문에서 해당 Ability Task의 생성자를 호출합니다.
* 2. Ability Task의 생성자 본문에서 NewTask 템플릿 함수를 호출하며, 자동적으로 Ability에 대응되는 ASC를 TWeakObjPtr 유형으로 참조하게됩니다.
*/
UGDAT_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask<UGDAT_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
if (MyObj->AbilitySystemComponent->IsValidLowLevel())
UE_LOG(LogTemp, Error, TEXT("ASC registered!"));
MyObj->MontageToPlay = MontageToPlay;
MyObj->EventTags = EventTags;
MyObj->Rate = Rate;
MyObj->StartSection = StartSection;
MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale;
MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds;
return MyObj;
}
사용자 정의 Ability Task는 이를 활용하고자 하는 Ability의 ActivateAbility 함수 본문에서 생성되어 바인딩 및 활성화 동작을 수행합니다. Abilty Task는 "NewTask" 템플릿 함수를 통해 생성 가능하며, 생성자의 인자로 전달된 UGameplayAbility 형식의 객체를 통해 해당 Gameplay Ability가 등록되어 있는 ASC를 참조하는 TWeakObjPtr 형식의 포인터를 데이터 멤버로 자동적으로 가지게 됩니다.
5. CustomAbilityTask::Activate : AT의 활성화
void UGDAT_PlayMontageAndWaitForEvent::Activate()
{
if (Ability == nullptr)
{
return;
}
bool bPlayedMontage = false;
if (AbilitySystemComponent.IsValid())
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance != nullptr)
{
// Bind to event callback
EventHandle = AbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &UGDAT_PlayMontageAndWaitForEvent::OnGameplayEvent));
//...
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UGDAbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!"));
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UGDAbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent"));
}
SetWaitingOnAvatar();
}
ASC의 Gameplay Event 활용은 추후에 설명.
#5. Gameplay Effect(UGameplayEffect)
1. 개념
[정의] : GAS에서 'Gameplay Effect'는 GAS 내 Gameplay Ability의 Target이 된 액터들의 Attribute 수치 값의 변경을 수행합니다. 따라서, 특정 Ability에 붙어서, Ability의 활성화와 함께 Ability의 Target이 된 액터에게 전달되어, AttributeSet의 특정 Attribute들의 수치 값을 변경합니다. 이때, GE가 Ability의 Target이 된 액터의 Attribute 수치 값을 얼마큼 변경할 것인지 결정하는 방법은 크게 두 가지입니다. (1) Modifiers 항목 활용, (2) Execution 항목 활용.
1. Modifiers 항목 활용 : 비교적 간편하다, 하지만 복합적인 수치 계산이 불가
2. Execution 항목 활용 : 비교적 복잡하다, 하지만 복합적인 수치 계산이 가능하며, 캡슐화가 가능하다.
2. GE의 Modifiers : GE를 활용한 간단한 Attribute 수치 값 계산 및 변경
1. GE_CharacterAttributes
먼저, BP 형식의 GE 생성 및 정의 방법입니다. Gameplay Effect 클래스를 부모로 하는 블루프린트 클래스를 생성합니다. 위 예제는 사용자 캐릭터의 게임 시작 시점에 결정되는 초기 AttributeSet 수치 값을 변경해 주는 GE입니다. 복합적인 계산이 아니라, 각 Attribute 별 초기 값을 설정해 주는 비교적 간단한 변경 작업이므로, Modifiers 항목을 활용했습니다.
3. GE의 Executions : GE를 활용한 복합적 Attribute 수치 값 계산 및 변경
1. DamageExecCalculation.h
// Copyright 2020 Dan Kestranek.
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "GDDamageExecCalculation.generated.h"
/**
*
*/
UCLASS()
class GASDOCUMENTATION_API UGDDamageExecCalculation : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
UGDDamageExecCalculation();
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
GE의 Execution 항목에 추가 가능한 "UGameplayEffectExecutionCalculation" 클래스를 상속받는 사용자 정의 Gameplay Effect Execution 클래스를 선언합니다. 그리고, Execute_Implementation 함수를 오버라이딩 해줍니다.
2. DamageExecCalculation.cpp
// Copyright 2020 Dan Kestranek.
#include "Characters/Abilities/GDDamageExecCalculation.h"
#include "Characters/Abilities/GDAbilitySystemComponent.h"
#include "Characters/Abilities/AttributeSets/GDAttributeSetBase.h"
// Declare the attributes to capture and define how we want to capture them from the Source and Target.
struct GDDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
GDDamageStatics()
{
// Snapshot happens at time of GESpec creation
// We're not capturing anything from the Source in this example, but there could be like AttackPower attributes that you might want.
// Capture optional Damage set on the damage GE as a CalculationModifier under the ExecutionCalculation
DEFINE_ATTRIBUTE_CAPTUREDEF(UGDAttributeSetBase, Damage, Source, true);
// Capture the Target's Armor. Don't snapshot.
DEFINE_ATTRIBUTE_CAPTUREDEF(UGDAttributeSetBase, Armor, Target, false);
}
};
static const GDDamageStatics& DamageStatics()
{
static GDDamageStatics DStatics;
return DStatics;
}
UGDDamageExecCalculation::UGDDamageExecCalculation()
{
RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
}
void UGDDamageExecCalculation::Execute_Implementation(const FGameplayEffectCustomExecutionParameters & ExecutionParams, OUT FGameplayEffectCustomExecutionOutput & OutExecutionOutput) const
{
UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();
AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->GetAvatarActor() : nullptr;
AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->GetAvatarActor() : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
// Gather the tags from the source and target as that can affect which buffs should be used
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Armor = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);
Armor = FMath::Max<float>(Armor, 0.0f);
float Damage = 0.0f;
// Capture optional damage value set on the damage GE as a CalculationModifier under the ExecutionCalculation
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
// Add SetByCaller damage if it exists
Damage += FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);
float UnmitigatedDamage = Damage; // Can multiply any damage boosters here
float MitigatedDamage = (UnmitigatedDamage) * (100 / (100 + Armor));
if (MitigatedDamage > 0.f)
{
// Set the Target's damage meta attribute
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, MitigatedDamage));
}
// Broadcast damages to Target ASC
UGDAbilitySystemComponent* TargetASC = Cast<UGDAbilitySystemComponent>(TargetAbilitySystemComponent);
if (TargetASC)
{
UGDAbilitySystemComponent* SourceASC = Cast<UGDAbilitySystemComponent>(SourceAbilitySystemComponent);
TargetASC->ReceiveDamage(SourceASC, UnmitigatedDamage, MitigatedDamage);
}
}
1. Execution Calculation에서 활용할 Attribute 항목들을 담고 있는 구조체를 선언하고, 이들을 GE의 Source 혹은 Target으로부터 어떻게 캡처할 것인지 정의합니다.
2. FGameplayEffectCustomExecutionParameters 구조체에서 제공하는 "AttemptCalculateCapturedAttributeMagnitude" 함수를 통해 특정 Attribute 항목을 캡처해 옵니다.
3. 인자로 전달받은 아웃 매개변수 "FGameplayEffectCustomExecutionOutput"의 "AddOutputModifier" 함수를 호출하여 최종 수치 값을 AttributeSet에 적용합니다.
#6. Gameplay Cue(UGameplayCueNotify_Static/Actor)
1. 개념
Gameplay Cue는 GAS에서 게임의 특정 상태 변화를 시각적, 그리고 청각적으로 표현하기 위해 사용하는 시스템입니다. Gameplay Cue는 단독으로 사용되기보단, Gameplay Ability 혹은 Gameplay Effect와 연계하여 사용됩니다. 일반적으로, "UGameplayCueNotify_Static" 클래스 혹은 "UGameplayCueNotify_Actor" 클래스를 상속받아, Gameplay Cue 객체를 선언 및 정의합니다. Gameplay Cue는 고유의 Gameplay Cue Tag(FGameplayTag)를 가지며, 이와 연계하는 GA 혹은 GE는 단순히 해당 GC의 Gameplay Tag를 통해 연계가 가능해집니다!
2. GE_GunDamage : 발사체의 충돌 대미지를 관리하는 GE와 연계된 GC
1. BP_GC_GunImpact
1. ASC : UAbilitySystemComponent::ApplyGameplayEffectToSelf() 호출
2. GameplayCueManager : UGameplayCueManager::HandleGameplayCue 호출
3. IGameplayCueInterface : IGameplayCueInterface::HandleGameplayCue() 호출
4. UGameplayCueSet: UGameplayCueSet::HandleGameplayCue() 호출, UGameplayCueSet::HandleGameplayCueNotify_Internal() 호출
5. UGameplayCueNotify_Static : UGameplayCueNotify_Static:::HandleGameplayCue() 호출
6. UGameplayCueNotify_Static 객체의 OnActive, OnExectue 등의 함수 호출.
void UGameplayCueNotify_Static::HandleGameplayCue(AActor* MyTarget, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters)
{
SCOPE_CYCLE_COUNTER(STAT_HandleGameplayCueNotifyStatic);
if (IsValid(MyTarget))
{
K2_HandleGameplayCue(MyTarget, EventType, Parameters);
switch (EventType)
{
case EGameplayCueEvent::OnActive:
OnActive(MyTarget, Parameters);
break;
case EGameplayCueEvent::WhileActive:
WhileActive(MyTarget, Parameters);
break;
case EGameplayCueEvent::Executed:
OnExecute(MyTarget, Parameters);
break;
case EGameplayCueEvent::Removed:
OnRemove(MyTarget, Parameters);
break;
};
}
else
{
ABILITY_LOG(Warning, TEXT("Null Target"));
}
}
"UGameplayCueNotify_Static" 클래스를 부모로 하는 블루프린트 클래스를 생성합니다.(물론 C++로 구현해도 됩니다). 그리고, 총의 발사체의 충돌 효과를 위한 GC를 구현하기 위해, "UGameplayCueNotify_Static::OnExectue" 콜백 함수를 정의해 줍니다. GE가 GA의 Target의 ASC에 적용되어, GC가 트리거 되는 기본 메커니즘은 위와 같습니다. 상당히 복잡하긴 한데, 결국 "UGameplayCueNotify_Static" 객체 내부에서 OnExecute, OnActive... etc 등의 함수들이 호출되는 것을 알 수 있습니다.
2. BP_GE_GunDamage
발사체 충돌과 관련된 GE 객체의 Gameplay Cue Tag에 위에서 정의한 GC의 고유 FGameplayTag를 삽입해 줍니다. 일반적으로, GC는 GA 혹은 GE와 연계할 때, GC 고유의 FGameplayTag를 활용합니다.
#7. Class Diagram
'게임개발 > Unreal C++' 카테고리의 다른 글
[Unreal]#26_Subsystem, 하위 시스템 (0) | 2024.03.19 |
---|---|
[Unreal]#27_Enhanced Input (2) | 2024.03.01 |
[Unreal]#25_Collision Data (1) | 2023.12.02 |
[Unreal]#24_Deactivate와 DeactivateImmediate함수 (0) | 2023.04.16 |
[Unreal]#23_FString, FText, FName 변환 (0) | 2023.02.05 |