관리 메뉴

cyphen156

Don't Starve 모작 프로젝트 with Win32 API #3 사용자 입력 없이 항상 돌아가는 프로그램 작성하기 본문

프로젝트/dont'starve 모작

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

 

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