[게임 프로그래밍 패턴] Chapter9 게임 루프

Jangmanbo·2023년 9월 27일
0
while(true)
{
    char* command = readCommand();
    handleCommand(command);
}

최신 GUI 어플리케이션들은 마우스나 키보드 입력 이벤트를 기다린다.

while(true)
{
    processInput();	// 이전 호출 이후 들어온 유저 입력 처리
    update();	// 시뮬레이션
    render();	// 렌더링
}

그러나 게임은 유저 입력이 없어도 계속 돌아간다.

위의 예시는 1. 한 프레임에 얼마나 많은 작업을 하는가 2. 코드가 실행되는 플랫폼의 속도에 따라서 프레임 레이트가 결정된다.

게임 루프의 핵심은 어떤 하드웨어에서라도 일정한 속도로 실행될 수 있도록 하는 것이다.
요즘 개발 중인 게임은 정확히 어느 하드웨어에서 실행될 지 알 수 없기 때문이다.


게임 루프 패턴

루프를 한 번 돌 때마다
1. 유저 입력 처리
2. 게임 상태 업데이트
3. 게임 화면 렌더링

※ 시간 흐름에 따라 게임 플레이 속도 조절

주의사항

그래픽 UI와 이벤트 루프가 있는 OS나 플랫폼에서 게임을 만들 경우, 루프가 두 개 있는 셈이므로 서로를 잘 맞춰야 한다.

  1. 제어건을 가져와 우리 루프만 남겨놓기
    • main()에 게임 로프를 두고 루프 안에서 PeekMessage()를 호출해 OS로부터 이벤트를 가져와 전달 (PeekMessage는 유저 입력이 올 때까지 기다리지 않는다)
  2. 플랫폼 이벤트 루프를 무시하기 어려울 경우
    • requestAnimationFrame()을 호출해서 브라우저가 게임 코드를 콜백으로 호출하기를 기다리기

예제

while(true)
{
    processInput();	// 이전 호출 이후 들어온 유저 입력 처리
    update();	// 시뮬레이션
    render();	// 렌더링
}

아까 보았던 이 예시 코드는 게임 실행 속도를 제어할 수 없다.

a. 한 숨 돌리기

while(true)
{
    double start = getCurrentTime();
    processInput();
    update();
    render();
    
    sleep(start + MS_PER_FRAME - getcurrentTime());
}

다음 프레임까지 남은 시간을 기다린다.

60FPS라면 한 프레임에 16ms다. 그동안 입력처리, 게인 진행, 렌더링을 다 할 수 있다면 프레임 레이트를 유지할 수 있다.
이 방법의 단점은 빨라지는 것은 막지만, 느려지는 것은 막지 못한다.

b. 한 번은 짧게, 한 번은 길게

  1. 업데이트할 때마다 정해진 만큼 게임 시간이 진행된다.
  2. 업데이트하는 데에는 현실 세계의 시간이 어느정도 걸린다.

2번이 1번보다 오래 걸리면 게임은 느려진다.

따라서 프레임 이후로 실제 시간이 얼마나 지났는지에 따라 시간 간격을 조절하면 된다.
프레임이 오래 걸린다면 게임 간격을 길게, 짧게 걸린다면 게임 간격을 짧게 잡는다.

이를 가변 시간 간격(유동 시간 간격)이라고 한다.

double lastTime = getCurrentTime();
while(true)
{
    double current = getCurrentTime();
    double elapsed = current - lastTime;
    processInput();
    update(elapsed);	// elapsed만큼 게임게임 진행
    render();
    lastTime = current;
}

예를 들어 총알은 속도와 지나간 시간(elapsed)을 곱해서 이동 거리를 구한다.

단점

  1. 2인용 네트워크 게임을 프레드는 50FPS, 조지는 5FPS로 플레이한다.
    따라서 프레드는 총알의 위치를 1초에 50번, 조지는 5번 업데이트하는데 부동소수점은 반올림 오차가 생기기 쉽다. 프레드PC는 조지PC보다 덧셈을 10배 더 많이 하기 때문에 오차가 더 크게 쌓여 두 PC에서의 총알 위치가 달라진다.

  2. 게임 물리 엔진은 실제 물리 법칙의 근차리를 취한다. 근사치가 튀지 않게 감쇠를 적용하는데, 감쇠 값이 바뀌다 보면 물리가 불안정해지는 문제가 있다.

c. 따라잡기

렌더링은 가변 신간 간격의 영향을 받지 않는다는 점을 활용해보자.
고정 시간 간격으로 업데이트하되, 렌더링 간격은 유연하게 하여 프로세서 낭비를 줄이는 것이다.

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();
}

이전 루프 이후 실제 시간이 얼마나 지났는지 확인한다.
게임의 현재가 실제 시간의 현재를 따라잡을 때까지 고정 시간 간격만큼 게임 시간을 여러 번 시뮬레이션한다.

MS_PER_UPDATE는 시각적 프레임 레이트가 아니다. 게임을 얼마나 촘촘하게 업데이트할지에 대한 값일 뿐이다.

중간에 끼는 경우

업데이트는 고정 시간 간격으로 진행되지만, 렌더링은 가능할 때마다 한다.
문제는 항상 업데이트 후에 렌더링 되는 건 아니라는 것이다. 따라서 움직임이 튀어보일 수 있다.

렌더링할 때 업데이트 프레임이 시간적으로 얼마나 떨어져 있는지 lag 값을 보고 알 수 있다.
lag가 0이 아니고 업데이트 시간 간격보다 적을 때는 업데이트 루프를 빠져나온다. (lag는 다음 프레임까지 남은 시간)

render(lag / MS_PER_UPDATE);

렌더링이 두 업데이트 중간에 있다면 render()는 0.5를 인수로 받아 두 프레임의 총알 위치의 사이에 그릴 것이다.

실제로는 장애물에 부딫지는 등 실제 위치와 다를 수 있다.
그러나 이러한 보간은 눈에 띄지 않아서 보간을 하지 않아 움직임이 튀는 것보다는 낫다.

0개의 댓글