게임의 뼈대라고 할 수 있는 프레임워크 세팅 작업을 시작해보자. 사용자 인풋 처리부터 시작한다.
사용자 입력과 처리를 위한 루틴
게임은 많은 장르와 형식을 가지고 있다. 액션게임, 슈팅게임, 전략 시뮬레이션 게임, 건설 시뮬레이션 게임 그리고 우리가 만들려는 롤플레잉 게임까지 말이다. 너무 다양해서 각 게임들 간에는 상이한 점이 많지만 모든 게임들은 몇 가지 공통점을 가지고 있다. 이는 아래와 같다.
- 게임은 사용자의 입력을 받는다.
- 게임은 그 입력 내용을 매순간순간 처리한다
- 처리된 결과에 따라 바뀐 상황을 매 순간순간 재 출력해준다.
여기서 중요한 건 매 순간순간이라는 단어다. 우리가 방향키를 눌렀을 때 우리의 주인공 캐릭터는 그 방향에 따라 움직여 야 한다. 이때 매 순간순간 캐릭터는 아주 조금씩 좌표가 이동하며 이런 이동 처리 내용이 누적되면서 캐릭터가 화면에서 부드럽게 움직이는 것처럼 보인다. 이 순간순간의 단위를 프레임이라고 하며 보통 1초에 몇 번의 프레임을 처리할 수 있는지는 게임의 완성도와 직결된다.
보통 60 프레임 이상되면 (1초에 60번 처리) 유저는 매우 부드러운 게임의 움직임들을 느끼며 이 이상의 프레임 처리를 해봐야 의미가 없다. 보통 게임들은 30~40 프레임 정도를 유지하도록 설계된다.
만약 25 프레임 아래로 내려갈 만큼 게임이 무거워지면 게임이 뚝뚝 끊기는 느낌이 들기 시작하며 게임을 가볍게 만들기 위한 최적화 작업이 필요할 것이다.
DempApp 개조
앞서서 사각형과 격자를 그리도록 세팅해두었던 우리의 DemoApp 앱을 개량하는 것으로 개발을 시작하자.
일단 이 DemoApp이라는 클래스명은 MSDN의 예제 프로그램으로서 지어진 이름이다. 이를 CGraphicBase라는 클래스명으로 바꾸자.
Main.h 에서는 아래와 같이 이름을 바꾸면 된다.

Main.cpp 에서는 이에 따라 각각의 클래스명과, 생성자, 소멸자 등이 따라서 바뀌어야 한다. 다 바꿔주자.
그리고 윈도우를 초기화 해주는 함수 Initialize(윈도우 클래스 생성) 하는곳에 윈도우 타이틀과 이와 관련한 윈도우 클래스명(윈도우가 쓰는)을 LastKingStanding으로 아래와 같이 바꿔주었다. (57, 61 라인).
참고로 63라인에 클라이언트 해상도도 바꿔주었지만 이는 의미 없다 아래서 다시 바꾼다.

이제 F5를 눌러 사각형과 격자가 아직 잘 나오면 모두 잘 고쳐진 것이니 넘어가자.
해상도 조정
이제 해상도를 1024 * 768로 조정하자. 이때 게임은 좌표값이 중요하므로 내가 제어할 화면 영역이 정확히 우리가 원하는 수치일 필요가 있다. 가로폭이 1024인 줄 알았는데 실제로 1015 값이고 하면 나중에 게임 만들 때 뭔가 틀어진다. 이를 위해 클라이언트 영역이 정확히 우리가 원하는 수치가 되기 위해서 전체 윈도우 창 크기를 보정해주는 코드가 있다.
아래와 같이 AdjustWindowRectEx 함수를 사용하면 인자로 들어간 RECT 자료형에 원래 값들이 조금 더 커진다 들어간다. 이는 내 윈도우의 메뉴바 크기, 창틀의 굵기 값들이 더해진 값이다.
이제 이 값을 이용해서 아래와 같이 세팅하면 된다. 그럼 기존의 게임 화면보다 조금 더 큰 화면이 만들어지는 걸 알 수 있다. 클라이언트 영역이 정확히 1024, 768이 되었기 때문.

PeekMessage

이제 우리의 GraphicBase 클래스가 된(CGraphicBase) 클래스의 RunMessageLoop() 함수를 보자 윈도우 API 기본 코드 베이스에서 많이 보이던 메시지 루프가 보인다 (DirectX 2D 2번 포스팅 참조).
Windows 시스템에서 올라오는 메시지를 메시지 큐에 받아 처리해주는 일을 반복적으로 하는 것.
라인 138의 GetMessage() 함수는 사용자의 입력을 받지 않는 동안은 멈춰있는 함수이다. 우리가 이 프로그램을 켜놓고 키보드나 마우스를 아무것도 건드리지 않고 있다면 어떤 프로그램은 할 일이 없을지 모른다.
하지만 게임이라는 건, 사실 유저가 가만히 있어도 할 일이 많은 프로그램이다 (주인공이 가만히 있어도 오크들은 와서 때린다).
그래서 GetMessage() 함수의 경우, 할 일이 없을때. “난 할일 없으니 윈도우님과 CPU님께서는 다른 일하세요”라는 의미로 프로세스 상태를 휴면 상대로 (sleep(), 한 0.0001초 동안 말이다) 세팅하는)세팅하는 기능이 있어서 게임과 잘 안 맞는다.
이는 컴퓨터의 자원을 효율적으로 쓰기 위한 기본 루틴이긴 하다.
하지만 우린 게임을 만들고 있고, 게임이란 건 컴퓨터로부터 최대한 많은 실행 시간과 메모리 자원 등을 가져올 필요가 있다. 사용자 입력이 없을 때도 말이다.
하다 못해 방금 전에 그렸던 프레임 화면을 또다시 그려주기라도 하면 프레임 수치라도 올라간다(60 이상, 물론 컴퓨터는 피곤할 것이다). 그래서 이땐 GetMessage대신 PeekMessage를 쓴다. 이 함수는 사용자 입력이 없거나 할 일이 없을 경우 멈추지 않고 바로 다음 라인의 게임 코드를 돌린다. 할 일 없는 경우 리턴 값이 false이다. 이에 따라 위의 메시지 루프를 다음과 같이 바꾸자.

146 라인의 mbLoop와 라인 155의 OnUpdate() 함수를 추가해줄 필요가 있다. 아래와 같이 추가해주자 (41라인부터 45라인) OnUpdate() 함수는 선언뿐 아니라 몸체도 만들어두자 아래와 같은 방법으로 한다.
42라인에 OnUpdate()를 마우스 선택한 채로 Ctrl + . -> 선언/정의 만들기 클릭 -> 파란 창으로 main.cpp에 몸체가 만들어진 게 보이면 ESC를 눌러 서브 창을 닫자.

mbLoop는 윈도우 프로시저(윈도우로부터 온 메시지를 받아 처리하는 핸들러 함수)에서 쓰기 위해 static형으로 선언되었다. 프로시저함수는 윈도우 만들 때 등록되는 함수이고 OS로부터 날아온 메시지를 받는 함수이다.
이는 OS가 이 함수를 쓴다고 볼 수도 있다. OS는 이를 명료하고, 편리하게 사용하기 위해서 static형으로 선언된 함수를 요구한다. static으로 선언하면 메모상에서 이 함수의 바이너리 코드가 저장되는 위치가 일반 클래스 메서드와 달라지며 그래서 함수 위치를 알아내기가 쉽고 간결해진다.
클래스 변수(mbLoop)는 static로 선언되었을 때 마치 주인 없는 전역 변수처럼 사용된다. 소속된 클래스가 있지만 홀로 존재하는것처럼 쓰는 것. 그래서 이를 쓰기 위해 main.cpp 파일에서 한 번 더 선언해줘야 한다. 마치 그 코드 파일안에서 전역변수 쓰듯 쓰는것. 나중에 컴파일, 링킹할때 해당 클레스와 연계되는 것으로 보인다. 아래와 같이 선언하자

이제 이 mbLoop가 false가 되면 메시지 루프를 나올 것이다 그걸 WinProc의 switch문중 WM_DESTORY 항목에 다음과 같이 넣어주자.

이후 F5 키를 눌러 이 프로그램이 아직까지 잘 컴파일되고 실행되는지 한번더 체크하자. 참고로 실행된 윈도우 우측 상단에 종료 버튼(X 버튼)을 눌렀을 때 잘 종료되는지 체크해보자. 이게 잘되면 mbLoop값이 false가 되면서 메시지 루프를 잘빠져 나온 것이다.
키보드 입력
화면 오른쪽 솔루션 탐색기에서 프로젝트명 오른 클릭 -> 추가(D) -> 새 항목 -> 해더 파일(.h) 선택 -> 파일 이름 DataType.h 기입 -> 파일 내용을 아래와 같이 입력

이 헤더를 Main.h에서 include 하자. 참고로 8라인에 보이는 건 구조체의 생성자이다. C의 구조체와 달리 C++에서는 구조체에도 초기값을 지정해줄 수 있는 생성자 기능을 지원한다. 위와 같이 써두면 이 구조체를 사용할 때 x, y에 0.f (부동소수점 0)이 초기값으로 들어간다.

이제 우리 클래스 CGraphicBase에서 사용할 포지션 변수를 하나 추가하자 아래 50라인에 추가하였다.

그러고 나서 사각형을 직접적으로 그려주는 OnRender() 함수의 190라인 부근에 (아래 그림에선 198, 199라인이다, EndDraw 직전) 사각형의 실제 좌표(left, right, top, bottom)에 다음과 같이 mtPos값들을 더해주도록 하자. 이제 mtPos값이 작은 사각형을 그릴 때 영향을 주게 될 것이다.

끝으로 아까 만들어서 메시지 루프를 타고 있는 OnUpdate() 함수에 다음과 같은 코드를 기입하자
참고로 난 한번 입력받을 때마다 이 mtPos의 변수들을 3씩 더하고 빼줬는데, 설정에 따라 이는 너무 빠를 수 있다. 이동키 한번 누르면 사각형이 화면 밖으로 사라져 버릴 수도 있는 것, 이럴 땐 0.00001f 같은 작은 값을 넣어보자.

참고로 GetAsyncKeyState는 메시지 루프와 상관없이 현재 키 입력 상태를 읽어서 가져오는 함수이다. 이후 XInput 등의 DirectX가 지원하는 Input환경을 사용할 때까지 유용하게 쓸 수 있다. (키보드 마우스만 쓸 거면 솔직히 그냥 써도 된다)
결과 화면
코드 자체는 어렵지 않으나 앞으로 게임의 확장을 위해 코드들이 좀 더 구조화될 필요가 있다. 앞으로는 씬과 레이어를 만들어서 이 게임에 대한 기본 프레임워크를 만들어가자.