순서 패턴(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];
};