언리얼 엔진의 게임플레이 어빌리티 시스템을 위한 게임플레이 어트리뷰트 및 어트리뷰트 세트
게임플레이 어트리뷰트 및 어트리뷰트 세트 사용하기
dev.epicgames.com
https://github.com/tranek/GASDocumentation?tab=readme-ov-file#concepts-as
GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s
My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - tranek/GASDocumentation
github.com
Definition
- 하나 이상의 GA로 AS를 구성하고, ASC에 등록을 한다.
- Owner Actor의 Constructor에서 AttributeSet을 생성하면 ASC에 자동으로 등록이 된다.
Design
- ASC가 여러 개의 AS를 가질 수는 있지만, 각 AS는 모두 서로 클래스가 달라야 한다.
- AS는 무시할만한 Memory Overhead를 야기한다.
- 그렇기에 얼마나 AS를 많이 사용할지는 개발자의 결정에 달려있다.
- 또한 AS는 Actor가 어떤 Attribute를 가질지 선택지를 제공하는 의미로 SubClass를 생성할 수 있다.
- Atribute는 내부에서 AttributeSetClassName.AttributeName으로 일컬어진다.
- 때문에 AS를 상속 받더라도, 상위 AS의 Attribute는 상위 AS의 이름을 그대로 유지한다.
Subcomponents with Individual Attributes
- 장비 파괴처럼 복수의 Damagable Component를 소유했을 때, Component의 최대 갯수를 알 수 있다면
AttributeSet에 각 Component에 대응하는 health Attribute를 만드는 것을 권장한다.- Damagable Component에는 가상의 Slot 값을 부여
- 부여된 Slot값을 통해 GameplayAbilities를 읽거나 Attribute에 접근해 데미지 계산이 가능
- Character/Pawn은 1개 이하의 health Attribute만 소유하는 것을 권장
- Attribute는 AttributeSet이 가져야 하는거지, Character/Pawn이 가져야만 하는 것이 아니기 때문.
- 하지만 다음 조건에서는 위와 같은 방식이 기대한 것만큼 잘 동작하지 않는다.
- Subcomponent가 Runtime 상 상한이 없는 경우
- Subcomponent가 Detach되어 다른 유저가 사용할 수 있는 경우
- 이 때에는 Attribute를 사용하지 말고 고전적인 float 타입의 변수를 Component에 직접 부여하는 것을 권장한다.
- 자세한 사항은 Item Attribute를 확인하길 바란다.
Add/Remove AttributeSet at Runtime
- AS은 Runtime 상에 ASC에 추가될 수 있다.
- 물론 ASC로부터 제거도 가능하지만 이는 매우 위험하다.
- 만약 client에서 server보다 먼저 AS를 제거하고 Attribute 값 변화가 client로 replicate 된다면,
Attribute를 찾을 수 없어 Crash가 난다.
- 그렇기에 Runtime 상에서 AS를 추가/제거를 할 때에는 대상 ASC에 대해 ForceReplication 함수를 호출해줘야 한다.
Item Attribute
- 장비류 아이템에 대한 Attribute를 구현하는 방식.
- 이런 방식은 전부 Item에 직접 값을 저장한다.
- 이 시스템은 1명 이상의 플레이어가 Lifetime동안 장비를 장착/탈착 할 수 있는 경우에 반드시 제공되어야 한다.
Plain Floats on the Item
- Attribute 대신 float 변수로 정보를 관리하는 방식
- Fortnite와 GAS 예시코드가 이런 방식을 채택
- 총기가 최대 Ammo 수, 현재 Ammo 수, 남은 Ammo 수 등을 저장하고 Client에 Replicate한다.
- 만약 총기가 남은 Ammo를 공유하며느 그 값을 Character의 Attribute로 가져와 AttributeSet에 저장한다.
- 다만 총기에서 Attribute를 사용하지 않기 때문에, UGameplayAbility에서 제공하는 일부 함수를 override해야 한다.
- 총기에서 사용하는 Plain Float에 대한 검증과 계산을 위해
- 총기를 GameplayAbilitySpec의 SourceObject로 지정하면 접근 권한을 Ability 내에서 처리할 수 있다.
- 자동사격 과정에서 총기의 Ammo가 Replicate로 인해 덮어씌어지는 Clobbering을 방지하기 위해,
PreReplication() 함수에서 IsFiring GameplayTag가 있는 동안 Ammo의 Replicate를 비활성화 한다.-
더보기
void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) { Super::PreReplication(ChangedPropertyTracker); DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag))); DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, SecondaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag))); }
- 장점
- AttributeSet을 사용하면서 발생하는 한계를 피할 수 있음
- 단점
- 현존하는 GameplayEffects의 흐름을 사용할 수 없음.
- UGameplayAbility의 핵심 함수를 ammo 관리를 위해 override 해야 함.
-
AttributeSet on the Item
- Item에서 AttributeSet을 사용하고 Player의 ASC를 이용하는 방법
- 하지만 근본적인 한계가 존재한다.
- Weapon이 가지고 있는 고유한 Attribute를 경우에 따라 Character로 옮겨와야 한다.
- 만약 Weapon을 Inventory에 넣는다면,
Weapon의 AttributeSet을 Character가 가진 ASC의 SpawnedAttributes에 옮겨와야 한다.
- 만약 AttributeSet이 OwnerActor가 아닌 곳에 존재하면 Compile Error가 발생한다.
- 이를 수정하기 위해 다음 작업이 진행되어야 한다.
- AttributeSet을 Constructor가 아닌 BeginPlay()에서 Construct
- IAbilitySystemInterface를 Implement
- 장점
- 제공되는 GameplayAbility와 GameplayEffect를 그대로 사용 가능
- Item에 간단한 세팅으로 기능 구현 가능
- 단점
- 각 무기 타입에 맞는 AttributeSet을 생성해야 한다.
- ASC는 기능적으로 Class당 하나의 AttributeSet을 가질 수 밖에 없다.
- ASC가 Attribute 변경사항을 적용할 때 SpawnedAttributes에서 조건에 맞는 첫번째 AS만 탐색하기 때문.
- 나머지 AttributeSet은 항상 무시 됨
- 위와 같은 이유로 플레이어 당 동일한 무기를 2개 이상 소지할 수 없음
- AttributeSet을 Runtime상에서 제거하는 것에서 오는 근본적인 리스크
- 예를 들어 탄속이 있는 무기로 적을 처치 했는데 발사와 처치 사이에 무기가 제거가 되면
관련 AttributeSet을 찾지 못해 Crash가 발생한다.
- 예를 들어 탄속이 있는 무기로 적을 처치 했는데 발사와 처치 사이에 무기가 제거가 되면
- 각 무기 타입에 맞는 AttributeSet을 생성해야 한다.
ASC on the Item
- ASC를 통째로 아이템에 박아 넣는 극단적인 방법.
- 장점
- GameplayAbility와 GameplayEffect의 모든 기능을 오롯이 사용 가능
- Attributeset Class를 재사용 할 수 있음
- ASC에서 1개만 존재하니까
- 단점
- 개발 비용이 까마득하고 검증되지 않음
Defining Attributes
-
더보기
UCLASS() class MYPROJECT_API UMyAttributeSet : public UAttributeSet { GENERATED_BODY() public: /** 퍼블릭 액세스 가능한 샘플 어트리뷰트 'Health'*/ UPROPERTY(EditAnywhere, BlueprintReadOnly) FGameplayAttributeData Health; };
class MyAbilitySystemComponent : public UAbilitySystemComponent { .... /** 샘플 어트리뷰트 세트. */ UPROPERTY() const UMyAttributeSet* AttributeSet; .... }
// 이런 코드는 일반적으로 BeginPlay()에 나타나지만, // 적절한 어빌리티 시스템 컴포넌트 구하기일 수 있습니다. 다른 액터에 있을 수도 있으므로 GetAbilitySystemComponent를 사용하여 결과가 유효한지 확인하세요. AbilitySystemComponent* ASC = GetAbilitySystemComponent(); // AbilitySystemComponent가 유효한지 확인합니다. 실패를 허용할 수 없다면 if() 조건문을 check() 구문으로 대체합니다. if (IsValid(ASC)) { // 어빌리티 시스템 컴포넌트에서 UMyAttributeSet를 구합니다. 필요한 경우 어빌리티 시스템 컴포넌트가 UMyAttributeSet를 생성하고 등록할 것입니다. AttributeSet = ASC->GetSet<UMyAttributeSet>(); // 이제 새 UMyAttributeSet에 대한 포인터가 생겨 나중에 사용할 수 있습니다. 초기화 함수가 있는 경우 여기서 호출하면 좋습니다. }
- Attribute는 FGameplayAttributeData 타입으로 Attribute를 선언한다.
- 위 코드에서는 Attribute Set 안의 값이 코드에서 직접 수정하지 않도록 const로 선언을 정의한다.
- AttributeSet은 Actor Construct에서 Instance화 하는 시점에 함수가 유효한 ASC를 반환하는 한,
이 시점 혹은 BeginPlay 도중에 자동으로 등록된다. - BP를 편집하여 Attribute Set Type을 ASC의 Default Start Data로 추가할 수 있다.
- ASC가 없는 GA를 수정하는 Gameplay Effect가 적용된다면, ASC는 일치하는 GA를 자동으로 생성한다.
- 하지만 이 Methond는 AS를 생성하거나, 기존 AS에 GA를 추가하지 않는다.
Helper Funcion Macro
- GAS에서는 GA와 상호작용할 기본 헬퍼 함수를 추가할 수 있다.
- GA 자체는 protected/private 접근자로 설정하고, 상호작용 함수를 public으로 설정하는 것이 좋다.
- 사용이 필수 사항은 아니지만, 모범 사례로 간주한다.
-
더보기
#define GAMEPLAYATTRIBUTE_REPNOTIFY(ClassName, PropertyName, OldValue) \ { \ static FProperty* ThisProperty = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \ GetOwningAbilitySystemComponentChecked()->SetBaseAttributeValueFromReplication(FGameplayAttribute(ThisProperty), PropertyName, OldValue); \ } #define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \ static FGameplayAttribute Get##PropertyName##Attribute() \ { \ static FProperty* Prop = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \ return Prop; \ } #define GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \ FORCEINLINE float Get##PropertyName() const \ { \ return PropertyName.GetCurrentValue(); \ } #define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \ FORCEINLINE void Set##PropertyName(float NewVal) \ { \ UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \ if (ensure(AbilityComp)) \ { \ AbilityComp->SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \ }; \ } #define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \ FORCEINLINE void Init##PropertyName(float NewVal) \ { \ PropertyName.SetBaseValue(NewVal); \ PropertyName.SetCurrentValue(NewVal); \ }
Initialize
- AttributeMetaData라는 GAS TableRow로 초기화 할 수 있다.
- 이는 외부 파일에서 Import할 수도 있고, Editor에서 수동으로 채워줄 수도 있다.
-
DataTable Asset 생성 할 때 AttributeMetaData를 선택한다. - 위와 같이 선언된 AttributeMetaData Table Asset에 추가 Row를 덧붙여 AS를 다수의 GA로 지원할 수 있다.
- 예를 들어 MyAttributeSet.Health 외에 MyAttributeSet.Attack Row를 추가하면 한 AS에서 제공하는 다수의 GA를 하나의 Table Asset으로 Initialize 할 수 있다.
- Column에 MinValue, MaxValue가 지원되지만 GA와 AS에 범위 제한 행동이 없으므로 이 항목들은 무효하다.
Gameplay Attribute Access 제어
- GA에 대한 직접 Access를 지어하는 것은 AS 통해 수행되며, FGameplayAttributeData를 확장하지 않는다.
- FGameplayAttributeData는 Data에 대한 Access만 저장하고 제공한다.
- GAS에서 제공하는 Macro를 사용하지 않고, 동일한 Macro에 구현부를 직접 작업해 값의 범위를 제한할 수도 있다.
-
더보기
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(UMyAttributeSet, Health); float GetHealth() const; void SetHealth(float NewVal); GAMEPLAYATTRIBUTE_VALUE_INITTER(Health);
float UMyAttributeSet::GetHealth() const { // Health의 현재 값을 반환하지만, 0 미만의 값은 반환하지 않습니다. // 이 값은 Health에 영향을 미치는 모든 모디파이어가 고려된 후의 값입니다. return FMath::Max(Health.GetCurrentValue(), 0.0f); } void UMyAttributeSet::SetHealth(float NewVal) { // 0 미만의 값을 받지 않습니다. NewVal = FMath::Max(NewVal, 0.0f); // 어빌리티 시스템 컴포넌트 인스턴스가 있는지 확인합니다. 항상 인스턴스가 있어야 합니다. UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent(); if (ensure(ASC)) { // 적절한 함수를 통해 현재 값이 아닌 베이스 값을 설정합니다. // 그러면 적용한 모디파이어가 계속해서 적절히 작동합니다. ASC->SetNumericAttributeBase(GetHealthAttribute(), NewVal); } } AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetHealthAttribute()).AddUObject(this, &AGASAbilityDemoCharacter::OnHealthChangedInternal);
Gameplay Effect와의 Interaction
- GA의 값을 제어하는 가장 일반적인 방법은 관련 Gameplay Effect를 처리하는 것이다.
-
- PostGameplayEffectExecute는 Attribute의 Base 값을 변경되는 GameplayEffect가 호출되기 전에 호출된다.
- GameplayEffect가 호출(Execute) 되기 전에만 호출되고, 직접 접근해 수정할 때에는 호출되지 않는다.
- 또한 BaseValue가 아닌 current 값에 영향을 주는 GameplayEffect에서도 호출되지 않는다.
- 위 예시에서 PostGameplayEffectExecute 함수는 public 접근자가 지정되어야 한다.
- 함수 내에서 새로운 Body를 작성하되, Super::PostGameplayEffectExecute가 확실히 호출되어 있어야 한다.
- PostGameplayEffectExecute는 Attribute의 Base 값을 변경되는 GameplayEffect가 호출되기 전에 호출된다.
- 더보기
class UMyAttributeSet : public AttributeSet { ..... public: void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override; ..... }
void UMyAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) { // 잊지 말고 부모 구현을 호출하세요. Super::PostGameplayEffectExecute(Data); // 프로퍼티 게터를 사용하여 이 호출이 Health에 영향을 미치는지 확인합니다. if (Data.EvaluatedData.Attribute == GetHealthAttribute()) { // 이 게임플레이 이펙트는 Health를 변경합니다. 적용하되 우선 값을 제한합니다. // 이 경우 Health 베이스 값은 음수가 아니어야 합니다. SetHealth(FMath::Max(GetHealth(), 0.0f)); } }
Replication
- Multiplayer의 경우, 여타 Property Replicate와 유사하게 AS을 통해 GA Replicate를 할 수 있다.
-
더보기
class UMyAttributeSet : public UAttributeSet { ..... /** 네트워크를 통해 새 Health 값이 도착할 때 호출됨 */ UFUNCTION() virtual void OnRep_Health(const FGameplayAttributeData& OldHealth); /** 리플리케이트할 프로퍼티 표시 */ virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; protected: /** 샘플 어트리뷰트 'Health'*/ UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Health) FGameplayAttributeData Health; .... };
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth) { // 디폴트 게임플레이 어트리뷰트 시스템의 repnotify 행동을 사용합니다. GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth); } void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { // 부모 함수를 호출합니다. Super::GetLifetimeReplicatedProps(OutLifetimeProps); // Health에 대한 리플리케이션을 추가합니다. DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always); }
- Replicate 할 Attribute에 PROPERTY에 ReplicatedUsing 지정자를 추가하고, Callback 함수를 선언한다.
- Source File에서 Callback 함수를 정의한다.
- Callback 함수 내에는 GAMEPLAYATTRIBUTE_REPNOTIFY 매크로로
Replicate 되는 Attribute에 대한 RepNotify 행동을 실행한다. - 만약 이 AS가 처음 Replicate되는 Attribute라면, GetLifetimeReplicatedProps에 추가한다.
주요 함수
PreAttributeChange
/**
* Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
* There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
* This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
*
* NewValue is a mutable reference so you are able to clamp the newly applied value as well.
*/
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
- Attribute의 CurrentValue가 바뀌기 전에 호출되는 함수
- NewValue를 이용해 CurrentValue를 Clamp한 상태로 적용할 때 사용 함.
-
더보기
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); }
- 단, Attribute에 직접 접근하여 변경하는 것이 아닌 Macro에서 제공하는 Setter 함수나 GameplayEffects로만 호출된다.
- 그리고 Epic에서는 이 함수를 Attribute 변경에 대한 Delegate로 사용을 권장하지 않는다.
- 그저 Attribute 값의 보정(Clamp) 용도로 사용을 권장한다.
PostGameplayEffectExecute
/**
* Called just after a GameplayEffect is executed to modify the base value of an attribute. No more changes can be made.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) { }
- Attribute의 BaseValue가 Instant GameplayEffect로 인해 변경될 때 호출되는 함수
- GameplayEffect로 인해 Attribute가 변동될 때 좀 더 복잡한 작업을 수행할 수 있다.
- 함수가 호출된 시점에는 이미 Attribute 값이 변경이 된 후다.
- 하지만 아직 Client에는 Replicate 되지 않았다.
- 때문에 여기서 Clamping을 하는 것은 Client에서는 발생하지 않고, 그 결과만 받는다.
OnAttributeAggregatorCreate
/** Callback for when an FAggregator is created for an attribute in this set. Allows custom setup of FAggregator::EvaluationMetaData */
virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const { }
- Attribute가 set에서 생성되어 Aggregator가 생성될 때.
- FAggregatorEvaluateMetaData에 대한 Custom Setup을 할 수 있다.
- Attribute에서 Modifier로 적용하는데 가장 작은 음수 값을 가져올 수 있다.
- Paragon에서 여러 개의 감속 디버프가 걸릴 때 가장 낮은 비율로 적용할 때 이 기능을 사용
-
더보기
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const { Super::OnAttributeAggregatorCreated(Attribute, NewAggregator); if (!NewAggregator) { return; } if (Attribute == GetMoveSpeedAttribute()) { NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods; } }
'UE5 > GAS' 카테고리의 다른 글
[GAS] Ability Task (0) | 2024.05.16 |
---|---|
[GAS] Gameplay Effect (0) | 2024.05.15 |
[GAS] Gameplay Attribute (0) | 2024.05.13 |
[GAS] Ability System Component (0) | 2024.05.13 |
[GAS] Gameplay Ability System 소개 (0) | 2024.05.10 |