cyphen156
Don't Starve 모작 프로젝트 with Win32 API #3 사용자 입력 없이 항상 돌아가는 프로그램 작성하기 본문
Don't Starve 모작 프로젝트 with Win32 API #3 사용자 입력 없이 항상 돌아가는 프로그램 작성하기
cyphen156 2024. 10. 8. 00:37현재 윈도우 기본 메세지 루프를 사용하고 있다. 이 방식의 최대 단점은 사용자 입력이 윈도우에서 발생해야만 프로그램이 동작하고, 그 외에는 무한정 대기하고 있다는 것이다.
잠깐 생각해보면 백그라운드 작업시 돌아가지 않는 게임을 생각할 수 있다. 윈도우가 최상단에서 실행되고 있어 무효화 영역이 발생하지 않을때만 프로그램이 동작하고, 조금이라도 무효화 영역이 발생하거나, 백그라운드로 넘어가면 일시정지되어 프로그램이 멈추는 것들을 생각하면 될 것같다.
최종 완성된 결과물은 다음과 같이 동작한다.
이러한 원인이 발생되는 함수가 다음 반복문에 존재하는 조건 GetMessage()함수의 사용때문이다.
// 기본 메시지 루프입니다:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
GetMessage()함수는 기본적으로 항상 1를 반환하다가, 특정조건(WM_QUIT)을 만나면 0을 반환하는 Bool 반환 함수이다.
그런데 PeekMessage()함수는 반대로 0을 반환하다가 메세지 큐에 입력이 들어오면 1을 반환하여 이벤트 트리거로서 사용된다.
반복문을 다음과 같이 수정하면 된다.
while (1)
{
// 사용자 입력 처리
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (WM_QUIT == msg.message)
{
break;
}
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 분기에 걸리지 않는 백그라운드 프로세싱 작성
else
{
}
}
이제 구체적인 동작을 하도록 작성해보자.
우선 initInstance함수에 선언된 hWnd 변수를 전역적으로 사용할 수 있도록 전역변수
HWND hWnd를 선언하고, initInstance함수 내부에서 변수 선언부를 제거하고 대입연산으로 변경시키자.
왜 이런짓을 했냐면 무한반복을 돌건데 nullptr로 invalidRect함수를 첫 인자를 nullptr로 전달하면 이 프로그램 윈도우에서 작동하는게아니라 모든 윈도우에서 작동되기 때문에 컴퓨터가 멈춘다. 그것을 방지하기 위해 initInstance 내부에 선언된 hwnd 변수를 코드상단부에 전역변수로 선언하여 사용한것이다.
#include "framework.h"
#include "dontstarveCopy.h"
static HWND hWnd;
// 좌표 변수
static POINT point;
static BOOL reverse;
static POINT endPos;
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
그리고 윈도우 내부에서 밖으로 사라지는 현상을 방지하기 위해 윈도우 사이즈를 측정해온다. 이것을 위한 변수가 Rect hWndInfo변수이다.
이 변수의 값은 윈도우 크기가 변경될 때 마다 바뀌어야 하므로, 정적변수로 선언, case WM_SIZE:에서 이벤트를 처리한다.
case WM_SIZE:
// 윈도우 크기 변경 시 RECT 값 업데이트
GetWindowRect(hWnd, &hWndInfo);
endPos.x = hWndInfo.right - hWndInfo.left;
endPos.y = hWndInfo.bottom - hWndInfo.top;
break;
그 다음 비트맵 이미지를 화면에 계속해서 렌더링 하는데, point의 좌표를 reverse == true일 때는 x++, y++; !reverse일 때에는 x--, y--로 자동으로 이동하도록 구현하였다. 이 때 윈도우 밖으로 튀어나가 오브젝트가 안보이는것을 방지하기 위한 조건을 추가한다.
그리고나서 InvalidateRect를 통해 무효화 영역을 발생시켜 다시 그린다.
else
{
if (reverse)
{
if (point.x + scaledWidth < endPos.x - (scaledWidth / 2))
{
++point.x;
}
if (point.y + scaledHeight < endPos.y - scaledHeight)
{
++point.y;
}
}
else if (!reverse)
{
if (point.x > 0)
--point.x;
if (point.y > 0)
--point.y;
}
InvalidateRect(hWnd, NULL, FALSE);
}
이것만으론 부족하다 아까 윈도우 크기가 변경될 수 있을 가능성을 주었다.
현재의 코드는 배경 픽셀을 단 한번만 색칠하고 있기 때문에 윈도우 사이즈가 변경되도 배경이 채워지지 않아 캐릭터가 사라진다.
이것을 수정하기 위한 변수 BOOL changedhWndSize를 전역변수로 선언하고 WM_SIZE: 가 동작할 때마다 이 값을 변경해주자.
BOOL changedhWndSize;
case WM_SIZE:
// 윈도우 크기 변경 시 RECT 값 업데이트
GetWindowRect(hWnd, &hWndInfo);
endPos.x = hWndInfo.right - hWndInfo.left;
endPos.y = hWndInfo.bottom - hWndInfo.top;
changedhWndSize = true;
break;
그리고 WM_PAINT: 또한 변경이 필요해진다.
// MemDC가 처음일 때만 생성
if (MemDC == NULL) {
MemDC = CreateCompatibleDC(hdc);
HBITMAP hMemBitmap = CreateCompatibleBitmap(hdc, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top);
oldBitmap = (HBITMAP)SelectObject(MemDC, hMemBitmap);
}
else if (changedhWndSize)
{
MemDC = CreateCompatibleDC(hdc);
HBITMAP hMemBitmap = CreateCompatibleBitmap(hdc, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top);
oldBitmap = (HBITMAP)SelectObject(MemDC, hMemBitmap);
changedhWndSize = false;
}
최종 코드는 다음과 같습니다.
DST_3.cpp
// dontstarveCopy.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//
#include "framework.h"
#include "dontstarveCopy.h"
#include "Gdiplus.h"
#pragma comment (lib, "gdiplus")
using namespace Gdiplus;
static HWND hWnd;
// GDIPlus 토큰 초기화
ULONG_PTR gdiplusToken;
// 좌표 변수
static POINT point;
static BOOL reverse;
static POINT endPos;
static int scaledWidth;
static int scaledHeight;
#define MAX_LOADSTRING 100
// 전역 변수:
HINSTANCE hInst; // 현재 인스턴스입니다.
WCHAR szTitle[MAX_LOADSTRING]; // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING]; // 기본 창 클래스 이름입니다.
// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
HDC MemDC;
HBITMAP hBitmap, oldBitmap;
HBITMAP hBitmaps[5];
BOOL changedhWndSize;
static RECT hWndInfo;
static unsigned int iFrame = 1;
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
// GDI+ 초기화
GdiplusStartupInput gpsi;
GdiplusStartup(&gdiplusToken, &gpsi, nullptr);
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 여기에 코드를 입력합니다.
//
//
// 전역 문자열을 초기화합니다.
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_DONTSTARVECOPY, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 애플리케이션 초기화를 수행합니다:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// 기본 좌표 설정
point = { 0, 0 };
// 이미지 역전 설정
reverse = true;
changedhWndSize = false;
// 비트맵 로드
//
/*hBitmaps[0] = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));
hBitmaps[1] = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP2));
hBitmaps[2] = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP3));
hBitmaps[3] = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP4));
hBitmaps[4] = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP5));*/
/*for (int i = 0; i < 5; ++i)
{
if (hBitmaps[i] == NULL) {
MessageBox(NULL, L"비트맵을 로드할 수 없습니다.", L"Error", MB_OK);
}
}*/
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_DONTSTARVECOPY));
MSG msg;
// 메세지 루프
while (1)
{
// 사용자 입력 처리
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (WM_QUIT == msg.message)
{
break;
}
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 분기에 걸리지 않는 백그라운드 프로세싱 작성
else
{
if (reverse)
{
if (point.x + scaledWidth < endPos.x)
{
++point.x;
}
if (point.y + scaledHeight < endPos.y - (scaledHeight / 2))
{
++point.y;
}
}
else if (!reverse)
{
if (point.x > 0)
--point.x;
if (point.y > 0)
--point.y;
}
InvalidateRect(hWnd, NULL, FALSE);
}
}
// GDI Plus 해제
GdiplusShutdown(gdiplusToken);
return (int) msg.wParam;
}
//
// 함수: MyRegisterClass()
//
// 용도: 창 클래스를 등록합니다.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_DONTSTARVECOPY));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_DONTSTARVECOPY);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 함수: InitInstance(HINSTANCE, int)
//
// 용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다.
//
// 주석:
//
// 이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고
// 주 프로그램 창을 만든 다음 표시합니다.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 함수: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 용도: 주 창의 메시지를 처리합니다.
//
// WM_COMMAND - 애플리케이션 메뉴를 처리합니다.
// WM_PAINT - 주 창을 그립니다.
// WM_DESTROY - 종료 메시지를 게시하고 반환합니다.
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 메뉴 선택을 구문 분석합니다:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_SIZE:
// 윈도우 크기 변경 시 RECT 값 업데이트
GetWindowRect(hWnd, &hWndInfo);
endPos.x = hWndInfo.right - hWndInfo.left;
endPos.y = hWndInfo.bottom - hWndInfo.top;
changedhWndSize = true;
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// MemDC가 처음일 때만 생성
if (MemDC == NULL) {
MemDC = CreateCompatibleDC(hdc);
HBITMAP hMemBitmap = CreateCompatibleBitmap(hdc, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top);
oldBitmap = (HBITMAP)SelectObject(MemDC, hMemBitmap);
}
else if (changedhWndSize)
{
MemDC = CreateCompatibleDC(hdc);
HBITMAP hMemBitmap = CreateCompatibleBitmap(hdc, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top);
oldBitmap = (HBITMAP)SelectObject(MemDC, hMemBitmap);
changedhWndSize = false;
}
// 백 버퍼(MemDC)에 배경 그리기
FillRect(MemDC, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1));
// 미리 로드된 비트맵을 백 버퍼에 그리기
hBitmap = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1 + (iFrame / 5)));
HDC hTempDC = CreateCompatibleDC(MemDC);
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hTempDC, hBitmap);
BITMAP bitmap;
GetObject(hBitmap, sizeof(BITMAP), &bitmap);
int originalWidth = bitmap.bmWidth;
int originalHeight = bitmap.bmHeight;
scaledWidth = originalWidth / 10;
scaledHeight = originalHeight / 10;
// 비트맵을 백 버퍼에 그리기
if (reverse) {
StretchBlt(MemDC, point.x + scaledWidth, point.y, -scaledWidth, scaledHeight, hTempDC, 0, 0, originalWidth, originalHeight, SRCCOPY);
}
else {
StretchBlt(MemDC, point.x, point.y, scaledWidth, scaledHeight, hTempDC, 0, 0, originalWidth, originalHeight, SRCCOPY);
}
// 백 버퍼(MemDC) 내용을 화면(hdc)에 복사
BitBlt(hdc, 0, 0, ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top, MemDC, 0, 0, SRCCOPY);
// 자원 해제
SelectObject(hTempDC, hOldBitmap);
DeleteDC(hTempDC);
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
switch (wParam)
{
case VK_LEFT:
{
reverse = false;
point.x -= 5;
if (iFrame > 1)
{
--iFrame;
}
break;
}
case VK_RIGHT:
{
reverse = true;
point.x += 5;
if (iFrame < 24)
{
++iFrame;
}
break;
}
case VK_UP:
point.y -= 5;
break;
case VK_DOWN:
point.y += 5;
break;
}
InvalidateRect(hWnd, NULL, FALSE);
break;
case WM_DESTROY:
SelectObject(MemDC, oldBitmap);
for (int i = 0; i < 5; ++i) {
DeleteObject(hBitmaps[i]);
}
DeleteDC(MemDC);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// 정보 대화 상자의 메시지 처리기입니다.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
실습 코드는 다음 경로에 있습니다.
GitHub - cyphen156/Win32API
Contribute to cyphen156/Win32API development by creating an account on GitHub.
github.com
+++리소스 폴더가 용량이 너무 커서 구글드라이브로 대체합니다.
https://drive.google.com/drive/folders/1QGqzlj973L0dYcrACfHqXE9IgCX2qwYp?usp=sharing
Google Drive: 로그인
이메일 또는 휴대전화
accounts.google.com
'프로젝트 > dont'starve 모작' 카테고리의 다른 글
Don't Starve 모작 프로젝트 with Win32 API #3-2 게임 프로젝트를 위한 풀스크린 모드, 프레임 레이트 출력하기 (5) | 2024.10.10 |
---|---|
Don't Starve 모작 프로젝트 with Win32 API #2 키입력을 통한 리소스 변환 렌더링 (0) | 2024.10.08 |
Don't Starve 모작 프로젝트 with Win32 API #1 리소스 추출 (6) | 2024.10.08 |