응애맘마조

221114 ~ 221121 강의 본문

공부/2D강의

221114 ~ 221121 강의

TH.Wert 2022. 11. 23. 02:16

https://github.com/THWert/FlappyBird 먼저 깃허브 소스코드 공유부터 하겠습니다.

학원에서 C/C++을 끝내고 DirectX를 들어가기 전 일주일간(14일부터 21일까지) 수업 들었던 기본적인 API에 대해 정리해보려고 합니다.  파일명은 1번 줄에 주석으로 적어두겠습니다. 작성 순서가 알맞지 않을 수도 있지만 빼놓지 않고 전부 작성하겠습니다.

지금부터 작성되는 코드는 지금은 구하기 어려운 플래피 버드(Flappy Bird)입니다. 실제 모바일 게임처럼 새의 이미지나 파이프 이미지를 사용하지 않고 API 기본 함수로 작성되어있는 사각형 그리기와 색칠하는 기능만을 이용하여 작성하였습니다.

//stdafx.h

#pragma once

#include <Windows.h>
#include <iostream>
#include <time.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <assert.h>

using namespace std;

#include "Systems/Window.h"

#include "Utilities/SingletonBase.h"
#include "Utilities/Vector3.h"
#include "Utilities/ResourceManager.h"
#include "Utilities/BoundingBox.h"

#include "Systems/Keyboard.h"
#include "Systems/Mouse.h"
#include "Systems/Timer.h"

#pragma comment(lib, "msimg32.lib")

#define SAFE_DELETE(p) { if(p) {delete(p); (p) = nullptr;} }
#define WinMaxWidth 1280 
#define WinMaxHeight 720

extern HDC memDC;


typedef UINT uint;

lib는 GDI 컴포넌트입니다. 작성하지 않아도 정상적으로 작동됩니다.

typedef로 작성한 HDC는 Handle Device Context로 출력에 필요한 정보를 가지는 데이터 구조체, 좌표, 색, 굵기 등 출력에 필요한 모든 정보를 담고 있습니다.

그래서 결국 HDC란 DC의 정보를 저장하는 데이터 구조체의 위치를 알기 위해서 사용합니다.

주의할 점은 HDC는 포인터가 아닙니다. 항상 실제 객체의 메모리 주소를 가리킬 뿐이고 물리적으로 메모리 구조가 바뀌어도 DC의 실제 위치를 찾아갑니다.

//Window.h

#pragma once

#include "stdafx.h"

using namespace std;

struct WinDesc
{
	wstring AppName;
	HINSTANCE instance;
	HWND handle;
	UINT width;
	UINT height;
};

class Window
{
public:
	Window(WinDesc desc);
	~Window();

	WPARAM Run();

private:
	static LRESULT CALLBACK WndProc
	(
		HWND handle,
		UINT message,
		WPARAM wParam,
		LPARAM lParam
	);

	static WinDesc desc;
	static class Program* program;
};

Window.h 입니다.

static LRESULT CALLBACK WndProc는 메시지를 처리하는 함수입니다.
HWND handle 어떤 창인지 알아보는 창의 식별자입니다.
UINT message 어떤 메시지가 들어왔는지 체크를 합니다.
WPARAM wParam은 키보드의 어떠한 키가 눌렸는지 관련된 정보가 들어옵니다.
LPARAM lParam은 마우스의 좌표와 클릭 여부를 체크합니다.

//Window.cpp

#include "stdafx.h"
#include "Window.h"
#include "Program.h"

WinDesc Window::desc;
Program* Window::program = nullptr;

Window::Window(WinDesc desc)
{
	WNDCLASSEX WndClass;
	{
		WndClass.cbClsExtra = 0;
		WndClass.cbWndExtra = 0;
		WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
		WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
		WndClass.hIcon = LoadIcon(NULL, IDI_WINLOGO);
		WndClass.hIconSm = WndClass.hIcon;
		WndClass.hInstance = desc.instance;
		WndClass.lpfnWndProc = (WNDPROC)WndProc;
		WndClass.lpszClassName = desc.AppName.c_str();
		WndClass.lpszMenuName = NULL;
		WndClass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
		WndClass.cbSize = sizeof(WNDCLASSEX);
	}

	WORD wHr = RegisterClassEx(&WndClass);
	assert(wHr != 0);

	desc.handle = CreateWindowEx
	(
		WS_EX_APPWINDOW,
		desc.AppName.c_str(),
		desc.AppName.c_str(),
		WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		NULL,
		(HMENU)NULL,
		desc.instance,
		NULL
	);

	RECT rect = { 0,0,(LONG)desc.width,(LONG)desc.height };
	UINT centerX = (GetSystemMetrics(SM_CXSCREEN) - (UINT)desc.width) / 2;
	UINT centerY = (GetSystemMetrics(SM_CYSCREEN) - (UINT)desc.height) / 2;

	AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
	MoveWindow
	(
		desc.handle,
		centerX, centerY,
		rect.right - rect.left, rect.bottom - rect.top,	
		true);

	ShowWindow(desc.handle, SW_SHOWNORMAL);
	SetForegroundWindow(desc.handle);

	ShowCursor(true);
	Window::desc = desc;
}

Window::~Window()
{
	DestroyWindow(desc.handle);
	UnregisterClass(desc.AppName.c_str(), desc.instance);
}

WPARAM Window::Run()
{
	MSG msg = { 0 };

	Keyboard::Create();
	Mouse::Create();
	Timer::Create();

	Timer::Get()->Start();

	program = new Program();

	while (true)
	{
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			if (msg.message == WM_QUIT)
				break;

			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else {}
	}
	Timer::Delete();
	Mouse::Delete();
	Keyboard::Delete();
	SAFE_DELETE(program);

	return msg.wParam;
}

HDC memDC;
LRESULT Window::WndProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
{
	Mouse::Get()->InputProc(message, wParam, lParam);

	PAINTSTRUCT ps;
	HDC hdc;
	HBITMAP hBitmap, oldBitmap;
	static RECT crtRect;

	ZeroMemory(&ps, sizeof(PAINTSTRUCT));

	switch (message)
	{
	case WM_CREATE:
	{
		SetTimer(handle, 1, 0, NULL);
		break;
	}
	case WM_PAINT:
		GetClientRect(handle, &crtRect);
		hdc = BeginPaint(handle, &ps);
		{
			memDC = CreateCompatibleDC(hdc);
			hBitmap = CreateCompatibleBitmap(hdc, crtRect.right, crtRect.bottom);
			oldBitmap = (HBITMAP)SelectObject(memDC, hBitmap);
			PatBlt(memDC, 0, 0, crtRect.right, crtRect.bottom, WHITENESS);
			{
				if (Timer::Get() != nullptr)
					Timer::Get()->Print();
				if (Mouse::Get() != nullptr)
					Mouse::Get()->Print();
				if (program != nullptr)
					program->Render();
			}
			BitBlt(hdc, 0, 0, crtRect.right, crtRect.bottom, memDC, 0, 0, SRCCOPY);
			SelectObject(memDC, oldBitmap);
			DeleteObject(hBitmap);
			DeleteDC(memDC);
		}
		EndPaint(handle, &ps);
		break;

	case WM_TIMER:
	{
		if (Timer::Get() != nullptr)
			Timer::Get()->Update();
		if (Mouse::Get() != nullptr)
			Mouse::Get()->Update();
		if (Keyboard::Get() != nullptr)
			Keyboard::Get()->Update();
		if (program != nullptr)
			program->Update();

		InvalidateRect(desc.handle, nullptr, false);
		break;
	}
}

	if (message == WM_CLOSE || message == WM_DESTROY)
	{
		PostQuitMessage(0);
		return 0;
	}

	return DefWindowProc(handle, message, wParam, lParam);
}

Window.cpp입니다. (※주의 : 이 부분은 설명이 많습니다.)

cbClsExtra, cbWndExtra는 잘 사용하지 않습니다.
hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH)는 배경 색입니다. (색상은 흰색)

hCursor = LoadCursor(NULL, IDC_ARROW)는 커서 모양 선택입니다. (기본 화살표로 지정)
hIcon = LoadIcon(NULL, IDI_WINLOGO)는 윈도우 로고입니다. (실행 후 좌측 상단 로고 모양)
hIconSm = WndClass.hIcon은 작은 커서입니다. (해당 프로그램에서는 사용하지 않음)
hInstance = desc.instance는 어떤 프로그램인지 확인합니다.
lpfnWndProc = (WNDPROC)WndProc는 메시지 처리 함수의 주소가 들어갑니다.
lpszClassName = desc.AppName.c_str()는 클래스 이름입니다.
lpszMenuName = NULL은 메뉴 이름입니다.
style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC은 창 스타일입니다.
cbSize = sizeof(WNDCLASSEX)는 어떤 윈도우를 사용해서 만드는지 확인합니다.
WORD wHr = RegisterClassEx(&WndClass)는 위쪽에서 설정한 내용을 등록합니다.

assert는 오류가 발생할 것 같은 곳에 넣어두면 프로그램이 저절로 개발자에게 알려줍니다. 만약 해당 함수에 걸리게 되면 버그 발생 위치, call, stack 등 여러 정보를 알게 됩니다.

desc.handle = CreateWindowEx는 윈도우를 생성합니다.
WS_EX_APPWINDOW는 스타일의 확장된 옵션입니다.
desc.AppName.c_str()는 클래스 이름입니다.
desc.AppName.c_str()는 윈도우 창 이름입니다.
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW는 스타일입니다.
CW_USEDEFAULT는 윈도우 창의 X좌표입니다.
CW_USEDEFAULT는 윈도우 창의 Y좌표입니다.
CW_USEDEFAULT는 윈도우 창의 가로 크기입니다.
CW_USEDEFAULT는 윈도우 창의 세로 크기입니다.
NULL은 부모창이 핸들입니다.
(HMENU)NULL은 메뉴의 식별자입니다.
desc.instance는 어떤 윈도우 프로그램인지 식별자입니다.
마지막 NULL은 사용하지 않습니다.

GetSystemMetrics : 컴퓨터에서 하드웨어에 따라 달라지거나 사용자에 의해서 재설정 가능한 정보들을 프로그램에서 얻어서 사용할 때 사용합니다.
SM_CXSCREEN : 현재 화면 해상도의 X축 크기입니다.
SM_CYSCREEN : 현재 화면 해상도의 Y축 크기입니다.

AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE)는 윈도우 차 크기 조정을 세팅합니다.
desc.handle은 핸들입니다.
centerX, centerY는 X, Y값입니다.
rect.right - rect.left, rect.bottom - rect.top, true는 윈도우의 너비와, 높이입니다.

ShowWindow(desc.handle, SW_SHOWNORMAL)는 윈도우 창을 출력합니다.
SetForegroundWindow(desc.handle)은 최상위의 화면을 설정합니다.
위 파일에서는 사용하지 않았지만 SetFocus(desc.handle)은 윈도우 포커스를 조정합니다.

ShowCursor(true)는 description을 설정합니다.

TranslateMessage(&msg)는 msg 변수에 키보드 메시지가 들어 있을 경우 키에 대응하는 문자를 만들어냅니다.
DispatchMessage(&msg);는 윈도우 프로시저로 보냅니다.

case WM_CREATE에서는 윈도우 상태가 변하면 호출됩니다.
memDC = CreateCompatibleDC(hdc)는 인수로 주어진 hdc와 메모리 DC를 생성합니다.
hBitmap = CreateCompatibleBitmap(hdc, crtRect.right, crtRect.bottom)은 메모리 DC에 그리기 위해서는 해당 함수로 생성한 원하는 크기의 비트맵을 선택해야 합니다.
oldBitmap = (HBITMAP)SelectObject(memDC, hBitmap)은 DC에 저장된 GDI Object의 핸들 값을 변경할 때 사용합니다.
GDI(Graphic Device Interface) : 그래픽 객체를 표시하고 모니터나 프린터와 같은 출력장치로 전송)

PatBlt(memDC, 0, 0, crtRect.right, crtRect.bottom, WHITENESS)는 지정된 사각 영역을 모두 채색하되 현재 DC에 선택되어있는 브러시와 화면의 색상을 논리 연산합니다. 논리 연산에 따라 두 색상을 다양하게 혼합할 수 있습니다.

Timer::Get() != nullptr는 nullptr이 출력되면 코드가 터지기 때문에 안전성 때문에 사용합니다.
Timer::Get()->Print()는 FPS를 출력합니다.
Mouse::Get()->Print()는 마우스 좌표를 출력합니다.

BitBlt(hdc, 0, 0, crtRect.right, crtRect.bottom, memDC, 0, 0, SRCCOPY)는 이미지를 화면에 출력합니다.

InvalidateRect(desc.handle, nullptr, false)는 지정된 창의 업데이트 영역에 사각형을 추가합니다.

//Timer.h

#pragma once

typedef INT64 int64;

class Timer :public SingletonBase<Timer>
{
public:
	friend class SingletonBase <Timer>;

	static bool Stopped() { return isTimerStopped; }
	static float Delta() { return isTimerStopped ? 0.0f : timeElapsed; }

	void Update();
	void Print();

	void Start();
	void Stop();

	float FPS() const { return framePerSecond; }
	float Running() const { return runningTime; }

private:
	Timer(void);
	~Timer(void);

	static bool isTimerStopped;
	static float timeElapsed;

	int64 tickPerSecond;
	int64 currentTime;
	int64 lastTime;
	int64 lastFPSUpdate;
	int64 fpsUpdateInterval;

	UINT frameCount;
	float runningTime;
	float framePerSecond;

	RECT fpsRect;
	wstring fpsStr;
};

Timer.h입니다.

여기서는 시간 간격과 CPU 주기를 다룹니다.

static float timeElapsed는 이전 프레임에서 현재 프레임 시간의 사이를 말합니다.
tickPerSecond는 초당 틱카운트입니다.
currentTime은 현재시간입니다.
lastTime은 마지막으로 기록된 시간입니다.
lastFPSUpdate는 마지막 FPS 업데이트 시간입니다.
fpsUpdateInterval은 FPS 업데이트 간격입니다.

frameCount는 초당 프레임 갯수입니다.
runningTime은 게임 시작부터 경과시간입니다.
framePerSecond는 FPS입니다.

fpsRect는 FPS 정보가 들어갈 상자입니다.
fpsStr은 FPS 정보가 들어갈 문자입니다.

//Timer.cpp

#include "stdafx.h"
#include "Timer.h"

bool Timer::isTimerStopped = true;
float Timer::timeElapsed = 0.0f;

Timer::Timer(void)
	:tickPerSecond(0), currentTime(0), lastTime(0), lastFPSUpdate(0),
	fpsUpdateInterval(0), frameCount(0), runningTime(0), framePerSecond(0)
{
	QueryPerformanceFrequency((LARGE_INTEGER*)& tickPerSecond);
	fpsUpdateInterval = tickPerSecond >> 1;

	fpsRect = { WinMaxWidth - 175, 25, WinMaxHeight, 50 };
	fpsStr = L"";
}

Timer::~Timer(void) {}

void Timer::Update()
{
	if (isTimerStopped) return;

	QueryPerformanceCounter((LARGE_INTEGER*)&currentTime);
	timeElapsed = (float)(currentTime - lastTime) / (float)tickPerSecond;
	runningTime += timeElapsed;

	frameCount++;
	if (currentTime - lastFPSUpdate >= fpsUpdateInterval)
	{
		float tempCurrentTime = (float)currentTime / (float)tickPerSecond;
		float tempLastTime = (float)lastFPSUpdate / (float)tickPerSecond;
		framePerSecond = (float)frameCount / (tempCurrentTime - tempLastTime);

		lastFPSUpdate = (INT64)currentTime;
		frameCount = 0;
	}

	lastTime = currentTime;
}

void Timer::Print()
{
	fpsStr = L"FPS : " + to_wstring((int)FPS());
	DrawText(memDC, fpsStr.c_str(), -1, &fpsRect, DT_CENTER | DT_VCENTER);
}

void Timer::Start()
{
	if (!isTimerStopped)
		assert(false);

	QueryPerformanceCounter((LARGE_INTEGER*)&lastTime);
	isTimerStopped = false;
}

void Timer::Stop()
{
	if (isTimerStopped)
		assert(false);

	INT64 stopTime = 0;
	QueryPerformanceCounter((LARGE_INTEGER*)&stopTime);
	runningTime += (float)(stopTime - lastTime) / (float)tickPerSecond;
	isTimerStopped = true;
}

Timer.cpp입니다.

QueryPerformanceFrequency((LARGE_INTEGER*)& tickPerSecond)는 고해상도 타이머의 주기(1초당 진동 수(Hz))를 반환합니다.
IpFrequency에는 해당 타이머의 주기가 설정됨 (여기서는 tickPerSecond)

fpsUpdateInterval = tickPerSecond >> 1은 1초에 맞는 진동수 1초를 말합니다.
(진동수가 너무 커서 1초에 맞게 조절했습니다.)

QueryPerformanceCounter((LARGE_INTEGER*)&currentTime)은 시간 간격 측정에 상용할 수 있는 고해상도 타임 스탬프인 성능 카운터의 현재 값을 검색하고, 현재 CPU의 틱을 받아오고, 현재 성능 카운터 값을 계수로 받는 변수에 대한 포인터를 넘겨줍니다.

//Mouse.h

#pragma once

#define MAX_INPUT_MOUSE 8

class Mouse : public SingletonBase<Mouse>
{
public:
	friend class SingletonBase<Mouse>;

	void SetHandle(HWND handle) { this->handle = handle; }

	void Update();
	void Print();

	LRESULT InputProc(UINT message, WPARAM wParam, LPARAM lParam);

	Vector3 GetPosition() { return position; }

	bool Down (DWORD button) { return buttonMap[button] == BUTTON_INPUT_STATUS_DOWN; }
	bool UP   (DWORD button) { return buttonMap[button] == BUTTON_INPUT_STATUS_UP; }
	bool Press(DWORD button) { return buttonMap[button] == BUTTON_INPUT_STATUS_PRESS; }

	Vector3 GetMoveValue() { return wheelMoveValue; }

private:
	Mouse();
	~Mouse();

	HWND handle;
	Vector3 position;

	byte buttonStatus   [MAX_INPUT_MOUSE];
	byte buttonOldStatus[MAX_INPUT_MOUSE];
	byte buttonMap      [MAX_INPUT_MOUSE];

	Vector3 wheelStatus;
	Vector3 wheelOldStatus;
	Vector3 wheelMoveValue;

	DWORD timeDblClk;
	DWORD startDblClk[MAX_INPUT_MOUSE];
	int buttonCount  [MAX_INPUT_MOUSE];

	enum
	{
		MOUSE_ROTATION_NONE = 0,
		MOUSE_ROTATION_LEFT,
		MOUSE_ROTATION_RIGHT,
	};

	enum
	{
		BUTTON_INPUT_STATUS_NONE = 0,
		BUTTON_INPUT_STATUS_DOWN,
		BUTTON_INPUT_STATUS_UP,
		BUTTON_INPUT_STATUS_PRESS,
		BUTTON_INPUT_STATUS_DBLCLK,
	};

	RECT PosRect;
	wstring PosStr;
};

Mouse.h입니다.

여기서는 마우스의 움직여서 나오게 되는 좌표와 클릭, 클릭 중, 클릭 해제, 더블 클릭에 대해 다룹니다.

//Mouse.cpp

#include "stdafx.h"
#include "Mouse.h"

Mouse::Mouse()
{
	position = Vector3(0, 0, 0);

	wheelStatus    = Vector3(0.0f, 0.0f, 0.0f);
	wheelOldStatus = Vector3(0.0f, 0.0f, 0.0f);
	wheelMoveValue = Vector3(0.0f, 0.0f, 0.0f);

	ZeroMemory(buttonStatus   , sizeof(byte) * MAX_INPUT_MOUSE);
	ZeroMemory(buttonOldStatus, sizeof(byte) * MAX_INPUT_MOUSE);
	ZeroMemory(buttonMap      , sizeof(byte) * MAX_INPUT_MOUSE);

	ZeroMemory(startDblClk, sizeof(DWORD) * MAX_INPUT_MOUSE);
	ZeroMemory(buttonCount, sizeof(int  ) * MAX_INPUT_MOUSE);

	timeDblClk = GetDoubleClickTime();
	startDblClk[0] = GetTickCount();
	for (int i = 1; i < MAX_INPUT_MOUSE; i++)
		startDblClk[i] = startDblClk[0];

	DWORD tLine = 0;
	SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &tLine, 0);

	PosRect = { WinMaxWidth - 200, 0, WinMaxHeight, 25 };
	PosStr = L"";
}

Mouse::~Mouse() {}

void Mouse::Update()
{
	memcpy(buttonOldStatus, buttonStatus, sizeof(buttonOldStatus));

	ZeroMemory(buttonStatus, sizeof(buttonStatus));
	ZeroMemory(buttonMap   , sizeof(buttonMap));

	buttonStatus[0] = GetAsyncKeyState(VK_LBUTTON) & 0x8000 ? 1 : 0;
	buttonStatus[1] = GetAsyncKeyState(VK_RBUTTON) & 0x8000 ? 1 : 0;
	buttonStatus[2] = GetAsyncKeyState(VK_MBUTTON) & 0x8000 ? 1 : 0;

	for (DWORD i = 0; i < MAX_INPUT_MOUSE; i++)
	{
		int tOldStatus = buttonOldStatus[i];
		int tStatus    = buttonStatus[i];

		if      (tOldStatus == 0 && tStatus == 1)
			buttonMap[i] = BUTTON_INPUT_STATUS_DOWN;
		else if (tOldStatus == 1 && tStatus == 0)
			buttonMap[i] = BUTTON_INPUT_STATUS_UP;
		else if (tOldStatus == 1 && tStatus == 1)
			buttonMap[i] = BUTTON_INPUT_STATUS_PRESS;
		else
			buttonMap[i] = BUTTON_INPUT_STATUS_NONE;
	}

	POINT point;
	GetCursorPos(&point);
	ScreenToClient(handle, &point);

	wheelOldStatus.x = wheelStatus.x;
	wheelOldStatus.y = wheelStatus.y;

	wheelStatus.x = point.x;
	wheelStatus.y = point.y;

	wheelMoveValue = wheelStatus - wheelOldStatus;

	DWORD tButtonStatus = GetTickCount();
	for (DWORD i = 0; i < MAX_INPUT_MOUSE; i++)
	{
		if (buttonCount[i] == BUTTON_INPUT_STATUS_DOWN)
		{
			if (buttonCount[i] == 1)
			{
				if ((tButtonStatus - startDblClk[i]) >= timeDblClk)
					buttonCount[i] = 0;
			}
		}
		buttonCount[i]++;

		if (buttonCount[i] == 1)
			startDblClk[i] = tButtonStatus;


		if (buttonMap[i] == BUTTON_INPUT_STATUS_UP)
		{
			if (buttonCount[i] == 1)
			{
				if ((tButtonStatus - startDblClk[i]) >= timeDblClk)
					buttonCount[i] = 0;
			}
			else if (buttonCount[i] == 2)
			{
				if ((tButtonStatus - startDblClk[i]) <= timeDblClk)
					buttonCount[i] = BUTTON_INPUT_STATUS_DBLCLK;

				buttonCount[i] = 0;
			}
		}
	}
}

void Mouse::Print()
{
	PosStr = L"X : " + to_wstring((int)position.x) + L", Y : " + to_wstring((int)position.y);
	DrawText(memDC, PosStr.c_str(), -1, &PosRect, DT_CENTER | DT_VCENTER);
}

LRESULT Mouse::InputProc(UINT message, WPARAM wParam, LPARAM lParam)
{
	if (message == WM_LBUTTONDOWN || message == WM_MOUSEMOVE)
	{
		position.x = LOWORD(lParam);
		position.y = HIWORD(lParam);
	}

	if (message == WM_MOUSEWHEEL)
	{
		short tWheelValue = (short)HIWORD(wParam);

		wheelOldStatus.z = wheelStatus.z;
		wheelStatus.z += (float)tWheelValue;
	}
	return TRUE;
}

Mouse.cpp입니다.

timeDblClk = GetDoubleClickTime()는 마우스의 현재 두 번 클릭 시간을 검색합니다.
startDblClk[0] = GetTickCount()는 OS 부팅할 때부터 지나간 시간을 msec 단위로 올려줍니다. (1초 = 1000msec)

DWORD tLine = 0은 내려가는 줄 수입니다. (기본값 : 3)
SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &tLine, 0)는 파라미터를 통해 윈도우즈의 시스템 설정을 변경하거나 현재 시스템의 설정 값을 얻어오는 변수입니다. (수직 스크롤)

GetAsyncKeyState : 키가 눌러져 있으면 최상위 비트를 1로 만들고 CapsLock, ScrollLock 같은 토글키가 ON 되어있으면 최하위 비트를 1로 만듭니다. (특정키의 눌러짐 상태를 조사하고 싶으면 0x8000입력)

GetCursorPos(&point)는 전체화면 기준의 마우스 좌표를 얻어옵니다.
ScreenToClient(handle, &point)는 전체화면 기준의 마우스 좌표를 윈도우 클라이언트 좌표로 반환합니다.

DrawText(memDC, PosStr.c_str(), -1, &PosRect, DT_CENTER | DT_VCENTER)는
텍스트를 특정 사각형 안에 출력합니다.
매개변수는 핸들, 문자열, 길이(-1이면 널 종료 문자열까자 포함해서 자동으로 계산), 텍스트를 출력할 RECT 구조체의 사각형, 텍스트 서식을 넣습니다.

//Keyboard.h

#pragma once

#define MAX_INPUT_KEY 255

class Keyboard : public SingletonBase<Keyboard>
{
public:
	friend class SingletonBase<Keyboard>;

	void Update();

	bool Down (DWORD key) { return keyMap[key] == KEY_INPUT_STATUS_DOWN; }
	bool Up   (DWORD key) { return keyMap[key] == KEY_INPUT_STATUS_UP; }
	bool Press(DWORD key) { return keyMap[key] == KEY_INPUT_STATUS_PRESS; }

private:
	Keyboard();
	~Keyboard();

	byte keyState   [MAX_INPUT_KEY];
	byte keyOldState[MAX_INPUT_KEY];
	byte keyMap     [MAX_INPUT_KEY];

	enum
	{
		KEY_INPUT_STATUS_NONE = 0,
		KEY_INPUT_STATUS_DOWN,
		KEY_INPUT_STATUS_UP,
		KEY_INPUT_STATUS_PRESS,
	};
};

Keyboard.h 입니다.

여기서는 키보드 입력에 관해서 다룹니다.

//Keyboard.cpp

#include "stdafx.h"
#include "Keyboard.h"

void Keyboard::Update()
{
	memcpy(keyOldState, keyState, sizeof(keyOldState));
	ZeroMemory(keyState, sizeof(keyState));
	ZeroMemory(keyMap  , sizeof(keyMap));

	GetKeyboardState(keyState);

	for (DWORD i = 0; i < MAX_INPUT_KEY; i++)
	{
		byte key = keyState[i] & 0x80;
		keyState[i] = key ? 1 : 0;

		int oldState = keyOldState[i];
		int state    = keyState   [i];

		if      (oldState == 0 && state == 1)
			keyMap[i] = KEY_INPUT_STATUS_DOWN;
		else if (oldState == 1 && state == 0)
			keyMap[i] = KEY_INPUT_STATUS_UP;
		else if (oldState == 1 && state == 1)
			keyMap[i] = KEY_INPUT_STATUS_PRESS;
	}
	GetKeyState(0);
}

Keyboard::Keyboard()
{
	ZeroMemory(keyState   , sizeof(keyState));
	ZeroMemory(keyOldState, sizeof(keyOldState));
	ZeroMemory(keyMap     , sizeof(keyMap));
}

Keyboard::~Keyboard() {}

Keyboard.cpp입니다.

GetKeyboardState(keyState)는 256개의 가상 키 상태를 지정된 버퍼에 복사합니다.

GetKeyState(0)는 키의 토글상태를 알아옵니다.
키가 눌려졌을때(DOWN), 떨어졌을때(UP)을 호출합니다.

(이전에 누른 기록이 있는지 확인하는게 Old)

//WinMain.cpp

#include "stdafx.h"

int WINAPI WinMain(HINSTANCE instance, HINSTANCE prevInstance, LPSTR param, int command)
{
	srand((uint)time(NULL));

	WinDesc desc;
	desc.AppName = L"WinAPI";
	desc.instance = instance;
	desc.handle = NULL;
	desc.width = WinMaxWidth;
	desc.height = WinMaxHeight;

	Window* window = new Window(desc);
	WPARAM wParam = window->Run();
	SAFE_DELETE(window);

	return wParam;
}

이번엔 실행을 하게 되는 WinMain.cpp입니다. API는 C/C++과는 다르게 int main()으로 작성하여 실행되지 않고 WinMain으로 해서 실행됩니다.

16번 줄에서는 윈도우 동적 할당을 통해 윈도우 창을 생성합니다.
17번 줄에서는 Window.cpp로 Run을 실행하게 됩니다.

//Program.h

#pragma once

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

	void Update();
	void Render();

	void CreateWall();

private:
	class JumpingQuad* jq = nullptr;
	vector<class Wall*> walls;

	float elapsedTime = 0.0f;
	float duration = 1.0f;
};

elapsedTime은 경과시간을 나타냅니다.
duration은 지속시간을 나타냅니다.

//Program.cpp

#include "stdafx.h"
#include "Program.h"

#include "Geometries/Quad.h"
#include "Geometries/JumpingQuad.h"
#include "Geometries/Wall.h"

Program::Program()
{
	ResourceManager::Create();
	jq = new JumpingQuad(50, WinMaxHeight / 2 - 50, 100, WinMaxHeight / 2);
	CreateWall();
}

Program::~Program()
{
	ResourceManager::Delete();
	SAFE_DELETE(jq);

	for (Wall* wall : walls)
		SAFE_DELETE(wall);
}

void Program::Update()
{
	jq->Update();
	if (Keyboard::Get()->Press(VK_SPACE))
		jq->Jump();

	for (Wall* wall : walls)
	{
		wall->Update();
		if (wall->CheckOutSide())
		{
			wall->RePosition(walls.back()->GetPosition() + 500);
			rotate(walls.begin(), walls.begin() + 1, walls.end());
		}
	}

	if (BoundingBox::AABB(jq->GetBox(), walls.front()->GetUpperBox())
		|| BoundingBox::AABB(jq->GetBox(), walls.front()->GetLowerBox()))
		jq->SetIntersected(true);
	else
		jq->SetIntersected(false);
}

void Program::Render()
{
	jq->Render();
	for (Wall* wall : walls)
		wall->Render();
}

void Program::CreateWall()
{
	for (int i = 0; i < 6; i++)
		walls.push_back(new Wall(WinMaxHeight + i * 500));
}

Program.cpp입니다.
CheckOutSide는 벽이 화면에서 왼쪽 끝까지 가게 되면 다시 오른쪽으로 돌려내는 코드입니다.

//Vector3.h

#pragma once

class Vector3
{
public:

	Vector3()
		:x(0), y(0), z(0) {}
	template<typename T, typename U, typename V>
	Vector3(T x, U y, V z)
	{
		this->x = (int)x;
		this->y = (int)y;
		this->z = (int)z;
	}
	Vector3 operator- (const Vector3& other)
	{
		Vector3 temp(x - other.x, y - other.y, z - other.z);
		return temp;
	}

	int x, y, z;
};

Vector3.h입니다. 여기서 x, y ,z값을 받아서 다른 클래스에서 작성될 오브젝트의 크기, 움직임 값을 입력하게 될 것입니다.

//Vector3.cpp

#include "Vector3.h"
#include "stdafx.h"

Vector3.cpp입니다. 헤더 파일만 포함하므로 넘어가겠습니다.

//SingletonBase.h

#pragma once

template<typename T>

class SingletonBase
{
public:
	static void Create()
	{
		if (instance == nullptr)
			instance = new T();
	}

	static void Delete()
	{
		delete instance;
		instance = nullptr;
	}

	static T* Get() { return instance; }

protected:
	static T* instance;
};

template<typename T> T* SingletonBase<T>::instance = nullptr;

이번엔 Singleton(싱글턴)입니다.

싱글턴 패턴은 생성자가 여러 차례 호출되어도 실제로 생성되는 객체는 하나로 만약, 계속 생성자로 호출되면 최초에 생성된 객체만 반환합니다.(싱글턴은 헤더 파일만 있고 cpp파일은 없습니다.)

//ResourceManager.h

#pragma once

class ResourceManager : public SingletonBase<ResourceManager>
{
public:
	friend class SingletonBase<ResourceManager>;

	HBRUSH& GetBrush(string brushName);
	void AddBrush(string key, COLORREF color);

private:
	ResourceManager();
	~ResourceManager();
	void InitBrushes();

	unordered_map<string, HBRUSH> brushMap;
};

ResourceManager.h입니다. 여기서는 색상을 추가하는 부분입니다.

//ResourceManager.cpp

#include "stdafx.h"
#include "ResourceManager.h"

HBRUSH& ResourceManager::GetBrush(string brushName)
{
	if (brushMap.find(brushName) != brushMap.end())
	{
		return brushMap.find(brushName)->second;
	}

	return GetBrush("White");
}

void ResourceManager::AddBrush(string key, COLORREF color)
{
	HBRUSH brush;
	brush = (HBRUSH)CreateSolidBrush(color);
	brushMap.insert(make_pair(key, brush));
}

ResourceManager::ResourceManager()
{
	InitBrushes();
}

ResourceManager::~ResourceManager()
{
	for (pair<string, HBRUSH> b : brushMap)
		DeleteObject(b.second);
}

void ResourceManager::InitBrushes()
{
	AddBrush("Red"  , RGB(255, 0  , 0  ));
	AddBrush("Green", RGB(0  , 255, 0  ));
	AddBrush("Blue",  RGB(0  , 0  , 255));
	AddBrush("White", RGB(255, 255, 255));
	AddBrush("Black", RGB(0  , 0  , 0  ));
}

ResourceManager.cpp입니다. 여기선 색상을 설정하는 코드를 작성했습니다.
8번 줄의 if문은 브러시 이름을 찾아서 넣고 만약 InitBrushes에 없는 이름이 있다면 흰색을 넣게 되는 함수입니다.
여기서 색상 설정은 했지만 실제로 실행하면 붉은색과 파란색 이외에는 사용하지 않았습니다.

(색상은 RGB 값으로 넣습니다. 색상은 따로 첨부한 링크 https://m.blog.naver.com/PostView.nhn?blogId=khd9345&logNo=222215513334&categoryNo=0&proxyReferer=

 

색상 코드와 삼원색(三原色). RGB 색상코드(10진수)와 HTML 색상코드(16진수 색상코드)

■ RGB 색상코드(10진수)와 HTML 색상코드(16진수 색상코드) ○ 10진수로 표현된 색상 코드 10진수...

blog.naver.com

여기에서 확인하시면 됩니다.

//BoundingBox.h

#pragma once

class BoundingBox
{
public:
	BoundingBox(RECT* rect);
	~BoundingBox() = default;

	static bool AABB(BoundingBox* a, BoundingBox* b);

private:
	RECT* rect;
};

BoundingBox.h입니다. 여기에서는 플레이어가 될 클래스 헤더 파일입니다.(RECT를 사용해 사각형으로 만들었습니다.)

//BoundingBox.cpp

#include "stdafx.h"
#include "BoundingBox.h"

BoundingBox::BoundingBox(RECT* rect)
	:rect(rect) {}

bool BoundingBox::AABB(BoundingBox* a, BoundingBox* b)
{
	if (a == nullptr || b == nullptr) return false;

	RECT owner = *a->rect;
	RECT other = *b->rect;

	if (owner.right  >= other.left
	 && owner.left   <= other.right
	 && owner.top    <= other.bottom
	 && owner.bottom >= other.top)
		return true;
	else
		return false;
}

BoundingBox.cpp입니다.

16번 줄부터 상하좌우에서 닿게 되는 경우 충돌로 인식하게 하는 함수입니다.(&&을 쓰는 이유는 모든 조건을 만족해야만 충돌이기 때문에 사용하였습니다.)

//Quad.h

#pragma once

class Quad
{
public:
	Quad();
	Quad(long left, long top, long right, long bottom);
	virtual ~Quad();

	void SetPosition(int x, int width);
	void SetHeight(int y, int height);
	bool CheckOutSide();

	void Update();
	void Render();

	void Move();
	void SetIntersected(bool bIntersected) { this->bIntersected = bIntersected; }

	BoundingBox* GetBox() { return box; }

private:
	class BoundingBox* box = nullptr;
	bool bIntersected = false;

protected:
	RECT rect;
};

Quad.h입니다.

여기에서는 플레이어부분을 다룹니다.
(BoundingBox와 다른 점은 BoundingBox는 플레이어를 만들었지만 Quad에서는 충돌 검사와 색상을 넣습니다.)

class BoundingBox* box = nullptr는 충돌체로서 충돌이 되었는지 안되었는지 판단합니다.

//Quad.cpp

#include "stdafx.h"
#include "Quad.h"

Quad::Quad()
{
	rect.left = rect.top = 0;
	rect.right = rect.bottom = 100;

	box = new BoundingBox(&rect);
}

Quad::Quad(long left, long top, long right, long bottom)
{
	rect.left   = left;
	rect.top    = top;
	rect.right  = right;
	rect.bottom = bottom;

	box = new BoundingBox(&rect);
}

Quad::~Quad()
{
	SAFE_DELETE(box);
}

void Quad::SetPosition(int x, int width)
{
	rect.left = x;
	rect.right = x + width;
}

void Quad::SetHeight(int y, int height)
{
	rect.top = y;
	rect.bottom = y + height;
}

bool Quad::CheckOutSide()
{
	if (rect.right <= -100)
		return true;
	else
		return false;
}

void Quad::Update()
{
	Move();
}

void Quad::Render()
{
	if (bIntersected)
		FillRect(memDC, &rect, ResourceManager::Get()->GetBrush("Red"));
	else
		FillRect(memDC, &rect, ResourceManager::Get()->GetBrush("Blue"));
}

void Quad::Move()
{
	if (Keyboard::Get()->Press('W'))
	{
		rect.bottom -= 10;
		rect.top -= 10;
	}
	else if (Keyboard::Get()->Press('S'))
	{
		rect.bottom += 10;
		rect.top += 10;
	}
	if (Keyboard::Get()->Press('A'))
	{
		rect.left -= 10;
		rect.right -= 10;
	}
	else if (Keyboard::Get()->Press('D'))
	{
		rect.left += 10;
		rect.right += 10;
	}
}

rect.bottom = y + height를 하는 이유는 API는 아래로 갈수록 커지기 때문에 y를 더합니다.

//JumpingQuad.h

#pragma once
#include "Quad.h"

class JumpingQuad : public Quad
{
public:
	JumpingQuad(long left, long top, long right, long bottom);
	~JumpingQuad();

	void Jump();
	void Update();
	void Render();

private:
	bool bJump = false;
	float vy = 0.0f;
	float jumpAccel = -1.0f;
	float jumpSpeed = 10.0f;

	bool doonce = true;

	long bottom = 0;

	bool bStart = false;
	float elapsedTime = 0.0f;
	float duration = 5.0f;

	RECT timeRect = { 0, 0, 100, 40 };
};

JumpingQuad.h 입니다.

vy는 중력으로서 떨어지는 역할을 하게 됩니다.

//JumpingQuad.cpp

#include "stdafx.h"
#include "JumpingQuad.h"

JumpingQuad::JumpingQuad(long left, long top, long right, long bottom)
	:Quad(left, top, right, bottom) {}

JumpingQuad::~JumpingQuad() {}

void JumpingQuad::Jump()
{
	if (!doonce) return;
	doonce = false;
	bJump = true;
	vy = jumpSpeed;
	bottom = rect.bottom - 10;
}

void JumpingQuad::Update()
{
	Quad::Update();

	if (elapsedTime >= duration)
	{
		if (bJump == true)
		{
			rect.bottom -= vy;
			rect.top -= vy;

			vy += jumpAccel;

			if (rect.bottom > bottom)
			{
				bJump = false;
				doonce = true;
			}
		}
		else
		{
			rect.bottom += 3;
			rect.top += 3;
		}
	}
	elapsedTime += Timer::Delta();
}

void JumpingQuad::Render()
{
	Quad::Render();
	DrawText(memDC, to_wstring(vy).c_str(), -1, &timeRect, DT_CENTER | DT_VCENTER);
}

elapsedTime >= duration을 넣은 이유는 초반에 WASD로 움직이는지 테스트 하기 위해서 시간을 벌기 위해 넣었습니다.
rect.bottom > bottom은 점프를 누른 시점의 높이까지 오면 점프를 false로 바꿉니다.

//Wall.h

#pragma once
#include "Quad.h"

class Wall
{
public:
	Wall(long x);
	~Wall();

	void Update();
	void Render();

	void RePosition(long x);
	long GetPosition() { return position; }
	bool CheckOutSide() { return lower->CheckOutSide(); }

	BoundingBox* GetUpperBox() { return upper->GetBox(); }
	BoundingBox* GetLowerBox() { return lower->GetBox(); }

private:
	Quad* upper = nullptr;
	Quad* lower = nullptr;

	int width = 100;
	int spacing = 150;

	int position = 0;
	int speed = 10;
};

Wall.h 입니다.

게임상 장애물로서 다가오는 벽을 나타냅니다.

spacing = 150은 벽 사이의 간격을 나타냅니다.

//Wall.cpp

#include "stdafx.h"
#include "Wall.h"

Wall::Wall(long x)
	:position(x)
{
	upper = new Quad();
	lower = new Quad();

	upper->SetPosition(x, width);
	lower->SetPosition(x, width);

	int upperHeight = rand() % (WinMaxHeight / 4) + (WinMaxHeight / 4);
	upper->SetHeight(0, upperHeight);
	lower->SetHeight(upperHeight + spacing, WinMaxHeight - (upperHeight + spacing));
}

Wall::~Wall()
{
	SAFE_DELETE(upper);
	SAFE_DELETE(lower);
}

void Wall::Update()
{
	position -= speed;
	upper->SetPosition(position, width);
	lower->SetPosition(position, width);
}

void Wall::Render()
{
	upper->Render();
	lower->Render();
}

void Wall::RePosition(long x)
{
	upper->SetPosition(x, width);
	lower->SetPosition(x, width);

	int upperHeight = rand() % (WinMaxHeight / 4) + (WinMaxHeight / 4);
	upper->SetHeight(0, upperHeight);
	lower->SetHeight(upperHeight + spacing, WinMaxHeight - (upperHeight + spacing));
	position = x;
}

int upperHeight = rand() % (WinMaxHeight / 4) + (WinMaxHeight / 4) 코드에서 4를 나눈 이유는 시작시 쏠림 방지를 위해서입니다.

마지막으로 실행되는 영상입니다.

긴 글 읽어주셔서 감사합니다.

'공부 > 2D강의' 카테고리의 다른 글

221128 강의  (0) 2022.11.29
221125 강의  (0) 2022.11.26
221124 강의  (0) 2022.11.25
221123 강의  (0) 2022.11.24
221122 강의  (0) 2022.11.23
Comments