[ Unreal Engine 5 / #23 Observer Pattern ]

SeungWoo·2024년 10월 17일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
24/31
post-thumbnail

옵저버 패턴(Observer Pattern)

객체의 상태 변화를 관찰하는 관찰자(옵저버) 객체들이 있고, 주체가 되는 객체(주제)가 변화를 통지하면 관찰자들이 그 변화에 대응할 수 있도록 하는 디자인 패턴입니다. 이를 통해 객체 간의 느슨한 결합을 유지하면서도 효율적으로 상태 변화를 전달할 수 있습니다.

  • Observer Pattern의 이점
    • 모듈화된 관리 : 유닛 처치, 자원 수집, 미션 클리어 등 각각의 게임플레이 로직은 독립적으로 관리된다. 업적 시스템은 다양한 게임플레이 이벤트를 구독하고, 이를 바탕으로 업적 달성 여부를 판단하기 때문에 서로 결합되지 않는다.
    • 확장성 : 새로운 업적을 추가할 때 기존 게임플레이 로직을 변경할 필요 없이, 해당 이벤트를 구독하는 방식으로 간단히 추가할 수 있다. 예를 들어, "저그 유닛 500마리 처치" 업적을 추가하려면, 유닛 처치 이벤트에 대해 새로운 구독자(Observer)를 추가하면 된다.
    • 유지보수 용이성 : 게임의 여러 이벤트와 업적 시스템이 느슨하게 결합되어 있기 때문에, 각각의 요소를 독립적으로 수정하거나 유지보수할 수 있다.
  • 옵저버 패턴의 구조

    • Subject (주제) : 상태를 가지고 있으며 옵저버들이 그 상태의 변경을 구독하고 통지받을 수 있도록 하는 인터페이스를 제공하는 객체입니다.
    • Observer (옵저버) : 주제의 상태 변화에 반응하는 객체로, 주제에 등록되어 있다가 상태가 변경되면 업데이트 메서드를 호출받아 변경 사항에 대응합니다.
    • ConcreteSubject (구체적인 주제) : 주제 인터페이스를 구현한 클래스이며 상태를 가지고 있고, 그 상태가 변경되면 모든 옵저버들에게 통지합니다.
    • ConcreteObserver (구체적인 옵저버) : 옵저버 인터페이스를 구현한 클래스이며, 주제로부터 알림을 받고 상태를 업데이트합니다.
  • 언리얼 엔진에서 옵저버 패턴 구현 예제
    언리얼에서 옵저버 패턴을 구현할 때는 주로 델리게이트(Delegate)를 사용하여 주제와 옵저버 간의 통신을 설정할 수 있습니다

Subject(주제): HealthComponent

class UHealthComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    // 델리게이트 선언 (옵저버들이 구독할 이벤트)
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth);

    // 체력이 변경될 때 호출되는 델리게이트
    FOnHealthChanged OnHealthChanged;

    void ChangeHealth(float Delta)
    {
        Health += Delta;
        OnHealthChanged.Broadcast(Health);
    }

private:
    float Health;
};

Observer(옵저버) : UIHealthBar

class AUIHealthBar : public AActor
{
    GENERATED_BODY()

public:
    // 체력 컴포넌트를 옵저버로 등록하는 메서드
    void SetHealthComponent(UHealthComponent* HealthComp)
    {
        if (HealthComp)
        {
            HealthComp->OnHealthChanged.AddDynamic(this, &AUIHealthBar::UpdateHealthBar);
        }
    }

    // 체력 변화 시 UI를 업데이트하는 메서드
    UFUNCTION()
    void UpdateHealthBar(float NewHealth)
    {
        // 새로운 체력 값으로 UI를 갱신하는 로직
    }
};
  • UHealthComponent는 주제(Subject)로, 체력이 변경되면 OnHealthChanged 델리게이트를 통해 옵저버들에게 통지합니다.
  • AUIHealthBar는 옵저버(Observer)로, SetHealthComponent 메서드를 통해 UHealthComponent에 자신을 등록하고 체력 값이 변경될 때 업데이트 메서드를 호출받아 UI를 갱신합니다.

Tile Project 이어서

  • 언리얼 UInterface로 C++ 클래스를 하나 만든다
    Observer.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Observer.generated.h"

// 이 클래스를 UClass가 아닌 UInterface 선언하여 인터페이스로 정의
UINTERFACE(MinimalAPI)
class UObserver : public UInterface
{
	GENERATED_BODY()
};

class PUZZLESTUDY_API IObserver
{
	GENERATED_BODY()

public:

	// 주체로부터 상태 변화를 수신 하는 함수
    // BlueprintNativeEvent로 정의 되며, C++ 또는 블루프린트에서 구현 가능
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Observer")
	void OnNotify(int32 UpdateScore);

};
  • 옵저버 패턴의 핵심인 IObserver 인터페이스는 주체로부터 알림을 받을 때 호출되는 OnNotify 메서드를 포함

  • 인터페이스를 선언 했으니, 주체가 되는 Subject를 만들어 보겠습니다 Object형태의 C++ 클래스를 하나 만든다

UGameStateSub.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Observer.h"
#include "GameStateSub.generated.h"

/**
 * 역할 : 주체 클래스 -> 상태를 관리하고 변경 시 옵저버들에게 알림을 보냄
 */
UCLASS()
class PUZZLESTUDY_API UGameStateSub : public UObject
{
	GENERATED_BODY()
	

private: 
	// 옵저버 리스트 : 등록된 옵저버들이 상태 변화를 수신함
	TArray<TScriptInterface<IObserver>> Observers;

	// 주체의 상태 정보 ( 예: 플레이어 점수 )
	int32 PlayerScore;

public:
	// 생성자 : 초기 점수 설정
	UGameStateSub();

	// 옵저버 등록 : 주체가 상태 변화를 알릴 옵저버를 등록함
	void RegisterObserver(TScriptInterface<IObserver> observer);

	// 옵저버 등록 해제 : 주체가 상태 변화 알림을 중지할 옵저버를 제거함
	void UnregisterObserver(TScriptInterface<IObserver> observer);

	// 상태 변화 발생 시 모든 옵저버에게 알림
	void NotifyObservers();

	// 상태( 점수 )를 변경하는 함수. 상태가 변경되면 NotifyObserver()가 호출됨
	void IncreaseScore(int32 Amount);

	// 현재 점수 반환
	int32 GetScore() const { return PlayerScore; }
};

UGameStateSub.cpp

#include "GameStateSub.h"

UGameStateSub::UGameStateSub()
{
	// 초기 점수 설정
	PlayerScore = 0;
}

void UGameStateSub::RegisterObserver(TScriptInterface<IObserver> observer)
{
	// 옵저버를 리스트에 추가
	Observers.Add(observer);
}

void UGameStateSub::UnregisterObserver(TScriptInterface<IObserver> observer)
{
	// 옵저버를 리스트에서 제거
	Observers.Remove(observer);
}

void UGameStateSub::NotifyObservers()
{
	// 등록된 모든 옵저버들에게 상태 변화를 알림
	for (TScriptInterface<IObserver> Observer : Observers)
	{
		// Observer 객체가 유효하고 IObserver 인터페이스를 구현하고 있는지 확인
		if (Observer.GetObject() && Observer.GetObject()->GetClass()->ImplementsInterface(UObserver::StaticClass()))
		{
			// 옵저버의 OnNotify 함수 호출 ( 점수 변화를 전달 )
			// 실질적인 클래스 인스턴스 -> 함수를 호출
			// 인터페이스 -> 호출
			IObserver::Execute_OnNotify(Observer.GetObject(), PlayerScore);
		}
	}
}

void UGameStateSub::IncreaseScore(int32 Amount)
{
	// 점수를 증가시키고 상태 변화를 옵저버들에게 알림
	PlayerScore += Amount;
	NotifyObservers();
}

TScriptInterface

  • 정의

    • Unreal Engine에서 인터페이스와 관련된 객체를 안전하게 참조할 수 있도록 하는 템플릿 클래스
    • C++에서 인터페이스(interface) 타입을 다룰 때 사용.
      인터페이스는 객체가 특정 기능을 구현해야 한다는 계약을 정의하는데, TScriptInterface는 특히 인터페이스가 Blueprint에서도 사용될 수 있도록 도와줌
  • TScriptInterface의 특징

    • 타입 안전성 : TScriptInterface는 특정 인터페이스 타입에 맞는 객체만 참조하도록 강제하여, 타입 안전성을 제공합니다.
    • C++와 Blueprint 통합 : 이 클래스는 C++뿐만 아니라 Blueprint에서도 인터페이스를 쉽게 다룰 수 있도록 도와줍니다. C++ 코드에서 정의된 인터페이스를 Blueprint에서 사용할 때 매우 유용합니다.
    • 경량 참조 : TScriptInterface는 객체에 대한 경량 참조를 유지하며, 인터페이스가 구현된 객체가 있는지, 유효한지 확인할 수 있는 메커니즘을 제공합니다.
  • TScriptInterface가 필요한 이유

    • 안전한 참조 관리 : TScriptInterface는 인터페이스와 관련된 객체 참조를 관리. 인터페이스를 구현한 객체에 대한 참조를 보관하고, 필요할 때 인터페이스 기능을 호출.
    • Blueprint와의 통합 : 인터페이스는 C++뿐만 아니라 Blueprint에서도 사용할 수 있어야 하는 경우가 많습니다. TScriptInterface는 Blueprint에서 쉽게 사용할 수 있는 형태로 제공되므로, C++과 Blueprint 사이에서 인터페이스를 쉽게 사용할 수 있게 해줌
    • 다형성 지원 : 한 객체가 여러 인터페이스를 구현할 수 있기 때문에 TScriptInterface는 객체가 특정 인터페이스를 구현하고 있는지 확인하고, 그 인터페이스의 기능만 안전하게 사용할 수 있도록 도와줌

TScriptInterface 사용 예제

// 인터페이스 정의
UINTERFACE(MinimalAPI)
class UMyInterface : public UInterface
{
    GENERATED_BODY()
};

class IMyInterface
{
    GENERATED_BODY()

public:
    virtual void MyFunction() = 0;
};
  • 이 인터페이스를 사용하는 클래스를 정의
class AMyActor : public AActor, public IMyInterface
{
    GENERATED_BODY()

public:
    virtual void MyFunction() override
    {
        UE_LOG(LogTemp, Warning, TEXT("MyFunction called!"));
    }
};
  • TScriptInterface를 사용하여 IMyInterface를 구현한 객체에 접근
void TestScriptInterface(TScriptInterface<IMyInterface> InterfaceObject)
{
    if (InterfaceObject)
    {
        InterfaceObject->MyFunction(); // 인터페이스의 함수 호출
    }
}

// 호출할 때는 이렇게 객체를 전달합니다
AMyActor* MyActorInstance = GetWorld()->SpawnActor<AMyActor>();
TScriptInterface<IMyInterface> InterfaceReference(MyActorInstance);

TestScriptInterface(InterfaceReference);
  • TScriptInterface는 특정 인터페이스 타입에 대한 참조를 유지하며, 이를 통해 클래스의 내부 구현 세부 사항을 숨기고 인터페이스가 정의한 계약(추상 메서드들)만제공함으로써 객체와 상호작용하도록 합니다.
  • 이것은 캡슐화와 유사한 개념으로, 객체 지향 설계에서 객체의 내부를 외부에서 숨기고, 공개된 인터페이스만 사용하게 하는 원칙

TScriptInterface 활용한 옵저버 예제

// interface 
UINTERFACE(MinimalAPI)
class UObserverInterface : public UInterface
{
    GENERATED_BODY()
};

class IObserverInterface
{
    GENERATED_BODY()

public:
    // 옵저버들이 구현해야 하는 함수
    virtual void OnNotify(int32 UpdatedValue) = 0;
};
// 옵저버 클래스 정의 
class AMyObserver : public AActor, public IObserverInterface
{
    GENERATED_BODY()

public:
    virtual void OnNotify(int32 UpdatedValue) override
    {
        UE_LOG(LogTemp, Warning, TEXT("Observer notified with value: %d"), UpdatedValue);
    }
};
class AMySubject : public AActor
{
    GENERATED_BODY()

private:
    // TScriptInterface를 사용하여 옵저버 목록을 관리
    TArray<TScriptInterface<IObserverInterface>> Observers;

public:
    // 옵저버를 등록하는 메서드
    void RegisterObserver(TScriptInterface<IObserverInterface> NewObserver)
    {
        if (NewObserver)
        {
            Observers.Add(NewObserver);
        }
    }

    // 옵저버들에게 알림을 보내는 메서드
    void NotifyObservers(int32 UpdatedValue)
    {
        for (auto& Observer : Observers)
        {
            if (Observer)
            {
                Observer->OnNotify(UpdatedValue); // 인터페이스 메서드 호출
            }
        }
    }

    // 주제의 상태를 변경하고 옵저버들에게 알림을 보냄
    void ChangeState(int32 NewValue)
    {
        // 주제의 상태를 변경 (예시로 NewValue 사용)
        NotifyObservers(NewValue);
    }
};
// 주제와 옵저버를 설정하고, 상태가 변경될 때 옵저버들이 알림을 받는 예제입니다.
void SetupObserverPattern()
{
    // 주제와 옵저버 생성
    AMySubject* MySubject = GetWorld()->SpawnActor<AMySubject>();
    AMyObserver* MyObserver1 = GetWorld()->SpawnActor<AMyObserver>();
    AMyObserver* MyObserver2 = GetWorld()->SpawnActor<AMyObserver>();

    // 옵저버들을 인터페이스로 등록
    TScriptInterface<IObserverInterface> ObserverInterface1(MyObserver1);
    TScriptInterface<IObserverInterface> ObserverInterface2(MyObserver2);

    MySubject->RegisterObserver(ObserverInterface1);
    MySubject->RegisterObserver(ObserverInterface2);

    // 주제의 상태 변경
    MySubject->ChangeState(42); // 옵저버들에게 값 42를 전달하며 알림
}

Tile Project

  • 공부한 TScriptInterface와 Interface로 옵저버를 등록할 예정이다

  • 해당 옵저버들을 만들어보자

GameWidgetObserver.h

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Observer.h"
#include "GameWidgetObserver.generated.h"

UCLASS()
class PUZZLESTUDY_API UGameWidgetObserver : public UUserWidget, public IObserver
{
	GENERATED_BODY()

private:
	int32 CurrentScore;


public:
	virtual void OnNotify_Implementation(int32 UpdateScore) override;

	UFUNCTION(BlueprintImplementableEvent, Category = "UI")
	void UpdateScoreUI();
};

GameWidgetObserver.cpp

#include "GameWidgetObserver.h"

void UGameWidgetObserver::OnNotify_Implementation(int32 UpdateScore)
{
	CurrentScore = UpdateScore;

	UpdateScoreUI();
}

void UGameWidgetObserver::UpdateScoreUI()
{
}
  • 그리고 옵저버들을 등록하기 위해서 만든 AMatch3GameMmode의 Begineplay에 등록할 예정이다

AMatch3GameMmode.cpp

void AMatch3GameMmode::BeginPlay()
{
  ... 생략
  
  	// Observer 주체 생성
	UGameStateSub* ObserverGameState = NewObject<UGameStateSub>();
	// 등록할 옵저버 생성
	UGameWidgetObserver* ScoreWidget = CreateWidget<UGameWidgetObserver>(GetWorld(), LoadClass<UGameWidgetObserver>(nullptr, TEXT("")));

	if (ScoreWidget)
	{
		ScoreWidget->AddToViewport();

		// 위젯을 옵저버로 등록
		ObserverGameState->RegisterObserver(ScoreWidget);
	}
}
profile
This is my study archive

0개의 댓글