순서패턴 - 업데이트 메서드 (update method)
업데이트 메서드 - 컬렉션에 들어 있는 객체별로 한 프레임 단위의 작업을 진행하라고 알려줘서 전체를 시뮬레이션 한다.
1
2
3
4
5
6
7
8
|
while(true) {
// 오른쪽으로 간다.
for (double x = 0; x < 100; ++x) { skeleton.setX(x); }
// 왼쪽으로 간다.
for (double x = 100; x > 0; --x) { skeleton.setX(x); }
}
|
cs |
다음 코드는 스켈레톤이 좌우로 왔다갔다 하는 코드이다.
하지만 이러한 코드는 실제 게임에 적용 할 순 없다.
수많은 객체가 있고 이들을 객체 내부에서 반복하도록 한다면 많은 문제가 생길것이다.
그래서 해결책 중 하나는 게임 메인 루프 즉 외부 루프를 통해서 객체를 업데이트 하는 방식을 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
Entity skeleton;
bool patrollingLeft = false;
double x = 0;
// 메인 게임 루프
while(true) {
if(patrollingLeft) {
x--;
if(x == 0) patrollingLeft = false;
}
else {
x++;
if(x == 100) patrollingLeft = true;
}
skeleton.setX(x);
// 유저 입력을 처리하고 게임을 렌더링한다.
}
|
cs |
게임 메인 루프를 통해 업데이트 하는 방식이다 이전 루프의 경우는 루프 두 개만 사용하여
간단하게 구현 되어 코드만 보고도 암시적으로 어느 루프가 진행중인지 알 수 있었다.
하지만 이 코드는 매 프레임마다 외부 게임 루프로 나갔다가 직전에서 다시 시작해야 하기 때문에
patrollingleft 변수를 써서 방향을 명시적으로 기록했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
Entity leftStatue;
Entity rightStatue;
int leftStatueFrames = 0;
int rightStatueFrames = 0;
// 메인 게임 루프
while(true) {
// 해골 병사용 코드...
if(++leftStatueFrames == 90) {
leftStatueFrames = 0;
leftStatue.shootLightning();
}
if(++rightStatueFrames == 80) {
rightStatueFrames = 0;
rightStatue.shootLightning();
}
// 유저 입력을 처리하고 게임을 렌더링한다.
}
|
cs |
이러한 방식은 객체가 점점 늘어나게 되면 코드를 유지보수하기 힘들어 진다.
코드를 한데 뭉쳐지게 되어 사용 할 수 없는 방식이다.
이런 경우에 모든 개체가 자신의 동작을 캡슐화하는 방법이 있다.
이를 위해 추상메서드 update() 를 정의하고 추상계층을 더하는 방식이 있다.
게임 루프는 이제 매 프레임마다 객체 컬렉션을 쭉 돌면서 update() 를 실행하게 된다.
월드는 객체 컬렉션을 관리한다. 각 객체는 한 프레임 단위의 동작을 시뮬레이션 하기 위해 업데이트
메서드를 구현한다.
- 업데이트 메서드 패턴은 이럴 때 쓸 수 있다.
- 동시에 동작해야 하는 객체나 시스템이 게임에 많다.
- 각 객체의 동작은 다른 객체와 거의 독립적이다.
- 객체는 시간의 흐름에 따라 시뮬레이션되어야 한다.
주의사항
모든 객체는 매 프레임마다 시뮬레이션 되지만 진짜로 동시에 되는 건 아니다.
게임 루프는 컬렉션을 돌면서 모든 객체를 업데이트한다. update 함수에서는 다른 게임 월드 상태에 접근할 수 있는데, 특히 업데이트 중인 다른 객체에도 접근할 수 있다. 이러다 보니 객체 업데이트 순서가 중요하다.
객체 목록에서 A가 B보다 앞에 있다면, A는 B의 이전 프레임 상태를 본다. B 차례가 왔을 때 A는 이미 업데이트 했기 때문에 A의 현재 프레임 상태를 보게된다. 플레이어게는 모두가 동시에 움직이는 것처럼 보일지 몰라도 내부에서는 순서대로 업데이트 된다.
순차적으로 업데이트하면 게임 로직을 작업하기가 편하다. 객체를 병렬로 업데이트하다 보면 꼬일 수 있다. 체스에서 흑과 백이 동시에 이동할 수 있다고 해보자. 둘 다 동시에 같은 위치로 말을 이동하려 든다면 어떻게 할까? 순차 업데이트에서는 이런 문제를 피할 수 있다.
업데이트 도중에 객체 목록을 바꾸는 건 조심해야 한다.
업데이트 메서드 패턴에서는 많은 게임 동작이 업데이트 메서드 안에 들어가게 된다. 그중에는 업데이트 가능한 객체를 게임에서 추가, 삭제하는 코드도 포함된다.
해골 경비병을 죽이면 아이템이 떨어진다고 해보자. 객체가 서로 생기면 보통은 별 문제없이 객체 목록 뒤에 추가하면 된다. 계속 객체 목록을 순회하다 보면 결국에는 새로 만든 객체까지 도달해 그것까지 업데이트하게 될 것이다.
하지만 이렇게 하면 새로 생성된 객체가 스폰된 걸 프레이어가 볼 틈도 없이 해당 프레임에서 작동하게 된다. 이게 싫다면 업데이트 루프를 시작하기 전에 목록에 있는 객체 개수를 미리 저장해놓고 그 만큼만 업데이트하면 된다.
1
2
3
4
5
|
int numObjectsThisTurn = numObjects_;
for(int i = 0; i < numObjectsThisTurn; ++i) {
objects_[i]->update();
}
|
cs |
여기서 object_는 게임에서 업데이트 가능한 객체 배열이다
numObjects_ 는 객체의 개수이다.
하지만 이러한 코드는 순회 도중에 객체를 삭제하는것은 의도치않게 객체 하나를 건너 뛸 수도 있다.

object_ 배열이 1번을 가리키고 있었고 0번이 삭제되면서 배열이 당겨질 수 있다.
하지만 다음 루프에서 배열은 2번을 가리키기 때문에 널포인터 참조가 발생 할 수 있다는 단점이 있다.
방법 중 한 가지는 객체 삭제를 뒤로 미루는것이다.
객체의 삭제 여부를 알 수 있는 변수를 하나 두고 업데이트 도중에 삭제 예정인 객체는 건너 뛴다.
전체 목록을 다 돌고 나서 삭제하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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_;
};
|
cs |
개체 클래스를 구현하였다.
개체의 현재 위치정보를 담고있는 double형 배열 2가지이다.
1
2
3
4
5
6
7
8
9
10
|
class World {
public:
World() : numEntities_(0) {}
void gameLoop()l
private:
Entity* entities_[MAX_ENTITIES];
int numEntities_;
};
|
cs |
월드는 개체 컬랙션을 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void World:gameLoop() {
while(true) {
// 유저 입력 처리...
// 각 개체를 업데이트한다.
for(int i = 0; i < numEntities_; ++i) {
entities_[i]->update();
}
// 물리, 렌더링...
}
}
|
cs |
이제 매 프레임마다 개체들을 업데이트 하면 업데이트 메서드 구현이 끝난다.
이러한 개체를 상속받는 방식은 별로 좋지는 않다.
이러한 방법을 해결하기위해 컴포넌트 패턴을 사용하는 방법이 있다.
update함수가 개체 클래스가 아닌 객체의 컴포넌트에 있게 되고 이러면 작동을 정의하고
재사용 하기 위해 개체 클래스 상속 구조를 복잡하게 만들지 않아도 된다.
skeleton 클래스와 statue 클래스를 정의한다면 다음과 같은 상속형태를 가질 수 있고
entity 개체는 world에서 관리하게 된다.
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);
}
}
}
|
cs |
일정 시간 간격마다 update 를 해야 하는 개체가 있다고 할 때
가변 시간 간격을 이용하는 방법도 있다.
즉 매번 update() 는 얼마나 많은 시간이 지났는지 알아야 하기 때문에 지난 시간을 인수로 받는다.
그 이후 가변 시간 간격을 다음과 같이 처리한다.이 코드는 시간 간격이 커지면 순찰 범위가 커질 수 있다는 단점이 있다.
디자인 결정
업데이트 메서드를 어느 클래스에 둘 것인가?
Entity 클래스
이미 Entity 클래스가 있다면 다른 클래스를 추가하지 않아도 된다는 점에서 가장 간단하다. 하지만 Entity 종류가 많다면 새로운 동작을 만들때마다 클래스를 상속받아야 하기 때문에 코드가 망가지기 쉽고 작업하기 어렵다. 언젠가 단일 상속 구조로 코드를 매끄럽게 재사용할 수 없는 순간이 올 텐데, 이러면 방법이 없다.
컴포넌트 클래스
이미 컴포넌트 패턴을 쓰고 있다면 더 생각할 것이 없다. 업데이트 메서드 패턴이 게임 객체를 게임 월드에 있는 다른 객체와 디커플링하는 것과 마찬가지로, 컴포넌트 패턴은 한 객체의 일부를 객체의 다른 부분들과 디커플링한다. 컴포넌트는 알아서 자기 자신을 업데이트 할 것이고, 렌더링, 물리, AI는 스스로 알아서 돌아갈 것이다.
위임 클래스
클래스의 동작 일부를 다른 객체에 위임하는 것과 관련된 패턴이 더 있다. 상태 패턴은 상태가 위임하는 객체를 바꿈으로써 객체의 동작을 변경할 수 있게 해준다. 타입 객체 패턴은 같은 종류의 여러 객체가 동작을 공유할 수 있게 해준다. 이들 패턴 중 하나를 쓰고 있다면 위임 클래스에 두는 것이 자연스럽다. 여전히 update()는 Entity 클래스에 있지만 가상함수가 아니며 다음과 같이 위임 객체에 포워딩만 한다.
1
2
3
4
5
|
void Entity::update()
{
// 상태 객체에 포워딩한다.
state_->update();
}
|
cs |
새로운 동작을 정의하고 싶다면 위임 객체를 바꾸면 된다. 컴포넌트와 마찬가지로, 완전히 새로운 상속 클래스를 정의하지 않아도 동작을 바꿀 수 있는 유연성을 얻을 수 있다.
휴면 객체 처리
여러 이유로 일시적으로 업데이트가 필요 없는 객체가 생길 수 있다. 사용 불능 상태이거나 화면 밖에 있거나 아직 잠금 상태일 수도 있다. 이런 객체가 많으면 매 프레임마다 쓸데없이 객체를 더 순회하면서 CPU 클럭을 낭비하게 된다.
비활성 객체가 포함된 컬렉션 하나만 사용할 경우
- 시간을 낭비한다. 비활성 객체의 경우 활성 상태인지 나타내는 플래그만 검사하거나 아무것도 하지 않는 메서드를 호출하기 때문이다.
활성 객체만 모여 있는 컬렉션을 하나 더 둘 경우
- 두 번째 컬렉션을 위해 메모리를 추가로 사용해야 한다. 전체 객체를 대상으로 작업해야 할 수도 있기 때문에 모든 객체를 모아 놓은 마스터 컬렉션도 있기 마련이다. 이때 활성 객체 컬렉션은 엄밀히 말하자면 중복 데이터이다. 메모리보다 속도가 중요하다면 그 정도는 받아들일 만할 수도 있다. 절충하자면 컬렉션을 두 개 두되, 나머지 하나에는 전체 객체가 아닌 비활성 객체만 모아놓는 방법도 있다.
- 컬렉션 두 개의 동기화를 유지해야 한다. 객체가 생성되거나 완전히 소멸되면 마스터 컬렉션과 활성 객체 컬렉션 둘다 변경해야 한다.
보통 얼마나 많은 객체가 비활성 상태로 남아 있는가에 따라 방법을 결정하면 된다. 비활성 객체가 많을수록 컬렉션을 따로 두는게 좋다.