관리 메뉴

cyphen156

Don't Starve 모작 프로젝트 with Win32 API #3-2 게임 프로젝트를 위한 풀스크린 모드, 프레임 레이트 출력하기 본문

프로젝트/dont'starve 모작

Don't Starve 모작 프로젝트 with Win32 API #3-2 게임 프로젝트를 위한 풀스크린 모드, 프레임 레이트 출력하기

cyphen156 2024. 10. 10. 14:29

우선 프로젝트 코드를 정리하자. 

다음과 같이 정리하면 깔끔해진다.

+++pch.h 관련 설정 추가

// dontstarveCopy.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//
#include "pch.h"
#include "framework.h"
#include "dontstarveCopy.h"

#define MAX_LOADSTRING 100
#define WSREGULAR 5000
#define WSLARGE 10000

// 전역 변수:
HINSTANCE hInst;                                // 현재 인스턴스입니다.
HWND hWnd;                                      // 메인 윈도우
WCHAR szTitle[MAX_LOADSTRING];                  // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING];            // 기본 창 클래스 이름입니다.

// 커스텀 전역변수
RECT worldSize;
RECT windowInfo;
POINT charWorldPos;
POINT charWinPos;
DWORD lastFrameTime = 0;

// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

void SetWorldSize(bool isLarge) {
    if (isLarge) {
        worldSize = { 0, 0, WSLARGE, WSLARGE };
    }
    else {
        worldSize = { 0, 0, WSREGULAR, WSREGULAR };
    }
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // 전역 문자열을 초기화합니다.
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_DONTSTARVECOPY, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // 애플리케이션 초기화를 수행합니다:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_DONTSTARVECOPY));

    MSG msg;

    // TODO: 여기에 코드를 입력합니다.
    // 커스텀 전역변수 초기화
    SetWorldSize(true);  // true면 Large, false면 Regular
    GetWindowRect(hWnd, &windowInfo);   // 애플리케이션 실행시 윈도우 사이즈 가져오기
    charWinPos = { (windowInfo.bottom - windowInfo.top) / 2, (windowInfo.right - windowInfo.left) / 2 };              // 기본 좌표 : 화면정중앙
    charWorldPos = { (worldSize.bottom - worldSize.top) / 2, (worldSize.right - worldSize.left) / 2};            // 기본 좌표 : 월드 정중앙

    // 메세지 루프
    while (1)
    {
        // 사용자 입력 처리
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
        // 분기에 걸리지 않는 백그라운드 프로세싱 작성
        else 
        {

        }
    }

    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(NULL); // 메뉴 ON : 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;
   }
   // 풀스크린 모드로 설정
   SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
   int screenWidth = GetSystemMetrics(SM_CXSCREEN);
   int screenHeight = GetSystemMetrics(SM_CYSCREEN);
   SetWindowPos(hWnd, HWND_TOP, 0, 0, screenWidth, screenHeight, SWP_NOZORDER | SWP_FRAMECHANGED);

   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_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);

            EndPaint(hWnd, &ps);
        }
        break;
    
    case WM_DESTROY:
        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;
}


// 매초 60번의 렌더링을 위한 타이머 호출
void RenderFrame() {
    // 화면 갱신 요청
    InvalidateRect(hWnd, NULL, FALSE);
}

게임을 만들 때 필요한 것은 뭐가 있을까? 고민해보면 기본적으로 사각형모양의 월드맵 크기, 현재 캐릭터의 좌표, 윈도우 크기, 윈도우 내에서의 내 캐릭터의 좌표 정도가 필요하다. 이것들은 프로그램 내부에서 항상 존재해야 하기 때문에 전역변수로 설정하여 데이터섹션에 저장한다.

// 커스텀 전역변수
RECT worldSize;
RECT windowInfo;
POINT charWorldPos;
POINT charWinPos;

//풀스크린 설정용 키 변수
BOOL isFScreen;

그리고 추가로 필요한것은 캐릭터 외의 오브젝트들의 좌표, 그리고 월드맵을 구성할 텍스쳐들에 대한 좌표 정도가 될 것 같은데 이것은 나중에 하도록 한다.

우선 월드 사이즈 구현이다. Regular, Large의 두 옵션을 준다고 가정하자. 이것은 바로 아래 매크로를 통해 사용자가 선택하여 만들 수 있게 구현해두겠다.

#define WSLARGE 10000;
#define WSREGULAR 5000;

그리고 좌표를 모두 초기화 해준다. 

    // TODO: 여기에 코드를 입력합니다.
    // 커스텀 전역변수 초기화
    SetWorldSize(true);  // true면 Large, false면 Regular
    GetWindowRect(hWnd, &windowInfo);   // 애플리케이션 실행시 윈도우 사이즈 가져오기
    charWinPos = { (windowInfo.bottom - windowInfo.top) / 2, (windowInfo.right - windowInfo.left) / 2 };              // 기본 좌표 : 화면정중앙
    charWorldPos = { (worldSize.bottom - worldSize.top) / 2, (worldSize.right - worldSize.left) / 2};            // 기본 좌표 : 월드 정중앙
    
     // 풀스크린 변수 초기화
     isFScreen = true;

화면을 풀스크린으로 만들어볼거다.

우선 메뉴창 없애려면 MyRegisterClass함수를 다음과 같이 코드를 수정한다.

    wcex.lpszMenuName   = MAKEINTRESOURCEW(NULL); // 메뉴 ON : IDC_DONTSTARVECOPY

그리고 InitInstance함수를 다음과 같이 풀스크린모드 코드를 추갛나다.

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;
   }
   // 풀스크린 모드로 설정
   SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
   int screenWidth = GetSystemMetrics(SM_CXSCREEN);
   int screenHeight = GetSystemMetrics(SM_CYSCREEN);
   SetWindowPos(hWnd, HWND_TOP, 0, 0, screenWidth, screenHeight, SWP_NOZORDER | SWP_FRAMECHANGED);

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

그리고 풀스크린 모드를 해제하는 키를 추가하도록 하겠다.

풀스크린 설정/해제 키는 F11로 지정하도록 하겠다.

case WM_KEYDOWN:
    if (wParam == VK_F11)  // F11 키 입력 시 처리
    {
        if (isFScreen) {
            // 창 모드로 전환 (HD : 1280 X 720)
            SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
            SetWindowPos(hWnd, HWND_TOP, 100, 100, 800, 600, SWP_NOZORDER | SWP_FRAMECHANGED);
            isFScreen = false;
        }
        else {
            // 풀스크린 모드로 전환
            SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
            int screenWidth = GetSystemMetrics(SM_CXSCREEN);
            int screenHeight = GetSystemMetrics(SM_CYSCREEN);
            SetWindowPos(hWnd, HWND_TOP, 0, 0, screenWidth, screenHeight, SWP_NOZORDER | SWP_FRAMECHANGED);
            isFScreen = true;
        }
    }
    break;

이제 프레임 레이트를 연산하여 화면 좌상단에 출력하겠다. 

이 프레임 레이트는 else 구문 내에서 처리될 예정이다.

1초 마다 프레임 레이트를 출력하는데, 인스턴스 렌더링이 모두 완료됨을 의미하는 Rendering함수가 호출된 횟수를 출력하고 0으로 초기화 하면 될것 같다. Rendering()은 일단 임시함수로 설정하겟다.

전역변수 선언부에 다음과 같이 변수를 선언한다.

// 렌더링 횟수 카운트용 변수
int iRenderCnt = 0;
DWORD lastRenderTime = 0;

그리고 함수는 다음과 같이 작성한다.

void Rendering()
{
    ++iRenderCnt;
}

마지막으로 else 구문에선 다음과 같이 작업을 수행한다.

Rendering();

// 현재 시간 가져옴
DWORD currentTime = GetTickCount();

// 프레임 레이트 출력
if (currentTime - lastRenderTime >= 1000)
{
    HDC hdc = GetDC(hWnd);
    TCHAR fps[10];
    wsprintf(fps, L"FPS: %d", iRenderCnt);
    TextOut(hdc, 10, 10, fps, lstrlen(fps));
    ReleaseDC(hWnd, hdc);

    iRenderCnt = 0;
    lastRenderTime = currentTime;
}

보면 프레임레이트가 미쳐날뛴다.  우리의 목적은 이것을 초당 60번 이하로 떨어지지 않게 하는 것이다.

전체 코드는 다음과 같다.

DST_3-2.cpp

// dontstarveCopy.cpp : 애플리케이션에 대한 진입점을 정의합니다.
//
#include "pch.h"
#include "framework.h"
#include "dontstarveCopy.h"

#define MAX_LOADSTRING 100
#define WSREGULAR 5000
#define WSLARGE 10000

// 전역 변수:
HINSTANCE hInst;                                // 현재 인스턴스입니다.
HWND hWnd;                                      // 메인 윈도우
WCHAR szTitle[MAX_LOADSTRING];                  // 제목 표시줄 텍스트입니다.
WCHAR szWindowClass[MAX_LOADSTRING];            // 기본 창 클래스 이름입니다.

// 커스텀 전역변수
RECT worldSize;
RECT windowInfo;
POINT charWorldPos;
POINT charWinPos;
DWORD lastFrameTime = 0;

// 풀스크린 설정용 키 변수
BOOL isFScreen;

// 렌더링 횟수 카운트용 변수
int iRenderCnt = 0;
DWORD lastRenderTime = 0;

// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

// 월드 크기 지정
void SetWorldSize(bool isLarge);

// 화면에 렌더링하기
void Rendering();

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // 전역 문자열을 초기화합니다.
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_DONTSTARVECOPY, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // 애플리케이션 초기화를 수행합니다:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_DONTSTARVECOPY));

    MSG msg;

    // TODO: 여기에 코드를 입력합니다.
    // 커스텀 전역변수 초기화
    SetWorldSize(true);  // true면 Large, false면 Regular
    GetWindowRect(hWnd, &windowInfo);   // 애플리케이션 실행시 윈도우 사이즈 가져오기
    charWinPos = { (windowInfo.bottom - windowInfo.top) / 2, (windowInfo.right - windowInfo.left) / 2 };              // 기본 좌표 : 화면정중앙
    charWorldPos = { (worldSize.bottom - worldSize.top) / 2, (worldSize.right - worldSize.left) / 2};            // 기본 좌표 : 월드 정중앙

    // 풀스크린 변수 초기화
    isFScreen = true;

    
    // 메세지 루프
    while (1)
    {
        // 사용자 입력 처리
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
        // 분기에 걸리지 않는 백그라운드 프로세싱 작성
        else 
        {
            Rendering();

            // 현재 시간 가져옴
            DWORD currentTime = GetTickCount64();

            // 프레임 레이트 출력
            if (currentTime - lastRenderTime >= 1000)
            {
                HDC hdc = GetDC(hWnd);
                TCHAR fps[10];
                wsprintf(fps, L"FPS: %d", iRenderCnt);
                TextOut(hdc, 10, 10, fps, lstrlen(fps));
                ReleaseDC(hWnd, hdc);

                iRenderCnt = 0;
                lastRenderTime = currentTime;
            }
        }
    }

    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(NULL); // 메뉴 ON : 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;
   }
   // 풀스크린 모드로 설정
   SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
   int screenWidth = GetSystemMetrics(SM_CXSCREEN);
   int screenHeight = GetSystemMetrics(SM_CYSCREEN);
   SetWindowPos(hWnd, HWND_TOP, 0, 0, screenWidth, screenHeight, SWP_NOZORDER | SWP_FRAMECHANGED);

   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_KEYDOWN:
        if (wParam == VK_F11)
        {
            if (isFScreen) {
                // 창 모드로 전환 (HD : 1280 X 720)
                SetWindowLong(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
                SetWindowPos(hWnd, HWND_TOP, 100, 100, 800, 600, SWP_NOZORDER | SWP_FRAMECHANGED);
                isFScreen = false;
            }
            else {
                // 풀스크린 모드로 전환
                SetWindowLong(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
                int screenWidth = GetSystemMetrics(SM_CXSCREEN);
                int screenHeight = GetSystemMetrics(SM_CYSCREEN);
                SetWindowPos(hWnd, HWND_TOP, 0, 0, screenWidth, screenHeight, SWP_NOZORDER | SWP_FRAMECHANGED);
                isFScreen = true;
            }
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);

            EndPaint(hWnd, &ps);
        }
        break;
    
    case WM_DESTROY:
        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;
}


void SetWorldSize(bool isLarge) {
    if (isLarge) {
        worldSize = { 0, 0, WSLARGE, WSLARGE };
    }
    else {
        worldSize = { 0, 0, WSREGULAR, WSREGULAR };
    }
}

void Rendering()
{
    ++iRenderCnt;
}

실습 코드는 다음 경로에 있습니다.

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

 

DontStarveResource - Google Drive

이 브라우저 버전은 더 이상 지원되지 않습니다. 지원되는 브라우저로 업그레이드하세요. 닫기

drive.google.com