MVVM 패턴, 옵저버 패턴으로 체력바 만들기

김지윤·2025년 5월 5일
0

UE5_GAS

목록 보기
3/22
UCLASS()
class AURA_API UAuraUserWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable)
	void SetWidgetController(UObject* InWidgetController);
	
	UPROPERTY(BlueprintReadOnly)
	TObjectPtr<UObject> WidgetController;

protected:
	/**
	 * WidgetController가 바인드되는 순간 호출하는 함수
	 * 주로 자신이 참조하고 있는 다른 위젯에 WidgetController를 할당하거나
	 * WidgetController의 델리게이트에 자신의 함수를 바인드하는 데에 사용
	 * 
	 * 예시:	OverlayWidget이 GlobeProgressBar의 SetWidgetController를 호출해 WidgetController를 할당,
	 *		GlobeProgressBar는 WidgetController를 OverlayWidgetController로 캐스트해 자신의 함수를 바인드
	 */
	UFUNCTION(BlueprintImplementableEvent)
	void WidgetControllerSet();
};

내 모든 UserWidget은 WidgetController라는 이름의 UObject 포인터를 갖고 있다.
원한다면 런타임 중 Controller의 델리게이트에서 자신의 함수를 언바인드하거나 더 바인드하는 등의 로직을 구현할 수도 있다.

UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams& WidgetControllerParams)
{
	if (OverlayWidgetController == nullptr)
	{
		OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
		OverlayWidgetController->SetWidgetControllerParams(WidgetControllerParams);
		OverlayWidgetController->BindCallbacksToDependencies();
	}
	return OverlayWidgetController;
}

void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
	OverlayWidget = CreateWidget<UAuraUserWidget>(GetWorld(), OverlayWidgetClass);

	const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
	UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);

	"OverlayWidget->SetWidgetController(WidgetController);"
	WidgetController->BroadcastInitialValue();
	
	OverlayWidget->AddToViewport();
}

최초의 SetWidgetController는 HUD에서 호출된다.
OverlayWidgetController는 PlayerController, PlaterState, AbilitySystemComponent, AttributeSet을 할당받는다.
그 뒤 OverlayWidget에게 OverlayWidgetController가 할당되며, 본격적으로 MVVM 패턴 구현을 위한 로직이 호출된다.

void UAuraUserWidget::SetWidgetController(UObject* InWidgetController)
{
	WidgetController = InWidgetController;
	WidgetControllerSet();
}

이는 SetWidgetController의 구현인데, 자신의 멤버변수에 WidgetController를 할당하고 WidgetControllerSet을 호출한다.
WidgetControllerSet은 BlueprintImplementableEvent 선언된 함수다.

OverlayWidget의 WidgetControllerSet은 자신이 가진 ProgressBar들의 SetWidgetController를 호출, 방금 할당한 WidgetController를 전달해준다.
그럼 다시 그 ProgressBar들이 WidgetController를 할당받음과 동시에 WidgetControllerSet을 호출한다.

거기선 WidgetController의 델리게이트에 함수를 바인드한다.

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

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

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

OverlayWidgetController의 함수 구현 일부다.
AbilitySystemComponent의 델리게이트에 Health 값이 변화할 경우 자동으로 호출되는 델리게이트가 있다.
거기에 자신의 HealthChanged를 붙이고, HealthChanged는 방금 위 사진에서 ProgressBar가 붙인 OnHealthChanged 델리게이트를 호출하게 된다.
여기서 OnHealthChanged는 MULTICAST 델리게이트로, 원한다면 다른 위젯도 여기에 함수를 붙일 수 있다.
그럼 옵저버 패턴이 되는 거다.

생각보다 간단한 로직이지만 관심사의 분리가 명확하며 다양한 위젯과도 쉽게 연결이 가능하다.
즉, 재사용성과 확장성이 높다는 뜻이며 가독성 또한 훌륭한 패턴이다.
단, Controller가 무거워질 가능성이 있으므로 Overlay만 담당하는 WidgetController를 선언한 것처럼 Controller의 역할 또한 분리하는 것이 좋겠다.

profile
공부한 거 시간 날 때 작성하는 곳

0개의 댓글