(2022) 공부 (Study)/게임 개발 (Development)

[Win32API] 오브젝트 이동 : 시간 동기화 (2)

수낭 2022. 4. 13. 14:45

https://soonang2.tistory.com/26

 

[Win32API] 오브젝트 이동 : 시간 동기화 (1)

모든 컴퓨터는 각각 성능이 다 다르다. 이는 즉, CPU의 연산 처리 속도에 차이가 있다는 뜻으로, PeekMessage()의 if문에 걸리지 않아서 else() { }로 분기되어서 core->Progress()가 호출되는 속도가 "실행하

soonang2.tistory.com

시간 동기화 (1) 에 대한 글에서 이어지는 내용입니다.
위 포스팅을 보고 오시는 것을 추천드립니다.

 

이제 시간 동기화 관련 코드를 구현해보자.
다만 구현하기 전에 새로운 함수에 대해서 살펴보아야 한다.

초당 함수 호출 횟수 : 25823회

이전에 우리의 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만회)

 

QueryPerformanceCounter 함수 형태

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 값과 DeltaTime 값 출력 모습

 

// 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으로 형변환하여 반환하는 함수이다.)

이상으로 포스팅을 마칩니다.