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