* 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;
}
}
'WIN32API FrameWork > 원본' 카테고리의 다른 글
220503_WIN32API_Framework_3-2_입력받은대로 캐릭터 이동 (0) | 2022.05.05 |
---|---|
220503_WIN32API_Framework_3-1_Vector2 구조체와 연산 만들기 (0) | 2022.05.05 |
220502_WIN32API_Framework_3_GameObject 상속 구조 짜기 (0) | 2022.05.04 |
220429_WIN32API_Framework_1_기본코드 긁어와서 뼈대 짜기 (0) | 2022.05.03 |
WIN32API 기본 코드 살펴보기 (0) | 2022.05.02 |