본문 바로가기

카테고리 없음

04 관찰자패턴

https://medium.com/@GMGStudio/observer-pattern-how-to-use-this-crucial-design-pattern-in-game-development-with-unity-8967dffcde57

Observer pattern — How to use this crucial design pattern in game development with Unity

The observer pattern is used by many developers because it solves so many problems. The observer pattern is used by so many people that C#…

medium.com


옵져버클래스와 델리게이트 차이??

델리게이트가 내부 뜯으면 옵저버로 만든걸까



객체 사이에 일 대 다 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 업데이트 될 수 있게 만듭니다.

 void Physics::updateEntity(Entity& entity)
 {
 	bool wasOnSurfae = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if(wasOnSurface && entity.isOnSurface() == false)
    {
    	//방금 떨어지기 시작했으니 알아서 하시오
    	notify(entity, EVENT_START_FALL);
    }
 }

업적 시스템은 물리엔진이 알람을 보낼 때마다 받을 수 있도록 스스로를 등록한다.
업적을 체크하는 과정(우리 캐릭터가 맞는지, 떨어지기 전 다리 위에 있었는지.. 등등)
ㄴ 물리 코드는 전혀 몰라도 된다!

4.2 작동원리

옵져버 클래스만 상속받으면 관찰자가 될 수 있다.

class Observer
{
public:
	virtual ~Observer() {}
    virtual void onNotify(const Entity& entity, Event event) = 0;
}

ㄴ 상속받기 보다는 옵져버에 자신을 등록하는 형태를 많이 본 거 같은데
ㄴ 델리게이트도 아예 함수포인터를 넘겨주지 않나?

\

class Achievement : public Observer
{
    public:
        virtual void onNotify(const Entity& entity, Event event)
        {
            switch(event)
            {
                case EVENT_ENTITY_FELL:
                {
                    if(entity.isHero() && heroIsOnBridge_)
                    {
                        unlock(ACHIEVEMENT_FEILL_OFF_BRIDGE);
                    }
                }
                break;

                ...
            }
        }
    private:
    	void unlock(Achievement achievement);
        
}

대상


대상(subject) : 관찰 당하는 객체.
대상이 알람 메서드를 호출한다.

대상의 역할
1. 알림을 끈질기게 기다리는 관찰자 목록을 들고있는다.
2. 알람을 보내는 것이다.


대상의 역할
1. 알림을 끈질기게 기다리는 관찰자 목록을 들고있는다.

class Subject
{
private:
	// 관찰자 목록
    Observer* observers_[MAX_OBSERVERS];
    int numObservers_;
    
    // 밖에서  변경 가능하도록 API를 public으로 열어둔다.
    void addObserver(Observer* observer);
    void removeObserver(Observer* observer);
    
}

밖에서 관찰자 목록을 변경할 수 있기 때문에 누가 알람을 받을 것인지 제어할 수 있다.
대상은 관찰자와 상호작용하지만, 서로 커플링 되어 있지 않다.

대상이 관찰자들을 목록으로 들고 있기 때문에 관찰자끼리 서로 디커플링된다. 관찰자는 월드에서 같은 대상을 관찰하는 다른 관찰자가 있는지 알지 못한다.


2. 알람을 보내는 것이다.

class Subject
{
protected:
	void notify(const Entity& entity, Event event)
    {
    	for(Observer* obs : observers_)
        {
        // 관찰자들에게 알람을 보내는 것이다
        // 관찰자끼리는 누가 알람을 받았는지 알지 못한다(디커플링 되어있다)
        	obs->onNotify(entity, event);
        }
    }
}



물리 관찰

이제 객체에 hook을 걸어서 알림을 보낼 수 있게 하는 일과,
옵져버에서 알림을 받을 수 있도록 스스로를 등록하게 해야한다.

class Physics : public Subject
{
public:
	void updateEntity(Entity& entity);
}

이러면
Subject 클래스의
- notify() 매서드는 protected
ㄴ 밖에서 notify()에 접근할 수 없다
- addObserver(), removeObserver()는 public
ㄴ 물리시스템에 접근만 할 수 있다면 어디서나 물리시스템을 관찰할 수 있다.

더보기

진짜 코드였다면, 상속 대신 Physics 클래스가 Subject 인스턴스를 포함하게 만들었을 것이다. 이러면 물리 엔진 그 자체를 관찰하기 보다는, 별도의 '낙하이벤트' 객체가 대상이 된다.

등록 또한 physics.entityFell().addObserver(this); 이런 식으로 등록한다. (이벤트 시스템)

관찰자 시스템에서는 뭔가 관심있는 일을 하는 객체를 관찰하지만, 이벤트 시스템에서는 관심 있는 일 자체를 나타내는 객체를 관찰한다.

ㄴ subject가 아니라 subject의 entity를 관찰한다는거야?


특정 인터페이스를 구현한 인스턴스 포인터 목록을 관리하는 클래스만 구현하면
이제 notify()를 호출하여 전체 관찰자에게 알림을 전달하여 일을 처리하게 할 수 있다.

단점1. 너무 느려.

안느리다.

'이벤트', '메세지', '데이터바인딩' 이랑 다니면서 알림이 있을 때마다 동적 할당을 하거나 queuing하여서 느릴 수도 있다.
정적 호출보다야 약간 느리지만 상관 없다. 인터페이스를 통해 동기적으로 메서드를 간접 호출할 뿐 메시지용 객체를 할당하지도 않고 큐잉도 하지 않는다.


오히려 동기적임에 주의해라.
관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전까지는 다음 작업을 진행할 수 없다. 관찰자 중 하나라도 느리면 블록된다.

이벤트에 동기적으로 반응한다면 최대한 빨리 작업을 끝ㅌ내고 제어권을 다시 넘겨줘서 UI가 멈추지 않도록 해야한다. 오래걸리는 작업이 있다면 다른 스레드에 넘기거나 작업큐를 사용해야한다. 멀티스레드, 락과 함께 쓰려면 진짜 조심해야한다. 그때는 이벤트큐가 더 낫다.

단점2. 동적 할당을 너무 많이 해.

동적 할당 없이 쓰는 방법 : 관찰자 연결 리스트!

지금까지는 Subject가 Observer포인터 리스트를 들고 있는 방식.
대상에 포인터컬렉션을 따로 두지 않고, 관찰자 객체가 연결리스트의 노드가 되도록 바꿀 것이다.

class Subject
{
	Subject()
	: head_(NULL)
    {}
    
    private:
    Observer* head_;
}

class Observer
{
	Observer()
    : next_(NULL)
    {}
    
    private:
    Observer* next;
}

void Subject::addObserver(Observer* observer)
{
	observer->next_ = head_;	//friend선언이 편하대
    head_ = observer;
}


부작용 : A-B-C 순서로 등록하였는데 C-B-A순서로 호출된다. (앞쪽이 아니라 뒤쪽에 추가하면 상관없긴함..)
ㄴ 이게 문제라면 커플링이 있다는 소리다.

관찰자 자체를 리스트 노드로 활용하기 때문에, 관찰자는 하나의 대상 관찰자 목록에만 등록할 수 있다.
즉 관찰자는 한 번에 한 대상만 관찰할 수 있다. (대상마다 관찰자 목록이 따로 있다면 상관X)

대상과 관찰자 제거

대상이 삭제되면 더 이상 알람을 받을 수 없는데, 관찰자는 그걸 모르고 알람을 기다릴 수도 있다.
대상이 삭제되기 직전 마지막으로 '사망'알람을 보내줘야 한다. 즉 소멸자에서 대상의 removeObserver()만 호출하면 된다.


사라진 리스너 문제

유저가 상태창을 열면 상태창UI객체를 생성한다. 상태창을 닫으면 UI객체를 따로 삭제하지 않고 GC가 알아서 삭제하게 한다. 캐릭터는 맞을 때마다 알림을 보낸다. 캐릭터를 관찰하던 UI창은 알림을 받아 체력바를 갱신한다.

상태창을 닫을 때 관찰자를 등록해제 하지 않는다면?

캐릭터의 관찰자 목록에 상태창UI가 여전히 있어서 GC가 수거해가지 않는다. 상태창을 열 때마다 상태창인스턴스를 새로 만들어 관찰자 목록에 추가하기 때문에 관찰자 목록은 점점 커진다.
눈에 보이지 않는 UI요소를 업데이트하느라 CPU클럭이 낭비되고, 효과음까지 날 수 도 있다.


관찰자목록을 통해 코드가 커플링되어있을 시 어떤 관찰자가 알림을 받는지는 디버깅타임에만 알 수 있다.
코드를 이해하기 위해 양쪽 코드의 상호작용을 같이 확인해야 할 일이 많다면 관찰자 패턴을 쓰지 않고 두 코드를 명시적으로 연결하는게 더 낫다.