Home [게임 프로그래밍 패턴] Squencing Patterns: Update Method
Post
Cancel

[게임 프로그래밍 패턴] Squencing Patterns: Update Method

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 클래스에서 이 패턴 사용

출처

Update Method

This post is licensed under CC BY 4.0 by the author.

[게임 프로그래밍 패턴] Squencing Patterns: Game Loop

[게임 프로그래밍 패턴] Behavioral Patterns: Bytecode