Unreal GAS (9) - WidgetController (MVC 모델)

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
9/34

2025 / 09 / 23

  • Initializing WidgetController & Widgets

    • Model ← WidgetController ← Widget의 단방향 참조 구조를 만들었고,
      이제 WidgetController와 Widget이 필요한 정보를 초기화하도록 해야 함.

    • UAuraUserWidget

      UCLASS()
      class AURA_API UAuraUserWidget : public UUserWidget
      {
      	GENERATED_BODY()
      	
      protected:
      	UFUNCTION(BlueprintImplementableEvent)
      	void WidgetControllerSet();
      
      public:
      	UFUNCTION(BlueprintCallable)
      	void SetWidgetController(UObject* InWidgetController);
      
      	UPROPERTY(BlueprintReadOnly)
      	TObjectPtr<UObject> WidgetController;
      };
      
      // UAuraUserWidget.cpp
      void UAuraUserWidget::SetWidgetController(UObject* InWidgetController)
      {
      	WidgetController = InWidgetController;
      	WidgetControllerSet(); 
      }
      
      • SetWidgetController를 통해 WidgetController에 대한 레퍼런스를 가질 수 있게 한다.

        • SetWidgetController는 AuraHUD의 InitOverlay()에서 호출되며, GetOverlayWidgetController()를 통해 WidgetController가 생성된 이후, 미리 생성해 둔 OverlayWidget(AuraUserWidget 상속 클래스)의 SetWidgetController()를 호출한다.
      • 이렇게 레퍼런스를 얻은 WidgetController는 WidgetControllerSet 이벤트를
        블루프린트에서 각 위젯에서 오버라이드할 때 사용함으로서 필요한 이벤트를 바인딩 할 것이다.

        • 현재는 체력/마나 게이지 위젯인 WBP_HealthGlobe와 WBP_ManaGlobe에서 WidgetControllerSet을 오버라이드해서 AttributeSet의 체력/마나 수치가 변할 때마다 그에 맞춰 게이지의 Fill을 업데이트하도록 구현되어 있다.
        • OnHealthChanged와 같은 델리게이트는 UAuraWidgetController 상속 클래스인 UOverlayWidgetController에 선언되어있다.
    • UAuraWidgetController

      class UAbilitySystemComponent;
      class UAttributeSet;
      
      USTRUCT(BlueprintType)
      struct FWidgetControllerParams
      {
      	GENERATED_BODY()
      
      	FWidgetControllerParams() {}
      	FWidgetControllerParams(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS) 
      		: PlayerController(PC), PlayerState(PS), AbilitySystemComponent(ASC), AttributeSet(AS) {}
      
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	TObjectPtr<APlayerController> PlayerController = nullptr;
      
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	TObjectPtr<APlayerState> PlayerState = nullptr;
      
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent = nullptr;
      
      	UPROPERTY(EditAnywhere, BlueprintReadWrite)
      	TObjectPtr<UAttributeSet> AttributeSet = nullptr;
      };
      
      UCLASS()
      class AURA_API UAuraWidgetController : public UObject
      {
      	GENERATED_BODY()
      	
      protected:
      	UPROPERTY(BlueprintReadOnly, Category = "WidgetController")
      	TObjectPtr<APlayerController> PlayerController;
      
      	UPROPERTY(BlueprintReadOnly, Category = "WidgetController")
      	TObjectPtr<APlayerState> PlayerState;
      
      	UPROPERTY(BlueprintReadOnly, Category = "WidgetController")
      	TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
      
      	UPROPERTY(BlueprintReadOnly, Category = "WidgetController")
      	TObjectPtr<UAttributeSet> AttributeSet;
      
      public:
      	UFUNCTION(BlueprintCallable)
      	void SetWidgetControllerParams(const FWidgetControllerParams& WCParams);
      
      	virtual void BroadcastInitialValues();
      
      	virtual void BindCallbacksToDependencies();
      };
      
      // UAuraWidgetController.cpp
      void UAuraWidgetController::SetWidgetControllerParams(const FWidgetControllerParams& WCParams)
      {
      	PlayerController = WCParams.PlayerController;
      	PlayerState = WCParams.PlayerState;
      	AbilitySystemComponent = WCParams.AbilitySystemComponent;
      	AttributeSet = WCParams.AttributeSet;
      }
      
      void UAuraWidgetController::BroadcastInitialValues()
      {
      	// Implement in Child Class
      }
      
      void UAuraWidgetController::BindCallbacksToDependencies()
      {
      	// Implement in Child Class
      }
      • SetWidgetControllerParams()를 통해 Model, 즉 데이터를 얻어올 각 클래스들에 대한 레퍼런스를 지정해준다.
      • AuraWidgetController 또한 해당 클래스 자체로 사용하는 것이 아니라, 자식 클래스로 상속하여 해당 클래스들에서 세부 사항을 구현할 것이다.
      • OverlayWidgetController
        #pragma once
        
        #include "CoreMinimal.h"
        #include "UI/WidgetController/AuraWidgetController.h"
        #include "OverlayWidgetController.generated.h"
        
        DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangedSignature, float, NewHealth);
        DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxHealthChangedSignature, float, NewMaxHealth);
        DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnManaChangedSignature, float, NewMana);
        DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMaxManaChangedSignature, float, NewMaxMana);
        
        UCLASS(BlueprintType, Blueprintable)
        class AURA_API UOverlayWidgetController : public UAuraWidgetController
        {
        	GENERATED_BODY()
        	
        public:
        	virtual void BroadcastInitialValues() override;
        
        	virtual void BindCallbacksToDependencies() override;
        
        	UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
        	FOnHealthChangedSignature OnHealthChanged;
        
        	UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
        	FOnMaxHealthChangedSignature OnMaxHealthChanged;
        
        	UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
        	FOnManaChangedSignature OnManaChanged;
        
        	UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
        	FOnMaxManaChangedSignature OnMaxManaChanged;
        
        protected:
        	void HealthChanged(const FOnAttributeChangeData& Data) const;
        	void MaxHealthChanged(const FOnAttributeChangeData& Data) const;
        	void ManaChanged(const FOnAttributeChangeData& Data) const;
        	void MaxManaChanged(const FOnAttributeChangeData& Data) const;
        };
        
        // OverlayWidgetController.cpp
        #include "UI/WidgetController/OverlayWidgetController.h"
        #include "AbilitySystem/AuraAttributeSet.h"
        
        void UOverlayWidgetController::BroadcastInitialValues()
        {
        	UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
        	OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
        	OnMaxHealthChanged.Broadcast(AuraAttributeSet->GetMaxHealth());
        	OnManaChanged.Broadcast(AuraAttributeSet->GetMana());
        	OnMaxManaChanged.Broadcast(AuraAttributeSet->GetMaxMana());
        }
        
        void UOverlayWidgetController::BindCallbacksToDependencies()
        {
        	UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
        	
        	// Dynamic Delegate가 아니므로 AddUObject 사용
        	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetHealthAttribute())
        		.AddUObject(this, &UOverlayWidgetController::HealthChanged);
        	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxHealthAttribute())
        		.AddUObject(this, &UOverlayWidgetController::MaxHealthChanged);
        	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetManaAttribute())
        		.AddUObject(this, &UOverlayWidgetController::ManaChanged);
        	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AuraAttributeSet->GetMaxManaAttribute())
        		.AddUObject(this, &UOverlayWidgetController::MaxManaChanged);
        }
        
        void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData& Data) const
        {
        	OnHealthChanged.Broadcast(Data.NewValue);
        }
        
        void UOverlayWidgetController::MaxHealthChanged(const FOnAttributeChangeData& Data) const
        {
        	OnMaxHealthChanged.Broadcast(Data.NewValue);
        }
        
        void UOverlayWidgetController::ManaChanged(const FOnAttributeChangeData& Data) const
        {
        	OnManaChanged.Broadcast(Data.NewValue);
        }
        
        void UOverlayWidgetController::MaxManaChanged(const FOnAttributeChangeData& Data) const
        {
        	OnMaxManaChanged.Broadcast(Data.NewValue);
        }
        • OverlayWidgetController는 AuraHUD에서 생성하는 위젯으로, WidgetController는 액터가 아닌 오브젝트를 상속한 클래스이기 때문에 월드 상 위치가 존재하지 않는다. 따라서 AuraHUD는 WidgetController에 대한 접근을 할 수 있도록 레퍼런스를 지니고 있다.
        • OverlayWidgetController에서 아래의 몇몇 부분에 유의해야 한다.
          • 델리게이트 선언에 있어서의 이름 작성 컨벤션
            (FOnHealthChangedSignature와 같은 형태의 이름을 선언해두면, 실제 호출되는 델리게이트에서 OnHealthChanged와 같은 이름을 쓰기에 편하다)
          • AbilitySystemComponent에서 Attribute가 변경될 때 호출되는 델리게이트에 함수를 등록하기 위해 AddUObject()를 사용하는 것 (GetGameplayAttributeValueChangeDelegate()의 반환형인 FOnGameplayAttributeValueChange는 DYNAMIC 델리게이트로 선언된 것이 아니어서 AddUObject()로 호출될 함수를 등록해주어야 한다.)
          • 등록할 델리게이트의 반환형과 매개변수 타입을 일치하게 하기 위해 const FOnAttributeChangeData&를 매개변수 타입으로 지정해준 것
            (델리게이트의 매개변수 타입 등은 해당 델리게이트의 선언 매크로 지점으로 가보면 확인할 수 있다.)
          • BroadCast()로 전파되는 변경사항들은, 위 UAuraUserWidget을 상속하는 위젯 블루프린트에서 WidgetControllerSet()을 오버라이드하여 필요한 부분에 바인딩하여 사용하는 것이다.
    • AuraHUD

      UCLASS()
      class AURA_API AAuraHUD : public AHUD
      {
      	GENERATED_BODY()
      
      public:
      	UPROPERTY()
      	TObjectPtr<UAuraUserWidget> OverlayWidget;
      
      	UFUNCTION()
      	UOverlayWidgetController* GetOverlayWidgetController(const FWidgetControllerParams& WCParams);
      
      	UFUNCTION()
      	void InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS);
      
      private:
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<UAuraUserWidget> OverlayWidgetClass;
      
      	UPROPERTY()
      	TObjectPtr<UOverlayWidgetController> OverlayWidgetController;
      
      	UPROPERTY(EditAnywhere)
      	TSubclassOf<UOverlayWidgetController> OverlayWidgetControllerClass;
      };
      
      // AuraHUD.cpp
      #include "UI/HUD/AuraHUD.h"
      #include "UI/WidgetController/OverlayWidgetController.h"
      #include "UI/Widget/AuraUserWidget.h"
      
      UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
      {
      	if (OverlayWidgetController == nullptr)
      	{
      		NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
      		OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
      		OverlayWidgetController->SetWidgetControllerParams(WCParams);
      		OverlayWidgetController->BindCallbacksToDependencies();
      
      		return OverlayWidgetController; 
      	}
      
      	return OverlayWidgetController;
      }
      
      void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
      {
      	checkf(OverlayWidgetClass, TEXT("OverlayWidgetClass uninitialized. Please fill out BP_AuraHUD."));
      	checkf(OverlayWidgetClass, TEXT("OverlayWidgetControllerClass uninitialized. Please fill out BP_AuraHUD."));
      
      	UUserWidget* Widget = CreateWidget<UUserWidget>(GetWorld(), OverlayWidgetClass);
      	OverlayWidget = Cast<UAuraUserWidget>(Widget);
      
      	const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
      	UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);
      
      	OverlayWidget->SetWidgetController(WidgetController);
      	WidgetController->BroadcastInitialValues();
      
      	Widget->AddToViewport();
      }
      
      • AuraHUD는 상속하는 블루프린트(BP_AuraHUD)를 생성하여 이전에 생성한 BP_AuraGameMode의 HUD 클래스에 등록하며, BP_AuraHUD는 OverlayWidgetClass 및 OverlayWidgetControllerClass에 클래스를 등록해주어야 한다.
      • AuraHUD의 InitOverlay()는 싱글플레이와 멀티플레이에 상관 없이 필요한 모델 데이터가 전부 존재하는 것이 확실한 상황에서 호출되어야만 한다.
        (PlayerController, PlayerState, AbilitySystemComponent, AttributeSet을 Model로 사용하기 때문)
        이 때, 이미 그러한 조건이 확실히 갖춰진 곳이 있는데, 바로 AuraPlayerCharacter의 InitAbilityInfo()이다.
        ```cpp
        void AAuraPlayerCharacter::PossessedBy(AController* NewController)
        {
        	Super::PossessedBy(NewController);
        
        	/** Init Ability Actor Info for Server */
        	InitAbilityActorInfo();
        }
        
        void AAuraPlayerCharacter::OnRep_PlayerState()
        {
        	Super::OnRep_PlayerState();
        
        	// Init Ability Actor Info for Client
        	InitAbilityActorInfo();
        }
        
        void AAuraPlayerCharacter::InitAbilityActorInfo()
        {
        	AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
        	check(AuraPlayerState);
        
        	AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
        	AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
        	AttributeSet = AuraPlayerState->GetAttributeSet();
        
        	if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
        	{
        		if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
        		{
        			AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
        		}
        		
        	}
        }
        ```
        
        - InitAbilityInfo()는 캐릭터가 컨트롤러에 의해 소유되는 시점인 OnPossessed()거나 플레이어 스테이트가 준비됐을 때 호출되는 OnRep_PlayerState()에서 호출되도록 한 함수이다.
        따라서 여기서 호출한다면 필요한 모든 데이터가 이미 준비된 상태라고 볼 수 있으므로 AuraHUD의 InitOverlay()를 호출하기에 적절하다.
        - 즉, 게임이 실행되고 MVC 모델을 구성하기 위한 함수 호출 순서는
            1. InitAbilityActorInfo()(AuraPlayerCharacter) 
            2. GetOverlayWidgetController()(AuraHUD)
            (위젯 컨트롤러를 반환하되, 없으면 생성하여 반환)
            3. SetWidgetControllerParams()(OverlayWidgetController)
            (위젯 컨트롤러에 모델로 사용할 레퍼런스 설정)
            4. InitOverlay()(AuraHUD) 
            (AuraUserWidget을 생성함)
            5. BIndCallbackstoDependencies()(OverlayWidgetController)
            (Attribute 변경 시 호출 될 델리게이트에 위젯 컨트롤러의 델리게이트 호출 함수를 바인딩함)
            6. SetWidgetController()(AuraUserWidget)
            (AuraUserWidget에 위젯 컨트롤러 설정)
            7. WidgetControllerSet()(AuraUserWidget)
            (각 위젯 블루프린트에서 바인딩 된 이벤트 호출)
        - 위의 순서와 같으며, 이렇게 구성한 경우
            
            > Model → WidgetController에 대해 알지 못함
            WidgetController → Widget에 대해 알지 못하나 Model은 알고 있음
            Widget → Model에 대해 알지 못하지만 WidgetController는 알고 있음
            이렇게 단방향의 참조 구조를 가지게 된다.
            >
profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글