본문 바로가기
Unity3D/API

deltaTime은 언제 계산될까?

by 니키티스 2023. 7. 15.

deltaTime은 언제 계산될까?

우리가 유니티에서 맨날 사용하는 Time.deltaTime. 이동할 때는 필수고 실시간으로 움직일 땐 여기저기 안 들어가는 곳이 없다.

그런데 Time.deltaTime이 계산되는 기준은 무엇인가?

매 프레임 유니티는 어떤 작업을 수행하고 있으며, Time.deltaTime은 언제 계산하는 걸까?

이에 관해 유니티가 2020.1 버전까지 존재했던 프레임 파이프라인의 문제점과 2020.2 베타 버전에서 개선한 프레임 파이프라인의 순서를 단순화된 형태로 공개한 글이 있어 이를 써보고자 한다. 이 글에 따르면 Unity의 프레임 파이프라인은 현재와 같은 모습을 갖추기까지 많은 변화가 있었다고 한다. 거의 매 릴리즈마다 변화가 있었다고 하니 이전 버전에서는 어떻게 샘플링을 했는지는 정확하게는 모르겠다. 다만 적어도 2020.1 버전과 2020.2 베타 버전에서는 어떤 변화를 겪었는지 알 수 있을 것이다.

참고 문서

더욱 원활한 게임플레이를 위한 Unity 2020.2 Time.deltaTime 개선 | Unity Blog

 

더욱 원활한 게임플레이를 위한 Unity 2020.2 Time.deltaTime 개선 | Unity Blog

입력 지연은 까다로운 주제입니다. 정확하게 측정하기도 쉽지 않고, 유발 요인도 입력 하드웨어, 운영체제, 드라이버, 게임 엔진, 게임 로직, 디스플레이 등으로 다양합니다. Unity가 다른 요인에

blog.unity.com

Time.deltaTime이 미세하게 변한다

2020.1 버전 이전까지는 Time.deltaTime 값이 일관되지 않아 움직임이 끊어지거나 불안정해지는 문제가 존재하였다.

예를 들면 프레임 속도가 144 fps로 고정되었다면 Time.deltaTime이 1/144초(약 6.94ms)가 아니라 다음과 같은 식으로 흔들리는 것이다.

6.854 ms 7.423 ms 6.691 ms 6.707 ms 7.045 ms 7.346 ms 6.513 ms

게임 개발 서적에서는 게임 루프를 단순하게 직관적으로 1. 입력을 받고 2. 프레임별 업데이트를 하고 3. 렌더링을 수행하는 구조로 묘사하고 있다.

while (true) 
{ 
        ProcessInput(); 
        Update();
        Render(); 
}

여기다가 시간을 계산하는 것까지 포함하면 이렇게 되겠다.

var time = GetTime(); 
while (true) 
{ 
        var lastTime = time; 
        time = GetTime(); 
        var deltaTime = time - lastTime; 
        ProcessInput(); 
        Update(deltaTime); 
        Render(deltaTime); 
}

하지만 최신 게임 엔진에서는 성능을 증가시키기 위해 파이프라이닝 기법을 사용하여 한 번에 두 개 이상의 프레임을 처리하도록 한다.

즉, 게임 개발 서적에서 설명하는 이미지가 이거라면

실제 게임 엔진에서는 파이프라인을 거쳐 이렇게 처리한다.

파이프라이닝을 하는 방식은 병렬 실행을 통해 동일한 시간에 더 많은 프레임을 내보낼 수 있다. 총 프레임 시간은 모든 파이프라인 단계의 합이 아니라, 가장 긴 단계에서 걸린 시간으로 계산할 수 있다.

하지만 실제 처리해야 하는 상황은 훨씬 복잡하다.

  1. 각 파이프라인 단계에서 소요되는 시간은 프레임마다 다르다. 키 입력도 그렇고, 렌더링도 그렇고 프레임별 상황에 따라 달라진다.
  2. 파이프라인 단계마다 소요되는 시간이 달라서, 빠른 단계가 지나치게 앞서 나아가지 않게 단계를 인위적으로 중단할 필요가 있다. 대표적인 방법으로, 이전 프레임이 프론트 버퍼[각주:1]로 플립되기까지 기다리는 방법이 있다.

우선, 2020.1의 일반적인 프레임 타임라인을 보자.

이 경우, 멀티스레드 렌더링, VSync(수직동기화)는 활성화되어 있고, Graphics Jobs[각주:2]는 비활성화되어 있고, QualitySettings.maxQueuedFrames[각주:3]가 2로 설정되어 있다고 가정한다.

Unity 2020.1 버전에서의 프레임 타임라인.

위 그림을 보면 Update를 실행하기 직전 Sample Time 과정을 거치는데 이때 Time.deltaTime을 측정한다고 볼 수 있다.

그런데 화면 새로고침이 6.94ms마다 이루어지더라도 Unity의 시간 샘플링은 결과가 매번 다르게 나타나고 있다.

Frame 1, 2, 3, 4 아래에 있는 실행시간은 모두 6.94ms이다.

하지만 샘플링된 시간 간에 간격은 모두 제각각이다.

t_deltaTime(#4) = 1.4 + 3.19 + 1.51 + 0.5 + 0.67 = 7.27 ms
t_deltaTime(#5) = 1.45 + 2.81 + 1.48 + 0.5 + 0.4 = 6.64 ms
t_deltaTime(#6) = 1.43 + 3.13 + 1.61 + 0.51 + 0.35 = 7.03 ms

평균 deltaTime은 $(7.27+6.64+7.03) / 3 = 6.98ms$로 화면 새로고침 속도(6.94ms)와 거의 비슷하다. 오래 측정하면 평균적으로는 6.94ms가 나오게 되지만, 실제 오브젝트의 움직임을 계산할 때 이 deltaTime을 그대로 사용하면 오브젝트 사이에 아주 미세한 떨림이 발생한다.

deltaTime의 작은 오차로 인해 이동에 약간의 오차가 생기는 것이다.

해당 글의 원본에 영상이 있으니 참고하자.

deltaTime 변화의 원인 찾기

그러면, 왜 Frame별 실행시간은 똑같은데 deltaTime 사이에는 차이가 있을까?

그 이유는 deltaTime을 측정하는(Sample Time) 시점에 있다.

디스플레이는 각 프레임을 6.94ms 간 보여준 후 장면을 전환한다. 게임 플레이어가 각 프레임을 보게 되는 시간의 길이이기 때문에 이게 실제로 deltaTime이 되어야 한다.

이 6.94ms는 처리(processing) 부분과 대기(sleeping) 부분으로 나눠진다(원문에서는 절전이라고 했으나, 대기라고 표현하는 게 더 자연스러워 수정했습니다).

프레임 타임라인을 다시 보자. 메인 스레드에서 deltaTime을 계산하고 있으니 메인 스레드를 기준으로 생각해 보자.

메인 스레드에서 ‘처리’ 부분은 OS 메시지 펌핑(Pump OS message), 입력 처리(Process input), 업데이트 호출(Update), 렌더링 명령 생성(Issue GPU command)이 있다. ‘대기’ 부분은 렌더 스레드 대기(Wait for render thread)가 있다. 처리 부분의 시간과 대기 부분의 시간을 합치면 실제 프레임 시간과 같다.

$$
t_{processing} + t_{waiting} = 6.94ms
$$

두 상태에 걸리는 시간은 변동될 수 있지만, 합은 언제나 일정하다. 처리 시간이 증가하면 대기 시간이 줄고, 그 반대도 성립한다.

t_issueGPUCommands(4) + t_pumpOSMessages(5) + t_processInput(5) + t_Update(5) + t_wait(5) = 1.51 + 0.5 + 0.67 + 1.45 + 2.81 = 6.94 ms
t_issueGPUCommands(5) + t_pumpOSMessages(6) + t_processInput(6) + t_Update(6) + t_wait(6) = 1.48 + 0.5 + 0.4 + 1.43 + 3.13 = 6.94 ms
t_issueGPUCommands(6) + t_pumpOSMessages(7) + t_processInput(7) + t_Update(7) + t_wait(7) = 1.61 + 0.51 + 0.35 + 1.28 + 3.19 = 6.94 ms

하지만 유니티에서는 업데이트의 시작 부분을 기준으로 시간을 측정한다.

따라서 렌더링 명령 생성(Issue GPU Commands), OS 메시지 펌핑(Pump OS Message), 입력 처리(Process input) 과정에서 걸리는 시간이 변하면 결과도 변해버린다.

유니티의 메인 스레드 루프를 간단하게 표현하면 이렇다.

while (!ShouldQuit())
{
    PumpOSMessages();
    UpdateInput();
    SampleTime(); // We sample time here!
    Update();
    WaitForRenderThread();
    IssueRenderingCommands();
}

해결 방법은 간단히, 시간을 측정하는 SampleTime을 수행하는 순서만 바꾸면 될 것 같아 보인다. 처리 부분 중간에 시간을 재는 게 문제니, 처리가 끝나고 시간을 재면 되지 않을까?

while (!ShouldQuit())
{
    PumpOSMessages();
    UpdateInput();
    Update();
    **WaitForRenderThread();
    SampleTime();**
    IssueRenderingCommands();
}

그런데 이렇게 하면 해결이 안 된다. 렌더링 시간 값이 Update()와 달라서 deltaTime에 문제가 생기는 것이다(렌더링이 끝나는 순간의 시간이 기준이 되어버려서, Update를 기준으로 할 때랑 달라진다고 해석했다. 이 부분은 해석을 이상하게 했을 수가 있으니 원문[각주:4]을 참고하자). 

SampleTime()을 Update() 다음으로 옮겨서 별 효과가 없으니 반대로 대기 부분을 프레임 시작 부분으로 옮겨보자.

while (!ShouldQuit())
{
    PumpOSMessages();
    UpdateInput();
    **WaitForRenderThread();
    SampleTime();**
    Update();
    IssueRenderingCommands();
}

이젠 다른 문제가 생겨버렸다. 렌더 스레드가 요청을 받고(IssueRenderingCommands) 거의 바로 렌더링 과정을 끝마쳐줘야 한다. 왜냐하면, PumpOSMessage()와 UpdateInput()을 실행하는 데에는 얼마 안 걸리니까 렌더링 스레드가 쓸 시간이 훨씬 줄어든 것이다. 그 말인즉슨 렌더링 스레드는 병렬 실행을 해도 별 이득을 못 본다는 거다!

다시 프레임 타임라인을 보자.

유니티는 매 프레임 렌더 스레드를 기다리도록(메인 스레드의 Wait for render thread) 해서 파이프라인을 동기화한다. 이는 메인 스레드가 화면에 그려지는 것보다 너무 앞서 실행되지 않도록 하기 위해 필요하다.

렌더 스레드는 렌더링을 끝내고 화면상에 프레임을 띄울 때 작업이 끝난다. 다르게 말해서 백 버퍼가 플립되어서 프론트 버퍼가 될 때까지 기다린다는 뜻이다.

하지만 이전 프레임이 화면에 표시되었는지는 렌더 스레드에는 중요하지 않고 이를 중요하게 여기는 것은 속도 조절이 필요한 메인 스레드뿐이다.

(렌더 스레드는 오직 GPU에 명령을 전달하는 역할만을 하기 때문에 이전 프레임이 화면에 표시되었는지 아닌지는 렌더 스레드가 관심이 없기 때문이다.)

따라서 렌더 스레드가 화면에 프레임이 나타날 때까지 대기하게 하지 말고 이 대기 단계를 메인 스레드로 옮기야 한다. 이를 WaitForLastPresentation()이라 한다. 그러면 메인 스레드 루프는 다음과 같다.

while (!ShouldQuit())
{
    PumpOSMessages();
    UpdateInput();
    **WaitForLastPresentation();**
    SampleTime();
    Update();
    WaitForRenderThread();
    IssueRenderingCommands();
}

이렇게 하면 시간이 대기 부분 직후에 샘플링되어 타이밍이 모니터 새로고침과 딱 맞춰진다. 시간이 프레임 시작 시에 샘플링되니 Update()와 Render()의 타이밍도 같아진다.

여기서 WaitForLastPresentation()이 마지막 프레임, 즉 $프레임_{n-1}$이 화면에 나타나기까지 기다리지 않는다는 점이 중요하다. 그 경우엔 이전 프레임이 완전히 렌더링 될 때까지 기다리니 파이프라이닝이 안 된다. 이 함수는 $프레임_{n-\text{QualitySettings.maxQueuedFrames}}$가 화면에 나타나길 기다린다. 이미 QualitySettings.maxQueuedFrames만큼의 프레임을 미리 그려놨기 때문에 메인 스레드를 마지막 프레임을 완료될 때까지 기다리지 않아도 계속 진행할 수 있다(단, maxQueuedFrames가 1이면 각 프레임이 새로운 프레임 시작 전에 완료되어야 한다).

안정성을 위한 추가 작업

위 방법을 구현하면 deltaTime이 훨씬 안정화된다! 하지만 여전히, 운영체제가 대기 상태의 엔진을 제때 가동하지 않아서 deltaTime이 떨릴 수 있다. 특히 여러 프로그램을 동시 실행 중인 데스크톱 플랫폼에서 이 현상이 두드러진다.

이를 해결하기 위해 그래픽스 API/플랫폼을 활용할 수 있다. 대부분의 그래픽스 API와 플랫폼은 화면에 표시되는 프레임의 정확한 타임스탬프를 추출할 수 있도록 해주므로 훨씬 안정적인 시간을 구할 수 있다.

물론 그래픽스 API별로 별도의 추출 코드를 짜야한다거나, 일부 그래픽스 API에서는 VSync가 꺼져 있으면 타임스탬프를 추출할 수 없다거나 하는 문제가 있긴 하다.

이러한 고생을 거치고 나면, WaitForLastPresentation()과 SampleTime() 단계를 하나로 결합할 수 있다!

while (!ShouldQuit()) 
{ 
        PumpOSMessages(); 
        UpdateInput(); 
        **WaitForLastPresentationAndGetTimestamp();** 
        Update(); 
        WaitForRenderThread(); 
        IssueRenderingCommands(); 
}

이를 통해 떨림 현상을 해결할 수 있다고 한다.

입력 지연 개선

입력 지연은 해결하기 어려운 문제이다. 입력 지연이 얼마나 일어났는지도 애매하고, 유발 요인도 입력 하드웨어, 운영체제, 드라이버, 게임 엔진, 게임 로직, 디스플레이 등 다양하다. 그중 Unity에서 영향을 미칠 수 있는 게 게임 엔진밖에 없기 때문에 게임 엔진 요인에 초점을 맞춰보자.

엔진 입력 지연은 입력 OS 메시지를 사용할 수 있게 되는 시점에서부터 그 입력이 반영된 이미지를 디스플레이로 내보낼 때까지 걸리는 시간을 의미한다. 메인 스레드 루프를 기준으로, 최악의 경우 입력 지연은 다음과 같이 길어질 수 있다.

(QualitySettings.maxQueuedFrames가 2로 설정되어 있다고 가정)

PumpOSMessages(); // Pump input OS messages for frame 0
UpdateInput(); // Process input for frame 0
**--------------------- // Earliest input event from the OS that didn't become part of frame 0 arrives here!**
WaitForLastPresentationAndGetTimestamp(); // Wait for frame -2 to appear on the screen
Update(); // Update game state for frame 0
WaitForRenderThread(); // Wait until all commands from frame -1 are submitted to the GPU
IssueRenderingCommands(); // Send rendering commands for frame 0 to the rendering thread
PumpOSMessages(); // Pump input OS messages for frame 1
UpdateInput(); // Process input for frame 1
WaitForLastPresentationAndGetTimestamp(); // Wait for frame -1 to appear on the screen
Update(); // Update game state for frame 1, finally seeing the input event that arrived
WaitForRenderThread(); // Wait until all commands from frame 0 are submitted to the GPU
IssueRenderingCommands(); // Send rendering commands for frame 1 to the rendering thread
PumpOSMessages(); // Pump input OS messages for frame 2
UpdateInput(); // Process input for frame 2
WaitForLastPresentationAndGetTimestamp(); // Wait for frame 0 to appear on the screen
Update(); // Update game state for frame 2
WaitForRenderThread(); // Wait until all commands from frame 1 are submitted to the GPU
IssueRenderingCommands(); // Send rendering commands for frame 2 to the rendering thread
PumpOSMessages(); // Pump input OS messages for frame 3
UpdateInput(); // Process input for frame 3
WaitForLastPresentationAndGetTimestamp(); // Wait for frame 1 to appear on the screen. This is where the changes from our input event appear.

되게 복잡해 보이지만, 설명하면 간단하다.

처음 시작 프레임은 frame 0이다. 위에서 세 번째 줄에 있는 ‘Earliest input event‘에서 입력이 들어오게 되는데 이는 UpdateInput()이 끝난 직후이므로 frame 0에서는 입력을 읽어낼 수 없다.

그다음 프레임인 frame 1에서야 UpdateInput()에서 입력을 읽어낼 수 있게 되는데, maxQueuedFrames가 2이므로 두 프레임 후인 frame 3가 되어서야 frame 1에서 그렸던 그림이 화면에 표시되게 된다.

즉, frame 0에 들어왔던 입력이 frame 3이 되었을 때 화면에 그려진다. 이 과정에서 WaitForLastPresentationAndGetTimestamp() 함수를 4번 거치게 된다. 이 대기하는 시간이 게임 루프의 대부분을 차지한다고 하면, 이 4번의 WaitFor… 함수를 기다리는 시간이 곧 입력 지연이 되지 않을까?

Unity가 프레임을 손실하지 않고 게임 루프에서 소요되는 시간이 대부분 처리가 아닌 대기인 경우, 새로고침 속도가 144hz일 때 엔진에서 유발되는 입력 지연은 최악의 상황에 4 * 6.94 = 27.76ms에 달하며, 이는 이전 프레임이 화면에 나타나기까지 네 번(즉, 4번의 새로고침 속도 간격) 대기하기 때문입니다.

실제로 Unity 블로그에서 기술된 바에 의하면 4번만큼 대기 함수(WaitForLastPresentationAndGetTimestamp())를 호출하기 때문에 4 프레임, 즉 4 * 6.94 = 27.76ms만큼 입력 지연이 발생한다고 한다. 이게 144hz의 모니터를 기준으로 해도 이런데 60hz 모니터라면 얼마나 심할까? 60hz 환경에서는 프레임 하나에 16.67ms가 걸리니 4 * 16.67 = 66.67ms만큼 지연이 발생한다.

너무 지연이 길다. 대기 과정을 OS 이벤트를 실행하는 PumpOSMessages 이전으로 옮겨보자.

while (!ShouldQuit())
{
    WaitForLastPresentationAndGetTimestamp();
    **PumpOSMessages();
    UpdateInput();**
    Update();
    WaitForRenderThread();
    IssueRenderingCommands();
}

이렇게 하면 첫 프레임(frame 0)에서 UpdateInput() 이후에 입력이 들어온다고 해도 실제로 대기 과정을 4번이 아니라 3번만 거치게 되니까 최악의 경우 입력 지연이 3 * 6.94 = 20.82ms가 된다.

더 줄여보자. Frame을 미리 계산해 놓는 것 때문에 입력 지연이 생긴다. QualitySettings.maxQueuedFrames를 1로 줄이면 입력 지연을 더 줄일 수 있다.

**--------------------- // Input event arrives from the OS!**
WaitForLastPresentationAndGetTimestamp(); // Wait for frame -1 to appear on the screen
PumpOSMessages(); // Pump input OS messages for frame 0
UpdateInput(); // Process input for frame 0
Update(); // Update game state for **frame 0** with the input event that we are measuring
WaitForRenderThread(); // Wait until all commands from frame -1 are submitted to the GPU
IssueRenderingCommands(); // Send rendering commands for frame 0 to the rendering thread
WaitForLastPresentationAndGetTimestamp(); // **Wait for frame 0 to appear on the screen. This is where the changes from our input event appear.**

(원문에서는 맨 위의 Wait이 frame -2가 화면에 나타나는 것을 기다린다고 되어 있었습니다. 그러나 맥락상 이전 프레임인 -1이 되는 것이 맞다고 생각하여 수정하였습니다.)

이렇게 하면 최악의 경우 입력 후에 대기 과정을 두 번밖에 안 거쳐서 2 * 6.94 = 13.88ms밖에 걸리지 않는다.

다만 여기서 주의할 점이 있는데, QualitySettings.maxQueuedFrames를 1로 설정하면 엔진의 파이프라이닝을 비활성화하기 때문에 원하는 프레임 속도에 도달하기 더 어려워진다고 한다. 당연하지만 FPS가 낮아지면 maxQueuedFrames가 2일 때보다도 입력 지연이 더 심해질 수 있다. 예를 들어 144 FPS가 maxQueuedFrames를 1로 낮춰서 72 FPS가 되었다면 입력 지연은 2*1/72=27.8ms가 된다. 이는 maxQueuedFrames가 2일 때의 지연 시간 20.82ms보다 더 나쁘다.

하드웨어 성능이 좋을 땐 입력 지연이 좋아지겠지만, 그렇지 않다면 더 나빠진다. 그래서 유니티에서는 이 설정을 사용하고 싶으면 게임 설정 메뉴에 옵션을 추가해서 플레이어가 직접 QualitySettings.maxQueuedFrames를 설정할 수 있게 하는 것을 추천한다고 한다.

입력 지연에 VSync가 미치는 효과

VSync, 즉 수직동기화가 입력 지연과 관계가 있어 이를 비활성화하면 입력 지연이 짧아질 수 있다고 한다.

인텔 공식 홈페이지에서 정의한 바에 따르면, 수직동기화(앞으로 VSync라고 함)는 게임 또는 애플리케이션의 이미지 프레임 속도를 디스플레이 모니터 재생률과 동기화하여 안정성을 높이는 방법이다. 다들 아시겠지만, VSync는 GPU에서 생성하는 프레임 속도와 모니터의 재생률(새로고침 속도)을 일치시킨다.

VSync는 프레임이 화면에 표시되는 순간을 늦춰 입력이 반영되는 데에 걸리는 시간이 더 길어진다. 따라서 VSync를 비활성화하면 입력 지연이 짧아진다.

VSync를 비활성화하면 렌더링이 끝나는 순간 백 버퍼가 프론트 버퍼로 플립된다. 그런데 디스플레이가 새로고침을 할 순간이 아닐 때 새 이미지를 받아, 새 프레임이 기존 프레임의 하단에 그려지는 현상이 발생한다. 이를 ’티어링(tearing)‘이라고 한다.

Screen tearing(Wikipedia) 출처.

티어링이 발생하면 애니메이션의 자연스러움을 포기하는 대신 입력 지연을 개선할 수도 있다고는 한다.

또, VSync를 비활성화하여 입력을 더 자주 받게 되어 입력 지연이 짧아질 수 있다. 게임의 FPS가 모니터 주사율보다 높으면(예: 60Hz 디스플레이에서 150 FPS) VSync를 비활성화하여 게임에서 한 번 새로고침할 때 여러 번 OS 이벤트를 전달받을 수 있게 되어 다음 입력을 받을 때까지 기다리는 시간이 줄어든다.

하지만 VSync를 비활성화하면 화질에 영향을 미치고 티어링으로 인해 멀미가 생길 수 있다. 따라서 여느 게임이 그렇듯 플레이어가 필요할 때 결정할 수 있게 게임 내에서 VSync 설정 옵션을 제공해 주는 편이 낫다.

결론

수정 사항을 모두 구현하면 Unity의 프레임 타임 라인이 완성된다.

이렇게 해서 오브젝트 움직임이 훨씬 자연스러워진다!

정리

여기서부터는 원문에 없는 필자의 생각이다.

이번 2020.2 베타 버전을 거치면서 프레임 타임라인이 많이 수정된 듯하다. 포스팅 링크를 들어가면 맨 아래쪽에 2020.2.0b1에서 실행한 슬로모션 비디오가 있다. 이를 보면 오브젝트의 떨림 현상이 거의 해결된 것 같다.

원래 목적으로 다시 돌아와서, Time.deltaTime이 언제를 기준으로 Sampling 되는지를 알아보도록 하자.

Unity 2020.1의 일반적인 프레임 타임라인

VSync를 활성화했을 때, 실제 프레임이 표시되는 시간 자체는 144hz 디스플레이 기준 6.94ms로 똑같이 나타난다.

그러나 2020.1 버전까지만 해도 시간 sampling은 Update() 함수 직전에 실행되었다. OS 메시지를 가져오고(Pump OS messages) 입력을 처리한 뒤에 Update 하기 직전 deltaTime을 계산하고 Update() 함수를 실행했다. 따라서 프레임 시간은 동일한데 deltaTime이 미세하게 떨리는 현상이 있었다.

이로 인해 오브젝트 이동시 떨림 현상이 나타났으며 입력 지연 현상이 최대 프레임 속도의 4배(144hz 디스플레이 기준 27.76ms)까지 나타났다.

Unity2020.2.0b1의 프레임 타임라인으로 추정되는 사진

2020.2.0b1 이후 버전에서는 시간 sampling의 기준이 바뀌었다. Wait for previous frame to be displayed, 그러니까 다음에 그려야 할 프레임이 화면에 표시될 때까지 대기하는 함수가 실행되고 그 직후에 시간을 측정한다. 시간을 sampling한 직후 OS 메시지 가져오기, 입력 처리 업데이트 등을 실행한다. 프레임의 처리 과정과 대기 과정 중간에 시간을 측정하지 않으니, deltaTime도 일정하게 유지된다.

t_deltaTime(#4) = 0.5 + 0.4 + 1.4 + 1.51 + 3.13 = 6.94 ms
t_deltaTime(#5) = 0.5 + 0.67 + 1.45 + 1.48 + 2.84 = 6.94 ms 
t_deltaTime(#6) = 0.5 + 0.4 + 1.43 + 1.61 + 3 = 6.94 ms

오브젝트 떨림 현상도 해결되었고 입력 지연 현상도 완화되어서 최대 프레임 속도의 3배 정도(144hz 디스플레이 기준 20.82ms)로 나아졌다.

이걸 좀 더 쉽게 요약하면 이렇다.

2020.1 버전까지는 Update() 직전에 deltaTime을 계산했다.

2020.2.0b 버전부터는 다음 프레임이 화면에 표시된 직후 deltaTime을 계산한다. 이제는 deltaTime을 계산하고 나서 OS 메시지 받아오기, 입력 처리까지 한 후에 Update()를 실행한다.

참고 자료

원문: https://blog.unity.com/kr/engine-platform/fixing-time-deltatime-in-unity-2020-2-for-smoother-gameplay 혹은 https://blog.unity.com/engine-platform/fixing-time-deltatime-in-unity-2020-2-for-smoother-gameplay

VSync의 정의: https://www.selecthub.com/resources/vsync/?amp=1, https://namu.wiki/w/수직동기화

VSync에 관한 레딧 글: https://www.reddit.com/r/Games/comments/1doh5l/vsync_and_input_lag/


 

  1. 실제 게임 렌더링은 실제 화면을 표시하는 프론트 버퍼에 바로 그림을 그리는 게 아니라, 백 버퍼에 그림을 그리고 나서 프론트 버퍼로 바꿔준다. [본문으로]
  2. Graphics Jobs? 멀티스레드 렌더링에서 코드를 여러 CPU 코어로 분할하여 병렬적으로 실행하는 것을 말한다. https://docs.unity3d.com/ScriptReference/PlayerSettings-graphicsJobs.html 참고. [본문으로]
  3. maxQueuedFrames란 Direct3D와 NVN 그래픽스 API에서 사용하는 옵션으로, 그래픽 드라이버가 미리 렌더링해놓는 프레임의 최대 개수를 말한다. 이게 많을수록 FPS는 높아지지만 프레임을 미리 그려놓는 만큼 입력 지연도 커진다. https://docs.unity3d.com/2020.2/Documentation/ScriptReference/QualitySettings-maxQueuedFrames.html 참고. [본문으로]
  4. 원문: However, this change doesn’t work correctly: rendering has different time readings than Update(), which has adverse effects on all sorts of things. One option is to save the sampled time at this point and update engine time only at the beginning of the next frame. However, that would mean the engine would be using time from before rendering the latest frame. [본문으로]

댓글