서적 정리/DirectX11을 이용한 3D 게임 프로그래밍 입문

27.예제 응용 프로그램 프레임워크

민돌이2 2022. 1. 20. 17:54

컴퓨터 프로그래밍에서 프레임워크(framework)는 복잡한 문제를 해결하거나 서술하는 데 사용되는 기본 개념 구조이다. frame(틀) + work(일하다)의 합성어로서 짜여진 틀, 뼈대를 갖고 어떤 일을 한다고 생각하면 된다.

프레임워크와 라이브러리의 차이점을 느끼기 힘들다. 둘 다 개발하는데 필요한 것을 구현해 둔 모음집이라는 생각은 든다.

1.프레임워크(framework)

제공받는 일정한 요소와 틀, 규약을 갖고 무언가를 만드는 일이라고 정의할 수 있다. 예를 들어 프라모델을 사면 부품들을 니퍼로 잘라서 결합해서 완성한다. 프라모델을 만들 때 규칙이나 방식을 제공해주는 부품이나 순서를 제공하는 것을 프레임워크라고 받아들이면 되겠다.

2.라이브러리(library)

프라모델을 만들 때 반드시 지켜야하는 규칙이 프레임에 빗대었다면 라이브러리는 그 외적인 것들을 의미한다. 예를 들어 항공모함이 있을 때 부가적으로 페인트칠을 하거나, 비행기, 바다 등을 추가해서 더 멋지게 만들 수 있다. 이런 도구들을 라이브러리라고 생각하면 된다.

 

요약하자면 프레임워크는 반드시 지켜야하는 룰이 있고, 라이브러리는 사용하든 안하든 자기 마음대로이다.

 

이 책의 예제들을 사용하므로 Direct3D 초기화 방식이나, 프레임워크 짜는 방식 위주로 공부하면 될 것 같다.

 

 

1.D3DApp 클래스

게임을 만들겠다 하면 일단 창을 띄워야 한다. 이 책에서 D3DApp 클래스는 아래의 역할을 담당한다.

1.주 창(main window)의 생성

2.응용 프로그램 메시지 루프 실행

3.Windows 메시지 처리

4.Direct3D 초기화

5.프레임워크 함수들의 정의

클라이언트는 D3DApp을 상속해서 가상 함수로 선언되 프레임워크 함수들을 오버라이딩하고, 파생한 D3DApp의 인스턴스를 하나만 생성해서 사용하면 된다.

#ifndef D3DAPP_H
#define D3DAPP_H

#include "d3dUtil.h"
#include "GameTimer.h"
#include <string>

class D3DApp
{
public:
	D3DApp(HINSTANCE hInstance);
	virtual ~D3DApp();
	
	HINSTANCE AppInst()const;
	HWND      MainWnd()const;
	float     AspectRatio()const;
	
	int Run();
 
	// 프레임워크 메서드들. 파생 클라이언트 클래스는 이 메서드들을 자신의 요구에 맞게 오버라이딩해야함
	virtual bool Init();
	virtual void OnResize(); 
	virtual void UpdateScene(float dt)=0;
	virtual void DrawScene()=0; 
	virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

	//마우스 입력의 처리를 위한 가상 메서드
	virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
	virtual void OnMouseUp(WPARAM btnState, int x, int y)  { }
	virtual void OnMouseMove(WPARAM btnState, int x, int y){ }

protected:
	bool InitMainWindow();
	bool InitDirect3D();

	void CalculateFrameStats();

protected:

	HINSTANCE mhAppInst;
	HWND      mhMainWnd;
	bool      mAppPaused;
	bool      mMinimized;
	bool      mMaximized;
	bool      mResizing;
	UINT      m4xMsaaQuality;

	GameTimer mTimer;

	ID3D11Device* md3dDevice;
	ID3D11DeviceContext* md3dImmediateContext;
	IDXGISwapChain* mSwapChain;
	ID3D11Texture2D* mDepthStencilBuffer;
	ID3D11RenderTargetView* mRenderTargetView;
	ID3D11DepthStencilView* mDepthStencilView;
	D3D11_VIEWPORT mScreenViewport;

	//응용 프로그램 창 설정
	std::wstring mMainWndCaption;
	D3D_DRIVER_TYPE md3dDriverType;
	int mClientWidth;
	int mClientHeight;
    
    	//4XMSAA 사용 여부
	bool mEnable4xMsaa;
};

 

 

2.프레임워크 메서드가 아닌 메서드들(비가상 멤버 함수)

1.D3DApp : 자료 멤버들을 기본값으로 초기화하는 생성자

2.~D3DApp : D3DApp가 획득한 COM 인터페이스들을 해제하는 소멸자

3.AppInst : 응용 프로그램 인스턴스 핸들의 복사본을 반환한다.

4.MainWnd : 주 창의 핸들의 복사본을 반환한다.

5.AspectRatio : 후면 버퍼의 종횡비(aspect ratio), 즉 높이에 대한 너비의 비율을 반환한다.

6.Run : 응용 프로그램 메시지 루프를 감싼 메서드이다. PeekMessage 함수를 사용하므로서 Windows 메시지가 전혀 없을 때에도 게임의 논리(logic)을 처리할 수 있다.

7.InitMainWindow : 응용 프로그램 주 창을 초기화 한다.

8.InitDirect3D : Direct3D를 초기화 하는 메서드이다.

9.CalculateFrameStats : 평균 초당 프레임 수(FPS)와 평균 프레임당 밀리초를 계산한다.

 

 

3.프레임워크 메서드들(가상 멤버 함수)

1.Init : 자원할당, 장면 물체 초기화, 광원 설정 등 응용 프로그램 고유의 초기화를 여기서 하면 된다. 
2.OnResize : WM_SIZE 메시지가 발생했을 때 D3DApp::MsgProc으로 부터 호출된다. 쉽게 말해 창의 크기가 변했을 때 호출 된다.

3.UpdateScene : 매 프레임마다 호출된다. 시간이 흐름에 따라 응용 프로그램을 갱신하는데 필요한 코드를 이 메서드에 넣어야 한다. 예를 들어 애니메이션 수행, 카메라 이동, 충동 검사, 사용자 입력 처리 등이 있다.

4.DrawScene : 매 프레임마다 호출된다. 현재 프레임을 후면 버퍼에 실제로 그리기 위한 렌더링 명령들을 제출하는 코드를 이 메서드에 넣어야 한다.

5.MsgProc : 응용 프로그램 주 창의 메시지 처리 기능을 구현하는 것이다. D3DApp:MsgProc이 아예 처리하지 않거나 응용 프로그램에 적합한 방식으로 처리하지 않는 어떤 메시지를 응용 프로그램이 직접 처리하고 싶은 경우에만 이 메서드를 재정의 하면된다.

 

이 외에 마우스 조작관련 메서드들이 있다.

virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y)  { }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }

마우스 메시지를 처리하고자 할 때 MsgProc을 재정의하는 대신 이 메서드들을 재정의하면 된다.

 

 

4.프레임 통계치

초당 프레임 수(FPS)는 1초에 렌더링 되는 프레임 개수를 의미한다. 일정 시간 기간 t동안 처리한 프레임 개수를 변수 n에 저장하고, 시간 기간 t로 나눈 fpsavg = n/t가 해당 시간 동안의 평균 fps이다.

D3DApp클래스에서 CalculateFrameStats 메서드에 구현되어 있다.

void D3DApp::CalculateFrameStats()
{
	static int frameCnt = 0;
	static float timeElapsed = 0.0f;

	frameCnt++;

	if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
	{
		float fps = (float)frameCnt; // fps = frameCnt / 1
		float mspf = 1000.0f / fps;

		std::wostringstream outs;   
		outs.precision(6);
		outs << mMainWndCaption << L"    "
			 << L"FPS: " << fps << L"    " 
			 << L"Frame Time: " << mspf << L" (ms)";
		SetWindowText(mhMainWnd, outs.str().c_str());
		
		//다음 평균을 위해 초기화
		frameCnt = 0;
		timeElapsed += 1.0f;
	}
}

프레임 수를 측정하기 위해서는 이 메서드를 매 프레임마다 호출해야 한다.

 

 

5.메시지 처리부

게임 응용 프로그램에서 Win32 메시지를 자주 다루지는 않는다. 응용 프로그램 핵심부는 유휴(idle) 처리 도중에 실행된다(창 메시지가 전혀 없을 때). 하지만 예외 처리는 반드시 해야하는 것 처럼 반드시 처리해야 할 주요 메시지들이 존재한다. D3DApp클래스에서 MsgProg가 담당한다.

WM_ACTIVATE은 응용 프로그램이 활성화 또는 비활성화될 때 전달된다.

case WM_ACTIVATE:
		if( LOWORD(wParam) == WA_INACTIVE )
		{
			mAppPaused = true;
			mTimer.Stop();
		}
		else
		{
			mAppPaused = false;
			mTimer.Start();
		}
		return 0;

WM_SIZE는 응용 프로글매의 창의 크기가 바뀔 때 발생한다.

case WM_SIZE:
		mClientWidth  = LOWORD(lParam);
		mClientHeight = HIWORD(lParam);
		if( md3dDevice )
		{
			if( wParam == SIZE_MINIMIZED )
			{
				mAppPaused = true;
				mMinimized = true;
				mMaximized = false;
			}
			else if( wParam == SIZE_MAXIMIZED )
			{
				mAppPaused = false;
				mMinimized = false;
				mMaximized = true;
				OnResize();
			}
			else if( wParam == SIZE_RESTORED )
			{
				if( mMinimized )
				{
					mAppPaused = false;
					mMinimized = false;
					OnResize();
				}
				else if( mMaximized )
				{
					mAppPaused = false;
					mMaximized = false;
					OnResize();
				}
				else if( mResizing )
				{
					
				}
				else
				{
					OnResize();
				}
			}
		}
		return 0;

후면 버퍼와 깊이·스텐실 버퍼의 크기를 클라이언트 영역 직사각형에 맞게 갱신하기 위해 처리한다. 창의 크기가 변할 때 마다 버퍼들의 크기를 바꿔야 하는데, 버퍼의 크기 변경은 D3DApp::OnResize에 구현되어 있다. 깊이·스텐실 버퍼는 파괴하고 다시 생성해야 하고, 렌더 타겟 뷰와 깊이 스텐실 뷰도 다시 생성해야 한다. 생각해 봐야 할 것은 사용자가 창 조절을 드래그로 계속 조절하는 중이라면 WM_SIZE 메시지가 계속 전달되고, 메시지가 전달 될 떄 마다 버퍼들을 계속 갱신하는 것은 비효율적 이기 때문에 창 조절을 끝낸 시점, 즉 마우스에서 손을 뗀 시점에 버퍼를 갱신해야 한다.

WM_ENTERSIZEMOVE WM_EXITSIZEMOVE는 이럴 때 사용한다.

case WM_ENTERSIZEMOVE:
		mAppPaused = true;
		mResizing  = true;
		mTimer.Stop();
		return 0;

case WM_EXITSIZEMOVE:
		mAppPaused = false;
		mResizing  = false;
		mTimer.Start();
		OnResize();
		return 0;

나머지 코드도 정리해보자면

	//창이 파괴되려 할 때 사용
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	//메뉴가 활성화되어 사용자가 키를 눌렀으나 그 키가 어떤 일도 담당하지 않을 때 사용
	case WM_MENUCHAR:
        // alt-enter를 눌렀을 때 삐 소리가 안나게 한다.
        return MAKELRESULT(0, MNC_CLOSE);

	//창이 너무 작아지지 않도록 최소값 설정
	case WM_GETMINMAXINFO:
		((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
		((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200; 
		return 0;

	//마우스 처리
	case WM_LBUTTONDOWN:
	case WM_MBUTTONDOWN:
	case WM_RBUTTONDOWN:
		OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;
	case WM_LBUTTONUP:
	case WM_MBUTTONUP:
	case WM_RBUTTONUP:
		OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;
	case WM_MOUSEMOVE:
		OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
		return 0;

 

 

 

6.전체화면 모드 전환

IDXGISwapCahin 인터페이스는 자동으로 Alt-Enter 키 조합을 인식해서 응용 프로그램을 전체화면 모드로 전환한다. 반대로 전체화면에서 창모드로 전환한다. 모드 전환으로 창 크기가 변하면 WM_SIZE 메시지가 전달되고, 응용 프로그램은 후면 버퍼와 깊이·스텐실 버퍼의 크기를 창의 새 크기에 맞게 변경해야 한다.

728x90