관찰자 패턴은 가장 널리 사용되고 잘 알려져 있는 패턴이다. 게임 개발자한테는 조금 생소할수 있다.
업적 달성
업적시스템을 추가한다고 보자. ‘원숭이 100마리 잡기’ , ‘다리에서 떨어지기’, ‘목검만으로 레벨 100 달성하기’ 등등 달성할 수 있는 업적이 수백개가 존재한다고 생각해보자.
업적 종류가 많아지면 업적 코드가 컨텐츠, 시스템 코드안에 파고 들것이다. 암세포처럼 다리에서 떨어지기 같은 경우에는 충돌체크 알고리즘중에 확인해야 할것이다.
이렇게 생각을 하면 업적 코드들은 한군데 모여있기 보다 여러 코드들 사이에 떨어져서 존재하게 될것이고 이것은 코드를 이해하기 어렵고 복잡하게 만들며 구조를 다른사람이 파악하기도 어려울 것이다.
그래서 특정 기능을 담당하는 코드는 한데 모아두는게 좋다. 문제는 업적은 여러 게임 플레이 환경요소에서 발생해야 한다는 것이다. 이런 코드 전부와 커플링(연결) 되지 안고도 업적코드가 한데모여서 동작하게 하려면 어떻게 해야할 것인가?
예를 들어서 평면에 물체가 놓여 있다고 생각해보자. 바닥으로 추락하는지 추적중인 물리 엔진 코드가 있다고 생각해보자. ‘다리에서 떨어지기’ 업적을 구현하기 위해 업적 코드를 물리 코드에 곧바로 넣어서 구현하면 매우 편리하다. 하지만 이러면 코드가 지저분해지니 다음과 같이 해보자
void Physics::updateEntity(Entity& entity)
{
bool wasOnSurface = entity.isOnSurface();
entity.accelerate(GRAVITY);
entity.update();
if (wasOnSurface && !entity.isOnSurface())
{
notify(entity, EVENT_START_FALL);
}
}
C++
복사
이 코드는 ‘이게 방금 덜어지기 시작했으니 누군지는 몰라도 알아서 하시오’ 알리기만 하는게 전부다.
업적 시스템은 물리엔진이 알림을 보낼때마다 받을 수 있도록 스스로 등록한다. 업적 시스템은 떨어지는 물체가 불쌍한 우리의 캐릭터가 맞는지, 그리고 떨어지기 전에 다리위에 있었는지를 확인한 뒤에 축포와 함께 업적을 잠금해제 시켜주면 된다.
이렇게 물리엔진 코드는 건들이지 않으면서 업적 목록을 바꾸거나 아예 업적 시스템을 떄어낼수도 있다. 물리엔진에서의 코드는 누가 받든 말든 계속 알림을 보내버린다.
작동원리
관찰자패턴을 구현해본적이 없어도 방금 설명했던 부분으로 어느정도는 감을 잡았을 것이다.
관찰자
다른 객체가 뭐 하는지 관심이 많은 Observer 클래스부터 보자. Observer 클래스는 다음과 같은 인터페이스로 정의된다.
class Observer
{
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
};
C++
복사
어떤 observer인터페이스를 구현하기만 하면 관찰자가 될 수 있다. 우리 예제는 업적 시스템에서는 다음과 같이 구현해보겠다.
class Achievements : public Observer
{
public:
virtual void onNotify(const Entity& entity, Event event)
{
switch (event)
{
case EVENT_ENTITY_FELL:
if (entity.isHero() && heroIsOnBridge_)
{
unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
}
break;
// Handle other events, and update heroIsOnBridge_...
// 그외에 이벤트를 처리하고, heroIsOnBrdige 값을 업데이트 해준다.
}
}
private:
void unlock(Achievement achievement)
{
// Unlock if not already unlocked...
// 아직 업적이 잠겨 있다면 잠금해제한다.
}
bool heroIsOnBridge_;
};
C++
복사
대상
알림 메서드는 관찰당한느 객체가 호출한다. 대상에게는 두가지 임무가 있다. 그중 하나는 알림을 끈질기게 기다리는 관찰자 목록을 들고 있는 것이다. 그래야 알림을 보낼수 있으니까
class Subject
{
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
};
C++
복사
여기서 중요한 점은 관찰자 목록을 밖에서 변경할 수 있도록 다음과 같이 API를 public으로 열어 놨다는 점이다.
class Subject
{
public:
void addObserver(Observer* observer)
{
// Add to array...
}
void removeObserver(Observer* observer)
{
// Remove from array...
}
// Other stuff...
};
C++
복사
이를 통해서 누가 알림을 받을 것인지를 제어 할 수 있다. 대상은 관찰자와 상호작용 하지만, 서로 커플링이 되어 있지는 않다. 예제 코드를 보면 물리 코드 어디에도 업적에 관련된 부분은 없지만 업적 시스템으로 알림을 보낼 수는 있다. 이게 관찰자 패턴의 장점이다.
그리고 대상(subject) 여러개의 관찰자를 관리한다는 점도 중요하다. 자연스럽게 관찰자들은 암시적으로 서로서로를 모른다. 커플링 되지 않는다. 오디오 엔진에서도 뭔가가 떨어질떄 소리를 낼 수 있도록 알림을 기다린다고 해보자. 대상이 관찰자 하나만 지원한다면, 오디오 엔진이 자기 자신을 관찰자로 등록 할 때 업적 시스템은 관찰자 목록에서 제거될 것이다.
관찰자를 위에 코드처럼 여러개를 관리할수 있게 해두었다면 각자 독릭적으로 옵저버(관찰자)들이 실행되게 된다. 관찰자는 월드에서 같은 대상을 관찰하는 다른 관찰자 있는지 없는지 알턱도 없고 알고 싶지 도 않다. 오로지 subject보낸 알림만 해결하는 것이다. 그리고 subject는 오로지 알림만 보낸다.
class Subject
{
protected:
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
{
observers_[i]->onNotify(entity, event);
}
}
// Other stuff...
};
C++
복사
물리 관찰
남은 작업은 물리 엔진에 훅(hook)을 걸어 알림을 보낼수 있게 하는 일과 업적 시스템에서 알림을 받아서 스스로 등록하게 만드는 것이다.
class Physics : public Subject
{
public:
void updateEntity(Entity& entity);
};
C++
복사
이렇게 하면 subject 클래스의 notify()메서드를 protected로 만들수 있다. subject를 상속받은 physics 클래스는 notify()를 통해서 알림을 보낼 수 있지만, 밖에서는 notify() 함수에 접근할수 없다.
반면 addObserver()와 removeObserver()는 public 이기 때문에 물리 시스템에 접근할수만 있으면 어디에서나 물리시스템을 관찰 할수 있다.
이제 물리엔진에 중요한 일이 생기게 되면, 예제처럼 notify()를 호출해 전체 관찰자에게 알림을 보내서 전달하여 일을 처리할수 있게 됬다.
하지만 관찰자 패턴에도 단점이 존재한다. 다름 게임프로그래머들에게 관찰자 패턴에 대해서 물어 봤더니 , 몇가지 불만사항들이 나왔다. 문제가 뭔지 하나씩 알아보자!!