[Win32API] 오브젝트 이동 : 시간 동기화 (2)
https://soonang2.tistory.com/26
[Win32API] 오브젝트 이동 : 시간 동기화 (1)
모든 컴퓨터는 각각 성능이 다 다르다. 이는 즉, CPU의 연산 처리 속도에 차이가 있다는 뜻으로, PeekMessage()의 if문에 걸리지 않아서 else() { }로 분기되어서 core->Progress()가 호출되는 속도가 "실행하
soonang2.tistory.com
시간 동기화 (1) 에 대한 글에서 이어지는 내용입니다.
위 포스팅을 보고 오시는 것을 추천드립니다.
이제 시간 동기화 관련 코드를 구현해보자.
다만 구현하기 전에 새로운 함수에 대해서 살펴보아야 한다.
이전에 우리의 PC에서의 초당 함수 호출 횟수를 확인하는 부분을 설계하여서
그 값을 살펴본 적이 있었고, 그 값을 무려 2만 ~ 4만의 수치가 나왔다.
즉, 초당 2만 ~ 4만의 프레임을 갖는다는 뜻이 된다.
하지만, 지금까지 다루던 GetTickCount() 함수는 겨우 초당 1000회, 즉 1ms 단위로 동작하는 함수이다.
이는 수만에 달하는 함수 호출 횟수를 다루기에는 부적합하다.
(GetTickCount() 함수가 1번 호출되는 동안, 약 26번의 프레임이 돌아가게 되는 것이다.)
그래서 등장한 것이 Queryperformancecounter (QPC) 이다.
https://docs.microsoft.com/ko-kr/windows/win32/sysinfo/acquiring-high-resolution-time-stamps
고해상도 타임스탬프 획득 - Win32 apps
Windows은 고해상도 타임 스탬프를 얻거나 시간 간격을 측정 하는 데 사용할 수 있는 api를 제공 합니다.
docs.microsoft.com
지금까지는 GetTickCount()로도 충분했기에 임시로 사용했지만, 이제부터는 1ms (초당 1000회) 한계를 해결하기
위한 함수인 고해상도 타임스탬프 : Queryperformancecounter (QPC) 함수를 사용할 것이다. (초당 1000만회)
WINBASEAPI
BOOL
WINAPI
QueryPerformanceCounter(
_Out_ LARGE_INTEGER* lpPerformanceCount
);
WINBASEAPI
BOOL
WINAPI
QueryPerformanceFrequency(
_Out_ LARGE_INTEGER* lpFrequency
);
위는 QPC 내부 구조이다. (상세한 내용은 보여지지 않고 있다.)
QPC의 매개변수로 받은 LARGE_INTEGER 변수를 _Out_ 용도로 값을 뱉어준다는 것을 알 수 있다.
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
} DUMMYSTRUCTNAME;
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;
QueryPerformanceCounter 함수는 LARGE_INTEGER라는 특수한 자료형을 매개변수로 취하고,
LARGE_INTEGER는 위와 같은 형태의 STRUCT 2개가 묶인 UNION 자료형이다.
GetTickCount()는 1초가 벌어지면 카운트 값 1000이 차이가 난다는 고정 값이 있었지만,
1초가 벌어졌을 때, 카운트 값 차이가 얼만큼 나는지도 직접 구해와야 한다.
m_llFrequency의 값이 10,000,000인 것을 볼 수 있다.
(1) 기존에 GetTickCount64()로 구현했던 코드
void CCore::progress()
{
static int callCount = 0;
++callCount;
static int iPrevCount = GetTickCount64();
int iCurCount = GetTickCount64();
if (iCurCount - iPrevCount > 1000) // 1초 경과시 진입
{
iPrevCount = iCurCount;
callCount = 0;
}
// Manager 클래스들 일괄 Update
CTimeMgr::GetInst()->Update(); // 매 프레임마다 TimerMgr이 업데이트 되도록
update();
// Manager 클래스들 일괄 Render
render();
}
(2) QPC를 이용해서 새로 구현한 코드
코드의 내용을 미리 보자면, 아래와 같다.
CTimeMgr.h 코드
#pragma once
class CTimeMgr
{
SINGLE(CTimeMgr);
private:
// Frame Per a Secound (FPS)
// = (1 / Delta Time)
// Time Per a Frame (Delta Time)
// = (1 / FPS)
LARGE_INTEGER m_llCurCount;
LARGE_INTEGER m_llPrevCount;
LARGE_INTEGER m_llFrequency;
double m_dDT; // 프레임 당(간) 걸린 시간
double m_FPS; // (1 / m_dDT)
double m_dAcc; // 1초 체크를 위한 누적 시간
UINT m_iCallCount;
UINT m_iFPS;
public:
void Init();
void Update();
public:
double GetDT() { return m_dDT; }
float GetfDT() { return (float)m_dDT; }
};
CTimeMgr.cpp 코드
#include "pch.h"
#include "CTimeMgr.h"
#include "CCore.h"
CTimeMgr::CTimeMgr()
: m_llCurCount{}
, m_llFrequency{}
, m_dDT(0.)
, m_FPS(0.)
, m_dAcc(0.)
, m_iCallCount(0)
{
}
CTimeMgr::~CTimeMgr()
{
}
void CTimeMgr::Init()
{
// 현재 카운트
QueryPerformanceCounter(&m_llPrevCount);
// 초당 카운트 횟수 (10,000,000)
QueryPerformanceFrequency(&m_llFrequency);
}
void CTimeMgr::Update()
{
QueryPerformanceCounter(&m_llCurCount);
// DeltaTime 값을 구한다. (1프레임 당 걸리는 시간)
m_dDT = (double)(m_llCurCount.QuadPart - m_llPrevCount.QuadPart)
/ (double)m_llFrequency.QuadPart; // QuadPart에 실제 longlong값이 들어있다.
// PrevCount를 최신 값으로 갱신
m_llPrevCount = m_llCurCount;
++m_iCallCount;
// deltaTime을 누적시키면, 현재까지 흐른 총 흐른 시간이 된다.
m_dAcc += m_dDT;
if (m_dAcc >= 1.)
{
// 초당 프레임 횟수 갱신
m_iFPS = m_iCallCount;
// Windows 창 bar에 출력
wchar_t szBuffer[255] = {};
swprintf_s(szBuffer, L"FPS : %d, DT : %lf", m_iFPS, m_dDT);
SetWindowText(CCore::GetInst()->GetMainHwnd(), szBuffer);
// 값들 0으로 다시 초기화
m_dAcc = 0.;
m_iCallCount = 0;
}
// m_FPS = 1. / m_dDT;
}
m_FPS = 1. / m_dDT 와 같은 방식으로 FPS 값을 갱신하게 되면 너무 들쑥날쑥한 값이 되므로,
m_dAcc += m_dDT 와 같이, DeltaTime을 1초가 되는 시점까지 누적시키고
if (m_dAcc >= 1.)
m_iFPS = m_iCallCount;
위와 같이, 직접 호출 횟수를 센 카운트 값을 FPS 값으로 갱신하였다.
LARGE_INTEGER m_llFrequency;
CPU 주파수에 따른 1초당 진행되는 틱수를 나타낸다. 변동이 없어서 한번만 읽어주면 된다.
LARGE_INTEGER m_llCurCount;
현재 performance counter 주파수의 포인터를 반환한다.
LARGE_INTEGER m_llPrevCount;
DeltaTime 측정을 위해 이전 Count 값을 기록한다.
// FPS는 매순간 변동이 있기 때문에,
// 항상 FPS는 어딘가에서 계산이 되고 있어야 한다.
if (GetAsyncKeyState(VK_LEFT) & 0x8000)
{
vPos.x -= 300.f * CTimeMgr::GetInst()->GetfDT();
}
if (GetAsyncKeyState(VK_RIGHT) & 0x8000)
{
vPos.x += 300.f * CTimeMgr::GetInst()->GetfDT();
}
if (GetAsyncKeyState(VK_UP) & 0x8000)
{
vPos.y -= 300.f * CTimeMgr::GetInst()->GetfDT();
}
if (GetAsyncKeyState(VK_DOWN) & 0x8000)
{
vPos.y += 300.f * CTimeMgr::GetInst()->GetfDT();
}
이렇게 구한 DeltaTime 값을 speed 값에 곱해주게 되면,
CPU 처리 속도와는 무관하게 어느 PC에서도 동일한 속도로 동작하게 된다.
(GetfDT() 함수는 DeltaTime 값을 float으로 형변환하여 반환하는 함수이다.)
이상으로 포스팅을 마칩니다.