Update Method
- 컬렉션에 들어 있는 객체별로 한 프레임 단위의 작업을 진행하라고 알려줘서 전체를 시뮬레이션.
Motivation
스켈레톤 예제: 무작정 몬스터들을 구현하면 코드가 복잡해진다.
- 한 프레임에 한 걸음씩 스켈레톤이 왼쪽, 오른쪽 왔다갔다하는 코드를 간단하게 작성하면 다음과 같다.
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()를 호출
- 각 객체는 한 프레임만큼 동작을 진행.
- 모든 게임 객체가 동시에 동작(하는것 처럼 보이게)
- 게임 루프: 객체 관리 동적 컬렉션 소유
- 컬렉션에 객체를 추가, 삭제하기만 하면, 레벨에 객체를 쉽게 넣다 뺏다 가능.
- 레벨 디자이너가 원하는 대로 데이터 파일 이용해 레벨 찍어냄.
The Pattern
- 게임 월드는 객체 컬렉션을 관리한다.
- 각 객체는 한 프레임 단위의 동작을 시뮬레이션하기 위한
update()
메서드를 구현 - 매 프레임마다 게임은 컬렉션에 들어 있는 모든 객체를 업데이트한다.
- 각 객체는 한 프레임 단위의 동작을 시뮬레이션하기 위한
When to Use It
- 동시에 동작해야하는 객체나 시스템이 게임에 많을 때
- 각 객체의 동작이 다른 객체와 거의 독립적일 때
- 객체는 시간의 흐름에 따라 시뮬레이션되어야 함.
- 게임 루프 패턴 다음으로 중요한 패턴
- 다수의 객체 상호작용 => 업데이트 메서드 패턴 거의 필수
- 체스말 같은 객체들이 많을 경우, 이 패턴이 잘 안 맞을 수 있음.
- 체스말 == 모든 말을 동시에 시뮬 안해도 됨, 매 프레임마다 업데이트 안해도 됨.(애니메이션은 업데이트해야됨 => 이 패턴이 도움됨)
Keep in Mind
코드를 한 프레임 단위로 끊어서 실행하는 게 더 복잡하다
유저 입력, 렌더링 등을 게임 루프가 처리하게 하면,
거의
언제나 프레임마다 실행하게 해야함.- 이 방식은 동작 코드를 프레임마다 조금씩 실행되도록 쪼개어 넣으려면 코드가 복잡해져서 구현 비용이 더 든다.
저자는 도중에 반환하지 않는 직관적인 코드를 유지하면서, 동시에 게임 루프 안에서 여러 객체를 실행할 수 있는 일석이조의 방법이 있기 때문에 ‘거의’라는 표현을 씀
동시에 여러 개의 실행 ‘스레드’를 돌릴 수 있는 시스템이 필요. 객체용 코드가 리턴 대신 중간에 잠시 멈췄다 다시 실행할 수만 있다면 코드를 훨씬 직관적으로 만들 수 있다.
스레드는 이런 용도로 쓰기에는 무거움. 제너레이터(generator), 코루틴(coroutine), 파이버(fiber) 같은 경량 동시성 구조(lightweight concurrency constructs)를 지원하는 경우, 이를 사용할 수 있다.
바이트 코드 패턴 == 애플리케이션 수준에서 여러 실행 스레드를 만드는 방법 중 하나
다음 프레임에서 다시 시작할 수 있도록 현재 상태를 저장해야한다
- 예를들면, 위 코드에서 이동방향을 따로 저장해야함.
- 상태 패턴을 사용하여 이전에 중단한 곳으로 되돌아갈 수 있음.
모든 객체는 매 프레임마다 시뮬레이션되지만 진짜로 동시에 되는 건 아니다
- 업데이트 순서의 중요성
- 더블 버퍼 => 순서가 문제되지 않게
- 순차적 업데이트 == 게임 로직 작업하기 편함
- 객체를 병렬로 업데이트하다보면 꼬일 수 있음.
- 체스 말, 동시에 같은 위치
- 유효한 상태를 유지할 수 있음.
- 여러 이동을 직렬화 => 온라인 게임에서 유효
- 객체를 병렬로 업데이트하다보면 꼬일 수 있음.
업데이트 도중에 객체 목록을 바꾸는 건 조심해야한다
- 업데이트 가능한 객체를 게임에서 추가, 삭제하는 코드가 있을 경우.
객체가 새로 생기는 경우
- 별 문제 없이 리스트 뒤에 추가.
하지만, 새로 생성된 객체가 렌더링되지않고(플레이어가 보지못한 상태)
해당 프레임에 작동
해결: 객체 개수 미리 저장, 그만큼만.
1
2
3
4
5
int numObjectsThisTurn = numObjects_;
for (int i = 0; i < numObjectsThisTurn; i++)
{
objects_[i]->update();
}
객체가 삭제되는 경우
- 몬스터가 죽었으면, 그 몬스터를 목록에서 빼야함.
- 삭제할 경우, 의도치않게 객체 하나를 건너뛸 수 있음.
1
2
3
4
for (int i = 0; i < numObjects_; i++)
{
objects_[i]->update();
}
- 이렇기 때문에, 객체를 삭제할 때 i를 업데이트하는 방법도 있음.
- 다 순회한 뒤, 삭제할 수 도 있음.
- 제거할 객체인지 저장해서, 뛰어넘기
멀티스레드가 돌고 있으면, 업데이트 도중 비싼 스레드 동기화를 피하기 위해, 리스트 변경을 지연해야함.
- 제거할 객체인지 저장해서, 뛰어넘기
Sample Code
- 업데이트 메서드는 단순하다.
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
가 가장 중요. - 게임 world == 개체 컬렉션을 관리
- 개체 컬렉션 관리
- 보통 컬렉션 클래스 사용.(코드 단순화를 위해 일반 배열 사용)
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_;
};
- 매 프레임마다 개체들을 덥데이트 => 구현 끝.
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...
}
}
Subclassing entities?!
- Entitiy 클래스를 상속
- 거대한 상속 == 유지보수 불가능(쪼개야함.)
- 클래스 상속보다 ‘객체 조합’이 낫다.
- 해결책: 컴포넌트 패턴
- 컴포넌트 패턴 사용 => update 함수는 entity 클래스가 아닌 entity 객체의
컴포넌트
에 있게된다. - 상속구조를 복잡하게 만들지 않아도 된다.
- 필요한 컴포넌트만 넣으면됨.
- 여기서는 업데이트 메서드의 예를 보여주기위해 상속을 이용한것.(실제로는 컴포넌트를 사용하는것이 좋다.)
Defining entities
- 순찰을 도는 해골 경비병, 번개를 쏘는 마법 석상 정의 예시
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_;
};
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...
}
};
- 석상 코드는 더 단순해짐.
- 변수를 클래스로 옮겨서, 석상 인스턴스가 타이머를 각자 관리할 수 있어 원하는 만큼 많이 만들 수 있다.
- 게임월드에 새로운 개체를 추가하기 쉬워짐.
- 데이터 파일이나 레벨 에디터 같은 걸로 월드에 개체를 유연하게 추가 가능.
가변 시간 간격: Passing time
update()를 부를 때마다 게임 월드 상태가 동일한 고정 단위 시간만큼 진행된다고 가정하고 있었음.
가변 시간 간격의 경우, 시간 간격을 크게 혹은 짧게 시뮬레이션해야한다.
- update()는 시간이 얼마나 지났는지 알아야함.
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);
}
}
}
- 해골 병사의 이동 거리는 지난 시간에 따라 늘어남.
주의:
시간 간격이 크면, 순찰 범위를 벗어날 수 있음.
Design Decisions
What class does the update method live on?
- 가장 중요한 결정은 update()를 어느 클래스에 두느냐이다.
the entity class
- 이미 entity 클래스가 있다면, 다른 클래스를 추가하지 않아도 된다는 점에서 가장 간단한 방법이다.
- 이 방법은 요즘 업계(2016년쯤)에는 이 방식을 멀리하고 있음
- entity 종류가 많은데 새로운 동작을 만들 때 마다 entity 클래스를 상속받아야 한다면, 코드가 망가지기 쉽고 작업하기 어렵다.
- 단일 상속 구조로 코드를 매끄럽게 재사용할 수 없는 순간이 옴.
The component class
- 컴포넌트는 알아서 자기 자신을 업데이트함.
- 업데이트 메서드 패턴이 게임 자체를 게임 월드에 있는 다른 개체와 디커플링하는 것처럼
- 컴포넌트 패턴은 한 개체의 일부를
개체의 다른 부분들
과 디커플링한다. - 렌더링, 물리, AI는 스스로 알아서 돌아간다.
A delegate class
- 클래스의 동작 일부를 다른 객체에 위임하는 패턴들
- 상태 패턴: 상태가 위임하는 객체를 바꿈, 객체 동작 변경 가능하게
- 타입 객체 패턴: 같은 종류의 여러 개체가 동작을 공유
- 위임 클래스에 update()
- update() 메서드는 개체 클래스에 있지만, 가상 함수가 아니며, 객체에 포워딩(forward)만 한다.
1
2
3
4
5
void Entity::update()
{
// Forward to state object.
state_->update();
}
- 새로운 동작 정의: 위임 객체 변경.
- 완전히 새로운 상속 클래스를 정의하지 않아도 동작을 바꿀 수 있는 유연성을 얻을 수있음.(컴포넌트와 마찬가지로)
How are dormant objects handled?
- 휴먼 객체 처리
- 일시적으로 업데이트가 필요 없는 객체 생김.
- 사용 불능 상태
- 화면 밖인 상태
- 잠금 상태
휴면상태 다수 => 매 프레임마다 CPU 클럭 낭비
- 대안: ‘살아 있는’ 객체만 따로 컬렉션에 모아두는 것
- 객체가 비활성화 => 컬렉션에서 제거
- 객체가 활성화 => 컬렉션에서 추가
비활성 객체가 포함된 컬렉션 하나만 사용할 경우
시간을 낭비
- 비활성 객체일 경우 활성상태인지를 나타내는 플래그만 검사 or 아무것도 안함
CPU 클럭 낭비 + 데이터 캐시 날림(의미 없는 객체 순회)
CPU는 읽기를 최적화하기 위해 데이터를 RAM에서 속도가 훨씬 빠른 on-chip cache로 가져옴. (지금 읽는 데이터 옆에 있는 데이터도 같이)
객체를 건너뛰면, 캐시에 데이터가 없어서 다시 느린 메모리에 접근해 데이터 가져워야함=> 데이터 지역성 패턴
활성 객체만 모여 있는 컬렉션 추가
- 두 개의 컬렉션 => 메모리 추가 사용
- 전체 개체를 모아놓은 마스터 컬렉션도 있음.
- 이때 활성 객체 컬렉션은 중복 데이터.
- 메모리보다 속가 중요하다면 허용가능.
- 절충=> 비활성 객체만 모아놓기
컬렉션 두 개의 동기화를 유지
- 생성, 소멸 시 두 컬렉션 모두 변경
- 비활성 객체가 많으면, 컬렉션을 따로.
See Also
- 업데이트 메서드 패턴 == 게임루프, 컴포넌트와 함께 게임코드의 핵심
- 캐시 성능 == 데이터 지역성 패턴
- 유니티 프레임워크 == MonoBehaviour를 포함한 여러 클래스에서 이 패턴 사용
- 마이크로소프트 XNA 플랫폼은 Game, GameComponent 클래스에서 이 패턴 사용
- Quintus라는 게임엔진은 주요 Sprite 클래스에서 이 패턴 사용