Unreal GAS (20) - Attritube Widget Controller, TBaseStaticDelegateInstance

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
21/34
  • Widget Attribute Tags

    • WBP_TextValueRow에 GameplayTag형 변수를 추가함.
      이를 통해 각 Row들에 각각 표기할 Attribute에 해당하는 GameplayTag를 설정하고,
      해당 GameplayTag와 일치하는 FAttributeInfoSignature에만 반응하도록 함.
  • Mapping Tags to Attributes

    • OverlayWidget에서 했던 것과 비슷하게, WidgetController가 Model 데이터들을 Broadcast 할 수 있게 해주는 작업을 해야 한다.
    1. 직접 작성하는 방법

      • AttributeMenuWidgetController에서 아래와 같이 BroadcastInitialValues()를 작성하면, WBP_AttributeMenu의 각 Row 위젯들마다 모두 자신의 GameplayTag에 해당하는 정보를 델리게이트로 전달받을 수 있음.
        void UAttributeMenuWidgetController::BroadcastInitialValues()
        {
        	UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
        	
        	check(AttributeInfo);
        	
        	// 아래 구문을 모든 Attribute마다 작성해주어야 함.
        	FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(FAuraGameplayTag::Attribute_Primary_Strength);\
        	Info.AttributeValue = AS->GetStrength();
        	FAttributeInfoSignature.Broadcast(Info);
        }
        • 하지만 이 방법은 새로운 Attribute가 추가될 때마다 AttributeMenuWidgetController에 새로운 위젯을 바인딩 시켜줘야 하는 번거로움이 있음.
        • 따라서 관용구가 추가되는 것 자체는 막을 수 없더라도, 관용구를 최대한 한 곳에만 집중해놓으면 사소한 코드 오류가 발생하는 횟수를 줄일 수 있으므로, AttributeMenuWidgetController에는 Attribute가 몇 개이든지 상관없이 모든 Attribute를 FAttributeInfoSignature로 발송할 수 있도록 하려 함.
        • AuraAttributeSet는 게임 내 캐릭터들이 사용하는 Attribute들이 존재하는 곳이고, 이미 Replication을 위한 관용구나, ATTRIBUTE_ACCESSORS와 같은 매크로가 많이 사용되고 있기 때문에 위의 목적을 위한 관용구를 추가하기에 적절한 곳임.
    2. Delegate 및 BindStatic()을 활용하는 방법

      • DECLARE_DELEGATE_RetVal() 매크로를 이용하여 반환값이 있는 델리게이트를 선언함.
      • DECLARE_DELEGATE_RetVal(’반환형’, ‘델리게이트명’) 의 형태로 작성하면 됨.
        DECLARE_DELEGATE_RetVal(FGameplayAttribute, FAttributeSignature) 델리게이트를 선언함.
        - FGameplayAttribute를 반환형으로 결정한 이유는, ATTRIBUTE_ACCESSORS 매크로 덕분에 각 Attribute들에 대한 Getter 함수가 알아서 생성되어 있고, 이 함수들의 반환형이 FGameplayAttribute이기 때문임.
        (Strength라는 이름의 Attribute가 있다면, ATTRIBUTE_ACCESSORS를 통해 해당 Attribute 자체를 반환하는 GetStrengthAttribute(), Attribute의 값만을 반환하는 GetStrength()가 둘 다 생성되는데, 여기서는 GetStrengthAttribute()를 사용함)
      // AuraAttributeSet.h
      DECLARE_DELEGATE_RetVal(FGameplayAttribute, FAttributeSignature);
      TMap<FGameplayTag, FAttributeSignature> TagsToAttributes;
      
      // AuraAttributeSet.cpp
      UAuraAttributeSet::UAuraAttributeSet()
      {
      	const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();
      
      	FAttributeSignature StrengthDelegate;
      	StrengthDelegate.BindStatic(GetStrengthAttribute);
      	TagsToAttributes.Add(GameplayTags.Attributes_Primary_Strength, StrengthDelegate);
      }
      • UAuraAttributeSet의 생성자에서 FAttributeSignature 델리게이트를 선언하고, BindStatic()을 통해 해당 델리게이트에 각 Attribute에 맞는 GetAttribute() 함수를 바인딩해준다. (Getter 함수들이 Static으로 되어 있기 때문에 BindStatic() 사용)
        GetAttribute()의 반환형은 FGameplayAttribute이기 때문에 델리게이트를 RetVal로 선언하여 FGameplayAttribute로 반환형을 정해준 것이다.
      void UAttributeMenuWidgetController::BroadcastInitialValues()
      {
      	UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
      	
      	check(AttributeInfo);
      	
      	for (auto& Pair : AS->TagsToAttributes)
      	{
      		FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(Pair.Key);
      		Info.AttributeValue = Pair.Value.Execute().GetNumericValue(AS);
      		AttributeInfoDelegate.Broadcast(Info);
      	}
      }
      • AttributeMenuWidgetController에서는 미리 생성된 TMap을 통해 for문으로 Attribute의 값을 FAuraAttributeInfo에 업데이트하고 이를 전파하는 내용만 있으면 된다.
        (새로 Attribute가 추가되도 AuraAttributeSet에서만 관련 내용을 추가하면 됨)
      • 델리게이트를 활용한 덕분에 관용구를 한 곳에 몰아넣을 수 있었고, WidgetController에서는 훨씬 깔끔한 함수 작성이 가능해졌다.
        하지만 이 또한 결국은 AttributeSet에 매번 델리게이트를 선언하고 BindStatic()으로 Attribute Getter 함수를 바인딩하여 TagsToAttributes 맵에 저장해두어야 한다는 반복적인 번거로움이 존재한다.
        WidgetController에 직접 작성하는 것보다야 훨씬 실수의 여지도 줄어들었지만, 여전히 코드가 많이 길다.
    3. TBaseStaticDelegateInstance를 활용하는 방법

      • TBaseStaticDelegateInstance를 직접 활용해 볼 수 있다.
        BindStatic()의 매개변수와 함수 구현을 직접 살펴보면 아래와 같은 형태로 되어 있다.

        ```cpp
        // 매개변수
        BindStatic(typename TBaseStaticDelegateInstance<FuncType, UserPolicy, std::decay_t<VarTypes>...>::FFuncPtr InFunc, VarTypes&&... Vars)
        
        /**
         * Binds a raw C++ pointer global function delegate
         */
        template <typename... VarTypes>
        inline void BindStatic(typename TBaseStaticDelegateInstance<FuncType, UserPolicy, std::decay_t<VarTypes>...>::FFuncPtr InFunc, VarTypes&&... Vars)
        {
        	new (*this) TBaseStaticDelegateInstance<FuncType, UserPolicy, std::decay_t<VarTypes>...>(InFunc, Forward<VarTypes>(Vars)...);
        }
        ```
      • 이 중에서 TBaseStaticDelegateInstance가 바로 함수를 묶는 함수 포인터의 역할을 한다.
        또한 어차피 TBaseStaticDelegateInstance를 new로 생성하는 역할만 하고 있는 것을 볼 수 있다.
        해당 클래스의 일부를 살펴보면 아래와 같다.

        ```cpp
        /**
         * Implements a delegate binding for regular C++ functions.
         */
        template <typename FuncType, typename UserPolicy, typename... VarTypes>
        class TBaseStaticDelegateInstance;
        
        template <typename RetValType, typename... ParamTypes, typename UserPolicy, typename... VarTypes>
        class TBaseStaticDelegateInstance<RetValType(ParamTypes...), UserPolicy, VarTypes...> : public TCommonDelegateInstanceState<RetValType(ParamTypes...), UserPolicy, VarTypes...>
        {
        private:
        	using Super            = TCommonDelegateInstanceState<RetValType(ParamTypes...), UserPolicy, VarTypes...>;
        	using DelegateBaseType = typename UserPolicy::FDelegateExtras;
        
        public:
        	using FFuncPtr = RetValType(*)(ParamTypes..., VarTypes...);
        
        	template <typename... InVarTypes>
        	explicit TBaseStaticDelegateInstance(FFuncPtr InStaticFuncPtr, InVarTypes&&... Vars)
        		: Super        (Forward<InVarTypes>(Vars)...)
        		, StaticFuncPtr(InStaticFuncPtr)
        	{
        		check(StaticFuncPtr != nullptr);
        	}
        
        	// IDelegateInstance interface
        
        // .....
        
        private:
        
        	// C++ function pointer.
        	FFuncPtr StaticFuncPtr;
        };
        ```
        • 주석을 보면, 일반적인 C++ 함수에 대한 델리게이트 바인딩을 시행한다고 되어 있다.
          즉, BindStatic()은 편리하게 사용할 수 있는 방법을 제공할 뿐이고, TBaseStaticDelegateInstance가 실질적인 델리게이트 바인딩을 시행한다고 보면 될 듯 하다.

          • 몇몇 변수나 함수 이름을 참고해본다면, TBaseStaticDelegateInstance는 Static 함수에 대한 델리게이트 바인딩을 시행하는 듯 하고, 그 외 다른 DelegateInstance들도 DelegateInstancesImpl.h 내부에 존재하는 것으로 보인다.
            다만 Attribute Getter 함수들은 Static 함수들이므로 여기서는 TBaseStaticDelegateInstance를 사용하는 것이다.
        • 그렇다면 굳이 델리게이트를 선언해서 BindStatic()으로 Attribute Getter 함수들을 묶기보다는, TBaseStaticDelegateInstance를 직접 사용한다면 훨씬 깔끔하게 처리가 가능할 것이다.

        • 이 중에서도 FFuncPtr가 변수 이름 그대로 함수 포인터 역할을 한다.

          #include "CoreMinimal.h"
          #include "AbilitySystemComponent.h"
          #include "AuraAttributeSet.generated.h"
          
          #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
           	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
           	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
           	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
           	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
          
          USTRUCT()
          struct FEffectProperties
          {
          	// ...
          };
          
          UCLASS()
          class AURA_API UAuraAttributeSet : public UAttributeSet
          {
          	GENERATED_BODY()
          	
          public:
          	UAuraAttributeSet();
          
          	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
          	
          	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
          	virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
          
          	TMap<FGameplayTag, TBaseStaticDelegateInstance<FGameplayAttribute(), FDefaultDelegateUserPolicy>::FFuncPtr> TagsToAttributes;
          
          // ...
          
          private:
          	void SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& OutEffectProperties) const;
          };
          • TBaseStaticDelegateInstance 대신 FGameplayAttribute(*)()와 같은 방식으로 원시적인 C++의 함수 포인터를 사용하는 것도 가능하다.
            C++의 원시적인 함수 포인터
            [반환형](*함수명)(매개변수)의 형태로 이루어짐.
            
            따라서 FGameplayAttribute(*)()는 매개변수가 없고, 
            FGameplayAttribute를 반환하는 함수를 지칭하는 것.
        • typedef, template/using/typename

          • typedef
            • 일종의 별칭을 선언하는 역할을 한다.
              • typedef TBaseStaticDelegateInstance<FGameplayAttribute(), FDefaultDelegateUserPolicy>::FFuncPtr FAttributeFuncPtr; 와 같은 방식으로 선언하면, FAttributeFuncPtr가 TBaseStaticDelegateInstance<FGameplayAttribute(), FDefaultDelegateUserPolicy>::FFuncPtr와 동일한 역할을 한다.
            • template/using/typename
              • template은 당연하겠지만, 템플릿을 사용한다고 선언하는 것이다.
              • using 키워드 또한 typedef와 마찬가지로 별칭을 선언하는 역할을 한다.
                typedef 키워드보다 일반적으로 더 많이 사용하는데, 이유는 using 키워드는 C++11 이후, 즉 모던 C++에서 등장한 것으로, 템플릿과 같이 사용이 가능하기에 더 유연하고, 가독성과 유지보수 측면에서 더 발전되었기 때문이라 한다.
              • typename은 보통 template을 사용하는 using문에서 같이 사용하며, 컴파일러가 혼동하는 것을 방지하기 위해서 사용한다.
                컴파일러가 using 문에 주어진 것이 타입인지 아닌지를 모를 수 있기 때문에, typename을 명시해주어 이것이 타입임을 미리 알려주는 것이라 한다.

          직접 typename 없이 빌드를 시도해봤더니, 약 30초 정도 빌드 대기를 하다가 “ 'TBaseStaticDelegateInstance<T,FDefaultDelegateUserPolicy,>::FFuncPtr': 종속 이름이 형식이 아닙니다.”라는 위와 같은 오류가 잔뜩 출력되고 빌드가 실패하는 모습을 볼 수 있었다.

  • Responding to Attribute Changes

    • AttributeMenuWidgetController는 초기화 될 때만 BroadcastInitialValues()를 통해 Attribute들의 값의 표기를 업데이트 하고 있기 때문에, Attribute가 실시간으로 변경되는 것을 반영하지 못한다.
    • 따라서 이를 위해, BindCallbacksToDependencies()에 적절한 람다 함수를 바인딩하여 값이 변경될 때마다 이를 전파할 수 있도록 한다.
      void UAttributeMenuWidgetController::BindCallbacksToDependencies()
      {
      	UAuraAttributeSet* AS = CastChecked<UAuraAttributeSet>(AttributeSet);
      
      	for (auto& Pair : AS->TagsToAttributes)
      	{
      		AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Pair.Value())
      			.AddLambda([this, Pair, AS](const FOnAttributeChangeData& Data)
      				{
      					FAuraAttributeInfo Info = AttributeInfo->FindAttributeInfoForTag(Pair.Key);
      					Info.AttributeValue = Pair.Value().GetNumericValue(AS);
      					AttributeInfoDelegate.Broadcast(Info);
      				}
      			);
      	}
      }
      • AbilitySystemComponent에서 제공하는 GetGameplayAttributeValueChangeDelegate()를 통해, Attribute의 값이 변할 때 실행될 함수 바인딩을 할 수 있다. (OverlayWidgetController에서도 사용한 함수)
      • AttributeSet에서 충분한 사전 작업을 해 둔 덕분에, TMap<FGameplayTag, FStaticFuncPtr<FGameplayAttribute()>> 변수인 TagsToAttributes에서 필요한 정보에 손쉽게 접근하여, for문으로 간단하게 람다 함수에 바인딩 하는 작업을 마칠 수 있었다.
profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글