https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-attributes-and-attribute-sets-for-the-gameplay-ability-system-in-unreal-engine?application_version=5.3

 

언리얼 엔진의 게임플레이 어빌리티 시스템을 위한 게임플레이 어트리뷰트 및 어트리뷰트 세트

게임플레이 어트리뷰트 및 어트리뷰트 세트 사용하기

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가 발생한다.

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에 대한 포인터가 생겨 나중에 사용할 수 있습니다. 초기화 함수가 있는 경우 여기서 호출하면 좋습니다.
     }


    1.  Attribute는 FGameplayAttributeData 타입으로 Attribute를 선언한다.
    2. 위 코드에서는 Attribute Set 안의 값이 코드에서 직접 수정하지 않도록 const로 선언을 정의한다.
    3. AttributeSet은 Actor Construct에서 Instance화 하는 시점에 함수가 유효한 ASC를 반환하는 한,
      이 시점 혹은 BeginPlay 도중에 자동으로 등록된다.
    4. BP를 편집하여 Attribute Set Type을 ASC의 Default Start Data로 추가할 수 있다.
    5. ASC가 없는 GA를 수정하는 Gameplay Effect가 적용된다면, ASC는 일치하는 GA를 자동으로 생성한다.
      1. 하지만 이 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가 확실히 호출되어 있어야 한다.
  • 더보기
    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

+ Recent posts