본문의 내용을 토대로 이해한 것을 다시 작성 및 번역한 것입니다.
업데이트 패턴
1 의도
배열을 돌면서 객체 별로 한 프레임 단위의 일을 시키기 위함
2 동기
플레이어가 던전에 들어가 적들에게 다가갔다. 공격 받..지 않았다. 매 프레임마다 어떤 활동을 부여하지 않았기 때문입니다. 적을 좌, 우로 움직이는 간단한 코드를 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | while (true) { // Patrol right. for (double x = 0; x < 100; x++) { skeleton.setX(x); } // Patrol left. for (double x = 100; x > 0; x--) { skeleton.setX(x); } } |
위의 코드는 while문을 한 번 돌 때를 한 프레임이라고 치면 한 프레임에 좌, 우로 100번씩 200번을 움직이게 됩니다. 우리는 해골이 매 프레임 움직이며 유저 입력에 바로 바로 대응할 수 있고 렌더링하길 원합니다.
아래와 같이 고쳐보도록합시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| Entity skeleton;
bool patrollingLeft = false;
double x = 0;
// Main game loop: while (true)
{
if (patrollingLeft)
{
x--;
if (x == 0) patrollingLeft = false;
}
else
{
x++;
if (x == 100) patrollingLeft = true;
}
skeleton.setX(x);
// Handle user input and render game...
}
|
이제 매 프레임 마다 해골들을 확인하게됩니다. 많이 복잡해졌습니다. 단순 해골하나 뿐이었는데 적을 더 추가해야겠습니다. 발키리가 긴장을 놓지 않도록 번개를 쏘도록 해보죠.
지금까지 해온대로 ‘제일 간단한 방식으로' 코드를 만들어 봅시다.
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
| // Skeleton variables... Entity leftStatue;
Entity rightStatue;
int leftStatueFrames = 0;
int rightStatueFrames = 0;
// Main game loop: while (true)
{
// Skeleton code...
if (++leftStatueFrames == 90)
{
leftStatueFrames = 0;
leftStatue.shootLightning();
}
if (++rightStatueFrames == 80)
{
rightStatueFrames = 0;
rightStatue.shootLightning();
}
// Handle user input and render game...
}
|
점점 코드가 복잡해지기 시작합니다. 유지보수는 점점 먼 얘기가 되어갑니다. 메인 루프에는 처리할 객체들과 실행코드가 가득 차있습니다. 이들을 한번에 실행시키기 위해 옥수수죽처럼 함께 뭉쳐놔야합니다. 어떻게 해결해야 할까요?
모든 객체가 자신의 동작을 캡슐화 해야합니다. 이렇게하여 게임 루프를 어지럽히지 않으면서 객체를 쉽게 추가, 삭제 할 수 있습니다. 이렇게 하기 위해 우리는 추상 메서드 update( )를 생성합니다. 메인 루프는 객체의 컬렉션을 관리하며 그들을 업데이트 할 수 있다는 것은 알지만 자료형은 모르게 됩니다. 이제 객체들을 게임 루프와 다른 객체들로부터 분리했습니다. 한 프레임당, 게임 루프는 컬렉션을 돌면서 객체의 update( ) 메서드를 호출합니다. 이제 객체는 한 프레임마다 동작을 할 수 있습니다. 매 프레임당 모든 오브젝트를 calling 하기 때문에, 객체들은 동시에 동작합니다. 게임 루프는 객체들의 동적인 컬렉션을 가지고 있습니다. 그래서 레벨로부터 추가, 삭제가 쉽습니다. 이제 더 이상 하드코딩이 되어있지 않기 때문에, 레벨 디자이너는 원하는데로 데이터 파일을 통해 레벨을 만들어 낼 수 있습니다. |
3 패턴
매 프레임 게임은 컬렉션에 있는 각각의 객체를 업데이트 시키는 것이 핵심!
매 프레임 게임은 컬렉션에 있는 각각의 객체를 업데이트 시키는 것이 핵심!
4 언제 쓸 것인가?
게임 루프 패턴이 빵을 써는 것이라면, 업데이트 패턴은 버터입니다.. 플레이어와 상호작용하는 객체들이 썰린 빵의 표면에 많이 산다면 업데이트 패턴은 어쩔 수 없이 쓰게될 것입니다.. 만약 게임에 마린들, 드래곤들, 고스트들 등등이 있다면 이 패턴은 좋은 선택입니다!
게임 루프 패턴이 빵을 써는 것이라면, 업데이트 패턴은 버터입니다.. 플레이어와 상호작용하는 객체들이 썰린 빵의 표면에 많이 산다면 업데이트 패턴은 어쩔 수 없이 쓰게될 것입니다.. 만약 게임에 마린들, 드래곤들, 고스트들 등등이 있다면 이 패턴은 좋은 선택입니다!
하지만 객체가 추상적이거나 살아있지 않은 체스말과 같다면 이 패턴은 사실 잘 안맞을 것입니다. 체스 같은 게임에서는, 모든 말들을 동시에 움직일 필요 없고, 알다시피 매 프레임 모든 말을 업데이트 할 필요가 없습니다.
업데이트 패턴은 이럴 때 쓰자!
|
5 주의사항
이 패턴은 꽤 심플해서, 딱히 어려울 것은 없습니다. 하지만 모든 코드의 라인은 결과에 연관이 있죠. 주의할 점에 대해서 알아보도록 하겠습니다.
이 패턴은 꽤 심플해서, 딱히 어려울 것은 없습니다. 하지만 모든 코드의 라인은 결과에 연관이 있죠. 주의할 점에 대해서 알아보도록 하겠습니다.
코드를 프레임마다 나눠 실행하는 것이 더 복잡하다.
위에 두 예제를 살펴봤습니다. 뒤에 것이 딱 보기에도 더 복잡하죠? 첫 번째 것은 단순히 해골을 앞 뒤로 움직이는 것이고, 두 번 째 것은 매 프레임 마다 제어권을 게임 루프에 양보합니다. 입력, 렌더링 같은 것들을 처리하려면 두 번째 방식이 필요하지만, 이런 방식은 더 복잡해 진다는 것을 기억하세요.
매 프레임을 떠날 때 당신은 현재 상태를 저장해야합니다.
첫 예제는 그냥 코드가 끝나면 다음 코드를 실행하면 됬습니다. 하지만 두 번째는 매 프레임 이전 프레임의 상태를 알 수 있도록 patrollingLeft 변수를 저장했습니다.
모든 객체는 같은 프레임에 실행되지만 동시에 실행되는 것은 아닙니다.
게임 루프는 앞에서 얘기 했듯이 모든 객체를 돌면서 업데이트 합니다. 각각의 객체는 update( )를 실행하면서도 다른 객체에 접근 할 수 있습니다. 게임 루프에서 서로 참조하는 객체 A, B가 있다고 해봅시다. A가 B보다 앞서 실행된다면 A는 B의 이전 프레임에서의 상태를 접근하게 되고 B는 A의 현재 상태에 접근하게 됩니다. 이것이 찰나의 순간이라 플레이어에게는 동시에 실행되는 것처럼 보이겠지만 실제로는 아닌 것입니다. 이렇기 때문에 순차적으로 진행되도록 만들어야합니다. 그렇지 않고 동시에 실행하면 상태가 서로 꼬일 수 있습니다.
업데이트하는 동안 객체 리스트를 수정하는 것을 조심하세요.
이 패턴을 사용할 때, 많은 게임 동작들은 업데이트 메소드에 들어갑니다. 그리고 오브젝트를 추가하거나, 삭제할 것입니다. 예를 들어 해골 병사를 죽이면 아이템이 떨어진다고 생각해봅시다. 객체가 새로 생기면 큰 트러블 없이 객체 목록뒤에 추가됩니다. 객체 목록을 계속 순회하다보면 결국 새로운 오브젝트에 도달해 그것을 업데이트 하겠죠.
이것은 플레이어가 보기도 전에 그것이 스폰되서 프레임이 진행되는 동안 새로운 해골 병사가 동작하게 되는 것을 의미합니다. (프레임 진행도중에 추가되기 때문에 갑자기 등장하는 느낌이 들 수 있겠죠. 사실 체감은 안되겠습니다만…)
1
2
3
4
5
| int numObjectsThisTurn = numObjects_;
for (int i = 0; i < numObjectsThisTurn; i++)
{
objects_[i]->update();
}
| cs |
이것이 싫다면 업데이트 루프를 실행하기 전에, 객체 리스트의 숫자를 저장하고 그만큼만 업데이트 합니다. 위와 같이 코딩하면 이번 프레임에 추가된 객체 전까지만 업데이트를 하게 됩니다. 다음 프레임부터 새로 추가된 객체도 업데이트 되게 됩니다.
하지만 삭제는 쉽지 않습니다. 업데이트 도중 객체를 삭제할 경우, 객체를 스킵하게 될 수도 있습니다.
예를 들어서 위의 객체 리스트를 업데이트하다가 영웅이 나쁜 괴물을 죽였다고 생각해봅시다. 그렇다면 객체 리스트에서 괴물(i=0)은 빠지게 되고, 영웅과 소작농은 앞의 인덱스로 이동하게(각각 i=0, i=1로) 됩니다. 영웅을 업데이트하다가 괴물을 죽여 앞의 인덱스로 이동 했으니(i=1 -> 0) 농부를 실행할 때는 2에 아무것도 없게 되어 업데이트를 진행할 수 없게 됩니다.
해결책
|
6 예제
이 패턴은 쉽게 이해할 수 있을 정도로(아니라면 죄송합니다.) 직관적이고, 아주 유용합니다. 장식 치레 없이 간단한 코드를 구현해보도록 하겠습니다.
해골과, 조각상을 표현할 Entity 클래스를 시작하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class Entity
{
public:
Entity()
: x_(0), y_(0)
{}
virtual ~Entity() {}
virtual void update() = 0;
double x() const { return x_; }
double y() const { return y_; }
void setX(double x) { x_ = x; }
void setY(double y) { y_ = y; }
private:
double x_;
double y_;
};
|
필요한 뼈대만 넣었습니다. (실제라면 그래픽이나 물리에 대한 많은 코드가 들어갔겠지만.) 가장 중요한 것은 추상 메소드 update( ) 입니다. 게임은 이 객체들의 컬렉션을 관리합니다. 우리는 관리를 게임 월드에 넘기도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| class World
{
public:
World()
: numEntities_(0)
{}
void gameLoop();
private:
Entity* entities_[MAX_ENTITIES];
int numEntities_;
};
|
준비는 끝났습니다. 이제 아래처럼 gameLoop를 구현하면 각 객체를 순회하며 update( )를 실행하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| void World::gameLoop()
{
while (true)
{
// Handle user input...
// Update each entity.
for (int i = 0; i < numEntities_; i++)
{
entities_[i]->update();
}
// Physics and rendering...
}
}
|
이제는 객체를 각각 해골과, 조각상으로 구현해보도록 합시다.
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 Skeleton : public Entity
{
public:
Skeleton()
: patrollingLeft_(false)
{}
virtual void update()
{
if (patrollingLeft_)
{
setX(x() - 1);
if (x() == 0) patrollingLeft_ = false;
}
else
{
setX(x() + 1);
if (x() == 100) patrollingLeft_ = true;
}
}
private:
bool patrollingLeft_;
};
|
앞의 두번 째 예제와 거의 비슷합니다. (patrollingLeft를 멤버변수화 하여 update( ) 호출 후에도 값을 유지하도록 한 것만 빼면 말입니다.)
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
| class Statue : public Entity
{
public:
Statue(int delay)
: frames_(0),
delay_(delay)
{}
virtual void update()
{
if (++frames_ == delay_)
{
shootLightning();
// Reset the timer.
frames_ = 0;
}
}
private:
int frames_;
int delay_;
void shootLightning()
{
// Shoot the lightning...
}
};
|
석상도 마찬가지입니다. 프레임 카운터와, 발사 딜레이를 지역 변수로 따로 관리했었지만(leftStatueFrames, rightStatueFrames 와 80, 90으로 하드코딩) 인스턴스가 각자 관리하도록 만들었습니다. 이제 석상을 무제한으로 만들 수 있습니다. 객체 자신이 모든걸 들고 있기 때문에 게임 월드에 새로운 객체 추가는 더욱 쉬워졌습니다.
프레임 사이의 시간으로 업데이트 하기
지금까지는 이전 프레임과 현재 프레임 사이의 간격이 고정되어있다고 가정하고 설명했습니다. 하지만 프레임 간 시간을 고정하지 않고 때마다 시간을 받아와 적용하는 게임도 있습니다. 바로 ‘가변 시간 간격’을 쓰는 게임들입니다. 매 업데이트마다 프레임 간에 얼마나 시간이 지났는지를 인수로 받아와 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| void Skeleton::update(double elapsed)
{
if (patrollingLeft_)
{
x -= elapsed;
if (x <= 0)
{
patrollingLeft_ = false;
x = -x;
}
}
else
{
x += elapsed;
if (x >= 100)
{
patrollingLeft_ = true;
x = 100 - (x - 100);
}
}
}
|
이제 해골 병사는 1씩 이동했었지만, 가변 시간에 따라 이동하도록 되었습니다.
7 디자인 결정
잠자고 있는(휴면 상태의) 객체를 어떻게 관리할까요?
잠자고 있는(휴면 상태의) 객체를 어떻게 관리할까요?
어떤 이유에서건 일시적으로 아무것도 업데이트하지 않는 객체가 있을 수 있습니다. 화면 밖이거나, 아직 언락 상태여서 비활성화 되어있을 수도 있습니다. 이것을 매 프레임마다 업데이트하는 것은 CPU 사이클을 낭비하는 짓이죠.
하나의 대안은 ‘살아있는' 객체만 따로 모아두는 것입니다. 객체가 비활성화 되면 컬렉션에서 제거합니다. 다시 활성화 되면 뒤에 추가하면 되죠. 이 방법은 실제로 필요한 객체만 순회하도록 할 수 있습니다.
반응형