WIN32API FrameWork/원본

220502_WIN32API_Framework_2_그리기, DeltaTime

hyrule 2022. 5. 4. 15:43

* HDC를 통한 그리기 방법 학습

* DeltaTime을 구현하여 프레임 변동해도 일정한 게임 속도 유지시키는 법 학습

 

 

//Class CGameManager
//GameManager.h

#pragma once

#include "GameInfo.h"
#include "SingletonMacro.h"




/*
[프레임과 게임 처리]
내 컴퓨터는 100프레임
PC방 컴퓨터는 200프레임

게임 코드는 1프레임당 한번 게임 로직을 처리
만약 게임 로직이 1프레임에 5씩 캐릭터를 이동시킨다면
내 컴퓨터에서 500 움직일 동안 PC방 컴퓨터는 1000을 움직이게 된다.
아주 심각한 문제

<해결법 - DeltaTime>
* 매 프레임마다 한 프레임을 그리는 데 걸렸던 시간을 변수에 저장한다.
* 그리고 다음 프레임을 그릴때 시간 단위를 위에서 저장한 변수로 처리한다.
* 그러면 프레임 수에 게임 속도가 빨라지거나 느려지는 문제가 발생하지 않는다.
* 그러므로 앞으로 모든 게임 로직 처리 함수에
인자로 DeltaTime을 넣어줄 것.
*/



//윈도우 클래스를 생성하고 전체 게임을 관리하는 클래스
//이 클래스틑 객체를 여러 개 만들 필요가 없으므로 싱글턴 패턴으로 만든다.
class CGameManager
{
//매크로를 활용해 싱글턴 패턴으로 생성
DECLARE_SINGLETON(CGameManager)


private:
	/*
	< 윈도우 핸들 >
	* 일종의 번호로 되어 있음.
		- 이 번호들은 커널 오브젝트에 속함.
	* 커널: 운영체제가 사용하는 특수한 영역 및 핵심 부분. 사용자는 컨트롤 불가능
	* 커널 오브젝트: 커널에 있는 메모리 - 커널에 값을 넣어 놓음
	* 핸들은 각자 자신이 부여받은 번호에 속한 오브젝트들을 컨트롤하는 역할을 한다.
		ex) HWND(윈도우 핸들): 윈도우 창을 컨트롤하기 위한 용도
			HDC: WIN32에서 제공하는 그리기 도구. 윈도우 창의 클라이언트와 연결 됨.
	*/
	HINSTANCE	m_hInst;
	HWND		m_hWnd;

	//HDC는 GetDC 함수에 HWND를 인자로 집어넣어 운영체제로부터 할당받을 수 있다.
	//사용을 다 했으면 ReleaseDC 함수를 통해 할당을 해제해 주어야 한다.
	//DC = Device Context
	HDC			m_hDC;
	static bool	m_Loop;

	//Timer 클래스 전방선언 후 cpp 파일에서 include
	class CTimer* m_Timer;







	//움직여 줄 사각형 구조체
	RECT m_Rect;
	//움직일 방향(충돌시 반대 방향으로 움직이게)
	int m_Dir;

public:
	bool Init(HINSTANCE hInst);

	//Win32API 기본 코드의 메시지 루프 부분을 작동시킬 함수.
	int Run();

private:

	//[게임의 흐름을 구성하는 단계]

	//1. 게임로직을 작성할 함수.
	//이후 인자로 전달해 줄 Deltatime은 여기서 구한다.
	void Logic();

	//2. 사용자의 입력
	void Input(float DeltaTime);

	//3. 입력받은 내용에 대해서 데이터 업데이트
	//	+인공지능들의 데이터 업데이트
	//화면에 표시할 좌표를 만든다던지 하는 로직을 구현할 함수
	void Update(float DeltaTime);

	//4. 업데이트된 데이터를 토대로 충돌을 수행
	//위에서 계산된 로직을 토대로 충돌하는 오브젝트들이 생겼는지 판단
	void Collision(float DeltaTime);

	//4. 출력이 되어야 하는 물체들을 판단.(화면 밖의 오브젝트들)
	//5. 화면에 출력이 되어야 하는 물체들을 출력
	void Render(float DeltaTime);


	


	//윈도우 클래스 구조체를 생성하여 반환한다/
	void Register();
	
	//윈도우 창을 생성한다.
	//WIN32API 기본코드의 Init_Instance 함수와 같은 기능
	bool Create();

	//전역 함수로 선언하여 함수의 주소가 하나만 존재하도록 한다.
	static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

};
//Class CGameManager
//GameManager.cpp


#include "GameManager.h"
#include "resource.h"
#include "Timer.h"

//클래스 바깥에서 정적 변수 초기화
DEFINITION_SINGLETON(CGameManager)
bool CGameManager::m_Loop = true;

CGameManager::CGameManager()
{
    //메모리 릭 체크 함수
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    //_CrtSetBreakAlloc(100);


}

CGameManager::~CGameManager()
{
    SAFE_DELETE(m_Timer);


    //프로그램이 종료되면 DC를 제거한다.
    //m_hWnd에서 생성된 m_hDC를 삭제하라.
    ReleaseDC(m_hWnd, m_hDC);
}

bool CGameManager::Init(HINSTANCE hInst)
{
	//인자로 들어오는 hInst를 저장하고
	m_hInst = hInst;

    //Register 함수를 통해 윈도우 클래스 구조체를 만들어주고 등록한다.
	Register();

    //윈도우 창을 생성하고 등록한다.
	Create();

    //타이머 생성, 동적할당이므로 소멸자에서 제거 예약
    m_Timer = new CTimer;
    m_Timer->Init();

    //DC를 할당받는다.
    //할당받은 이후에는 반드시 ReleaseDC를 이용해 할당을 해제해 주어야 한다.
    m_hDC = GetDC(m_hWnd);
    



    m_Rect = { 800, 100, 900, 200 };

    m_Dir = 1;



	return true;
}

int CGameManager::Run()
{
    //운영체제가 만들어준 메시지를 얻어오기 위한 구조체이다.
    MSG msg;


    //메시지 루프 반복 여부를 m_Loop 변수로 판단한다.
    while (m_Loop)
    {
        //PeekMessage - 주석 분리하여 블로그에 업로드함
        //메시지 큐가 차있으면 메시지를 가져오고 큐에서 삭제(PM_REMOVE)
        //메시지 큐가 비어있으면 false를 반환.
        if (PeekMessage(&msg, m_hWnd, 0, 0, PM_REMOVE))
        {
            //두 함수는 한 쌍
            //설명 주석은 분리하여 블로그에 따로 업로드하였음.
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        //메시지 큐에 처리할 메시지가 없으면
        //(PeekMessage 함수가 false를 반환하면)
        //여기로 들어와서 게임 로직을 처리한다.
        else
        {
            Logic();
        }
 
    }

    return (int)msg.wParam;
}

void CGameManager::Logic()
{
    //매번 Logic이 실행될 때마다 DeltaTime 갱신
    m_Timer->Update();
    float DeltaTime = m_Timer->GetDelatTime();


    Input(DeltaTime);
    Update(DeltaTime);
    Collision(DeltaTime);
    Render(DeltaTime);


}

void CGameManager::Input(float DeltaTime)
{
}

void CGameManager::Update(float DeltaTime)
{
}

void CGameManager::Collision(float DeltaTime)
{
}

void CGameManager::Render(float DeltaTime)
{
    //fps 출력
    char    FPSText[64] = {};
    sprintf_s(FPSText, "* FPS: %d", (int)m_Timer->GetFPS());
    TextOutA(m_hDC, 1100, 0,FPSText, strlen(FPSText));

    char    DeltaTimeText[64] = {};
    sprintf_s(DeltaTimeText, "* DeltaTime: %.5f", m_Timer->GetDelatTime());
    TextOutA(m_hDC, 1100, 20, DeltaTimeText, strlen(DeltaTimeText));

    static float CurrentTime = 0.f;
    CurrentTime += DeltaTime;
    char    CurrentTimeText[64] = {};
    sprintf_s(CurrentTimeText, "* Time: %.5f", CurrentTime);
    TextOutA(m_hDC, 1100, 40, CurrentTimeText, strlen(CurrentTimeText));


    /*
    < 텍스트 출력 >
    * TextOutA: 멀티바이트 문자열(char 문자열)을 출력하는 함수이다.
    * TextOutW: 유니코드 문자열(wchar_t 문자열)을 출력하는함수이다.
    * TextOut: 현재 프로젝트의 설정에 따라 두 함수 중에서 자동 선택해주는 함수이다.
        #ifdef UNICODE
        #define TextOut  TextOutW
        #else
        #define TextOut  TextOutA
    */
    TextOut(m_hDC, 50, 50, TEXT("텍스트 출력"), lstrlen(TEXT("텍스트 출력")));

    

    //< 도형 출력 >
    //가운데가 흰색으로 칠해져있고, 테두리가 검은색인 사각형을 그린다.
    Rectangle(m_hDC, 100, 100, 200, 200);
    
    //원 출력
    //지정한 사이즈의 사각형 안에 들어가는 원을 그린다.
    Ellipse(m_hDC, 200, 100, 300, 200);

    //점 찍기
    SetPixel(m_hDC, 350, 100, RGB(255, 0, 0));
    SetPixel(m_hDC, 351, 100, RGB(255, 0, 0));
    SetPixel(m_hDC, 350, 101, RGB(255, 0, 0));
    SetPixel(m_hDC, 351, 101, RGB(255, 0, 0));

    //MoveToEx: 선을 그리기 위해서 시작점을 지정한다.
    MoveToEx(m_hDC, 100, 300, nullptr);

    //LineTo: 선의 끝 지점을 지정하고 선을 연결해서 그린다.
    LineTo(m_hDC, 200, 400);

    //LineTo를 계속 써서 선을 계속 이어 그릴수도 있다.
    //새로운 선을 다시 그리고 싶으면 MoveToEx를 사용하면 된다.
    LineTo(m_hDC, 300, 300);

    //RECT 구조체는 Long(=정수) 타입을 저장하므로 소수점 연산이 안 된다.
    //위아래로 움직일 것이므로 Top, Bottom 부분만 Float 타입으로 만들어서
    //프레임이 달라져도 속도가 일정하게 만들어 보자.
    static float Top = (float)m_Rect.top;
    static float Bottom = (float)m_Rect.bottom;

    Rectangle(m_hDC, m_Rect.left, Top, m_Rect.right, Bottom);
    
    Top += m_Dir * 500 * DeltaTime;
    Bottom += m_Dir * 500 * DeltaTime;
    if (Top <= 0)
        m_Dir *= -1;
    else if (Bottom >= 720)
        m_Dir *= -1;

}


void CGameManager::Register()
{
    WNDCLASSEXW wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = m_hInst;
    wcex.hIcon = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_ICON1));
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_ICON1));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = nullptr;
    wcex.lpszClassName = TEXT("GameFramework");
    RegisterClassExW(&wcex);
}

bool CGameManager::Create()
{
    m_hWnd = CreateWindowW(TEXT("GameFramework"), TEXT("GameFramework"), WS_OVERLAPPEDWINDOW,
        100, 0, 0, 0, nullptr, nullptr, m_hInst, nullptr);


    if (!m_hWnd)
    {
        return FALSE;
    }


    RECT rc = { 0, 0, 1280, 720 };
    AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW,FALSE);
    MoveWindow(m_hWnd, 50, 50, rc.right - rc.left,
        rc.bottom - rc.top, TRUE);


    ShowWindow(m_hWnd, SW_SHOW);


    
    UpdateWindow(m_hWnd);

    return TRUE;

}

LRESULT CGameManager::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    //WIN32API 기본 코드에서 가져온 내용
    // 
    //message 큐로부터 받은 message에 따라 어떤 동작을 할지 결정.
    switch (message)
    {
    case WM_DESTROY:
        m_Loop = false;
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

//Class Timer
//Timer.h


#pragma once

#include "GameInfo.h"

/*
* 윈도우는 고해상도 타이머를 내장하고 있고 이 고해상도 타이머의 값을 얻어올 수 있다.
* 프레임과 프레임 사이의 시간을 구하고자 한다면 고해상도 타이머의 값을 얻어와서 구할 수 있다.
*/


class CTimer
{
public:
	CTimer();
	~CTimer();

private:
    //타이머의 1초당 진동수를 저장할 변수
	LARGE_INTEGER m_Second;
	/*
    * LARGE_INTEGER: 공용체(union)
        - 공용체는 메모리 공간을 공유하는 메모리 타입이다.
	typedef union _LARGE_INTEGER {
    struct {
        DWORD LowPart;
        LONG HighPart;
    } DUMMYSTRUCTNAME;
    struct {
        DWORD LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
    } LARGE_INTEGER;
	
    * 32비트 기준
        - DWORD LowPart = Unsigned int (4byte)
        - LONG  HighPart= int(4byte)
        - 둘이 합친 공간인 8byte는
        - LONGLONG Quadpart와 공유된다.
	*/

    //타이머의 시간을 저장하기 위한 변수
    LARGE_INTEGER m_Time;

    //한 프레임을 그리는 데 걸리는 시간
    float m_DeltaTime;

    //FPS를 구하기 위한 변수
    int m_FPS;
    int m_Tick;
    float m_FPSTime;

    //프레임 제한을 위한 변수들. 일단 Init 함수에서 할당해놓음.
	float		m_FrameLimitTime;
	float		m_FrameTime;

public:
    void Init();
    void Update();

public:
    float GetDelatTime() const
    {
        return m_DeltaTime;
    }

    int GetFPS() const
    {
        return m_FPS;
    }

};
//Class Timer
//Timer.cpp



#include "Timer.h"

CTimer::CTimer() :
	m_Second{},
	m_Time{},
	m_DeltaTime(0.f),
	m_FPS(0),
	m_FPSTime(0.f),
	m_Tick(0),
	m_FrameTime(0.f)
{
	//프레임 제한을 초당 60프레임으로. 0 = 프레임 무제한
	m_FrameLimitTime = 1.0f / 60.0f;

}

CTimer::~CTimer()
{
}

void CTimer::Init()
{
	//m_Second에 내장 고해상도 타이머의 1초당 진동수를 저장
	QueryPerformanceFrequency(&m_Second);

	//현재 고해상도 타이머 상에서의 시간을 얻어온다.
	QueryPerformanceCounter(&m_Time);
}

void CTimer::Update()
{
	m_DeltaTime = 0.f;
	//프레임 제한을 걸었으면 해당 프레임 제한 시간이 되기 전까지는
	//타이머 안에서 무한루프를 돌면서 대기한다.
    
	//do-while문으로 무조건 1회는 실행시킨다.
    //->프레임 제한을 해제하면 m_FrameLimitTime == 0
	//->while문에 넣으면 아예 코드를 스킵해버려서 문제 발생의 여지가 있으므로
    //do-while문에 넣어준다.
	do
	{
		//매 프레임마다 시간을 한 번씩 기록한다.
		LARGE_INTEGER Time;
		QueryPerformanceCounter(&Time);

		//( 방금 기록한 시간 - 이전 프레임에서 기록한 시간 ) / 초당 진동 수
		//위의 3개 변수 모두 LONG LONG 타입이므로 하나는 형변환을 해 주어야 한다.
		m_FrameTime = (Time.QuadPart - m_Time.QuadPart) / (float)m_Second.QuadPart;

		//이전 타이머 값에 현재 시간을 기록한다.
		m_Time = Time;

		//루프를 돌면서 시간을 더해준다.
		m_DeltaTime += m_FrameTime;
	} while (m_DeltaTime < m_FrameLimitTime);

	

	//FPSTime에 DeltaTime을 누적하여 더하고, 틱도 하나 증가시킨다.
	//FPSTime 동안에 프레임이 몇틱 누적됐는지 확인 ->
	m_FPSTime += m_DeltaTime;
	++m_Tick;

	//초당 한번 프레임 틱을 계산하고 프레임 체크 타이머를 초기화한다.
	if (m_FPSTime > 1.0f)
	{
		m_FPS = m_Tick;
		m_Tick = 0;
		m_FPSTime = 0.0f;
	}



}