Game Develop

[C++] DeltaTime에 관하여. (프로그램 수행시간 측정하기) 본문

C++/C++

[C++] DeltaTime에 관하여. (프로그램 수행시간 측정하기)

MaxLevel 2020. 10. 11. 16:02

- 서론

                                                                                                                                                      

 

DeltaTime값에 대해 미리 말하자면 '각 컴퓨터의 성능에 따라 FPS(Frame Per Second, 초당 프레임)가 다름으로써 생기는 값의 차이를 없애기 위한 값'이다. DeltaTime의 개념자체는 간단하기 때문에 이 값을 어떻게 구하는지까지도 알아보도록 하겠다.

 

 

- DeltaTime이 왜 필요한가?

                                                                                                                                                      

 

 키보드 오른쪽방향키를 누르고 있을 때 캐릭터의 x좌표값을 +1씩 한다고 가정하자.

 얼추 아래와 같은 코드가 될 것이다.

 

1
2
3
4
5
6
7
void Update()
{
    if (KEY_DOWN(VK_RIGHT)) // 오른쪽키를 누르면
    {
        character.Position.x += 1;
    }
}
cs

 

그런데 Update()라는 함수는 프레임마다 실행되는 함수이다.

초당 프레임이 100을 보장해주는 컴퓨터라면 이 캐릭터는 1초 후의 위치는 100이 될 것이다. (1 x 100)

그러나 초당 프레임이 10정도밖에 보장해주지않는 컴퓨터라면 이 캐릭터의 위치는 10이 될 것이다. (1 x 10)

 

출처 : https://inyongs.tistory.com/18

 

당연하게도 실제 플레이어가 게임을 할 때 이런 일이 발생해서는 안된다.

이런 상황을 방지하기 위해 초당 값의 변경이 일정하게 보장받아야하는 값에는 DeltaTime이란걸 곱해준다.

이 값을 곱해주면 각 컴퓨터의 FPS가 다르더라도 1초후 이동하는 값은 같아진다.

 

 

1
2
3
4
5
6
7
void Update()
{
    if (KEY_DOWN(VK_RIGHT)) // 오른쪽키를 누르면
    {
        character.Position.x += 1 * DeltaTime;
    }
}
cs

 

 

이 DeltaTime이란 값은 '각 프레임 사이에 걸리는 시간'이다. 

 

100FPS라 가정하면 초당 100번 실행되니 DeltaTime은 1/100초가 된다.

마찬가지로 10FPS면 DeltaTime은 1/10초가 된다.

 

실제로 이 값을 적용했을 경우 1초후의 값이 같은지 계산해보자.

 

FPS가 100인 컴퓨터:

  => 1초후의 이동거리 = 100 * (1 * 1/100) = 1 

       

FPS가 10인 컴퓨터:

  => 1초후의 이동거리 = 10 * (1 * 1/10) = 1 

 

1초후의 이동거리 = FPS * (임의의 스피드값(여기선 1) * DeltaTime) 

 

예상대로 FPS가 다르더라도 1초후의 이동거리는 1로 똑같다.

그저 DeltaTime이란 값을 곱해줌으로써 문제가 해결됐다.

 

 

 

- DeltaTime은 어떻게 구하는가?

                                                                                                                                                      

 

하지만 이 DeltaTime이라는 편리한 값은 어디서 툭 튀어나오는게 아니다. 엄연히 어떠한 계산을 통하여 산출된 값이다.

각각의 컴퓨터의 메인보드에는 고해상도 타이머가 존재한다. 이 타이머는 정확하고 일정한 주파수(1초에 각 타이머의 성능에 맞는 진동수)를 가지고 있다. 

 

우리가 계산에 사용할 함수는 두가지다.

 

BOOL QueryPerformanceCounter( LARGE_INTEGER *lpPerformanceCount );

 

이 함수는 매개변수로 넘어간 변수에 '함수가 호출된 시점의 타이머값'을 넘겨준다.

BOOL형인 이유는 타이머가 지원되는 환경이 아닌 경우 0을 반환하지만 거의 그럴일이 없다고 무방하다.

이 함수를 호출해서 반복문으로 출력을 찍어보면 끊임없이 숫자가 카운팅 되고있다. 

(진동수가 카운팅되고 있다고 보면된다)

이 값은 시스템 부팅후부터 계속 카운팅되는 값이기 때문에 함수호출 여부를 떠나 계속 카운팅되고있다.

 

 

이렇게 계속 카운팅되고 있다.

 

BOOL QueryPerformanceFrequency( LARGE_INTEGER *lpFrequency );

 

이 함수는 매개변수에 현재 시스템의 고해상도 타이머의 주파수(1초당 진동수)를 반환한다.

이 값은 이 함수를 호출하는 시스템마다 다르기 때문에(컴퓨터의 사양차이) 컴퓨터마다 호출했을때의 값이 다를 수 있다.

실제로 이 함수를 호출할 경우 아래와 같이 값이 찍힌다.

 

시스템마다 고정값이다.

 

결과적으로 우리는 QueryPerformanceCounter() 함수로 함수호출시점의 진동수의 카운팅을 알 수 있고 QueryPerforamanceFrequency()함수로 초당진동수를 알 수 있다. 감이 좋으신분들은 아마 어떻게 써먹어야할지 이미 알아챘을수도 있다.

현재 프레임의 진동수에서 바로 이전프레임의 진동수를 뺀다. 그렇다면 딱 한 프레임동안 진동수가 얼마나 카운팅 됐는지 알 수 있다. 그러면 이 진동수를 우리가 아는 시간 초 단위로 바꿔야한다. 

우리는 QueryPerformanceFrequency()함수로 초당진동수를 알고있다. 그러면 한 프레임동안의 진동수를 초당진동수로 나눠버리면 그 진동수가 몇초동안 진행됐는지 알 수 있다.

 

예를 들어, 현재 프레임의 진동수가 1100이고 바로 이전프레임의 진동수가 1000이다. 그리고 초당진동수는 1000이라 하자. 그러면 한 프레임동안 진동수가 100번 카운팅 됐다는건데 1000번 진동할때 1초이니 100번진동할때는 0.1초이다.

이걸 식으로 표현하면 아래와 같다.

 

  DeltaTime(프레임사이의 시간) =

      (현재 프레임의 진동수카운팅 - 이전 프레임의 진동수카운팅) / 초당진동수 

 

 

코드는 다음과 같다.

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
 
Timer::Timer()
{    
 
    QueryPerformanceFrequency((LARGE_INTEGER*)&periodFrequency);
//1초동안 CPU의 진동수를 반환하는 함수 (고정값. 시스템 부팅시 고정됨)
 
  
    QueryPerformanceCounter((LARGE_INTEGER*)&lastTime); 
// 생성자함수는 Update()함수보다 이전에 실행되기 때문에 lastTime에 저장.
 
    timeScale = 1.0 / (double)periodFrequency; 
   // 미리  '1 / 초당진동수'  의 형태로 만들어 놓는다.
   // 컴퓨터는 나눗셈연산보다 곱셈연산이 더 빠르기때문에 매 프레임마다 연산을 해야할경우
역수로 만들어놓고 실제 계산때 곱셈하게 만들어놓는게 좋다.
}
 
 
void Timer::Update()
{
    QueryPerformanceCounter((LARGE_INTEGER*)&curTime); // 현재 프레임 진동수 저장.
 
    deltaTime = (double)(curTime - lastTime) * timeScale; // (현재 프레임 진동수 - 이전 프레임 진동수) * ( 1 / 초당진동수 )
 
    lastTime = curTime; // 현재 프레임을 이전 프레임으로 저장. 이후 계속 반복.
}
 
double Timer::GetDeltaTime() // 필요한곳에서 가져다 쓰기.
{
    return deltaTime;
}
 
cs

 

만약 특정 함수의 수행시간을 알고싶다면 아래와 같이 코드를 짜면 된다.

 

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
 
 
int main()
{
  __int64 afterTime;
  __int64 beforeTime;
__int64 periodFrequency;
    double timeScale;
    double time;
 
   QueryPerformanceFrequency((LARGE_INTEGER*)&periodFrequency); // 현재 CPU진동수.
 
    timeScale = 1.0 / (double)periodFrequency; // 역수로 만들기.
 
    QueryPerformanceCounter((LARGE_INTEGER*)&beforeTime); // 함수 수행 전 진동수 측정.
   
    TestFunction(); // 수행시간을 측정하고자 하는 함수.
 
    QueryPerformanceCounter((LARGE_INTEGER*)&afterTime); // 함수 수행 후 진동수 측정.
 
    time = (afterTime - beforeTime) * timeScale; // 계산.
 
    cout << time << endl// 출력
}
 
 
cs