저번 시간엔 말머리만 C++ 이라 달아두고 실제 내용은 C++이 아니었습니다 ㅋㅋ;;
그럼 본격적으로 C++로 코딩 해보는 시간입니다. (물론 저도 배운지 3년된 초급자라 잘 못합니다... 대충 저보다 잘하는 사람이 전 세계적으로 수만명은 있다고 생각되지만 이런 글을 쓰네요 ㅠㅠ)
주의!!
지금은 잘 안쓰는 new/delete나 C++98 스러운 코드들도 범람하고, 모던 C++도 튀어나올수 있습니다;;
여러분들은 만들때부터 스마트 포인터나, 모던 C++로 하시는게 나중에 유지보수하기 편할겁니다^^;;
(전 새 코드로 유지보수하려면 수만줄은 고쳐야 되서 포기한 케이스지만, 현업에선 해야될겁니당 ㅠㅠ)
인터페이스
#ifndef APP_H
#define APP_H
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
//네임스페이스는 쓰시고 싶으면 쓰시면 됩니다.
class Application
{
public:
private:
};
#endif
클래스 기본함수들은 만들어 주세요. (제가 혼자 다 적기에는 공간 낭비가 너무 심해여;;)
또 복사컨스트럭터나 배정오퍼레이터 등등은 금지 시켜 줘도 됩니다. (싱글턴패턴 처럼 하면 되는데 싱글턴으로 만들 필요는 없습니다.)
App 자체가 프로그램 전체에서 하나만 돌기때문에 싱글턴 패턴으로 하셔도 됩니다.
C++11 이상이 지원되는 컴파일러라면
Application(const Application& rths) = delete; 의 형식으로 구현 하시는게 더 좋은 방향입니다. (from EMC++)
또 재정의 방지를 위해 #pragma once 대신에 #ifndef #define #endif를 쓰는 이유는 OS 및 컴파일러 호환성 때문입니다.
그냥 윈도우 앱으로만 만드실거면 #pragma once도 문제는 없습니다. (나중에 게임 코드도 마찬가지)
C++17 에서는 이런거 대신에 C#이나 JAVA 처럼 using 이 도입된다고는 하는데, 안 써봐서 잘모르겠습니다 ㅠㅠ;
다음은 Initial Data 자료구조입니다.
앱을 시작할때 쓰는 기본적인 정보들입니다. 스트럭트나 클래스 어떤 걸로 만들어도 상관 없지만
그냥 public에 둬도 문제는 없습니다. 딱 한번만 쓰기때문에 별로 중요한게 아니거든요.
나중에 여러가지 파서를 통해 data 로딩을 한다면 거기서 세팅 해줘도 됩니다. 저는 JSON을 쓰는데
XML 이나 ini 어떤걸 쓰셔도 무방합니다. 아니라면 binary 파일을 직접 읽어와도 되구요.
struct init
{
const char* title; //title of window
HINSTANCE instance; //instance from winmain.
bool isFullscreen; //window is fullscreen or not
int width;//window width
int height; //window height
};
이렇게 자료구조를 만들었으면 컨스트럭터에 파라미터로 넣어서 기본값으로 해주세요.
암시적 변환을 비허용 하고싶으시면 explicit 키워들을 붙이시면 됩니다만, 딱히 필요는 없습니다.
그러면 클래스의 인터페이스에 필요한 다른 몇가지 함수들입니다.
class Application
{
public:
Application(const init& initData);
~Application(void);
//Application functions called by main
void Update(void);
//Function called in the main game loop
void ProccessMessages(void);
//Function to control the window
void Quit(void);
void SetFullScreen(bool isFullScreen);
void SetShowCursor(bool isShow);
void ChangeResolution(int width, int height);
int GetWidth(void) const;
int GetHeight(void) const;
private:
static LRESULT CALLBACK WinProc(HWND win, UINT msg, WPARAM wp, LPARAM pl);
HINSTANCE m_instance; // The instance from main
WNDCLASS m_winClass; // The windows class
DWORD m_style; // The windows style
HWND m_window; // The handle to the window
bool m_isQuitting; // flag for quitting
bool m_isFullScreen; // flag for fullscreen
int m_width; // The window width
int m_height; // The window height
//이하 생략...
};
퍼블릭 함수들을 설명하자면
Update는 게임을 돌리는 메인 루프가 됩니다.
ProcessMessage는 저번에 만든 메세지 펌프입니다. 루프에서 불러주시면 됩니다.
Quit은 게임 종료에 대한 함수입니다.
SetFullscreen은 게임의 풀스크린 여부,
SetShowWindow는 최소화여부
ShowCursor는 마우스 커서의 가시여부
ChangeResolution은 윈도우 창 크기 변경 여부. (이건 창크기만 조절 합니다. 해상도는 렌더링 엔진에서 해야하죠.)
두 게터 함수들은 창 크기의 가로 세로 사이즈를 받아옵니다.
그 아래 프라이빗 함수와 data들은 윈도우 구현을 위한 기본 정보들과
앱 클래스에서 쓰이는 정보들을 담고 있습니다. "..."으로 표현한건 필요하다고 생각되는것들은 직접 추가하시면 되기 때문에
생략 표시를 했습니다;;
ㅎ;;
구현
namespace {
const char* CLASS_NAME = "GAME_ENGINE_WINDOW";
const DWORD FULLSCREEN_STYLE = WS_POPUP | WS_VISIBLE;
const DWORD WINDOWED_STYLE = WS_POPUP | WS_CAPTION;
}//end unnamed namespace
일단 이름없는 네임스페이스를 만들어서 const 정보들을 만듭니다. 얘네들은 한번만 쓰거나
풀스크린 모드, 윈도우모드를 전환할때 씁니다. 이런건 SDL같은 라이브러리를 이용하면 만들 필요가 없습니다 (만세!)
//잡설주의
//생짜 바닥부터 만들고 있기때문에 우리는 만드는검니다... 또르르...
//다시드는 생각인데 그냥 SDL 씁시다... SDL이나 라이브러리 쓴다고 더 쉬워지는것도 아니기 때문에...
//똑같이 힘들게 바닥부터 할수 있습니다 하하. 이때까지 한건 똑같이 작업하면 되니 얼마나 좋습니까!
//위의 앱클래스에서 instance 이런거 지우고 SDL_window* window; 이렇게 넣으면 되니 얼마나 좋습니까! ㅋㅋ
또 그냥 winAPI를 통해 만들면 window 사이즈와 클라이언트 사이즈가 또 달라집니다...
예를 들어 그냥 size를 1280 x 720 을 하면 윈도우의 내부말고 다른 영역까지의 사이즈로 치기때문에...
실제 게임 스크린에 오차가 생깁니다. 그래서 AdjustWindowRect 같은 함수를 불러 줘야하는데... 으 귀찮으니 패스 하고 SDL 씁시다 ㅠㅠ
이미 라이브러리의 맛을 알아버렸기 때문에 저는 으윽...
이건 제가 예전에 쓴 코드입니다.
void AdjustAndCentered(DWORD style, RECT& size, int& xStart, int& yStart)
{
DEVMODE dm = { 0 };
int winWidth, winHeight;
//Get the size of the screen
dm.dmSize = sizeof(dm);
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm);
//Make client area of window the correct size
AdjustWindowRect(&size, style, false);
//Calculate new width and height
winWidth = size.right - size.left;
winHeight = size.bottom - size.top;
//Get start position for center
xStart = (dm.dmPelsWidth / 2) - (winWidth / 2);
yStart = (dm.dmPelsHeight / 2) - (winHeight / 2);
}
대충 이런식으로 괴롭게 만들어야하지만 라이브러리를 이용하면 편하게 만들수 있습니다.
으윽... 이친구도 이름없는 네임스페이스에 넣어주세요...
그러엄 이제부터 컨스트럭터를 만들어 봅시다... (괴롭습니다 ㅠㅠ)
Application::Application(const Init& initData)
{
m_instance = initData.instance;
m_width = initData.width;
m_height = initData.height;
m_isFullScreen = initData.isFullScreen;
m_style = (initData.isFullScreen) ? FULLSCREEN_STYLE : WINDOWED_STYLE;
m_isQuitting = false;
}
대충 이렇게 데이터를 카피시켜 주시고요,
아래에 이거 추가해 주세요.
//Set up our WNDCLASS (mostly defaults)
m_winClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; //for OpenGl
m_winClass.cbClsExtra = 0;
m_winClass.cbWndExtra = 0;
m_winClass.lpszMenuName = 0;
m_winClass.hIcon = LoadIcon(0, IDI_APPLICATION);
m_winClass.hCursor = LoadCursor(0, IDC_ARROW);
m_winClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
m_winClass.hInstance = initData.instance; //Instance from Main
m_winClass.lpszClassName = CLASS_NAME; //Window class name
m_winClass.lpfnWndProc = WinProc; //Static Member Proc Func
//Register our window class
RegisterClass(&m_winClass);
저번에 만들었던 프로시저 함수도 가져 오시고,
클래스도 등록시켜줍시다.
style 에 있는친구중에 CS_HREDRAW 이랑 CS_VREDRAW는 가로랑 세로로 다시그릴때 쓰는 기본 값들입니다.
CS_OWNDC는 다른 그래픽 API를 쓰기때문에 쓴거구요. 보통의 앱들은 매 프레임마다 다시 그릴필요가 없지만...
게임은 다시 그리기 때문이죠 ㅠㅠ
윈도우에서 뭐든 윈도우에 그리려면 DC(device context)에 request를 해야하는데 (한글 번역이 청탁인데 이건 아닌거 같아염;;)
일반적인 프로그램은 따로 드로잉 리소스를 보유할 필요가 없지만, 게임의 경우는 매 프레임 마다 그려야 하기때문에
DC를 받아와야 합니다. 그래서 OwnDC라는 플래그를 넣는거구요.
hInstance는 저번에 윈메인에서 봤던 그 친구.
lpszClassName은 저도 잘모르겠지만 이 클래스를 알아보는 ID 같은거라 보면 됩니다. 나중에 이걸 unregister 할때 씁니다.
lpfnWndProc은 저번에 대충 만들다만 프로시저 함수포인터입니다.
그리고 그 아랫줄에 이거도 추가해주세요.
int xStart;
int yStart;
RECT size = { 0 };
size.right = initData.width;
size.bottom = initData.height;
AdjustAndCentered(m_style, size, xStart, yStart);
아까 만든 함수로 윈도우의 사이즈와 위치를 조절시켜줍니다.
그럼 이제 윈도우를 만듭니다.
m_window = CreateWindow(
CLASS_NAME, /*Class name*/
initData.title, /*Window Title*/
m_style, /*Window Style*/
xStart, /*x Starting pos*/
yStart, /*y Starting Pos*/
size.right - size.left, /*Width*/
size.bottom - size.top, /*Height*/
0, /*Parent Window*/
0, /*Menu*/
m_instance, /*HInstance*/
this /*Lparam This will be available in WM_CREATE*/
);
이제 마지막으로
if (m_isFullScreen == true) SetFullScreen(m_isFullScreen);
ShowWindow(m_window, true);
UpdateWindow(m_window);
까지 추가해줍니다.
또 이 사이사이에 사운드 라이브러리나, 그래픽라이브러리의 Initialize도 해줍니다.
여기까지 컨스트럭터였습니다. 함수를 통해 여러부분으로 나눠서 해도 되고 이렇게 한곳에 다 둬도 되지만
디버깅을 용이하게 하기위해선 함수로 나눠두는 습관이 좋습니다. 저는 한 저걸 5단계로 나눈거 같군요.
그리고 SDL이나 라이브러리를 쓰면 코드 한두줄로 처리할수 있는 initialze 파트였습니다 ;;
다음은 디스트럭터입니다.
Application::~Application(void)
{
UnregisterClass(CLASS_NAME, m_instance);
m_instance = 0;
}
아까 말한 언레지스터로 instance를 비워줍니다. destroy window는 윈도우 프로시저에서 해줄거기때문에 여기서 안하고 넘어갑시다.
이제 다른 함수들입니다.
void Application::Update(void)
{
while (!m_isQuitting)
{
ProccessMessages();
//Update Game State Mangager
//if Game State Manager is Quitting, set Close message
}
}
대충 이런식으로 업데이트 함수를 처리해줍시다. 프로세스 메세지 함수는 이겁니다.
저번에 봤던 메세지 펌프죠.
void Application::ProccessMessages(void)
{
MSG message;
while (PeekMessage(&message, m_window, 0, 0, PM_REMOVE))
{
TranslateMessage(&message);
DispatchMessage(&message);
}
}
이친구는 업데이트 루프에서 돌려야합니다. 지금은 업데이트 루프가 앱 업데이트에 있으니 여기서 돌리지만,
나중에 게임 스테이트 매니저 (이런 비슷한것들)를 만들게 된다면 그곳에서 돌려주시면 됩니다.
그럼 다른 별로 안중요한 함수들입니다.
void Application::Quit(void)
{
SendMessage(m_window, WM_CLOSE, 0, 0);
}
게임을 종료하는 함수입니다. 윈도우에다 이제 이 게임은 끝났다고 말하는거죠. 학교다닐때 쉬는시간 종소리 같은 느낌이죠. ㅋㅋㅋ
기타 게터함수나 세터함수는 생략 하겠습니다. 궁금한건 MSDN에 다 있으니 거기서 찾으시면 빠르게 정확하고 올바르게 할수 있습니다;;
(본격 날로먹는중 ㅋㅋ)
이제 구현부분의 마지막 핵심 함수입니다.
이때까지 자주 언급되던 프로시저 함수입니다...
LRESULT CALLBACK Application::WinProc(HWND win, UINT msg, WPARAM wp, LPARAM lp)
{
static Application* s_pApp = nullptr;
switch (msg)
{
case WM_CREATE:
{
CREATESTRUCT* pCS = reinterpret_cast<CREATESTRUCT*>(lp);
s_pApp = reinterpret_cast<Application*>(pCS->lpCreateParams);
//Create graphics and so on...
break;
}
case WM_DESTROY:
{
s_pApp->m_isQuitting = true;
s_pApp->m_window = 0;
PostQuitMessage(0);
break;
}
case WM_CLOSE:
{
//First destroy the graphics
//Then destroy the window
DestroyWindow(win);
break;
}
default:
return DefWindowProc(win, msg, wp, lp);
}
return 0;
}
대충 지금은 이렇습니다. 이제 create 메세지와
destroy 메세지, close 메세지 정도는 우리가 컨트롤 하는겁니다...
뭐 다른 메세지들도 추가되는데 다음 시간에 뭘 할지 부터 고민 해야 될듯 하군요... (Api를 SDL로 바꿔버리는게 정신건강에 이로울거 같은 예감이네요;;)
이제 APP Layer에서 할일은 거의 끝났습니다. 키보드와 마우스 인풋을 받고, (추가 하고 싶다면 엑박패드 컨트롤러라던지) 등등의 인풋 받기,
위에서 가끔 언급한 게임 스테이트 매니저 정도가 남아있는거 같군요... 사실 여기까지가 다른 라이브러리를 이용하는거랑 유일 하게 다른 부분이라 볼수 있죠.
음 이런 비슷한 내용이 궁금하시면 유튜브나 구글에 핸드메이드 히어로라고 멋진 동영상 튜토리얼이 있습니다.
대충 1강의당 1시간 정도 하고, (말이 빠르셔서 좀 힘들수도 있지만 발음이 되게 좋아서 영어공부도 되죠.)
지금은 354강까지 나갔더군요...
제 허접한 튜토리얼이랑은 다르게 이분은 진짜 프로페셔널 개발자분이 하시는 강의라 좋은 강의입니다.
===============================
임시 저장하고 다시 읽어 보다가 이거 하나 빠져먹었더군요... ㅈㅅ함니다;;
풀스크린 설정하는 함수인데, 이걸 잊고 그냥 넘어가다니;; 컴파일도 못하실뻔 하셨습니다;; ㅈㅅㅈㅅ...
void Application::SetFullScreen(bool fullScreen)
{
/*Create variables to adjust window size and start position*/
RECT rect = { 0, 0, m_width, m_height };
int xStart = 0, yStart = 0;
m_isFullScreen = fullScreen;
/*Check if we are going into full screen or not*/
if (fullScreen)
{
/*Get the current display settings*/
DEVMODE dmScreenSettings;
dmScreenSettings.dmSize = sizeof(dmScreenSettings);
EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dmScreenSettings);
/*Change the resolution to the resolution of my window*/
dmScreenSettings.dmPelsWidth = m_width;
dmScreenSettings.dmPelsHeight = m_height;
dmScreenSettings.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
/*Make sure my window style is full screen*/
m_style = FULLSCREEN_STYLE;
/*Check if it worked. If it didn't set to window mode.*/
if (ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
{
m_isFullScreen = false;
m_style = WINDOWED_STYLE;
ChangeDisplaySettings(NULL, 0);/*If So Switch Back To The Desktop*/
//DebugTools::MyMessageBox("FullScreen is not supported. You are being switched to Windowed Mode");
}
}
else
{
ChangeDisplaySettings(NULL, 0);/*If So Switch Back To The Desktop*/
/*Make sure I am in windows style*/
m_style = WINDOWED_STYLE;
}
/*This will change my windows style*/
SetWindowLong(m_window, GWL_STYLE, m_style);
/*This will make window the correct size and find the start position*/
AdjustAndCenter(m_style, rect, xStart, yStart);
/*This changes my window size and start position*/
MoveWindow(m_window,
xStart,
yStart,
rect.right - rect.left,
rect.bottom - rect.top,
TRUE);
/*This is required after SetWindowLong*/
ShowWindow(m_window, SW_SHOWNORMAL);
/*This sets my window to the front.*/
SetForegroundWindow(m_window);
}
이거도 추가해주세요...
void Application::ChangeResolution(int width, int height)
{
m_width = width;
m_height = height;
SetFullScreen(m_isFullScreen);
}
void Application::SetShowCursor(bool show)
{
ShowCursor(show);
}
이제 진짜 끝...
그럼 다음에 봐요 ㅜ