관찰자 패턴
Observer Pattern
관찰자 패턴의 특징은 누가 받든 상관없이 알림을 보낼 수 있게 된다는 것이다.
먼저, Event의 enum class, Achievement class 에 대해 설명을 하고 가려한다.
Event는 enum으로 구현했고, 원하는 Event가 생길 때 마다 추가해주는 방식이다.
enum Event
{
EVENT_ENTITY_FELL,
EVENT_GET_IN_THE_CAR
};
Achievement class는 각 업적을 객체로 뽑을 수 있도록 구현했고, 간단하게 그냥 이름, 상태만 지정할 수 있도록 구현했다.
enum Achievement_state
{
NOT_ACHIEVED,
ACHIEVED,
HIDDEN
};
class DESIGNPATTERNSTUDY_API Achievement
{
public:
Achievement(FString name_, Achievement_state state_)
: Achievement_Name(name_), Achievement_State(state_){}
~Achievement();
void SetAchievementState(Achievement_state state);
FString GetName(){return Achievement_Name;}
private:
FString Achievement_Name;
Achievement_state Achievement_State;
};
#include "Achievement.h"
Achievement::~Achievement()
{
}
void Achievement::SetAchievementState(Achievement_state state)
{
Achievement_State = state;
}
관찰자
Observer는 다음과 같이 인터페이스로 정의된다.
class DESIGNPATTERNSTUDY_API Observer
{
public:
virtual ~Observer();
virtual void onNotify(const AActor* Actor, Event event) = 0;
};
관찰자로 만들고 싶은 클래스를 Observer를 상속받아 구현한다.
class Achievements : public Observer
{
public:
virtual void onNotify(const AActor* Actor, Event event) override;
Achievements();
private:
void Unlock(Achievement Achievement);
Achievement* ACHIEVEMENT_FELL_OFF_BRIDGE;
Achievement* ACHIEVEMENT_GET_IN_THE_CAR;
};
void Achievements::onNotify(const AActor* Actor, Event event)
{
switch (event)
{
case EVENT_ENTITY_FELL:
Unlock(*ACHIEVEMENT_FELL_OFF_BRIDGE);
break;
case EVENT_GET_IN_THE_CAR:
Unlock(*ACHIEVEMENT_GET_IN_THE_CAR);
break;
}
}
Achievements::Achievements()
{
ACHIEVEMENT_FELL_OFF_BRIDGE = new Achievement("Fell Off Bridge", NOT_ACHIEVED);
ACHIEVEMENT_GET_IN_THE_CAR = new Achievement("Get In the Car", NOT_ACHIEVED);
}
void Achievements::Unlock(Achievement Achievement)
{ // 사실 잠금해제하는 함수이지만, 귀찮으니까 그냥 달성함수로 바꿨다.
if(GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, Achievement.GetName());
}
Achievement.SetAchievementState(ACHIEVED);
}
업적이 완료되었는지 확인만 가능하게 화면에 해당 업적의 이름을 띄울 수 있게 구현하였다.
대상
알림 메서드(onNotify)를 호출하는 관찰당하는 객체
이 때 대상은 알림을 기다리는 관찰자들의 목록을 가지고 있어야 한다.
관찰자"들"인 이유는 관찰자를 하나만 참조하고 있다면 관찰자가 두가지 이상일 때 먼저 등록한 관찰자가 역할을 다 할 수 없을 것이다.
관찰자 목록에 추가 / 제거 하는 메서드들은 API를 public으로 열어놔 다른 코드에서도 접근할 수 있도록 구성한다.
class DESIGNPATTERNSTUDY_API Subject
{
public:
Subject();
~Subject();
void addObserver(Observer* Observer)
{
Observers.Add(Observer);
}
void removeObserver(Observer* Observer)
{
Observers.Remove(Observer);
}
void Notify(const AActor* Actor, Event event);
private:
TArray<Observer*> Observers;
Achievements* Achievements_;
protected:
};
Subject::Subject()
{
Achievements_ = new Achievements;
addObserver(Achievements_);
}
Subject::~Subject()
{
}
void Subject::Notify(const AActor* Actor, Event event)
{
for(int i = 0; i < Observers.Num(); i++)
{
Observers[i]->onNotify(Actor, event);
}
}
즉, 관찰자들을 여러개를 등록한 후, 해당 이벤트가 발생하면 Observer에게 해당 이벤트 발생 알림을 보낸다. 이 때 Observer 들이 서로를 방해할 수 없도록 배열로 구현하여 모든 Observer에게 알림을 쏠 수 있도록 구현한다. (이 때 당연히 관찰자는 다른 관찰자가 어떤 행동을 하는 지 알 수 없음, Subject 자체도 관찰자가 어떤 행동을 하는 지 알 수 없음)
디커플링된 코드의 모습을 발견할 수 있다.
물리 관찰
hook을 걸어 알림을 보낼 수 있게 해야하는 일이 남았다.
게임 내에서 플레이어가 어떤 행동을 했을 경우 / 물리엔진에서의 트리거가 발동된 경우 등등 event 를 대상에게 전달해준다.
사실 공부하고 있는 교재에서는 Subject를 상속받아 구현했지만, 나는 그냥 플레이어 캐릭터에 Subject 객체를 생성해 접근해서 구현해주었다.
최대한 간단하게 하려 이렇게 구현했는데, 당연히 코드가 광범위해지면 바꿔야 하는 부분이다.
AMyCharacter_Observer::AMyCharacter_Observer()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AMyCharacter_Observer::BeginPlay()
{
Super::BeginPlay();
mySubject = new Subject();
}
// Called every frame
void AMyCharacter_Observer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if(UGameplayStatics::GetPlayerController(GetWorld(), 0)->WasInputKeyJustPressed(FKey(FName("F"))))
{
mySubject->Notify(this, EVENT_ENTITY_FELL);
}
if(UGameplayStatics::GetPlayerController(GetWorld(), 0)->WasInputKeyJustPressed(FKey(FName("G"))))
{
mySubject->Notify(this, EVENT_GET_IN_THE_CAR);
}
}
// Called to bind functionality to input
void AMyCharacter_Observer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
위 코드는 Observer_Level에서 사용하고 있는 PlayerCharacter 코드이다.
매 프레임마다 "F" key와 "G" key의 입력을 감지해 각각의 이벤트를 대상에게 알림보낼 수 있도록 Notify 함수를 호출하는 것을 볼 수 있다.
해당 level을 실행하면 아래 스크린샷과 같이 입력을 감지하여 해당 업적을 완료시키는 모습이다.

물론, 지금은 업적이 완료되었는지, 숨김 상태인지 등등은 체크하지 않은 상태이다.
즉, 옵저버 패턴을 이용해 관찰자들을 등록하고, 이벤트가 발생되면 각 관찰자들에게 알림을 보내고, 관찰자들은 해당 알림에 맞는 일을 찾아 하게 되는 로직을 구현한 것이다.
※ 전체 소스코드 git link
https://github.com/haram1117/GameDesignPatterns_Study
GitHub - haram1117/GameDesignPatterns_Study: GameDesignPatterns_Study
GameDesignPatterns_Study. Contribute to haram1117/GameDesignPatterns_Study development by creating an account on GitHub.
github.com
※ Observer Pattern 1 커밋
Observer Pattern 1 · haram1117/GameDesignPatterns_Study@2c45a67
Show file tree Hide file tree Showing 12 changed files with 238 additions and 0 deletions.
github.com
'게임 개발 > 디자인 패턴' 카테고리의 다른 글
| Game Programming Design Patterns - 프로토타입 패턴 (1) (1) | 2022.08.09 |
|---|---|
| Game Programming Design Patterns - 관찰자 패턴 (2) (0) | 2022.08.05 |
| Game Programming Design Patterns - 경량 패턴 (0) | 2022.08.02 |
| Game Programming Design Patterns - 명령 패턴 (2) (0) | 2022.08.02 |
| Game Programming Design Patterns - 명령 패턴 (1) (0) | 2022.07.30 |