Home [게임 프로그래밍 패턴] Squencing Patterns: Double Buffer
Post
Cancel

[게임 프로그래밍 패턴] Squencing Patterns: Double Buffer

순서 패턴(Squencing Patterns)

  • 게임 월드에서 중요한 축: “시간”
  • 게임 월드 시간을 위한 패턴들
  • 게임 루프 패턴: 게임 월드 시간이 돌아가는 중심축
  • 업데이트 메서드 패턴: 객체의 시간을 다루는 패턴
  • 더블 버퍼 패턴: 순간적인 스냅샷들의 정면(facade)을 뒤로 숨김

Double Buffer

  • 의도: 여러 순차 작업의 결과 한번에 보여줌.

Motivation

How computer graphics work (briefly)

  • 모니터: 왼->오 픽셀 그림.
  • 프레임버퍼: 픽셀을 가져오는 곳
  • 비디오 디스플레이는 프레임 버퍼를 반복해서 읽는다.
    • 문제: 화면 찢김(tearing), 코드에서 픽셀을 입력하는 속도보다 비디오 드라이버가 앞지름.

  • 문제 해결: 비디오 드라이버는 한번에 전체 픽셀을 다 읽어야함
    • 프레임 버퍼 두개 => 하나는 프레임에 보이는 것으로 GPU가 언제든지 읽을 수 있는 것.
    • 화면 깜빡임에 맞춰 버퍼 변경 => 테어링 문제 해결 -결

The Pattern

  • 버퍼 클래스: 변경이 가능한 상태인 버퍼를 캡슐화
  • 정보를 읽을 때: 항상 current버퍼에 접근.
  • 정보를 쓸 때 next버퍼에 접근

When to Use It

  • 순차적으로 변경해야하는 상태
  • 변경 도중에 접근 가능해야함.
  • 바깥에서는 작업 중인 상태에 접근하지 못하도록
  • 상태에 값을 쓰는 도중에도 기다리지 않고 바로 접근 가능해야할 때

Keep in Mind

  • 코드 전체에 미치는 영향이 적음.

교체 연산 자체에 시간이 많이 걸림

  • 교체: 원자적(atomic)이어야함.
    • 교체 중에는 두 버퍼 모두에 접근 불가능
    • 대부분 포인터만 변경, 하지만 혹시 모른다.

버퍼가 두개 필요

  • 메모리 부담

예제

  • 단순한 그래픽 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Framebuffer
{
public:
  Framebuffer() { clear(); }

  void clear()
  {
    for (int i = 0; i < WIDTH * HEIGHT; i++)
    {
      pixels_[i] = WHITE;
    }
  }

  void draw(int x, int y)
  {
    pixels_[(WIDTH * y) + x] = BLACK;
  }

  const char* getPixels()
  {
    return pixels_;
  }

private:
  static const int WIDTH = 160;
  static const int HEIGHT = 120;

  char pixels_[WIDTH * HEIGHT];
};
  • scene 클래스는 다음과 같다.
    • draw()를 여러번 호출하여 버퍼에 원하는 그림을 그린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Scene
{
public:
  void draw()
  {
    buffer_.clear();

    buffer_.draw(1, 1);
    buffer_.draw(4, 1);
    buffer_.draw(1, 3);
    buffer_.draw(2, 4);
    buffer_.draw(3, 4);
    buffer_.draw(4, 3);
  }

  Framebuffer& getBuffer() { return buffer_; }

private:
  Framebuffer buffer_;
};

  • 게임코드는 매 프레임마다 어떤 장면을 그려야 할지를 알려준다.
    • getBuffer(): 비디오 드라이버에서 내부 버퍼에 접근할 수 있도록.
    • draw() 중간에 드라이버가 픽셀버퍼전체를 읽을 수 있다.
    • 플리커링 문제 발생
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Scene
{
public:
  Scene()
  : current_(&buffers_[0]),
    next_(&buffers_[1])
  {}

  void draw()
  {
    next_->clear();

    next_->draw(1, 1);
    // ...
    next_->draw(4, 3);

    swap();
  }

  Framebuffer& getBuffer() { return *current_; }

private:
  void swap()
  {
    // Just switch the pointers.
    Framebuffer* temp = current_;
    current_ = next_;
    next_ = temp;
  }

  Framebuffer  buffers_[2];
  Framebuffer* current_;
  Framebuffer* next_;
};
  • 위와 같이, 버퍼 두개, 포인터 스왑을 통해 해결가능.

그래픽스 외의 활용법

  • 변경 중인 상태에 접근하는 문제 해결
    • 원인1: 다른 스레드나 인터럽트에서 상태에 접근하는 경우(그래픽스 예제)
    • 원인2: 어떤 상태를 변경하는 코드가, 동시에 지금 변경하려는 상태를 읽는 경우.
      • 물리나 인공지능같이 객체가 서로 상호작용할 때 이런 경우를 쉽게 볼 수 있다.

Artificial unintelligence

  • 슬랩스틱 코미디 기반 게임에 들어갈 행동 시스템을 만든다 가정.
    • 무대 준비 완료 + 여러 actor가 몸개그 중
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Actor
{
public:
  Actor() : slapped_(false) {}

  virtual ~Actor() {}
  virtual void update() = 0;

  void reset()      { slapped_ = false; }
  void slap()       { slapped_ = true; }
  bool wasSlapped() { return slapped_; }

private:
  bool slapped_;
};
  • 배우를 위한 상위 클래스는 위와 같다.
  • 매 프레임마다 update를 호출해 배우를 업데이트 해줘야 한다.
    • 유저 입장에서는 모든 배우가 한 번에 업데이트되는 것처럼 보여야한다.
  • 무대는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Stage
{
public:
  void add(Actor* actor, int index)
  {
    actors_[index] = actor;
  }

  void update()
  {
    for (int i = 0; i < NUM_ACTORS; i++)
    {
      actors_[i]->update();
      actors_[i]->reset();
    }
  }

private:
  static const int NUM_ACTORS = 3;

  Actor* actors_[NUM_ACTORS];
};
  • stage클래스는 배우를 추가 가능
    • 관리하는 배우 전체를 업데이트할 수 있는 update()메서드를 제공.
  • 내부적으로는 하나씩 업데이트됨.
    • 딱 한번만 반응하기 위해, reset을 바로한다.
  • Actor를 상속받는 구체 클래스 Comedian을 정의는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Comedian : public Actor
{
public:
  void face(Actor* actor) { facing_ = actor; }

  virtual void update()
  {
    if (wasSlapped()) facing_->slap();
  }

private:
  Actor* facing_;
};
  • 아래와 같이 서로를 바라보게하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
Stage stage;

Comedian* harry = new Comedian();
Comedian* baldy = new Comedian();
Comedian* chump = new Comedian();

harry->face(baldy);
baldy->face(chump);
chump->face(harry);

stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);

  • Harry를 때리면
    • Stage 클래스의 update메서드는 한 레임만에 전체 액터에게 전파된다.
1
2
3
harry->slap();

stage.update();
  • 하지만 harry의 순서를 바꾸면, 다른 결과가 나오게된다.

1
2
3
stage.add(harry, 2);
stage.add(baldy, 1);
stage.add(chump, 0);
  • harry가 마지막에 업데이트 되니, 전파는 일어나지 않는다.
    • 쓰는 동시에 값을 읽기 때문에 발생하는 문제
    • 결국, 업데이트 중 다른 것에 영향을 미침.
    • 배치 순서에 따라 현 프레임에서 반응이 나타날 수 있고, 다음 프레임에서 반응이 나타날 수 있다.

Buffered slaps

  • ‘맞은’ 상태만을 버퍼에 저장하는 방법이 있음.
    • reset 대신에 swap을 추가.
    • 이제, 먼저 업데이트한 다음에 상태를 교체한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Actor
{
public:
  Actor() : currentSlapped_(false) {}

  virtual ~Actor() {}
  virtual void update() = 0;

  void swap()
  {
    // Swap the buffer.
    currentSlapped_ = nextSlapped_;

    // Clear the new "next" buffer.
    nextSlapped_ = false;
  }

  void slap()       { nextSlapped_ = true; }
  bool wasSlapped() { return currentSlapped_; }

private:
  bool currentSlapped_;
  bool nextSlapped_;
};
1
2
3
4
5
6
7
8
9
10
11
12
void Stage::update()
{
  for (int i = 0; i < NUM_ACTORS; i++)
  {
    actors_[i]->update();
  }

  for (int i = 0; i < NUM_ACTORS; i++)
  {
    actors_[i]->swap();
  }
}

디자인 결정

버퍼를 어떻게 교체?

  • 교체연산은 쓰기, 읽기 둘다 사용못하게 해야함.
    • 최대한 빠르게

포인터, 레퍼런스

  • 빠름.
  • 외부코드는 버퍼에대한 포인터를 저장할 수 없다는 한계.
    • 주기적으로 다른 버퍼를 읽어야함.
  • 버퍼에 남아 있는 데이터는 바로 이전 프레임 데이터가 아닌 2프레임 전 데이터.

이전 프레임버퍼를 사용하는 고전적인 예: 모션블러.

모션블러효과: 현재 프레임 이미지에 이전 프레임 값을 살짝 섞어 실제 카메라에서 보이는 것처럼 이미지를 뭉개줌.

버퍼끼리 데이터를 복사

  • 유저가 다른 버퍼를 재지정하게 할 수 없다면, 복사해야함.
  • 다음 버퍼에는 딱 한 프레임 전 데이터가 들어 있을 경우.
    • 두 버퍼를 교체하는것보다 좋다.
  • 교체시간이 오래걸림.
    • 전체 복사를 다 복사하기 때문.

얼마나 정밀하게 버퍼링할 것?

  • 버퍼가 어떻게 구성되어 있는가?
  • 데이터 덩어리? 객체 컬렉션 안에 분산?
    • 그래픽 예제: 전자
    • 액터 예제: 후자

버퍼가 한 덩어리

  • 간단히 교체 가능.
    • 포인터 대입 두번만으로 버퍼 교체 가능

여러 객체가 각종 데이터 들고 있으면

  • 교체가 더 느리다.
    • 전체 객체 컬렉션을 순회하면서 교체해야함.
    • 상대적 오프셋을 응용하면 최적화 가능.
    • 아래 코드에서 static 함수이기 때문에 한번의 호출로 모든 객체의 상태를 변경할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Actor
{
public:
  static void init() { current_ = 0; }
  static void swap() { current_ = next(); }

  void slap()        { slapped_[next()] = true; }
  bool wasSlapped()  { return slapped_[current_]; }

private:
  static int current_;
  static int next()  { return 1 - current_; }

  bool slapped_[2];
};

출처

This post is licensed under CC BY 4.0 by the author.

[게임 프로그래밍 패턴] Design Patterns Revisited: State

[게임 프로그래밍 패턴] Squencing Patterns: Game Loop