UI / Widget 설계 (Model View Controller)

Lee Raccoon·2024년 9월 11일
0

언리얼 공부

목록 보기
11/11

UI / Widget 설계

Model View Controller 개념 도입

웹 개발 경험이 있기 때문에 MVC 패턴에 대해서는 잘 알고 있다.
쉽게 말하자면 유저가 컨트롤러를 통해 모델을 조작하고 모델이 뷰에게 값을 주면 뷰가 그것을 보여주는 형식이다.

조금은 다를지 몰라도 이 개념을 게임 UI에서 다룰 수 있다.
또한 실제 Fortnite같은 AAA게임에서도 이를 사용하고 있다고 한다.

  • View
    우리가 보는 UI, 위젯
  • Model
    View에 나타나는 다양한 수치들 ( 예 : 체력, 마나, 레벨, 경험치 )

그렇다면 View 입장에서는 Model을 알아야 이를 보여줄 수 있는데
Model의 값을 View에 주는 방법에는 수많은 방법이 있다.

하지만 View와 Model 사이에 델리게이트를 바인드 시켜서 값을 주고 받는다고 해도 바인딩 시킬 때는 View 입장에서 Model을 알아야하기 때문에 의존성이 발생할 수 밖에 없다.
그렇다면 View의 재사용성이 떨어지고 모듈화가 어렵게 된다.
그러니까 A라는 View를 사용할 때 A라는 모델밖에 사용할 수 없다는 뜻.
확장성을 챙기는 것은 항상 달콤한 일이기에.. 이를 해결하고 싶어진다.

그렇기 때문에 Controller로써 클래스를 하나 만든 후, 여기에서 모델의 데이터를 처리하여 View로 Broadcast 해주는 역할을 한다면?
Model 자신은 어떤 Controller와 연결되어 있는지 몰라도 된다.
Controller 자신은 어떤 View와 연결되어 있는지 몰라도 된다.

그럼, Model 입장에서는 Controller를 바꿔도 Model 입장에서는 변경 사항이 전혀 없다는 뜻
Controller 입장에서는 View를 바꿔도 변경 사항이 전혀 없다는 뜻이다.
이제 변경사항이 생겨도 View나 Model 자체를 갈아엎기보다는 조금의 수정 사항만 갈아끼우면 된다.

실제 구현해보기

View : 우리가 사용할 위젯이다.
Controller : 위젯 컨트롤러라는 클래스를 직접 UObject로 만들어 줄 것이다.
Model : 필요한 값을 가지고 있는 클래스이다. 현 예시에서는 Attribute Set이다.

구현 방법

  • View(Widget)은 각자 자신의 Controller를 가지고 있으며, 필요한 값을 Controller에서 구독한다. (Delegate Bind)
  • Controller는 Model의 변경을 감지하면 이를 변경값과 함께 Broadcast한다.
  • Model은 값이 변경될 때마다 이를 Broadcast한다. (Attribute Set은 이미 이게 구현돼있다.)

Widget 만들기

프로젝트에서 사용할 위젯으로써 위젯 클래스를 하나 만들어 주자.
앞으로 프로젝트에서 사용하는 모든 위젯은 이 위젯을 상속받는다는 개념이다.

class AURA_API UAuraUserWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UFUNCTION(BlueprintCallable)
	void SetWidgetController(UObject* InWidgetController);
	
    //모든 위젯은 Controller를 가지고 있다.
	UPROPERTY(BlueprintReadOnly)
	TObjectPtr<UObject> WidgetController;

protected:
	//Controller가 설정돼었을 때 실행할 함수, Controller가 있음을 보장받음으로 여기서 필요한 값을 구독한다.
	UFUNCTION(BlueprintImplementableEvent)
	void WidgetControllerSet();
};

Controller 만들기

Controller Base를 만들어주자.
값을 받고 처리해주고를 다 여기서 하기 때문에 여기서 할 일이 좀 많다.

/*
이 구조체는 Controller가 의존성을 가질 Model들을 모아둔 것이다.
Controller를 생성할 때 해당 구조체를 넘겨주어 초기화 할 수 있도록 한다.
*/
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;
};

그 다음 Controller의 헤더이다.

UCLASS()
class AURA_API UAuraWidgetController : public UObject
{
	GENERATED_BODY()
	
public:
	//구조체를 넣으면 해당 값으로 Model들을 설정한다.
	UFUNCTION(BlueprintCallable)
	void SetWidgetControllerParams(const FWidgetControllerParams& WCParams);
    
    //처음 Widget에 Controller가 설정되었을 때 호출할 함수이다.
    //이걸 안해주면 위젯 내의 값은 모델의 값이 변하기 전에는 초기값으로 남기 때문에
	virtual void BroadcastInitialValues();
    
    //Model의 값을 구독하는 함수
	virtual void BindCallbackToDependencies();
protected:
	//Model들
	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;
    //
};

여기까지 했다면 이제 어떤 위젯을 만들 것이냐에 따라서
이 클래스를 상속받아 Controller를 만들어주면 된다.

예시 ) HUD 만들기

예시로 간단히 체력, 마나를 보여주는 HUD를 만들어보자

위젯 만들기

만들어둔 베이스 위젯으로 위젯 블루프린트를 만들어 사용하였다.
체력, 마나 통 위젯은 뭐 프로그레스 바로 대충 만들 수 있을 것이니 생략하고
이를 붙여서 만들었다.

체력 마나 위젯도 각자의 Controller가 존재하기 때문에 이를 설정해준다.

Controller 만들기

체력 하나만 예시로 들어 만들어보겠다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangedSignature, float, NewHealth);

UCLASS(BlueprintType, Blueprintable)
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
	GENERATED_BODY()
	
public:
	virtual void BroadcastInitialValues() override;
	virtual void BindCallbackToDependencies() override;
	
    //View가 구독할 델리게이트
	UPROPERTY(BlueprintAssignable, Category = "GAS|Attributes")
	FOnHealthChangedSignature OnHealthChanged;
protected:
	//모델의 Broadcast에 바인드할 함수
	void HealthChanged(const FOnAttributeChangeData& Data) const;
};

//cpp
void UOverlayWidgetController::BroadcastInitialValues()
{
	const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);
	OnHealthChanged.Broadcast(AuraAttributeSet->GetHealth());
}

void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData& Data) const
{
	OnHealthChanged.Broadcast(Data.NewValue);
}

void UOverlayWidgetController::BindCallbackToDependencies()
{
	const UAuraAttributeSet* AuraAttributeSet = CastChecked<UAuraAttributeSet>(AttributeSet);

	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
		AuraAttributeSet->GetHealthAttribute()).AddUObject(this, &UOverlayWidgetController::HealthChanged);
}

똑같은 형식으로 값만 바꿔주면 마나도 만들어줄 수 있다.

HUD 클래스 만들기

자 이제 필요한 위젯과 컨트롤러를 다 만들었으니 HUD 클래스를 만들어서 이 위젯과 컨트롤러를 연결시켜주자.

//WidgetController가 있으면 반환하고 없으면 생성한다. 싱글톤 개념이랄까
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
	if (OverlayWidgetController == nullptr)
	{
		OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
		OverlayWidgetController->SetWidgetControllerParams(WCParams);
		OverlayWidgetController->BindCallbackToDependencies();

		return OverlayWidgetController;
	}

	return OverlayWidgetController;
}

//Model에 관한 인자들을 받아서 Controller를 View에 연결해주는 함수이다.
void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
	checkf(OverlayWidgetClass, TEXT("Overlay Widget Class가 초기화되지 않았습니다. BP_AuraHUD에서 설정해주세요"));
	checkf(OverlayWidgetControllerClass, TEXT("Overlay Widget Controller Class가 초기화되지 않았습니다. 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();
}

그래서 이걸 어디서 호출해요?

이걸 부를 수 있는 가장 적절한 곳은 Model이 존재한다는 것을 보장 받을 수 있는 곳에서 호출해주는 것이 좋다.

지금같은 경우에는 Attribute Set이 Model인 경우인데, 이것이 확실히 존재한다고 할 수 있는 곳은 캐릭터에서 AbilityActorInfo를 설정할 때라고 할 수 있겠다.
Controller에서 필요한 파라미터인 값들도 모두 여기서 알 수 있기 때문에 아주 좋은 곳이다.
InitAbilityActorInfo가 뭐하는 건지 모르겠으면 ASC 게시글로..

void AAuraCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	//Init Ability Actor Info for the Server
	InitAbilityActorInfo();
}

void AAuraCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	//Init Ability Actor Info for the Cilent
	InitAbilityActorInfo();
}
void AAuraCharacter::InitAbilityActorInfo()
{
	AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
	check(AuraPlayerState);
    //오너와 아바타를 설정해주는 함수이다. 왜 PlayerState인지는 이미 알아보았다.
	AuraPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(AuraPlayerState, this);
	AbilitySystemComponent = AuraPlayerState->GetAbilitySystemComponent();
	AttributeSet = AuraPlayerState->GetAttributeSet();

	//멀티의 경우 자신 이외의 캐릭터는 컨트롤러가 null일 수 있기 때문에 assert가 아닌 null체크만 해준다.
	if (AAuraPlayerController* AuraPlayerController = Cast<AAuraPlayerController>(GetController()))
	{
		if (AAuraHUD* AuraHUD = Cast<AAuraHUD>(AuraPlayerController->GetHUD()))
		{
			AuraHUD->InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
		}
	}
}

이렇게만 해주면?

야호~ HUD 완성!

profile
영차 영차

0개의 댓글