티스토리 뷰

dev

Game Loop

maxidea1024 2019. 8. 8. 11:06

게임 루프(Game Loop)에 대한 이해

 

Tistory markdown 지원이 매끄럽지 못한듯 싶습니다. github에도 올려두었으니 참고하세요.

 

https://github.com/maxidea1024/public-articles/blob/master/tick.md?fbclid=IwAR3AfnI38qB57vOVd-bN5mVbJB6k1JeOglwZpGtNp_MPfejuZXs2G2hteBQ

 

maxidea1024/public-articles

Public Articles. Contribute to maxidea1024/public-articles development by creating an account on GitHub.

github.com

 

게임 루프는 모든 게임에서 핵심이며, 게임루프가 없는 상태로는 실행되지 않습니다. 이렇게 중요한 부분임에도 게임 개발에 입문하는 대부분의 게임 프로그래머에게는 이 주제에 대한 적절한 정보를 제공하는 문서는 찾아보기 힘든것 같습니다. 워낙 뻔한 내용이고 근간이 되는 부분이라 되려 지나쳐 버리는건 아닌가 싶습니다.

이 게임 루프에 대한 이해가 필요한 여러가지 이유가 있겠지만, 시스템을 좀더 효율적으로 구성하고 컨텐츠의 목표 퀄리티를 어느선까지 설정해야하는지 그리고 어느정도의 내구성을 가지는지에 대한 고민을 해볼 수 있는 기회가 되었으면 좋겠습니다.

최소한 다음과 같은 일들은 미리 방지하길 바라며 이글을 작성하였습니다.

  • 개발할때는 안그랬는데, 출시후에 게임이 너무 들쑥날쑥해서 플레이를 할수 없다고 하네요.
  • 애니메이션이 자주 생략되고 끊김이 너무 자주 보여요.
  • 개발자 컴퓨터에서 재현이 안되는 버그가 있어요.
  • 총을 쐈는데 적이 잘 맞지 않아요.
  • 모바일에서 배터리 소모가 너무 많거나, 발열이 심해요.

위의 내용을 살펴보면, 개발과정에서 발생하는 문제라기 보다는 출시 이후에 발생하는 문제임을 알수 있을것입니다. 출시 이후에 근본적인 부분을 검증하고 수선하는건 굉장히 고통스럽고 수고스러운 일입니다.

물론, 아래의 내용들을 이해하고 완벽하게 적용한다고 해서 위의 문제들이 아예 발생하지 않는것은 아닐 것입니다. 다만, 대응을 할때 어떤 전략과 이해를 가지고 대하느냐에 따라서 해결 시간이나 결과가 다를것이라고 생각됩니다.

그리고 아래의 내용들과는 별개로 한프레임당 처리 소요시간짧게 가져가야합니다. 개발자는 언제나 최소 자원으로 최대 효과를 내야하는 어쩌면 말도 안되는 상황을 이겨내야합니다. 쉽지 않지만 말입니다.

게임루프(Game Loop)

게임 엔진의 기본흐름

engineflow

모든 게임은 사용자의 입력, 게임 상태 업데이트, AI 처리, 음악 및 음향 효과 재생 및 게임 표시등의 순서로 구성됩니다. 이 시퀀스는 게임 루프를 통해 처리됩니다.
소개에서 말했듯이 이 게임 루프모든 게임의 핵심입니다.

Update만 따로 떼어내서 살펴보면 아래 그림과 비슷할것입니다.

render_subsystems

실제 Update는 위의 그림 보다 더 복잡할 수 있습니다.

이 글에서는 개별적인 기능들에 대해서 상세히 다루지 않을 것입니다. 이 글의 주제인 게임 루프에만 집중할 것이며, 이를 위해서 최대한 단순하게 게임을 업데이트(Update)하고 표시(Render)하는 두가지 항목으로 단순화해서 설명하도록 하겠습니다.

가장 간단한 형태의 게임 루프 예제 코드는 다음과 같습니다.

bool running = true;

while (running) {
    Update();
    Render();
}

이 보다 더 간단한 루프의 형태는 없을겁니다. 하지만, 이 루프는 시간을 전혀 고려하지 않고 게임이 실행된다는데 문제가 있습니다. 하드웨어 속도가 느릴수록 게임 속도가 느려지는 문제가 있습니다. 시간을 고려해서 이러한 부분을 해결해야합니다. 게임처럼 끊임 없이 갱신되어야 하는 경우 타이밍은 타협이 안되는 굉장히 중요한 부분입니다. 느린 하드웨어에서 타이밍을 맞추려면 차라리 프레임을 건너뛰어야합니다.

FPS

FPS 즉, Frames Per Second의 약자입니다. 말뜻 그대로 1초에 몇프레임 처리할 수 있는지를 나타내는 수치입니다. 위에서 예시된 코드에서 보자면 Update()Render()1초몇회 호출 되었는지를 나타내는 수치입니다.

게임 속도(Game Speed)

게임 속도는 게임 상태가 초당 업데이트되는 횟수 즉, Update() 함수가 1초에 몇 회나 호출되는지를 나타내는 수치입니다. 이 정의에 대해서 꼭 기억하시기 바랍니다. 그렇지 않으면, 아래에서 설명하는 내용들 중 일부분에서 혼선이 올수 있습니다.

주의

FPS != 게임속도 이 부분을 꼭 기억하셔야 합니다. FPS게임속도를 동일시 할수도 있겠지만, 구현 방법에 따라서는 의미가 구분지어져야 설명이 되는 부분들이 있으므로 주의가 필요합니다.

일정한(Uniform) 게임 속도에 따른 FPS

타이밍 문제에 대한 쉬운 해결책은 초당 25프레임(한 프레임당 40ms)으로 게임을 실행하는 것입니다.

uniform_tick_timeline

const uint32 FRAMES_PER_SECOND = 25; // 25fps
const uint32 MSECS_PER_FRAME = 1000 / FRAMES_PER_SECOND; // 40ms

uint32 nextFrameMs = GetMilliseconds();
uint32 sleepMs = 0;
bool running = true;

while (running) {
    Update();
    Render();

    nextFrameMs += MSECS_PER_FRAME;

    sleepMs = nextFrameMs - GetMilliseconds();
    if (sleepMs > 0) {
        // 의도한 프레임당 시간내에 처리 완료 (빠른 하드웨어 혹은 일시적 Idle)
        Sleep(sleepMs);
    } else {
        // 의도한 프레임당 시간내에 처리 못함 (느린 하드웨어 혹은 일시적 Load)
    }
}

위와 같은 방식으로 처리하면 다음과 같은 잇점이 있을 수 있습니다. Update()초당 25번 호출된다는 것을 알고 있기 때문에 코드를 작성하는 것이 아주 간단해집니다. 초당 25번 호출상수로 정해버리면 코딩하는게 굉장히 단순해집니다. 예를들어, 이런 종류의 게임 루프에서 레코딩(데모 플레이)을 구현하는 것은 쉽습니다. 게임에서 임의의 값(Random)을 사용하지 않으면, 사용자의 입력 변경 사항을 기록하고 나중에 다시 플레이할 수 있습니다. 게임에서 데모플레이 구현시 사용되는 방식입니다. 테스트 하드웨어에서 FRAMES_PER_SECOND를 적절한 값으로 설정할 수 있지만, 더 빠르거나 느린 하드웨어에서는 어떤 현상이 발생하는지 알아보도록 하겠습니다.

느린 하드웨어

nonuniform-tick-timeline

하드웨어가 정의된 FPS를 처리할 수 있다면 아무런 문제가 없습니다. 그러나 하드웨어가 이를 처리할 수 없을때 문제가 시작될것입니다. 게임이 점점 더 느려질것입니다. 일시적으로 느려졌다가 쾌적한 상황으로 돌아올수도 있습니다만, 최악의 경우 게임이 실제로 느리게 진행되는 매우 무거운(느린) 프레임들과 정상적으로 실행되는 프레임들이 있습니다. 타이밍이 가변적이어서 게임을 플레이할 수 없게 만들 수 있습니다. 아예 느린 경우도 있겠지만 빠른 프레임과 느린 프레임사이에 심한 절뚝임(jerky) 현상을 유발하여 플레이어로 하여금 매우 나쁜 플레이 경험을 안겨주게 됩니다. "눈이 너무 아파요.", "피격이 잘되다 안되다 그래요." 등의 반응이 있을 수 있습니다.

빠른 하드웨어

빠른 하드웨어에서는 게임을 실행하는데 전혀 지장이 없지만, CPU / GPU 클럭을 낭비하게 됩니다. 1000FPS를 쉽게 수행할 수 있을때 25FPS 또는 30FPS에서 게임을 실행하면 하드웨어 자원을 제대로 활용하지 못하는것일 수 있습니다. 500만원짜리 PC로 "지뢰찾기" 게임만 한다면, 비싼 PC를 구입한 보람이 없을것입니다. 하지만, 빠르게 움직이는 물체의 경우 시각적으로 많은 호소력을 잃게 됩니다. 비유를 하자면, 400키로로 달릴수 있는 하이퍼카에 속도 제한 장치를 단것과 같은 이치입니다. 반면에 이점은 모바일에서 강점으로 볼 수 있습니다. 게임을 끊임없이 실행하지 않으면 배터리 시간을 절약할 수 있습니다. 예를들어, 로비에서는 20FPS로 실행하고, 인게임에서는 60FPS로 실행하게 한다던지의 전략을 구사할 수 있을것입니다. 게임 기획단계에서 목표 FPS를 정하는것이 좋습니다. 충분히 미세한 간격의 프레임을 표현할 필요도 없는 게임에서 과도한 FPS로 제한없이 사용하는것은 클럭낭비일 수 있습니다. 특히나 모바일에서는 배터리가 중요한 요소이므로 이부분을 고려한다면 배터리 타임을 길게 가져갈 수 있을것입니다.

결론

FPS를 일정한 게임 속도에 의존하게 만드는 것은 빠르게 구현되고 게임 코드를 단순하게 유지하는 솔루션입니다. 하지만, 몇가지 문제가 있습니다. 높은 FPS를 정의하면 느린 하드웨에서 문제가 발생하고, 낮은 FPS를 정의하면 빠른 하드웨어에서 시각적인 매력을 잃게 됩니다. 결론적으로 그리 좋은 방법은 아니라고 생각됩니다.

가변 FPS에 따른 게임속도

구현

게임 루프의 또 다른 구현은 가능한 빨리 실행하고 FPS가 곧 게임 속도가 되도록 하는 것입니다. 게임은 이전 프레임의 시간차이(delta time)로 업데이트됩니다.

uint32 prevFrameMs;
uint32 currFrameMs = GetMilliseconds();

bool running = true;
while (running) {
    prevFrameMs = currFrameMs;
    currFrameMs = GetMilliseconds();

    const uint32 deltaMs = currFrameMs - prevFrameMs;
    Update(deltaMs);
    Render();
}

Update() 함수는 deltaMs(틱간 시간차)를 고려해야하기 때문에 게임코드가 조금 더 복잡해집니다. 처음에는 이것이 모든 경우에 대한 이상적인 해결책으로 보입니다. 기존의 저를 포함한 대부분 프로그래머들이 이런 종류의 게임 루프를 구현하는 것을 보았습니다. 이글을 쓰는 현 시점에서 생각해보건데 그들이 이 글을 읽고 조금더 문제에 대해서 깊히 생각할 수 있었으면 어땠을까 생각해봅니다.(개인적인 생각입니다.)
이 루프가 느린 / 빠른 하드웨어 모두에 문제가 있을 수 있음을 아래에서 살펴보도록 하겠습니다.

느린 하드웨어

느린 하드웨어는 때때로 게임이 "무겁게"(심하게 느려지는)되는 일부 지점에서 특정 지연을 유발할 수 있습니다. 이는 특정 시간에 너무 많은 다각형(Polygon) 표시되는 3D 게임에서 발생할 수 있습니다. 카메라가 비추는 곳에 많은 오브젝트가 있을 경우등을 생각해 볼 수 있습니다. 이 프레임 속도의 저하는 입력응답 시간 및 플레이어의 반응 시간에도 악영향을 줍니다.
게임을 업데이트하면 지연이 느껴지고 게임 상태가 띄엄띄엄 업데이트 됩니다. 결과적으로 플레이어와 AI의 반응 시간이 느려지고 간단한 조작이 실패하거나 심지어 불가능해질 수도 있습니다. 예를들어, 일반적으로 FPS로 피할 수 있는 장애물은 느린 FPS로는 피할 수 없습니다. 느린 하드웨에서 더 심각한 문제는 물리를 사용하면 물리 시물레이션으로인해서 게임 전체 시스템이 붕괴될수도 있습니다. 물리 시물레이션은 이터레이션(반복)을 통해서 최종 결과를 뽑아내야하기 때문에 중간 프레임을 건너 뛸수 없으므로, 물리 시물레이션 처리만으로 모든 CPU자원을 사용해 버릴수 있습니다. Profiler등의 도구를 통해서 순간적 느려짐(Hitch)이 발생하는 곳을 찾아내서 개선해야할것입니다. 하지만, 한계는 있을것입니다. 아무리 최적화한다해도 완벽하게 대응할 수 없다면 극단적으로 해당 기능을 제거하는것도 고려해야할수 있습니다. 기획단계에서 최저사양을 미리 정해준다면 조금은 더 수월해질 수 있을것입니다. 혹은 옵션별로 활성/비황성화하는 것도 전략일 수 있습니다.

빠른 하드웨어

빠른 하드웨어에서 위의 게임 루프가 어떻게 잘못될 수 있는지 궁금할 것입니다. 아래에 설명하는 문제는 정밀도의 유실로 인해서 생기는 문제들입니다. float 또는 double 값의 메모리 공간은 제한되어 있으므로 일부 값은 표현할 수 없습니다. 예를들어, 0.1은 2진수로 표현할 수 없기 때문에 이중으로 저장될때 반올림됩니다. 혹자는 double로 사용하면 어지간한 문제는 해결될 수 있지 않느냐하겠지만, 게임내에서 double을 사용하기에는 다소 부담이 있고, double을 사용한다고 해도 완전히 오류를 없앨수는 없습니다.

아래에 설명하는 내용은 근본적으로 Floating point rounding errors로 인해서 발생하는 문제입니다.
Floating point rounding errors 관련해서는 이글을 참고하시면 도움이 될것입니다.

설명을 쉽게 하기 위해서 파이썬을 이용하겠습니다.
파이썬 언어를 몰라도 상관 없습니다. >>> 로 시작하는것은 콘솔입력 부분이고 아닌 부분은 출력부입니다.

>>> 0.1
0.10000000000000001

참고로, python 3.x 에서는 0.1로 표시됩니다. 여기에서는 이러한 부분을 고려해야함을 보여주는 것이라고 생각해주시면 좋을듯 싶습니다.

이것 자체는 어쩌면 자주 봐오던 결과입니다. 하지만, 이것으로 인해서 생기는 부작용은 클 수 있습니다. 밀리세컨드 당 0.001 속도를 가진 자동차가 있다고 가정해 보겠습니다. 10초(10,000ms) 후에 자동차는 10.0(0.001 x 10,000)의 거리를 이동할 것입니다.

확인을 위해서 게임 루프가 하는것처럼 계산하기 위해 FPS를 입력 값으로 사용하여 계산해보도록 하겠습니다.

def predict_distance(fps, running_time_msecs):
    # 한프레임당 소모하는 milliseconds
    msecs_per_frame = 1000 / fps
    # 흘러간 게임 시간
    total_msecs = 0
    # 이동한 거리
    distance = 0.0
    # 프레임(틱)당 이동 속도
    speed_per_frame = 0.001
    # 주어진 시간동안 지정한 속도를 기준으로
    # 게임루프를 흉내내서 이동거리 계산(예측)
    while total_msecs < running_time_msecs:
        distance += speed_per_frame * msecs_per_frame
        total_msecs += msecs_per_frame
    return distance

이제 40FPS으로 거리를 계산해 보겠습니다.

>>> predict_distance(fps=40, running_time_msecs=10000)
10.000000000000075

예상했던 10.0이 아닙니다. 반올림 오류가 커졌습니다. 자 그럼 80FPS 상황에서는 어떤일이 발생할지 살펴 보겠습니다.

>>> predict_distance(fps=80, running_time_msecs=10000)
9.999999999999966

막연하게 어느정도의 오류가 있겠지하고 짐작은 했었겠지만, 막상 실측을 해보니 오류가 더 커진, 다소 위험한 결과를 보여주고 있습니다. 80FPS에서 더 많은 값 더하기가 있기 때문에 반올림 오류가 커질 수 있습니다. 따라서 게임은 40FPS, 80FPS로 실행할때 게임상태가 달라질 수 있습니다.

>>> predict_distance(40, 10000) - predict_distance(80, 10000)
1.0835776720341528e-13

이 차이가 너무 작아서 게임 자체에서 볼수 없다고 생각할 수도 있는데, 생각과는 달리 이 잘못된 값을 사용하여 더 많은 계산을 수행하게 수치오류 누적으로 인해서 문제가 될수 있습니다. 이렇게 하면 작은 오류가 커질 수 있고 높은 프레임 속도로 게임 상태가 이상해질 수 있습니다. 그럴 가능성이 실제로 있을까 궁금해질것입니다. 네 매우 많이 있습니다. 이런 종류의 게임 루프를 사용한 게임을 보고 높은 프레임 속도에서 실제로 문제를 일으켰습니다. 프로그래머가 문제가 게임의 근간에 잠복하고 있음을 실제로 알게된 후에는 많은 코드를 수정해야 문제를 해결할수 있을지도 모릅니다. 즉, 문제가 일단 발생하면 디버깅 및 수선하기 굉장히 어려워진다는게 큰 문제입니다. 겉으로 쉽게 드러나는 문제는 수선하기 쉽지만, 잘 드러나지 않는 문제는 언제 터질지 모르는 시한폭탄이 되어버립니다. 하지만, 오류가 누적이 되지 않고 순간적으로 소숫점 아래에서의 오류는 큰 문제를 일으키지 않습니다. 여기서 설명한 부분은 오차의 누적으로 인한 심한 오차로 인한 문제를 얘기하였음에 집중하십시오.

결론

대부분 이렇게 사용해도 실제로 큰 문제가 발생하지 않을 수도 있습니다. 하지만, 게임 구성에 따라서는 이러한 문제가 나쁜쪽으로 부각되어 큰 문제를 일으킬수도 있습니다. 느린 / 빠른 하드웨어 모두 심각한 문제를 일으킬 수 도 있음을 위에서 단적인 예를통해 살펴 보았습니다. 또한 고정 프레임 속도를 사용할 때 보다 게임 업데이트 기능을 구현하기 살짝 더 어렵습니다. 물론, 대부분의 프로그래머들은 이러한 형태에 익숙해져 있기 때문에 별문제가 아닐수도 있습니다. 현재 대부분의 게임 엔진은 이러한 방식을 사용하고 있습니다.

최대 FPS의 일정한 게임 속도

구현

Constant Game Speed에 의존하는 첫번째 솔루션에서의 FPS는 느린 하드웨에서 실행할때 문제가 있습니다. 이 경우 게임 속도FPS가 모두 떨어집니다. 이에 대한 가능한 해결책은 게임을 해당 속도로 계속 업데이트하지만 렌더링 프레임 속도는 줄이는 것입니다. 다음 게임 루프를 사용하여 수행할 수 있습니다.

const uint32 TICKS_PER_SECONDS = 50;
const uint32 MSECS_PER_FRAME = 1000 / TICKS_PER_SECOND;
const uint32 MAX_FRAME_SKIP = 10;

uint32 nextFrameMs = GetMilliseconds();
uint32 skippedFrameCount;

bool running = true;
while (running) {
    skippedFrameCount = 0;
    while (GetMilliseconds() > nextFrameMs
            && skippedFrameCount < MAX_FRAME_SKIP) {
        Update();

        nextFrameMs += MSECS_PER_FRAME;
        skippedFrameCount++;
    }

    Render();
}

게임은 초당 50번 안정적으로 업데이트되며 렌더링은 최대한 빨리 수행됩니다. 렌더링이 초당 50회 이상 수행되면 일부 후속 프레임이 동일하므로 실제 비주얼 프레임은 초당 최대 50프레임으로 동기화됩니다. 느린 하드웨어에서 실행하는 경우 게임 업데이트 루프가 MAX_FRAME_SKIP에 도달할때까지 프레임 속도가 떨어질 수 있습니다. 실제로 이것은 렌더링 FPS5(FRAMES_PER_SECOND / MAX_FRAME_SKIP) 아래로 떨어지면 실제 게임 속도가 느려진다는 것을 의미합니다. 즉, 렌더링 속도가 프레임 속도 전반에 직접적인 영향을 주게 됩니다.

느린 하드웨어

느린 하드웨어에서는 초당 프레임이 떨어지지만 게임 자체는 정상속도로 실행됩니다. 하드웨어가 여전히 이것을 처리할 수 없다면, 게임 자체는 느리게 실행되고 프레임 속도는 전혀 부드럽지 않습니다. 조작도 잘안되고 플레이어의 눈 건강에도 악영향을 미치는 상황이 되어버립니다.

빠른 하드웨어

이 게임 루프는 빠른 하드웨어에서는 문제가 없지만 첫번째 방법과 마찬가지로 더 높은 프레임 속도에 사용할 수 있는 많은 클럭 사이클을 낭비하고 있습니다. 빠른 업데이트 속도와 느린 하드웨어에서 실행할 수 있는 범위의 균형을 찾는것이 중요합니다.

결론

최대 FPS로 일정한 게임 속도를 사용하는 것은 구현하기 쉽고 게임 코드를 단순하게 유지하는 솔루션입니다. 그러나 여전히 몇가지 문제가 있습니다. 높은 FPS를 정의하면 여전히 느린 하드웨어에 문제가 발생할 수 있지만, 낮은 FPS를 정의하면 빠른 하드웨어에 대한 시각적 호소력이 낭비됩니다. 최적의 FPS 값을 정하기란 쉽지 않습니다.

가변 FPS와 독립적인 일정한 게임 속도

구현

느린 하드웨어에서 더 빠르게 실행하고 더 빠른 하드웨에서 시각적으로 더 비대칭적으로 위의 솔루션을 더욱 향상 시킬 수 있을지 고민해 보겠습니다.

다음과 같이 상황을 가정해 보겠습니다. 게임 상태 자체는 초당 60번 업데이트할 필요가 없다고 판단할 수도 있습니다. 게임에 따라 다를 수 있지만, 플레이어 입력, AI게임 상태 업데이트는 초당 25프레임이면 충분합니다. 이제 Update()을 초당 25번 호출하는 것으로 가정하고 설명하도록 하겠습니다.
(실제로 몇몇 게임 엔진에서는 30FPS(어떤 엔진은 31.25FPS, 32ms) 정도로 동작하는 경우가 있습니다.)

반면 렌더링은 하드웨어가 처리할 수 있을만큼 빨라야합니다. 또한, 느린 프레임 속도는 게임 업데이트를 방해하지 않아야합니다. 실제 구현은 어떻게 이루어지는지 살펴보도록 하겠습니다.

const uint32 FRAMES_PER_SECONDS = 25;                   // 25fps
const uint32 MSECS_PER_FRAME = 1000 / TICKS_PER_SECOND; // 40ms
const uint32 MAX_FRAME_SKIP = 5;                        // 최대 프레임 건너뜀 횟수

uint32 nextFrameMs = GetMilliseconds();
uint32 skippedFrameCount;
float interpolation;

bool running = true;
while (running) {
    skippedFrameCount = 0;
    while (GetMilliseconds() > nextFrameMs
            && skippedFrameCount < MAX_FRAME_SKIP) {
        Update();
        nextFrameMs += MSECS_PER_FRAME;
        skippedFrameCount++;
    }

    interpolation = float(GetMilliseconds() + MSECS_PER_FRAME - nextFrameMs) / float(MSECS_PER_FRAME);
    Render(interpolation);
}

이런 종류의 게임 루프를 사용하면 Update() 구현이 쉬워집니다. 그러나 Render() 함수는 더욱더 복잡해지는 부담을 가지게 되었습니다. 보간을 인수로 취하는 예측 함수를 구현해야합니다. 하지만, 크게 복잡한 작업은 아닙니다. 이 보간법과 예측법이 어떻게 작동하는지 아래에서 설명하지만 먼저 왜 그것이 필요한지 아래에서 살펴보겠습니다.

보간의 필요성

interpolate_tick

게임 상태초당 25회 업데이트 되므로 렌더링에서 보간을 사용하지 않으면 프레임도 이 속도로 표시됩니다. 25FPS는 생각하는 것처럼 느리지는 않습니다.
(참고로 영화는 초당 24프레임으로 실행됩니다.)

따라서 시각적으로 좋은 경험을 위해서는 25FPS가 어느정도 적당할수도 있지만, 빠르게 움직이는 물체의 경우 FPS를 더 많이 수행할때 조금더 부드럽게 보일 수 있습니다. 그래서 우리가 할 수 있는 것은 빠름 움직임을 프레임 사이에서보다 매끄럽게 만드는 것입니다.

그리고 이것은 보간과 예측 함수가 해결책을 제공할 수 있습니다.

보간 및 예측

게임 코드가 초당 자신의 프레임으로 실행된다고 말했듯이 프레임을 그리거나 렝더링할 때 운좋게 정확한 틱위치에 딱 놓일수도 있겠지만, 많은 경우 2개의 틱 사이에 있을 가능성이 있습니다. 10회 게임 상태를 방금 업데이트했다고 가정하면 이제 장면을 렌더링할 것입니다. 이 렌더링은 10회~11회 게임 업데이트 사이에 있습니다. 그래서 렌더가 약 10.3에 있을 가능성이 있습니다. 아래 그림에서 보는바와 같이 0.3은 interpolation 값으로 사용됩니다.

interpolate_tick

다음과 같이 움직이는 자동차가 있다고 가정해 보겠습니다.

새 위치 = 위치 + 속도

T10에 해당하는 액터 틱에서 위치가 500이고 속도가 100인 경우 T11에 해당하는 액터 틱에서 600이라는 위치가 됩니다. 그러면 자동차를 렌더링할때 자동차를 어디에 위치시켜야 정확한지 알아보겠습니다. 마지막 액터 틱의 위치(이 경우 500)를 취할 수 있습니다만, 더 좋은 방법은 자동차가 정확히 10.3 위치를 예측하는 것입니다.

보여지는 위치 = 새 위치 + (속도 * 보간)

그러면 자동차는 530 위치에 렌더링 됩니다. 따라서 기본적으로 보간 변수에는 이전 액터 틱 사이의 값이 포함되어 있습니다. (이전=0.0, 다음=1.0)

다음으로 해야할 일은 자동차, 카메라가 렌더링 시간에 배치될 "예측" 기능을 만드는 것입니다. 이 예측 기능은 물체의 속도, 조향 또는 회전 속도를 기반으로 할 수 있습니다. 프레임 사이의 이동 및 애니메이션을 부드럽게 하기 위해 사용하기 때문에 복잡할 필요가 없습니다. 하지만, 충돌이 감지되기 직전에 객체가 다른 객체로 렌더링될 수 있습니다. 이러한 현상이 발생하는 이유는 위에서 단순이 위치의 변화만을 예측해서 처리했는데, 그사이 상태의 변화가 있을수 있는데 이를 처리하지 않았기 때문입니다. 그러나 우리가 전에 보았던 것처럼, 게임은 초당 25프레임으로 업데이트됩니다. 따라서 이런일이 발생하면 오류는 인간의 눈에는 거의 눈에 띄지 않습니다. 40ms내의 일부 시각적 부작용도 나타날수는 있다는 얘깁니다. 하지만, 거의 대두분의 경우에는 이러한 시각적인 오류는 눈치채지 못할것입니다. 그럼에도 불구하고 이러한 시각적 오류가 부각된다면 25FPS 대신에 30FPS를 사용하는 방법도 있을 수 있겠습니다. 몇몇 엔진은 30FPS 대신에 31.25FPS를 사용하는 엔진들도 있습니다. 왜 30이 아닌 31.25인지 궁금해할수 있습니다. 30FPS일 경우 한프레임당 시간이 떨어지지 않기 때문에 float error에 민감해질 수 있기 때문입니다.

30FPS일때 한 프레임 시간

>>> 1000/30
33.333333333333336

30FPS 기준으로 한 프레임에 해당하는 시간은 33.333333333333336ms입니다. 보는 바와 같이 나누어서 딱 떨어지지 않는 값입니다. 이는 float error를 가중시킵니다.

31.25FPS일때 한 프레임 시간

>>> 1000/31.25
32.0

30FPS와는 다르게 딱 떨어지는 값이 나옵니다. float error가 발생하지 않습니다.

느린 하드웨어

대부분의 경우 Update()Render() 보다 시간이 덜 걸립니다. 실제로 느린 하드웨에서도 Update() 함수가 초당 25번 실행될 수 있다고 가정할 수 있습니다. 따라서 게임은 초당 15프레임만 표시하더라도 플레이어 입력을 처리하고 많은 문제없이 게임 상태를 업데이트합니다.

빠른 하드웨어

빠른 하드웨어에서는 게임이 초당 25배의 일정한 속도로 계속 실행되지만 화면 업데이트는 이보다 훨씬 빠릅니다. 보간 / 예측 방법은 게임이 실제로 높은 프레임 속도로 실행되고 있다는 시각적 만족도를 달성할 수 있습니다. 좋은 점은 FPS를 임의로 설정할수 있다는 것입니다. 매 프레임마다 게임 상태를 업데이트 하지 않고 시각화만 하기 때문에 게임은 위에서 설명한 두 번째 방법보다 FPS가 더 높습니다.

결론

게임 상태를 FPS와 독립적으로 만드는 것이 게임 루프를 구현하는 가장 좋은 방법인 것 같습니다. 그러나 Render()에 예측 함수를 구현해야하는 부담이 생기지만, 간단한 계산만 해주면 되므로 부담은 크지 않습니다.

전반적인 결론

게임 루프에는 생각보다 많은 것이 있습니다. 우리는 몇가지 가능한 구현을 생각해 보았습니다, 반드시 피해야할 것중 하나가 있는것 같습니다. 그것은 가변 FPS게임 속도와 같아지는 것입니다. 일정한 프레임 속도는 모바일 장치를 위한 좋은 솔루션이 될수 있지만, 하드웨어에 있는 모든 성능을 뽑아내려면 FPS게임 속도와 완전히 독립적이며 높은 프레임 속도에 대한 예측 기능을 사용하는 게임 루프를 사용하는 것이 가장 좋습니다. 예측 가능에 신경 쓰지 않으려면 최대 프레임 속도로 작업할 수 있지만, 느리고 / 빠른 하드웨어에 적합한 게임 업데이트 속도를 찾는 것은 까다로울 수 있습니다.

끝으로, 아래 그림은 꼭 기억하셨으면 합니다.

fps_not_equal_to_gamespeed

다음편 예고

다음과 같은 주제들을 다뤄볼 예정입니다.

  • 이 글에서 나열된 방식들을 개선한 시스템 구현 및 설명
  • 고정 틱 시스템과 가변 틱 시스템의 실제 쓰임새등에 대한 사례 분석 (유불리 측면)
  • Tick LOD
  • 멀티스레딩 틱 시스템(단점 및 통용 방식들에 대한 설명)
  • Unreal/Unity3D 게임 루프 분석
  • C/C++ 고정/가변 틱 제어시스템 코드 제시
  • Unity3D에서 오브젝트(컴포넌트)별 고정/가변 틱 제어시스템 코드 제시
  • 시스템 전역 틱 시스템과 오브젝트별 개별 틱 시스템
  • 네트워크 동기화를 쉽게하기 위한 틱 시스템 고려사항
  • 캐시 친화적인 틱 시스템
  • 수직 동기화가 미치는 영향
  • 업데이트 우선순위
  • 틱 의존성(특정 오브젝트 업데이트 후 이어서 업데이트 해야하는 경우)
  • 등등...

내용이 많아질 경우 2, 3편으로 나누어서 써볼 생각입니다.

이 글에 사용된 모든 이미지들은 글쓴이가 직접 draw.io를 사용해 직접 그렸습니다.

'dev' 카테고리의 다른 글

정규표현식 기초  (0) 2019.08.05
python import statement  (0) 2018.12.22
Xlsxeller  (0) 2018.08.08
메모리 단편화 (Memory Fragmentation)  (0) 2017.09.01
TCP  (0) 2017.08.29
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/04   »
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 29 30
글 보관함