

객체의 상태 변화를 관찰하는 관찰자(옵저버) 객체들이 있고, 주체가 되는 객체(주제)가 변화를 통지하면 관찰자들이 그 변화에 대응할 수 있도록 하는 디자인 패턴입니다. 이를 통해 객체 간의 느슨한 결합을 유지하면서도 효율적으로 상태 변화를 전달할 수 있습니다.
모듈화된 관리 : 유닛 처치, 자원 수집, 미션 클리어 등 각각의 게임플레이 로직은 독립적으로 관리된다. 업적 시스템은 다양한 게임플레이 이벤트를 구독하고, 이를 바탕으로 업적 달성 여부를 판단하기 때문에 서로 결합되지 않는다.확장성 : 새로운 업적을 추가할 때 기존 게임플레이 로직을 변경할 필요 없이, 해당 이벤트를 구독하는 방식으로 간단히 추가할 수 있다. 예를 들어, "저그 유닛 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를 갱신하는 로직
}
};

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);
};

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!"));
}
};
void TestScriptInterface(TScriptInterface<IMyInterface> InterfaceObject)
{
if (InterfaceObject)
{
InterfaceObject->MyFunction(); // 인터페이스의 함수 호출
}
}
// 호출할 때는 이렇게 객체를 전달합니다
AMyActor* MyActorInstance = GetWorld()->SpawnActor<AMyActor>();
TScriptInterface<IMyInterface> InterfaceReference(MyActorInstance);
TestScriptInterface(InterfaceReference);
클래스의 내부 구현 세부 사항을 숨기고 인터페이스가 정의한 계약(추상 메서드들)만을 제공함으로써 객체와 상호작용하도록 합니다.캡슐화와 유사한 개념으로, 객체 지향 설계에서 객체의 내부를 외부에서 숨기고, 공개된 인터페이스만 사용하게 하는 원칙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를 전달하며 알림
}

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.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);
}
}