Deeper Learning

[WinAPI] PeekMessage, Core Class 설계 (슈팅게임) 본문

Game Development/WinAPI

[WinAPI] PeekMessage, Core Class 설계 (슈팅게임)

Dlaiml 2024. 6. 2. 19:35

Win32API를 활용해서 간단한 슈팅게임을 만드려고 한다. 

 

1. GetMessage -> PeekMessage

2. Core Class 소개 및 전체 클래스 설계 요약

 

GetMessage -> PeekMessage

 

이전 포스팅에서 설명하였던 것처럼 메시지 루프에서 메시지가 있을 때마다 이를 처리하는 현재 방식은 게임을 만들기에 적합하지 않다. 메시지가 없을 때에도 구현한 로직을 처리할 수 있도록 메시지 루프를 PeekMessage를 사용하여 변경하였다.

 

 while (true)
{
    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
        if (WM_QUIT == msg.message)
            break;
        // Handle Message
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    else
    {
        CCore::GetInstance()->Progress();
    }

}

 

WM_QUIT에서만 False를 반환하던 GetMessage와 다르게 PeekMessage는 사용할 수 있는 메시지가 없을 경우 False를 반환하는데 이를 활용해 if-else 문의 else 블록에서 게임에 필요한 로직을 추가하였다.

 

Win32API로 간단한 2D 슈팅게임을 만들기 위해 유튜브에서 학습을 위한 자료를 찾았는데 마침 학습 예시가 슈팅게임이라서 학습한 코드에 대해 설명하는 방식, 개발기 느낌으로 WinAPI 포스팅을 이어하겠다.

 

 

* 이번 포스팅에서는 Core 클래스와 Manager 클래스의 전체 구조, 관계에 대해 다루기 때문에 각 기능에 대한 상세한 설명보다는 코드에서 추상화된 개념과 전체적인 구조에 대해 설명하였다.

 

 

Core Class

메인 함수에서 메시지가 없을 때 CCore 클래스의 Progress 함수 하나만 호출한다. Core 클래스는 게임 전반을 관리하기 위한 주요 클래스로 하나의 Instance, 다른 클래스와 상호작용 용이, 메모리 절약의 특징을 가지는 Singleton 패턴을 사용하였다.

 

class CCore
{
public:
	static CCore* GetInstance()
	{
		static CCore mgr;
		return &mgr;
	}
private:
	CCore();
	~CCore();
	CCore(const CCore&) = delete;
	CCore& operator=(const CCore&) = delete;
}

 

생성자와 소멸자는 private 멤버로 선언하였고 복사 생성자, 복사 대입 연산자의 정의도 금지하여 Singleton 클래스의 단일 Instance를 보장하였다.

 

GetInstance을 정적 멤버함수, mgr을 정적 멤버로 선언하여 초기화가 여러 번 이루어지지 않도록 하고 GetInstance가 호출된 것이 처음이 아니면 이미 만든 Instance의 주소를 반환한다.

 

 

여러 객체를 필요로 하지않고 관리 역할을 하는 매니저 클래스들을 모두 Singleton 패턴으로 구현하였고 구현한 클래스들과 역할은 다음과 같다. 

 

  • Core: 다른 매니저 클래스를 초기화, 업데이트하는 등 프로그램 전체를 관리하는 클래스
  • KeyMgr: 입력가능한 키보드 인풋과 키보드 입력 상태를 관리하는 클래스
  • TimeMgr: WinAPI의 QueryPerformanceCounter를 사용하여 Delta time, FPS를 계산하는 등, 시간과 관련된 기능을 관리하는 클래스
  • PathMgr: 프로젝트의 현재 경로, 로드할 Content의 경로 등,  경로와 관련된 기능을 담당하는 클래스
  • SceneMgr: 사용자에게 보여줄 화면의 초기화, 업데이트, 렌더링을 담당하는 클래스
  • ResourceMgr: Texture, Sound 등 리소스를 관리하는 클래스
  • CollisionMgr: Object 간 충돌과 관련된 모든 기능, 설정을 담당하는 클래스

 

wWinMain 함수에서는 Core 클래스의 Init, Progress의 호출이 이루어진다. 이 두 함수를 따라가며 현재 어떻게 프로그램이 실행되고 있는지 알아보자.

  //  if (FAILED(CCore::GetInstance()->Init(g_hWnd, POINT{ 1280, 720 })))
  //  CCore::GetInstance()->Progress();

 

 

Core 클래스의 Init 함수이다. 여기에서는 아래와 같은 과정이 이루어진다.

 

  • 창 사이즈 조정
  • 다른 DeviceContext, Bitmap(Back Buffer)에서 화면을 초기화하고 그린 후 이를 사용자에게 보여줄 화면(Front Buffer)에 옮기는 Double Buffering
  • 주로 사용하는 Brush, Pen 배열 생성
  • 매니저 클래스 초기화
    • PathMgr: 현재 경로 및 상위 경로 파악
    • TimeMgr: Counter 활성화
    • KeyMgr: 키, 상태 벡터 초기화
    • SceneMgr: 초기 Scene 선택 및 초기 Scene 진입

 

int CCore::Init(HWND _hWnd, POINT _ptResolution)
{
	m_hWnd = _hWnd;
	m_ptResolution = _ptResolution;

	// Adjust window size
	RECT rt = { 0, 0, m_ptResolution.x, m_ptResolution.y };
	AdjustWindowRect(&rt, WS_OVERLAPPED, true);
	SetWindowPos(m_hWnd, nullptr, 100, 100, rt.right - rt.left, rt.bottom - rt.top, 0);

	m_hDC = GetDC(m_hWnd);

	// Double Buffering
	m_hBmap = CreateCompatibleBitmap(m_hDC, m_ptResolution.x, m_ptResolution.y);
	m_hSubDC = CreateCompatibleDC(m_hDC);

	HBITMAP hPrevBmap = (HBITMAP)SelectObject(m_hSubDC, m_hBmap);
	// 1pixel dummy initial bitmap 
	DeleteObject(hPrevBmap); 


	// Initialize Brush & Pen Tools
	CreatePaintTools();


	// Initialize TimeManager
	CPathMgr::GetInstance()->Init();
	CTimeMgr::GetInstance()->Init();
	CKeyMgr::GetInstance()->Init();
	CSceneMgr::GetInstance()->Init();

	return S_OK;
}

 

각 매니저 클래스의 구현은 나중에 특정 기능을 주제로 포스팅할 때 함께 소개하겠다.

 

 

다음은 Core의 Progress 함수이다.

 

매니저 클래스들을 Update하고 창보다 큰 Rectangle을 사용하여 화면을 초기화한다.

이후 SceneMgr의 Render 함수를 사용하여 Back Buffer에 오브젝트를 그리고 이를 BitBlt로 Front Buffer로 복사한다.

void CCore::Progress()
{
	// Update Managers
	CTimeMgr::GetInstance()->Update();
	CKeyMgr::GetInstance()->Update();
	CSceneMgr::GetInstance()->Update();
	CCollisionMgr::GetInstance()->Update();

	
	// Clear 
	Rectangle(m_hSubDC, -1, -1, m_ptResolution.x + 1, m_ptResolution.y + 1);
	CSceneMgr::GetInstance()->Render(m_hSubDC);
	BitBlt(m_hDC, 0, 0, m_ptResolution.x, m_ptResolution.y,
		m_hSubDC, 0, 0, SRCCOPY);

}

 

각 클래스 매니저의 Update에서는 아래 과정이 이루어진다.

  • TimeMGr: Counter를 업데이트하여 프레임(Tick) 간 Delta Time 도출
  • KeyMgr: 현재 프레임에서 키의 상태를 GetAsyncKeyState 함수를 사용하여 조사하여 키 들의 정보를 담고 있는 벡터 업데이트
  • SceneMgr: 현재 Scene에 있는 Object에 대한 포인터가 저장된 벡터를 순회하며 각 Object에서 Update 함수 호출, Player Object의 Update는 KeyMgr을 통해 현재 키 상태를 조회하고 방향키가 눌려있다면 위치 벡터 업데이트, Bullet Object의 Update는 방향벡터, 속도를 따라 위치 벡터 업데이트
  • CollisionMgr: Object 간 충돌 확인 및 충돌 정보 업데이트

 

 

회고

 

파이썬으로 개발할 때도 로깅, 예외처리, Blocking Issue 정보 메신저로 전송 등 프로그램 전체에 영향을 끼치는 여러 객체를 만들 필요가 없는 클래스들을 Singleton 패턴으로 구현해서 사용하곤 하였다.

 

라이브를 서비스 런칭, 운영하며 신규 기능 개발이 대부분이었던 신생 서비스 팀에 인공지능, AWS 인프라 개발자로 일을 하면서 클래스 설계가 매우 정형적인 인공지능 개발을 주로 해왔기 때문에 (Google, Meta의 주요 논문 코드를 따랐었다) 전체적인 그림을 그리고 클래스를 설계하는 경험이 많이 없었다. (있어도 얕았다)

 

고수준의 디자인 패턴, 객체 지향에 대해서도 마찬가지로 깊게 고민해볼 기회가 많이 없었어서 이번에 WinAPI를 학습하고 개발하면서는 틈틈이 찾아보고 학습하고 이를 적용시켜보려 한다.

 

 

Reference

[0] https://learn.microsoft.com/ko-kr/windows/win32/api/

 

Win32 API에 대한 프로그래밍 참조 - Win32 apps

이 섹션에서는 기술 영역 및 헤더로 구성된 Win32 API 참조 설명서를 제공합니다.

learn.microsoft.com

[1] https://www.youtube.com/playlist?list=PL4SIC1d_ab-ZLg4TvAO5R4nqlJTyJXsPK

 

Comments