게임 루프 (Game Loop)

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

 

 

동기

게임 루프 패턴은 이름에서부터 알 수 있듯이 게임이 아닌 분야에서는 그다지 쓰이지 않는다. 사실상 게임을 위해 존재하는 패턴이다. 구현 내용이 다를 수 있지만 거의 모든 게임에서 사용한다.

 

while (true) {
    char* command = readCommand();
    handleCommand(command);
}

대화형 프로그램을 만들다 보면 자연스럽게 위와 같은 형태가 만들어지게 된다.

그런데 게임은 다른 프로그램과는 달리 유저의 입력이 없어도 계속 돌아간다. 루프에서 유저의 입력을 처리하지만 유저의 입력을 마냥 기다리고 있지 않다는 점에서 차이가 있다. 이것이 게임 루프의 첫 번째 핵심이다.

 

while (true) {
    processInput();
    update();
    render();
}

게임 루프의 기본적인 코드는 위와 크게 달라지지 않는다.

 

 

루프가 유저의 입력을 기다리지 않고 계속 돌아간다면 루프가 도는데 시간이 얼마나 걸리는지를 따져봐야 한다.

게임 입장에서는 루프를 도는것이 시간이 흐르는 것과 동일하다. 물론 그동안 현실세계의 시간도 흘러간다.

이를 측정한것이 초당 프레임 수(FPS)이다.

게임 루프가 빠르게 돌면 FPS가 높아져서 부드럽고 빠른 게임 화면을 볼 수 있지만 루프가 느리면 끊기는 화면을 보게된다.

 

별다른 제한을 두지 않으면 루프는 무조건 빠르게 돈다. 그런데 플랫폼마다 성능에 차이가 있기 때문에 루프의 실행 횟수가 일정하지 않은 문제가 발생한다. 플랫폼에 따라 게임의 경험이 달라지는 것이다.

 

어떤 하드웨어에서도 일정한 속도로 실행될 수 있도록 하는 것이 게임 루프의 두 번째 핵심이다.

 

 

패턴

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

 

 

언제 쓸 것인가?

게임 루프 패턴은 다른 패턴과 다르게 "언제 쓸 것인가" 를 고민할 필요 없이 "무조건" 사용된다.

설령 내가 작성하지 않더라도 어딘가에는 게임 루프가 반드시 존재한다.

 

 

주의사항

게임 루프는 전체 코드 중에서도 가장 핵심이다. 그래서 최적화를 매우 깐깐하게 해야한다.

또한 플랫폼에 따라 해당 플랫폼의 이벤트 루프를 게임 루프로 사용해야 하는 경우도 있다.

 

 

예제

1) 최대한 빨리 달리기

while (true) {
    processInput();
    update();
    render();
}

처음 봤던 코드이다. 이 코드는 실행 속도를 제어할 수 없기 때문에 실행 환경에 따라 실행 속도가 다르다.

 

2) 한숨 돌리기

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

한 프레임이 빨리 끝나도 sleep에 의해 일정 시간 대기하게 되므로 실행 속도에 상한선이 생기지만 그보다 느려지는것은 막지 못한다.

 

3) 한 번은 짧게, 한 번은 길게

게임 루프의 문제는 결국 두가지이다.

 

1. 업데이트할 때마다 정해진 만큼 게임 시간이 진행되고,

2. 업데이트하는 데에는 현실 세계의 시간이 어느 정도 걸린다.

 

여기서 2번이 1번보다 오래 걸리면 게임이 느려지게 된다.

게임의 시간을 16ms 진행시키는데 현실 시간의 16ms보다 오래 걸리면 따라갈수가 없다.

그러면 한 번에 게임 시간을 16ms 이상 진행시킨다면 적어도 업데이트 횟수는 따라잡을 수 있다.

 

한 번에 게임 시간을 더 많이 진행시키려면 프레임을 고정적으로 16ms씩 처리하는것이 아니라 프레임 이후 실제 경과시간에 따라 간격을 가변적으로 잡으면 된다.

 

double lastTime = getCurrentTime();
while (true) {
    double current = getCurrentTime();
    double elapsed = current - lastTime;
    
    processInput();
    update(elapsed);
    render();
    
    lastTime = current;
}

매 프레임마다 직전 루프 이후 실제 경과 시간을 계산하고 그 값을 update에 넘겨주면 해당 시간만큼 게임 월드의 상태를 진행시킨다.

 

문제는 가변 시간 간격에 따른 연산 오차이다.

가변 시간 간격을 사용하면 어떤 오브젝트가 이동할 때, 업데이트 횟수에 상관없이 실제 시간동안 같은 거리를 이동하게 된다.

그런데 게임에서는 보통 부동 소수점을 쓰기 때문에 반올림 오차가 발생하기가 매우 쉽고 업데이트 횟수가 많을수록 오차가 더 크게 누적된다.

 

실행 환경에 관계 없이 실행 속도를 동일하게 제어하려고 적용한 가변 시간 간격이 연산 오차로 인해 서로 다른 결과를 발생시킬 수 있는 새로운 문제에 봉착하게 된다.

물리 엔진은 보통 실제 물리 법칙의 근사치를 취하게 되는데, 근사치가 튀는걸 막기 위해 감쇠를 적용하게 된다. 이 감쇠는 시간의 간격에 맞춰서 세심하게 조정해야 하지만 가변 시간 간격을 적용한다면 감쇠 값이 계속 바뀌어서 물리가 불안정해지게 되는 결과를 낳는다.

간단히 말해서 불안정성이 생긴다는 것이다.

 

4) 따라잡기

가변 시간 간격에 영향을 받지 않는 부분 중 하나는 렌더링이다.

모션블러같은 경우를 제외하고는 렌더링은 가변 시간을 고려하지 않고 고려할 필요도 없다.

그래서 물리나 AI 등은 고정 시간 간격을 적용하고 렌더링 같은것은 가변 시간을 적용하여 조금 더 유연하게 만든다.

 

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

주의할 점은 고정 시간 간격을 너무 짧게 잡지 않는것이다. 적어도 update를 실행하는 데 걸리는 시간보다는 간격이 커야한다.

 

5) 중간에 끼는 경우

그런데 만약 렌더링이 두 업데이트 사이에 끼게 된다면 움직임이 튀어보일 수 있다.

다만 이제는 가변 시간 간격을 알기때문에 렌더링 함수의 인수로 (가변 시간 간격 / 업데이트 시간 간격) 을 넘겨주어 값을 보간하여 렌더링한다.

보간의 결과가 틀릴수도 있지만 별로 눈에 띄지도 않고 보간을 하지 않아서 움직임이 튀는 것보다 훨씬 낫다.

 

 

디자인 결정

대부분의 경우 게임 루프를 만들일은 거의 없다.

프레임워크부터 만들어 나간다면 모를까 웹 브라우저는 루프를 따로 만들지 못하도록 막혀있고 엔진을 사용한다면 엔진이 제공하는 게임 루프를 사용할 가능성이 매우 높다.

무엇을 사용하던간에 장단이 존재하지만 게임루프는 어쨌든 반드시 사용한다는 점이 핵심이다.

 

전력 소모에 관해서 잠시 얘기하자면 PC 게임은 성능도 중요하지만 품질 역시 중요하기 때문에 시간이 남으면 FPS나 그래픽 품질을 더 높이는데에 시간을 사용하는 경우가 많아서 전력은 언제나 최대치를 사용하게 된다.

반대로 모바일 게임은 품질보다 성능이 중요하기 때문에 FPS를 제한한다.

 

마지막으로 앞서 언급한 예제들을 정리한다.

 

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

루프를 제어하지 않고 최대한 빠르게 실행한다. 구현이 간단하지만 실행 환경마다 실행 속도가 다르다.

 

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

루프의 끝에 지연이나 동기화를 추가하여 너무 빠르게 실행되는 것을 막는다. 전력의 효율이 높지만 게임이 너무 느려질 수도 있다.

 

◾ 가변 시간 간격 방식

환경에 관계 없이 맞춰서 플레이가 가능하다. 하지만 오차로 인해 게임을 불안정하고 비결정적으로 만들어버린다.

 

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

대체로 가장 좋지만 구현이 복잡하다. 특히 업데이트 시간 간격을 잘 조정해야 한다.

+ Recent posts