언어/디자인 패턴

[디자인 패턴]#13_이벤트 큐, Event Queue

Hardii2 2022. 10. 2. 11:45

 

[디자인 패턴] #13_이벤트 큐, Event Queue

게임 디자인 패턴 중 "디커플링 패턴"에 대해 알아보겠습니다.

"게임 프로그래밍 패턴"의 15 항목, "이벤트 큐"에 해당하는 내용입니다.

 

 


 

의도

 

  • 이벤트를 보내는 시점과 처리하는 시점의 디커플링

 

동기

1. 예제 코드

class Audio
{
public:
	static void playSound(SoundID soundId, int volume);
};

void Audio::playSound(SoundID soundId, int volume)
{
	ResourceId resource = loadSound(soundId);
	//특정 Sound를 Play하기 위한 처리 코드...
}

 

  • 사운드 재생을 처리하기 위한 "Audio" 클래스를 가정합니다.

 

2. 문제점

  1. Audio::playSound()는 동기적이며 다음 호출을 블록 하여, 성능 저하의 요인이 됩니다.
  2. 멀티 쓰레드 환경 이곳저곳에서 호출되는 playSound()는 "동기화"처리가 필수적입니다.
  3. "즉시성"때문에, 처리 시점에 다른 처리 과정에 "인터럽트"를 발생시켜 처리 위치와 시점을 관리하기 힘듭니다

 

패턴

1. 목표

  • 메세지를 보내는 곳과 받는 곳을 분리하는 것뿐만 아니라 보내는 시점과 받는 시점을 분리합니다.
  • 전달받은 요청 사항들을 취합하는 것과 더불어 "동기화"를 통해 처리 시점을 관리합니다.
  • 요청을 보내는 곳과 처리 결과를 받는 곳이 다르다는 가정하에 "요청 처리"의 제어권을 "받는 곳"이 독점합니다. 

2. 이벤트 큐의 역할?

  1. 요청이 들어온 순서대로 "큐"에 저장합니다.
  2. 요청을 보내는 곳에서는 요청을 큐에 보내고 기다리지 않고 리턴합니다.
  3. 요청의 처리는 "즉시" 수행되지 않고, 큐에 들어온 순서대로 나중에 처리됩니다.
  4. 요청을 받는 시점 + 요청을 처리하는 시점의 "디커플링"을 목표로 합니다.

3. 사용 전에 고려해야 할 점

  1. 중앙 "이벤트 큐"는 전역 변수와 같다.
  2. 이벤트를 보낸 시점의 게임 "World"의 상태와 이벤트를 처리하는 시점의 게임 "World"의 상태는 다르다!
  3. 요청을 보내는 곳과 처리하는 곳이 같을 경우 비 정상적인 "피드백 루프"에 빠질 수 있습니다!

 

예제 코드
struct PlayMessage
{
	SoundId id;
	int volume;
};

class Audio
{
public:

	static void Init()
	{
		_head = 0;
		_tail = 0;

		numPending = 0;
	}
	
	static void playSound(SoundID soundId, int volume)
	{
		assert((_tail + 1) % MAX_PENDING != _head);

		// pending[_tail]에 새로 들어온 요청을 저장합니다.
		pending[_tail].id = soundId;
		pending[_tail].volume = volume;
		
		// "_tail"을 증가시킵니다.
		_tail = (_tail + 1) % MAX_PENDING;
	}

	static void Update()
	{
		// 이벤트 큐에 쌓인 이벤트 요청이 없으면 리턴합니다.
		if (_head == _tail) return;

		// pending[_head], 즉 "_head"가 가리키는 이벤트 요청을 처리합니다.

		// _head를 증가시킵니다.
		_head = (head_ + 1) % MAX_PENDING;
	}

private:
	static int _head = 0;
	static int _tail = 0;
	
	static const int MAX_PENDING = 16;
	static int numPending;
	static PlayMessage pending[MAX_PENDING];

};

1. 기본 구조

  1. Message 정보를 기억하기 위한 메모리 공간, 구조체 정의
  2. 새로 들어온 요청을 취합하고, "tail" 위치에 저장하는 작업 -> Audio::playSound()
  3. "head"가 가리키는 요청을 처리하는 작업 -> Audio::Update()

 

원형 버퍼

1. 왜?

  • 일반 배열과 다르게 "연결 리스트"의 기능을 차용해서 "삽입"+"제거" 시 이동 작업을 생략할 수 있습니다!

2. 개념

원형 버퍼

  • "머리"는 큐에서 처리할 요청을 읽을 위치입니다.
  • "꼬리"는 반대로 새로 들어올 요청이 저장될 위치입니다. "마지막 요청이 저장된 위치"가 아닙니다!

 

  • Queue의 "머리" 위치부터 요청이 처리되어 Queue의 앞부분에 빈 공간이 발생합니다.
  • 이때, 단순히 "head += 1" 이동하는 것이 아니라,  "head = (head+1) % MAX_PENDING"으로 이동합니다.
  • "tail" 또한 마찬가지입니다.

 

디자인 결정 요소

1. 큐에 들어갈 이벤트? 혹은 메시지?

  • 이벤트 : N개의 리스너를 지원하며, 리스너의 범위(종류의 범위)가 더 넓습니다.
  • 메시지 : 대부분의 경우 리스너가 한 개입니다.

2. 누가 요청을 보내는가?

  • 하나라면, 암시적으로 누가 보낼지 미리 알 수 있으며, 리스너가 여러 개일 가능성이 높습니다.
  • 여러 개라면, 큐의 순환을 주의해야 하며, 요청을 구분하기 위해 보낸 측의 추가적인 정보가 필요할 수 있습니다.