KoreanFoodie's Study

윈도우 API 프로그래밍 7 : 게임 프레임워크 구성 본문

Game Dev/윈도우 API

윈도우 API 프로그래밍 7 : 게임 프레임워크 구성

GoldGiver 2021. 10. 15. 10:09

게임클래스의 하민우 교수님 강좌를 듣고 기억할 만한 내용을 리마인드하는 글입니다


윈도우 API 프로그래밍 7 : 게임 프레임워크 구성

지금은 간단한 게임이니까 WindowsProject1.cpp라는 단일 파일에 모든 기능을 구현했지만, 프로그램이 복잡해지면 사실 필요한 코드를 필요한 파일에 분할해서 구성해야 한다. 이 글에서는 이를 위한 밑작업을 어떻게 하는지를 다루고자 한다.

 

GameNode : 먼저 스켈레톤 클래스를 만들자

#pragma once
class GameNode
{
public:
	GameNode();
	~GameNode();

	virtual void Init() = 0; // 순수 가상 함수
	virtual void Update() = 0;
	virtual void Render() = 0;

};

먼저 가장 기본적으로 Init, Update, Render라는 가상 메소드를 가진 클래스를 생성한다(위의 파일은 GameNode.h임). 가상 메소드는 알다시피 실제 객체의 구현을 따라가는 메소드이며, ' = 0' 을 붙여주게 되면 순수 가상 함수로, 이를 상속하는 클래스가 반드시 실제 기능을 구현할 의무를 가진다. 물론 순수 가상 함수가 선언된 클래스는 추상 클래스로 취급하게 된다.

 

MainNode : 실제로 우리가 구현할 녀석

#pragma once
#include "GameNode.h"

class MainGame : public GameNode
{
private:

	struct tagBox
	{
		RECT    rt;
		float   speed;
	};


private:
	POINT       m_ptPos1;   // 조작할 렉트의 좌표 값
	RECT        m_rtBox1;                                     // 조작하는 렉트 정보
	float       m_fMoveSpeed;
	int         m_nScore;
	int         m_nLevel;

	POINT       m_ptMouse_prev;
	bool        m_isPicked;

	vector<tagBox>	m_vecBox;        // 떨어지는 렉트의 정보
	int             m_nDelay;


public:
	MainGame();
	~MainGame();

	virtual void Init() override;
	virtual void Update() override;
	virtual void Render() override;

};

(위의 파일은 GameNode.h) 거창하게 이야기 했지만, 사실 새롭게 짤 부분은 거의 없고, 기존에 WindowsProject1.cpp 파일에 있던 변수들이나 타입 정의들은 GameNode.h로, 실제 구현부에 해당하는 부분은 GameNode.cpp로 옮기면 된다. 

여기서는 기존 전역 변수와 차별성을 두기 위해 "m_"을 붙여 멤버 변수임을 명시했다. 

 

MainNode.cpp : 알맞은 메소드에 알맞은 코드를 옮기기

#include "framework.h"
#include "MainGame.h"

MainGame::MainGame()
{

}

MainGame::~MainGame()
{
}

void MainGame::Init()
{
	m_ptPos1 = { WINSIZEX / 2, WINSIZEY - 30 };   // 조작할 렉트의 좌표 값
	m_fMoveSpeed = 20;
	m_nScore = 0;
	m_nLevel = 0;

	m_isPicked = false;

	m_nDelay = 50;

}

void MainGame::Update()
{
    InvalidateRect(g_hWnd, NULL, true);   // 전체 영역을 다시 그린다. NULL이 들어가면 전체 화면.

    // 0x0001
    if (GetAsyncKeyState(VK_LEFT) & 0x8000 || GetAsyncKeyState('A') & 0x8000) {
        if (m_rtBox1.left >= m_fMoveSpeed)
            m_ptPos1.x -= m_fMoveSpeed;
        else m_ptPos1.x = WINSIZEX;
    }
    else if (GetAsyncKeyState(VK_RIGHT) & 0x8000 || GetAsyncKeyState('D') & 0x8000) {
        if (m_rtBox1.right <= WINSIZEX - m_fMoveSpeed)
            m_ptPos1.x += m_fMoveSpeed;
        else m_ptPos1.x = 0;
    }

    m_nLevel = (m_nScore > 0) ? (m_nScore / 100 + 1) : 1;

    // 포지션 위치에 따른 렉트 정보 업데이트
    m_rtBox1 = RECT_MAKE(m_ptPos1.x, m_ptPos1.y, 50);

    if (m_nDelay >= 50) {

        RECT rt;
        tagBox box;

        box.rt.left = rand() % (WINSIZEX - 30);
        box.rt.right = box.rt.left + 30;
        box.rt.top = -30;
        box.rt.bottom = 0;


        box.speed = rand() % 11 + 5;

        m_vecBox.push_back(box);

        m_nDelay = rand() % 20 + 30;
    }
    else m_nDelay += m_nLevel;

    vector<tagBox>::iterator iter;

    for (iter = m_vecBox.begin(); iter != m_vecBox.end(); iter++) {
        iter->rt.top += iter->speed;
        iter->rt.bottom += iter->speed;

        RECT rt;
        RECT rtIter = iter->rt;

        if (iter->rt.top > WINSIZEY) {
            m_nScore++;
            m_vecBox.erase(iter);
            break;
        }
        else if (IntersectRect(&rt, &m_rtBox1, &rtIter)) {
            m_nScore -= 10;
            m_vecBox.erase(iter);
            break;
        }

    }

}

void MainGame::Render()
{
    PAINTSTRUCT ps;
    // HDC : Device Context Handle : 출력을 위한 모든 데이터를 가지는 구조체
    HDC hdc = BeginPaint(g_hWnd, &ps);
    // TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...

    /*wstring wstr = L"코딩 지옥으로 입장을 허가합니다!!!";
    TextOut(hdc, 10, 10, wstr.c_str(), wstr.length());*/

    int roomSize = WINSIZEY / 9;

    RECT_DRAW(m_rtBox1);

    for (auto& elem : m_vecBox) {
        RECT_DRAW(elem.rt);
    }


    char szBuf[32];

    _itoa_s(m_nLevel, szBuf, 10, 10); // 정수값을 문자열 값으로 변환해주는 함수
    string str = string(szBuf);
    TextOutA(hdc, 10, 10, str.c_str(), str.length());

    _itoa_s(m_nScore, szBuf, 10, 10); // 정수값을 문자열 값으로 변환해주는 함수
    str = string(szBuf);
    TextOutA(hdc, 10, 30, str.c_str(), str.length());


    EndPaint(g_hWnd, &ps);

}

잘 보면 Init( )에서는 변수 초기화를, Update( )에서는 WM_TIMER에 해당하는 구현부를, Render( )에서는 WM_PAINT에 해당하는 코드를 가져온 것을 알 수 있다.

이제 WindowsProject1.cpp에서 GameNode 클래스에서 해당하는 함수를 call해주기만 하면 된다!

 

간단하게 바뀐 WindowsProject1.cpp

// 전역변수 부분
pMainGame = new MainGame;   // 할당
pMainGame->Init();


// ... delete pMainGame을 나중에 반드시 해 주어야 한다! 기본 Message Loop 이후에.

case WM_TIMER:      // 타이머에 의해서 호출
        if (pMainGame)
            pMainGame->Update();
        break;
    
case WM_PAINT:
        if (pMainGame)
            pMainGame->Render();
        break;

살펴보면 MainGame 포인터를 할당한 후, WM_TIMER와 WM_PAINT 블록에서 해당하는 클래스 메소드를 호출한 것으로 코드가 정리된 것을 확인할 수 있다.


더 자세한 내용이 궁금하시다면 직접 들어보시는 걸 추천드립니다!

Comments