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

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

GameLoop

  • 게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링.

Motivation

  • 거의 모든 게임에서 사용

    • 어느 것도 서로 똑같지 않음.
    • 전형적인 게임패턴
  • 배치모드(batch mode) 프로그램이 대부분이였음

Interview with a CPU

  • 즉각적인 피드백을 위해 대화형(interactive) 프로그램이 만들어짐. (게임 또한 있었음.)
  • 프로그램은 입력을 대기, 반응
1
2
3
4
5
while (true)
{
  char* command = readCommand();
  handleCommand(command);
}

Event loops

  • 최신 GUI 애플리케이션 내부도 비슷함.
1
2
3
4
5
while (true)
{
  Event* event = waitForEvent();
  dispatchEvent(event);
}
  • 하지만 게임은 입력이벤트가 없어도, 돌아간다.

  • 게임에선 루프는 끊임없이 돌아간다.

1
2
3
4
5
6
while (true)
{
  processInput();
  update();
  render();
}
  • processInput(): 이전 호출 이후의 유저 입력 처리
  • update(): 게임 시뮬레이션을 한단계 시뮬 (Ai, 물리)
  • render(): 게임화면에 그림

A world out of time

  • 루프 한 바퀴 == 틱 or frame

  • 게임루프 측정 == 초당 프레임 수(fps)를 얻을 수 있다.
  • 루프가 빠르면 부드럽고 빠른 화면, 느리면 반대

  • 한 프레임에 얼마나 많은 작업을 하는가가 중요
  • 코드가 실행되는 플랫폼의 속도 또한 중요
    • 하드 웨어, os 등

Seconds per second

  • 어떤 하드웨어에서라도 일정한 속도로 실행될 수 있도록 하는 것이 중요 `

The Pattern

  • 게임 루프는 게임하는 내내 실행됨.
  • 유저 입력을 처리한 뒤 게임 상태를 업데이트하고 게임화면을 렌더링한다.
    • 시간 흐름에 따라 게임플레이 속도를 조절한다.

When to Use It

  • 게임에서는 안쓰는 경우는 거의 없음.

    루프 코드에 따라 ‘엔진’ 과 ‘라이브러리’를 나눈다.

라이브러리: 게임 메인 루프를 들고 있으면서, 라이브러리 함수를 호출

엔진: 스스로가 루프를 들고 있으면서 사용자 코드를 호출

Keep in Mind

  • 최적화가 중요.

You may need to coordinate with the platform’s event loop

  • 그래픽 UI와 이벤트 루프가 들어있는 OS나 플랫폼에서는 애플리케이션 루프가 두개 있는 셈.
    • 서로 잘맞아야함.
  • 제어권을 가져와, 루프 하나만 남겨놓을 수 있음.
    • 오래된 윈도우 API로 게임을 만든다면 main()에 게임루프를 두고, 루프안에서 PeekMessage()를 호출해 OS로부터 이벤트를 가져와 전달 가능.
    • GetMessage와는 달리 PeekMessage는 유저 입력이 올때까지 기다리지 않음.
  • 플랫폼에 따라 내부이벤트 무시하기 어려울 수 있음.
    • 웹브라우저 == 이벤트 루프가 브라우저 실행 모델 깊숙한 곳에서 모든 것을 좌우하므로, 이것을 게임루프로 삼아야함.
    • requestAnimationFrame()같은 걸 호출, 브라우저가 코드를 콜백으로 호출해주기를 대기.

Sample Code

  • 여러 게임 시스템을 진행.

Run, run as fast as you can

1
2
3
4
5
6
while (true)
{
  processInput();
  update();
  render();
}
  • 가장 간단한 이 방식은 게임 실행 속도 제어 불가능

고정 시간 간격: Take a little nap

  • 60FPS로 돌린다면, 한 프레임에 16ms

    • 그동안 게임 진행과 렌더링을 다 할 수 있다면, 프레임 레이트를 유지할 수 있다.
  • 아래처럼 프레임 사이 남은 시간 대기

  • 코드는 다음과 같음.
1
2
3
4
5
6
7
8
9
while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();

  sleep(start + MS_PER_FRAME - getCurrentTime());
}
  • sleep(): 게임이 너무 빨라지지 않게 대기

  • 너무 느려지는 것은 막지 못함.

가변 시간 간격: One small step, one giant step

  • 문제

    1. 업데이트할 때마다 정해진 만큼 게임 시간이 진행
    2. 업데이트하는 데에는 현실 세계의 시간이 어느 정도 걸림.
  • 2 > 1 이면 게임은 느려짐.
  • 한 번에 게임 시간을 16ms 이상 진행하면, 업데이트 횟수가 적어도 따라잡기 가능
    • 실제 시간이 얼마나 지났는지에 따라 시간 간격을 조절하면 된다.
    • 프레임이 오래 걸릴수록, 게임 간격을 길게
    • 필요에 따라 업데이트 단계를 조절할 수 있다.
  • 가변 시간 간격, 유동 시간 간격이라고 함.
1
2
3
4
5
6
7
8
9
10
double lastTime = getCurrentTime();
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - lastTime;
  processInput();
  update(elapsed);
  render();
  lastTime = current;
}
  • 매 프레임마다 실제 시간이 얼마나 지났는지를 elapsed에 저장.
    • 게임 상태를 업데이트할 때 elapsed를 같이 넘겨, 지난 시간만큼 게임월드 상태를 진행
  • 고정시간간격: 매 프레임마다 총알 속도에 맞춰 총알이 움직임
  • 가변시간간격: 속도와 지나간 시간을 곱해, 이동거리를 구함.
    • 시간 간격이 커지면 총알을 더 많이 움직임.

장점

  • 다양한 하드웨어에서 비슷한 속도로 게임이 돌아감.
  • 더 빠른 하드웨어인 경우 더 부드러운 게임플레이.

단점

  • 게임이 비결정적, 불안정
    • 비결정적: 버그 재현 힘들다.
  • 부동소수점=> 반올림 오차 가능성
    • 게임이 더 빨리 실행되면,오차가 더 크게 쌓임.(결국 PC에 따라 같은 총알의 위치가 달라짐.)
  • 실시간으로 실행하기 위해 게임 물리 엔진은 실제 물리 법칙의 근사치를 취함.
    • 근사치가 튐(blowing up)을 막기 위해, 감쇠(dumping)을 적용한다.
    • 감쇠: 시간 간격에 맞춰 세심하게 조정해야함.
    • 감쇠값이 바뀌다보면, 물리가 불안정해짐.

고정 시간 간격: Play catch up

  • 렌더링: 가변시간 간격에 영향을 받지않음
    • 때가 오면 렌더링할 뿐.
    • 모션블러 같은 경우에는 영향 받지만, 미세한 차이
  • 물리, AI등을 고정시간 + 렌더링 간격은 유연 => 프로세서 낭비 줄임

  • 루프 이후로 실제 시간이 얼마나 지났는지를 확인
    • 게임의 현재가 실제 시간의 현재를 따라잡을 때까지 고정 시간 간격만큼 게임 시간을 여러 번 시뮬레이션.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;

  processInput();

  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }

  render();
}
  • 프레임 시작시 실제 시간이 얼마나 지났는지를 lag변수에 저장.
    • 이 값은 실제 시간에 비해 게임 시간이 얼마나 뒤쳐졌는지를 의미.
    • 고정 시간 간격 방식으로 루프를 돌며서, 실제 시간을 따라잡을 때까지 게임을 업데이트.
    • 다 따라잡으면 렌더링하고 다시 루프를 실행.

  • 시간 간격(MS_PER_UPDATE)은 더 이상 시각적 프레임 레이트가 아님.

    • 얼마나 촘촘하게 업데이트할 지에 대한 값.
    • 짧을수록 실제 시간을 따라잡기 더 오래걸림.
    • 길수록 게임 플레이가 끊겨보임.
    • 60FPS보다 더 빠르게 돌아가도록, 시간 간격을 충분히 짧게 잡아 좋은 PC에서 더 나은 시뮬레이션을 보여주도록하는게 이상적
  • 시간 간격이 너무 짧으면 안됨.

    • 가장 느린 하드웨어에서도 update()를 실행하는 데 걸리는 시간보다는 시간 간격이 커야함.
    • 그렇지 않으면 게임 시간은 계속 뒤쳐짐.

      내부 업데이트에 보호장치: 최대 업데이트 횟수 설정

  • 렌더링을 뒤로 뺌 => CPU 시간에 여유가 생김.

    • 느린 PC: 화면 조금 끊김.
    • 안전한 고정 시간 간격 => 여러 hw에서 일정한 속도로 게임 시뮬

      유니티의 MonoBehaviour::FixedUpdate() 가 이런 방식

자투리 시간 문제: Stuck in the middle

  • 유저 입장에서는 두 업데이트 사이에 렌더링되는 경우가 종종 있음.

  • 업데이트: 정확, 고정간격 / 렌더링: 가능할 때마다
    • 문제점: 항상 업데이트 후에 렌더링 되는 게 아님.

  • 이렇기 때문에 움직임이 튀어보임.
    • 하지만, 렌더링할 때 업데이트 프레임이 시간적으로 얼마나 떨어져 있는지 lag값을 보고 알 수 있음.
      • 0 < lag < 업데이트 시간일 경우 업데이트 루프를 빠져나옴. (lag == 다음 프레임까지 남은 시간)
  • 렌더링 시간에 다음 값을 넘김.
1
render(lag / MS_PER_UPDATE);

lag을 MS_PER_UPDATE로 나누어 정규화한것. render는 0.0~1.0 사이의 값을 처리한다.

  • 렌더러는 게임 객체들과 각각의 현재 속도를 안다.
    • 이를 통해 보간(extrapolation)
    • 예상 위치를 렌더링
    • 보간하지 않는것 보다는 나음.

Design Decisions

  • 다루지 못한 내용: 화면 재생 빈도와의 동기화(refresh rate), 멀티스레딩, GPU 까지 고려할 경우 루프는 더 복잡해짐.

게임 루프를 직접 관리? 플랫폼이 관리?

  • 웹브라우저는 직접 만들 가능성이 거의 없음.
  • 기존 게임엔진 또한 엔진 루프를 그대로 사용할 가능성이 높음

플랫폼 이벤트 루프 사용

  • 간단: 루프를 작성하고 최적화 고민 x
  • 플랫폼에 잘맞음: 두 모델의 차이를 신경 쓰지 않아도됨.
  • 시간을 제어할 수 없음: 대부분 게임을 고려하지 않고 설계 => 느릴 가능성

게임 엔진 루프 사용

  • 코드 직접 작성 안함:
    • 게임루프 만들기 쉽지 않음. 사소한 버그와 약간의 최적화에도 큰 영향
    • 엔진루프에서 아쉬운게 있어도 건드릴 수 없음.

직접 만든 루프

  • 완전한 제어
  • 플랫폼과의 상호작용
    • OS나 프레임워크에 시간이 주어지도록, 가끔 제어권을 넘겨줘야함.

전력소모 문제

  • CPU를 가능한 적게 사용하도록 해야함.
    • 한 프레임 후에 쉬어주도록해줘야 하는 성능 상한

최대한 빨리 실행

  • 게임 루프에서 sleep호출 하지 않음
    • 시간이 남으면 FPS나 그래픽 품질 더 높임
  • 전력을 많이 사용

프레임 레이트 제한

  • 모바일게임 == 게임 플레이 품질에 더 집중
  • 프레임 레이트에 상한(30FPS, 60FPS)을 둠.
  • 게임 루프에서 프레임 시간 안에 할 일이 전부 끝나면, 나머지 시간동안 sleep 호출

게임플레이 속도 제어

  • 게임루프 = 비동기 유저 입력 + 시간 따라잡기
    • 입력은 쉬움, 시간은 다루기 어려움.
    • 다양한 플랫폼 지원이 핵심

동기화 없는 고정 시간 간격 방식

  • 그냥 게임 루프를 최대한 빠르게.
  • 간단.
  • 게임 속도 == 하드웨어와 게임 복잡도에 영향

동기화하는 고정 시간 간격 방식

  • 루프 마지막에 지연 or 동기화 지점 => 게임 시간 컨트롤

  • 간단.
  • 전력 효율 높음.
    • 매 틱마다 좀 더 작업하기 보다 쉬어줘야 전력 아낌.
  • 게임이 너무 빠르지 않음
  • 게임이 너무 느려질 수 있다.
    • 한 프레임에서 게임 업데이트 + 렌더링하는 게 오래걸리면 재생(playback)이 느려짐
    • 업데이트와 렌더링을 분리하지 않음
    • 시간을 따라잡기 위해 렌더링 프레임을 낮추지 않다 보니 게임 플레이가 느려짐

가변 시간 간격 방식

  • 권장되지 않음.
  • 너무 느리거나 너무 빠른 곳에서도 맞춰 플레이 가능.
  • 게임 플레이를 불안정하고 비결정적으로 만든다.

업데이트는 고정시간, 렌더링은 가변시간

  • 적응력이 가장 높다.
  • 실제 시간을 따라잡아야 한다면 렌더링 프레임을 낮춤.

  • 너무 느리거나 너무 빨라도 잘 적응
    • 실시간 업데이트 시, 뒤처질 일은 없음.
    • 최고사양 HW => 부드러운 화면
  • 훨씬 복잡
    • 구현 복잡
    • 업데이트 시간 간격을 정할 때 고사양유저를 위해 최대한 짧게
    • 하지만 저사양 유저가 너무 느려지지 않도록 주의

참고

출처

gameloop

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

[게임 프로그래밍 패턴] Squencing Patterns: Double Buffer

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