Game Development, 게임개발/디자인패턴

Double Buffer Pattern, 이중 버퍼 패턴 [디자인패턴]

게임이 더 좋아 2021. 10. 6. 17:35
반응형
728x170

 

이중 버퍼의 목적은

여러 순차 작업의 결과를 한 번에 보여준다는 데에 있다.

그렇다면 어떻게 이렇게 할 수 있는지 알아보자

 


 

본질적으로 컴퓨터는 한 번에 하나를 수행한다.

CPU에서 Instruction에 대해 클럭을 소모하면서 순차적으로 하나씩 처리해나간다.

우리한테는 무척 빠른속도로 동작하기 때문에 동시에 하는 것처럼 보인다.

빠른 속도를 위해서는 큰 일을 잘 쪼개는 등 pipeline을 구현하면 더 빨라진다.

 

아무튼 컴퓨터는 빠른 속도로 작동하더라도 한 번에 하나씩 처리하는 것이 본질이다.

하지만 게임 유저의 입장으로 한 번에 여러 작업의 결과물을 봐야할 때가 있다.

 

대표적으로 렌더링, rendering을 생각해보자

 

유저에게 보여줄 게임 화면을 그릴 때는 멀리 있는 산, 구불구불한 언덕, 나무 전부를 한 번에 보여준다.

이 때 작업 결과물이 아닌 렌더링 되는 과정.. 막 폴리곤들이 보인다면 게임에 몰입할 수 없다. 

모든 화면은 부드럽고 빠르게 업데이트가 되어 유저가 이질감을 느끼지 않도록 해야한다.

그것을 매 프레임 반복해야한다.

이중 버퍼는 이런 문제를 해결한다.

앞서 렌더링이 되는 과정을 알고 설명을 이어나가보자


 

컴퓨터 모니터 같은 비디오 디스플레이는 한 번에 한 픽셀을 그린다.

화면 왼쪽에서 오른쪽으로 한 줄을 그린 후 다음 줄로 내려간다.

화면 우하단에 도달하면 다시 좌상단으로 돌아가서 같은 작업을 반복한다.

빠른 속도로 수행하기 때문에 사람의 눈으로는 픽셀들로 이루어진 정적인 이미지로 보인다.

 

이 과정들을 픽셀들을 좁은 호스를 통해 화면에 전달하는 방법과 유사하다.

각각의 색은 하나씩 비트별로 순서대로 호스를 통과하면서 화면에 뿌려지는 것이다.

호스는 어떤 색을 어디에 뿌려야 하는지 어떻게 아는 것일까?

 

애부분의 컴퓨터에서는 픽셀을 프레임버퍼, framebuffer 로부터 가져오기 때문에 알 수 있다.

프레임 버퍼는 메모리에 할당된 픽셀들의 배열로, 한 픽셀의 색을 여러 바이트로 표현하는 RAM의 한 부분이다. 호스는 화면에 색을 뿌릴 때 프레임버퍼로부터 한 바이트씩 색의 값을 가져온다. 

궁극적으로는 게임을 화면에 보여주려면 프레임버퍼의 값을 써넣으면 된다.

그래픽 알고리즘은 결국 모든 프레임 버퍼의 값을 써넣기 위해 존재하는 것이다.

다만 위에서 말했다시피 컴퓨터는 한 번에 하나만 수행할 수 있다.

순차적으로 실행된다는 의미다.

 

즉, 렌더링 코드가 실행되는 동안에는 다른 작업을 수행할 수 없다.

보통은 위 문장이 맞지만 렌더링 도중에도 실행되는 작업이 "일부" 있다.

그 중 하나가 게임이 실행되는 동안 비디오 디스플레이가 프레임버퍼를 반복해서 읽는 것인데, 여기에서 문제가 발생한다.

 

화면에 웃는 얼굴을 하나 그린다고 해보자.

코드에서는 루프를 돌편서 프레임버퍼에 픽셀 값을 입력한다.

**우리가 하나 몰랐던 것은 코드가 프레임버퍼에 값을 쓰는 도중에도 비디오 드라이버에서 프레임버퍼 값을 읽는다는 점이다.

우리가 입력해놓은 픽셀 값을 비디오 드라이버가 화면에 출력하면서 웃는 얼굴이 나오기 시작하지만 아직 다 입력하지 못한 버퍼 값까지 화면에 출력될 수 있다.

그렇게 된다면 그림의 일부만 나오는 tearing, 테어링이 발생한다. 찢긴다는 의미다.

https://gameprogrammingpatterns.com/double-buffer.html

이래서 이중 버퍼 패턴을 이용한다. 이 문제를 해결하기 위해서는 코드에서는 픽셀을 한 번에 하나씩 그리되,

비디오 드라이버에서는 하나씩 읽는 것이 아닌 한 번에 읽어서 보여줘야 하는 것이다.

즉, 이전 프레임에서는 얼굴이 안보이다가 다음 프레임에서 전체가 보이는 형태다.

작업 과정이 보여지면 그것이 바로 테어링 현상이다.

 


 

유저는 게임을 하는 사람이다.

즉, 에피소드 1이 끝난 후에는 2가 시작되는데

2가 시작되기 전에 1에 있던 것들 중에 2에 필요없는 것을 삭제하고 2에 필요한 것들을 준비해야 한다.

하지만 말했듯이 작업 결과가 아닌 작업 과정이 보여진다면 몰입이 떨어진다고 했다.

그래서 우리는 이러한 작업 과정을 보여주지 않고 전환할 방법을 강구했다.

 

만약 메모리를 조금 투자한다면 해결책을 얻을 수 있다.

에피소드 2를 미리 만들어놓아서 1이 끝나자마자 2로 이동하는 것이다.

그렇게 쉽게 가능하긴 하다. 그렇다면 2가 진행될 때 3을 1이 진행되었던 메모리에 다시 만들어놓으면

2에서 3도 무리 없이 가능하다.

 

이중 버퍼가 위와 같은 방식이다.

거의 모든 게임의 렌더링 시스템이 내부에서 이렇게 동작한다고 보면 된다.

프레임버퍼를 2개를 준비해

하나의 버퍼에서는 에피소드 1처럼 지금 프레임 값에 두고 GPU가 언제든지 읽을 수 있게 한다.

그동안 렌더링 코드는 다음 프레임에 대한 버퍼를 채우는 것이다.

프레임버퍼를 채운다.

렌더링코드가 장면을 다 그렸다면 버퍼를 교체한 뒤

GPU에게 다른 버퍼를 읽으라고 하면 된다.

 

다시 교체된 버퍼는 다른 프레임에 대한 버퍼를 채운다.

 

요약하자면

지금 GPU가 프레임버퍼를 한 번에 읽어서 유저에게 화면을 보여주는 동안

프레임버퍼는 다음 프레임을 위한 준비를 한다.

다음 프레임이 되었을 때 준비를 한 프레임버퍼를 GPU가 읽도록 하고

교체된 버퍼는 다시 다음프레임을 위한 준비를 한다.

 


 

우리가 알고 있는 게임의 신비다.

우리가 장면들을 연속적으로 보고있는 것처럼 느끼지만 눈이 정확하지 않기 때문에 그렇게 느끼고 있는 것이다.

이제는 버퍼 클래스로 알아보자

 

버퍼 클래스는 변경이 가능한 상태의 버퍼를 캡슐화한다.

버퍼는 점차적으로 수정되지만 밖에서는 한 번에 바뀌는 것처럼 보이게 하려고 한다.(화면같이)

이를 위해서 버퍼 클래스는 현재 버퍼와 다음 버퍼, 2개의 버퍼를 갖는다.

 

정보를 읽을 때는 현재 버퍼에서 접근하여 읽는다.

정보를 쓸 때는 다음 버퍼에 접근하여 쓴다.

변경이 끝나면 다음 버퍼와 현재 버퍼는 교체하고 하는 일도 바뀐다.

현재 버퍼는 교체되면 다음 버퍼가 되고

다음 버퍼는 교체되면 현재 버퍼가 된다.

 


앞서 어떻게 쓰는지는 알아보았다.

이중 버퍼 패턴은 언제 써야 할지를 그냥 알 수 있는 패턴 중의 하나다.

이중 버퍼 시스템이 없었다면 테어링 현상이 나타나고 시스템이 오작동했을 수도 있다.

 

그래도 언제 써야하는 지 명시적으로 말해보자

1. 순차적 변경상태가 존재한다.

2. 이 상태는 변경 도중에도 접근 가능해야 한다.

3. 바깥 코드에서는 작업 중인 상태에 접근할 수 없어야 한다.

4. 상태에 값을 쓰는 도중에도 기다리지 않고 바로 접근할 수 있어야 한다.

 


 

다만 이중 버퍼는 코드 구현 수준에서도 적용되므로 코드 전체에 미치는 영향이 적다.

뭐 가볍다고 생각할 지 모르나 그래도 조심해야 할 부분은 존재한다.

 

1. 교체 연산 자체에 시간이 길다.

이중 버퍼 패턴에서는 버퍼에 값을 다 입력했다면 버퍼를 교체한다고 했다.

교체 연산은 원자적, atomic이어야 한다. 

다시 말하면 교체 중에는 두 버퍼에 접근이 불가능해야 한다.

대부분은 버퍼를 가리키는 포인터만 바꾸면 되기 때문에 충분히 빠르지만, 혹시라도 버퍼에 값을 쓰는 것보다 교체가 더 오래 걸린다면 쓸모가 없다.

다음 프레임을 위한 시간도 안된다면 우선 화면을 나타내는 데에는 쓸 수 없다.

 

2. 버퍼가 2개가 필요하다.

이중 버퍼 패턴은 앞서 말했듯이 메모리가 필요한 작업이다.

항상 쌍으로 존재해서 구현되기 때문에 메모리에 제약이 있는 상태에서는 구현하기 힘들다.

만약 메모리가 부족하다면  이중 버퍼 패턴을 포기하고 상태를 변경하는 동안 읽는 것을 막는 방법을 찾아야 할 것이다.

** 한 번에 읽기 때문에 테어링이 없는 것 -> 중간에 접근한다면 테어링이 생긴다는 것

 


 

실제 코드로 알아보자

프레임버퍼에 픽셀을 그릴 수 있는 아주 단순한 그래픽 시스템을 만들 것이다.

 

class Framebuffer
{
public:
  Framebuffer() { clear(); }

  //모든 영역을 WHITE로 초기화
  void clear()
  {
    for (int i = 0; i < WIDTH * HEIGHT; i++)
    {
      pixels_[i] = WHITE;
    }
  }
  
  // 해당 픽셀을 BLACK으로
  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];
};

 

우선 FrameBuffer 클래스는 clear 메서드로 전체 버퍼를 흰색으로 채우거나

draw 메서드로 특정 픽셀에 검은색을 입력할 수 있다.

getPixels 메서드는 픽셀 데이터를 담고 있는 메모리 배열에 접근하는 것이다.

** GPU에서 구현하기 위해 픽셀 데이터를 읽기 위해 getPixels을 써서 값을 받아온다고 생각하면 된다.

 

이걸 Scene 클래스 안에 넣어 

해당 장면을 draw를 호출하여 버퍼에 원하는 그림을 그리게 된다.

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_;
};

 

이를 수행하면 멀리서 봤을 때 웃는 얼굴이 나온다.

아니면 매직아이로 보자

 

 

 

게임 코드는 매 프레임마다 어떤 장면을 그려야 할 지를 알려준다.

먼저 버퍼를 지운 뒤 한 번에 하나씩 그리고자 하는 픽셀을 찍는다.

동시에 Video Driver 에서도 내부 버퍼에 접근할 수 있도록 getBuffer()를 제공한다.

뭐 문제가 없어보이지만

문제가 있낀 한데

비디오 드라이버가 아무 때나 getPixels()를 호출해 버퍼에 접근할 수 있기 때문이다.

 

즉, 중간에 접근이 가능해진다면

buffer_.draw(1, 1);
buffer_.draw(4, 1);
// <- Video driver reads pixels here!
buffer_.draw(1, 3);
buffer_.draw(2, 4);
buffer_.draw(3, 4);
buffer_.draw(4, 3);

 

눈만 있는 그림이 나올 것이다.

다음 프레임에서도 렌더링하는 도중 버퍼를 읽게 된다면

깜빡거리는 화면을 보게 될 것이다.

이를 flickering이라고 한다.

 

이중 버퍼는 이를 해결할 수 있다.

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_;
};

 

이제 Scene 클래스에 버퍼 2 개가 buffers_배열에 들어있다.

버퍼에 접근할 때는 배열 대신 next_와 current_ 포인터 멤버 변수로 접근한다.

렌더링할 때는 next_ 포인터가 가리키는 다음 버퍼에 

비디오 드라이버는 current_ 포인터로 현재 버퍼에 접근해 픽셀 값을 가져온다.

 

이렇게 작업 중인 버퍼에 비디오 드라이버가 접근해서 테어링 현상과 같은 의도치 않은 상황을 막는다.

장면을 구현했다면 이제 swap() 만 호출하면 된다.

swap() 에서는 next_ , current_ 포인터만 맞바꾸면 된다.

 

비디오 드라이버에서 getBuffer()를 호출하면 이전에 화면에 그리기 위해 사용한 버퍼 대신

방금 그린 화면이 들어 있는 버퍼를 얻게 되는 것이다.

 


 

이렇게 좋은 2중 버퍼..  과연 렌더링에만 쓸까?

아니다.

 

변경 중인 상태에 대한 접근을 막고 결과만 받아온다는 것이 이중 버퍼로 해결하려는 문제의 핵심이다.

원인은 보통 2가지다.

1. 다른 스레드나, 인터럽트 상태에 접근하는 경우

2. 어떤 상태를 변경하는 코드가, 동시에 지금 변경하려는 상태를 읽는 경우다.

?? 2번은 잘 이해가 안간다.

 

 


 

예를 통해서 알아보자

게임에 들어갈 행동 시스템을 만든다고 해보자

게임에는 무대가 준비되어 있고, 그 위에서 여러 배우들이 이런 저런 몸개그를 하고 있다.

먼저 배우를 위한 상위 클래스를 만들자.

 

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()를 호출하여 배우가 뭔가를 할 수 있게 해준다.

특히 유저 입장에서는 모든 배우가 한꺼번에 업데이트되는 것처럼 보여야 한다.

 

배우는 서로 상호작용이 가능하다.

여기서 "상호작용"이란 "서로 때리는 것"이라고 하겠다.

 

update()가 호출 되었을 때 배우는 다른 배우 객체의 slap()을 호출해 때리고

wasSlapped()를 통해서 맞았는지 여부를 알 수 있다.

 

배우들이 상호작용할 수 있는 무대를 만들어보자

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() 메서드를 제공한다.

유저 입장에서는 배우들이 한 번에 움직이는 것처럼 보이겠지만 내부적으로는 하나씩 업데이트한다.

 

배우가 따귀를 맞았을 때 한 번만 반응하기 위해서 맞은 상태(slapped_)를 update 후 바로 초기화한다는 점도 주의하자.

 

다음으로는 Actor를 상속받는 구체 클래스 Comedian을 정의한다.

코미디언이 하는 일은 다른 배우 한 명을 보고 있다가 누구한테든 맞으면 보고 있던 배우를 때린다.

 

class Comedian : public Actor
{
public:
  void face(Actor* actor) { facing_ = actor; }

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

private:
  Actor* facing_;
};

 

이제 코미디언 몇 명을 무대 위에 세워놓고 어떤 일이 벌어지는지 보자.

3명의 코미디언이 각자 다음 사람을 바라보고 마지막 사람은 첫번째 사람을 보게 한다

cycle을 만들었다.

 

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를 때려보자

 

harry->slap();

//stage의 update는 인덱스 순서대로 배우에게 update문을 돌리는 것이다.
stage.update();

 

Stage 클래스의 update 메서드들은 순서대로 돌아가면서 배우 객체의 update() 를 호출하기 때문에

코드가 실행된 후 결과는 아래와 같다.

 

1. Stage가 actor 0인 해리를 업데이트 -> 해리가 따귀맞고 해리가 발디를 때림

2. Stage가 actor 1 인 발디 업데이트 -> 발디가 따귀맞고 발디가 첨프를 때림

3. Stage가 actor 2 인 첨프 업데이트 -> 첨프가 따귀맞고 첨프가 해리 때림

4. Stage Update 끝

 

즉, 처음에 해리가 때린 것이 한 프레임만에 전체 코미디언에게 적용되었다.

그럼 무대를 초기화하는 코드에서 나머지는 그대로 두고 무대에 배우를 추가하는 코드만 바꿔보자

 

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

 

그렇게 되면 위와 다른 상황이 벌어진다.

1. Stage가 actor 0인 첨프를 업데이트 -> 첨프가 따귀를 맞고 아무것도 하지 않음

2. Stage가 actor 1 인 발디 업데이트 -> 발디가 따귀맞고 아무것도 하지 않음

3. Stage가 actor 2 인 첨프 업데이트 -> 해리가 따귀맞고 발디를 때림

4. Stage Update 끝

 

?? 뭐지 왜 다르게 나올까

 

문제는 명확하다.

배우 전체를 업데이트할 때 배우의 맞은 상태(slapped_)를 바꾸는데 그와 동시에 같은 값을 읽기도 하다보니

업데이트 초반에 맞은 상태를 바꾼게 나중에 가서 영향을 미치는 것이다.

(해당 프레임 도중 정보가 수정되어서)

 

결과적으로 배우가 맞았을 때 배치 순선에 따라 이번 프레임에서 반응하거나 다음 프레임에서 반응하는 것이다.

*분명 동시에 업데이트 되는 것처럼 보여야 한다고 했는데 이러면 안된다.

 

이중 버퍼를 쓰는 것이다.

이번엔 맞은 상태만을 저장하는 버퍼를 만든다.

 

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_;
};

 

Actor 클래스의 slapped_ 상태가 2개로 늘었다.

currentSlapped_와 nextSlapped_이다. 앞서 본  현재, 다음 포인터와 비슷하다.

reset 메서드는 없어지고 swap 메서드가 생겼다.

Stage 클래스도 조금 고쳤다.

 

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();
  }
}

 

이제 update() 메서드는 모든 배우를 먼저 업데이트한 후에 상태를 교체한다.

결과적으로는 배우 객체는 맞았다는 것을 다음 프레임에서 알게 되는 것이다.

이제 모든 배우는 배치 순서와 상관없이 상태가 다음 프레임에서 한 번에 교체된다.

그렇게 되면 유저의 입장에서는 모든 배우가 한 프레임에 동시에 업데이트되는 것처럼 보인다.

 


 

이중 버퍼는 간단하지만 강했다.

하지만 쉬웠기에 디자인 하는 방법은 까다롭게 할 필요가 있었다.

 

제일 먼저 고려할 사항은

버퍼를 어떻게 교체할 것인가? 이다.

버퍼 교체 연산은 읽기 버퍼와 쓰기 버퍼 모두를 사용하지 못하게 한다는 점에서 매우 중요하다.

 

1. 버퍼 포인터나 레퍼런스를 교체함

 

앞서 말했듯이 swap메서드로 포인터만 교체하는 법을 쓰기도 한다.

장점은 확실하다.

-빠르다.

버퍼가 아무리커도 포인터만 바꾸면 되기에 무척 빠르다

 

-버퍼 코드 밖에서 버퍼 메모리를 포인터로 저장할 수 없다는 한계점

데이터를 실제로 옮기는 것이 아니라 주기적으로 다른 버퍼를 읽으라고 하는 것으로

버퍼 외부코드에서 버퍼 내 데이터를 직접 포인터로 저장하면 버퍼 교체 후 잘못된 데이터를 가리킬 수 있다.

*특히 비디오 드라이버가 프레임버퍼는 항상 메모리에서 같은 위치에 있을 거라고 기대하는 시스템에서 문제가 된다.

-버퍼에 남아 있는 데이터는 바로 이전 프레임 데이터가 아닌 2프레임 전의 데이터이다.

버퍼끼리 데이터를 복사하지 않는 한 이전 프레임은 다른 버퍼에 존재한다.

 

2. 버퍼끼리 데이터를 복사한다.

유저가 다른 버퍼를 재지정하지 못하게 할 때 사용하는 방법이다.

어쩔 수 없이 다음 버퍼 데이터를 현재 버퍼로 복사해주는 작업이 필요하다.

앞에서 본 코미디언 예제를 생각해보자

복사해야하는 상태가 불리안 변수 하나밖에 없기 때문에 버퍼 포인터를 교체하는 것과 차이가 없었지만

-교체시간이 길어진다.

교체를 하려면 전체 버퍼를 다 복사해야하기 때문에 버퍼 전체가 프레임버퍼같이 크다면 엄청난 시간이 걸릴 수 있다.

-다음 버퍼에는 딱 한 프레임 전 데이터가 들어있다.

이것은 이전 버퍼에서 바로 이전데이터를 얻는 것으로 포인터 교체보다 나은점이다.

 


그 다음 고려할 점은

얼마나 정밀하게 버퍼링할 것인가? 이다.

버퍼가 어떻게 구성되있는지부터 봐야하는데

버퍼가 하나의 큰 데이터 덩어리인가 객체 컬렉션 안에 분산되어 있는가를 봐야한다.

그래픽 렌더링에서는 전자고 코미디언에서는 후자다.

 

한 덩어리라면 간단히 교체가 가능하다.

버퍼 2개만 있기 때문에 한 번에 맞바꾸기만 하면 된다.

포인터를 쓴다면 포인터 대입 2번만으로 버퍼를 교체한다.

 

만약 여러 객체가 각종 데이터를 들고 있다면

교체가 느리다. 코미디언 예제에서는 다음 맞은 상태를 정리하기 위해서 매 프레임마다 버퍼된 상태를 건드려줘야 했기 때문에 이방식도 문제가 없었지만 만약 이전 버퍼를 건드리지 않는다면 버퍼가 여러 객체에 퍼져 있더라도 단일 버퍼와 같은 성능을 낼 수 있도록 최적화할 수 있는 방법이 있다.

 

이 방법은 Current와 next 의 포인터 개념을 객체 상대적 오프셋으로 응용하는 것이다.

 

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_; }

 

배우는 상태 배열의 current_ 인덱스를 통해 맞은 상태에 접근한다.

다음 상태는 배열의 나머지 한 값이므로 next()로 인덱스를 계산한다.

상태 교체는 current_ 값을 바꾸기만 하면 된다. 여기서 swap()이 정적 함수이기 때문에 한 번만 호출해도

모든 배우의 상태를 교체할 수 있다.

 

반응형
그리드형