WIN32 Timer, Double Buffering

Date:     Updated:

카테고리:

태그:

6일차

  • Default
    • Client.h
    • framework.h
    • Resouce.h
    • targetver.h
  • Engine
    1. Header
      1. define.h
      2. struct.h
    2. Core
      1. CCore.cpp
      2. CCore.h
    3. Manager
      1. KeyMgr
        1. CkeyMgr.cpp
        2. CkeyMgr.h
      2. TimeMgr
        1. CTimeMgr.cpp
        2. CTimeMgr.h
    4. Object
      1. CObject.cpp
      2. CObject.h
  • 리소스 파일
  • main.cpp
  • pch.h.
    1. struct.h
  • 거의 시간 동기화에 대한 내용이다. 또 Double Buffering인데.
  • 시간 동기화는 update함수에서 += 0.01f로 움직이면, 어떤 컴퓨터에서는 50프레임, 다른 곳에서는 150프레임 이렇게 나는데 그에 대한 차이가 발생하면 안되므로, 1초에 얼만큼 가도록 Delta Time이라는 1프레임당 걸리는 시간을 계산하여 곱해준다.
  • Double Buffering은 이전 시간에도 그랬지만, 윈도우 화면을 렌더링하면 계속 지우고 그리는 과정에서 중간 과정이 보이게 되어서 뭔가 화면이 깜빡이는 느낌이 들게 된다.
    • 그래서 그리는 용도의 하나의 캔버스(BITMAP)을 둔다. 거기에서 다 그린 후에, 사용자가 보게 되는 Window에 한 번에 가져온다. 즉, 가볍게 말하면 그리기 전용 캔버스, 보여주기 전용 캔버스. 캔버스 2개를 가지고 있는 것이다.
  1. CCore.h
#pragma once

#include "define.h"

class CCore {
	SINGLE(CCore);

private:
	HWND	m_hWnd;				//메인 윈도우 핸들. 
	POINT	m_ptResolution;		//메인 윈도우 해상도

	HDC		m_hDC;				//메인 윈도우에 Draw 할 DC

	HBITMAP m_hBit;				//사본용 . 
	HDC		m_memDC;

public:
	int init(HWND _hWnd, POINT _ptResolution);
	void progress();

private:
	void update();
	void render();

public:
	HWND GetMainHwnd() { return m_hWnd; }

};
  • m_Bit은 새로운 ‘그려주기 용’ 캔버스이다. m_memDC는 수업 초기에 있었던 한 윈도우(캔버스)에는 그려주기 용 도구? 그런 구조체가 있다고 했었다. 그것의 값이다.
  • GetMainHwnd()는 메인 윈도우의 Key값을 보내주는 함수이다.
  1. Define.h
#pragma once

#define SINGLE(type)public: \
					static type* GetInstance()\
					{\
						static type mgr;\
						return &mgr;\
					}\
					private:\
						type();\
						~type();

#define fDT CTimeMgr::GetInstance()->GetfDT()
#define DT	CTimeMgr::GetInstance()->GetDT()

  • fDT나 DT는 후술할 CTimeMgr에서 DeltaTime(1프레임 당 걸리는 시간)을 반환하는 것을 축약한 매크로이다.
  1. CTimeMgr.h
#pragma once

class CTimeMgr
{
	SINGLE(CTimeMgr);
private:
	//이동량을 프레임으로 쪼개서 이동을 시키면, 이번 프레임에 움직이는 양이 나옴.

	//이동량 * 1프레임에 걸리는 시간(delta Time).
	LARGE_INTEGER m_llCurCount;
	LARGE_INTEGER m_llPrevCount;
	LARGE_INTEGER m_llFrequency;

	double			m_dDT;			//프레임 간의 시간값. 
	double			m_dAcc;			//1초 체크를 위한 누적 시간. 
	UINT			m_iCallCount;	//1초당 호출 횟수. 
	UINT			m_iFPS;			//1초당 프레임. 
public:
	//GetTick은 1초당 1000이라서 정확도가 조금...
	//게다가 오차도 있어서 훨씬 더 정밀한 함수가
	//QueryPerformanceCounter(); -> 1000만 단위.
	void init();
	void update();

public:
	double GetDT() { return m_dDT; }
	float GetfDT() { return (float)m_dDT; }
};

  1. CTimeMgr.cpp
#include "pch.h"
#include "CTimeMgr.h"

#include "CCore.h"

CTimeMgr::CTimeMgr() 
	:m_llCurCount{}
	, m_llPrevCount{}
	, m_llFrequency{}
	,m_dDT(0.)
	,m_iCallCount(0)
	,m_dAcc(0.)
{

}

CTimeMgr::~CTimeMgr() {

}

void CTimeMgr::init()
{

	//현재 카운트
	QueryPerformanceCounter(&m_llPrevCount);
	//초당 카운트가 고정되어 있는것도 아님. 

	//초당 카운트 횟수(1000만)
	QueryPerformanceFrequency(&m_llFrequency);

}

void CTimeMgr::update()
{
	QueryPerformanceCounter(&m_llCurCount);

	//이전 프레임의 카운팅고, 현재 프레임 카운팅 값의 차이를 구한다. 
	m_dDT = (double)(m_llCurCount.QuadPart - m_llPrevCount.QuadPart) / (double)m_llFrequency.QuadPart;

	//이전 카운트 값을 현재값으로 갱신(다음번에 계산을 위해서)
	m_llPrevCount = m_llCurCount;

	m_iCallCount++;
	m_dAcc += m_dDT;	//DT 누적. 
	if (m_dAcc >= 1.) {
		m_iFPS = m_iCallCount;
		m_dAcc = 0.;
		m_iCallCount = 0;

		wchar_t szBuffer[255] = {};
		swprintf_s(szBuffer, L"FPS : %d, DT : %f", m_iFPS, m_dDT);
		SetWindowText(CCore::GetInstance()->GetMainHwnd(), szBuffer);
	}
}

  • QueryPerformanceCounter(1초당 1000만회 셈)을 통해 이전 프레임과 현재 프레임 카운팅의 값 차이를 구한다. (DeltaTime)
  • 또한 그걸 이용해 1초가 지날때마다, m_iCallCount가 ++된 값을 구해, 1초당 얼마나 많은 CTimeMgr::update()가 실행됐는지 알 수 있다.
  1. CTimeMgr.cpp
#include "pch.h"
#include "CCore.h"

#include "CTimeMgr.h"
#include "CkeyMgr.h"

#include "CObject.h"

//CCore* CCore::g_pInst = nullptr;
CObject g_obj;

CCore::CCore() 
	: m_hWnd(0), m_ptResolution{}, m_hDC(0)
	, m_hBit(0), m_memDC(0)
{
}

CCore::~CCore() {
	//m_hWnd에 엮여있던 m_hDC를 해제해준다. 
	ReleaseDC(m_hWnd, m_hDC);

	//Create로 만든 것은 DeleteDC로 해야한다. 
	DeleteDC(m_memDC);
	DeleteObject(m_hBit);
}

int CCore::init(HWND _hWnd, POINT _ptResolution) {
	m_hWnd = _hWnd;
	m_ptResolution = _ptResolution;

	//해상도에 맞게 윈도우 크기 조정. 

	RECT rt = { 0,0,m_ptResolution.x, m_ptResolution.y };
	//테두리 포함해서, 크기 보정해주는 것. 
	//rt 포인터에다가 다시 값을 변환해서 넣어줌. 
	AdjustWindowRect(&rt, WS_OVERLAPPEDWINDOW, true);

	//타이틀이나 그런 걸 제외한, 순수하게 물체가 그려질 수 있는 영역.
	//첫 번째로 들어가는 건, 어떤 윈도우인지. 
	//SetWindow는 타이틀창이나 테두리까지 포함한 크기임. 
	SetWindowPos(m_hWnd, nullptr, 100, 100, rt.right - rt.left, rt.bottom - rt.top, 0);
	
	//어디다가 그려요? m_hWnd에다가 그려요. 
	m_hDC = GetDC(m_hWnd);

	//이중 버퍼링 용도의 비트맵과 DC를 만든다. 
	//그림을 그릴 수 있는 건 윈도우에 달려있는, 해상도만큼의 픽셀(묶어서 비트맵이라고 부름)

	//결국 똑같은 해상도로 비트맵을 가지면 되는 거 아닌가? 해서 똑같은 해상도의 비트맵을 만듬. 
	//Compatible은 호환성. (호환성을 가지고 있는 비트맵)
	//비트맵 아이디 값을 받았음. 
	m_hBit = CreateCompatibleBitmap(m_hDC, m_ptResolution.x, m_ptResolution.y);

	//새로 만든 목적지의 DC가 필요함. (어떤 구조인지에 대한 Device Context가 필요함)
	m_memDC = CreateCompatibleDC(m_hDC);

	//이번엔 그림을 그릴 타겟, 목적지를 교체해주는 것. 위에서 만든 비트맵을 전달해줌. 
	//되돌아 리턴되는 건 원래 비트맵이지 않을까? 기본적으로 들어가있는 1픽셀짜리 Default 비트맵. 
	HBITMAP holdBit = (HBITMAP)SelectObject(m_memDC, m_hBit);
	DeleteObject(holdBit);

	//Manager initialize
	CTimeMgr::GetInstance()->init();
	CkeyMgr::GetInstance()->init();

	g_obj.SetPos(Vec2((float)(m_ptResolution.x / 2), (float)(m_ptResolution.y / 2)));
	g_obj.SetScale(Vec2(100, 100));

	return S_OK;
}

void CCore::progress() {

	//Manager Update
	CTimeMgr::GetInstance()->update();

	update();
	render();
}

void CCore::update()
{
	Vec2 vPos = g_obj.GetPos();
	//비동기 키 입출력 함수. <- 코드가 바로 실행됨. 백그라운드여도 바로 수행됨
	//return 되는 건 어느정도 상태 값을 알려줌.
	
	if (GetAsyncKeyState(VK_LEFT) & 0x8000) {
		//왼쪽 키가 눌렸을 경우.

		//정수대로 움직이면, 엄청난 이동속도가 됨. 
		//107.7이 좌표더라고 107이나 108이 됨. 

		//컴퓨터 성능에 따라 움직이는 양이 달라질 것. 
		//그래서 시간 동기화가 필요함. 1초마다 움직이는 양이 정해져야만 함. 
		vPos.x -= 200.f * fDT;

	}

	if (GetAsyncKeyState(VK_RIGHT) & 0x8000) {
		vPos.x += 200.f * fDT;
	}
	//물체들의 변경점을 체크하는 곳(좌표나 여러가지 변경점)

	g_obj.SetPos(vPos);
}

void CCore::render()
{
	//그리기 작업 하기 전, 화면 Clear
	//커다란 사각형으로 덮어버리기. 이러면 원래 자각형이 보이지도 않으니까. 

	//우리는 2개의 캔버스를 사용하여, 한 곳에다가 다 그린 후 -> 보여지는 화면에 옮기기. (이중 버퍼링)
	Rectangle(m_memDC, -1, -1, m_ptResolution.x + 1, m_ptResolution.y + 1);

	//그리기 작업. 
	Vec2 vPos = g_obj.GetPos();
	Vec2 vScale = g_obj.GetScale();
	Rectangle(m_memDC, int(vPos.x - vScale.x / 2.f),
		int(vPos.y - vScale.y / 2.f),
		int(vPos.x + vScale.x / 2.f),
		int(vPos.y + vScale.y / 2.f));

	Rectangle(m_memDC, int(vPos.x - vScale.x),
		int(vPos.y - vScale.y),
		int(vPos.x + vScale.x),
		int(vPos.y + vScale.y));

	//검은 잔상이 남는데, 움직일 때마다 전부 안지워줘서 그럼. 

	//물체가 많아지고 여러개가 되면 에러가 있늠
	//1. 잔상이 계속 남는 문제.  
	
	//다 그린 걸 한 픽셀씩 열심히 옮기는 것 -> 여기까지만 해도 프레임이 많이 떨어졌네. 
	//반복문이 너무 많음. -> 픽셀 한땀한땀 전부 옮기기 때문... 너무 혹사함. 
	BitBlt(m_hDC, 0, 0, m_ptResolution.x, m_ptResolution.y, m_memDC, 0, 0, SRCCOPY);
	

	//이런 반복적인 렌더링 처리를 하기 위한 장치가 GPU이다. 
	//DirectX는 GPU를 컨트롤하는 처리. 
}

  • 주석을 전부 달아놓긴 했지만, 중요한 부분만 보자면 ~CCore에서 Delete로 새로운 캔버스를 소멸자에서 지워줘야 한다.
  • init함수에서 새로운 Bitmap(캔버스)와 DC(어떤 구조인지에 대한 Device Context)를 만들어서 받아온다.
  • SelectObject로 그리기 전용 캔버스에다가 그리라고 선택한다. 그러면 기존에 사용하던 1픽셀짜리 Default 비트맵이 나오는데, 그건 Delete로 지워준다.
  • update문에서 기존에 좌표를 움직이던 함수에 *fDT로 델타타임을 곱해 시간 동기화를 해준다.
  • BitBlt()로 그리기 전용 비트맵에 그려놓은 그림을 사용자가 보는 캔버스에 옮겨놓는다.

WINAPI 카테고리 내 다른 글 보러가기

댓글 남기기