본문 바로가기
Lecture

Brick Breaker 게임 제작하기 - 2

by 작은별하나 2019. 12. 26.
반응형

그래픽 소스 생성

 

요리를 만들 때 재료를 준비하듯이 그래픽 프로그램을 작성할 때에는 그래픽 소스라는 재료를 준비해야 한다. 일반적으로 그래픽 소스는 그림이라고 할 수 있겠지만, 게임에서 사용하는 그래픽 소스는 그림이라는 구성 요소 이외에도 한가지 요소가 덧붙여져야 한다. 게임의 출력부는 여러가지 그래픽 소스를 배치하는 작업을 하게 되는데, 이 때 배치하는 그래픽 소스들이 서로 겹치게 되는 경우가 많다. 이 때 알파값을 이용하여 혼합(blend)과정을 거침으로써 게임의 오브젝트가 보다 자연스럽게 표현된다. 알파값을 그림에 주기 위해서는 포토샵과 같은 전문 그래픽 도구가 필요하다. 여기에서는 간단한 예로 그래픽 소스를 어떻게 생성하는지에 대해 설명토록 한다.

 

제일 먼저 포토샵을 실행한다. 새로운 이미지 파일을 만들기 위해서 [File]->[New]를 선택한다.

 

새로운 이미지를 생성하는 대화상자가 나오면 여기에 해상도를 적도록 한다. 해상도는 픽셀단위로 적는다. 예를 들어서 8x8 짜리 볼 이미지를 만들려면 다음과 같이 설정한다.

 

 

[포토샵] 새로운 이미지 생성

 

볼 이미지를 만들기 위해서 선택툴을 이용하여 원 이미지를 사용한다. 그런후에 색을 칠하도록 한다.

 

[포토샵] 볼 이미지 만들기

 

이제 마지막 작업은 배경색과 작업한 이미지가 혼합이 될 수 있도록 알파값을 설정하는 것이다. 알파값을 설정하기 위해서는 알파값을 저장할 수 있는 공간을 생성해주어야 하는데 이것을 포토샵에서는 채널이라고 한다. 기본 이미지 채널은 빛의 삼원색인 빨강, 초록, 파랑이 있고, 여기에 알파를 위한 채널을 추가한다. 포토샵의 채널 대화상자에서 "Create new channel" 버튼을 눌러서 알파채널을 생성한다. 알파채널에서는 하얀색은 불투명, 검정색은 투명을 뜻하며 중간값은 배경색과 이미지색이 혼합됨을 의미한다.

 

[포토샵] 볼 이미지 음영 처리

 

알파채널을 생성했으면 위에서 보는바와 같이 불투명한 부분에는 하얀색을 투명한 부분은 검정색을 칠하도록 하며, 부드러운 혼합을 위해서 중간색을 경계에 칠해주도록 한다.

 

블록격파 게임을 제작하기 위해서 다음과 같은 그래픽 소스를 제작하였다. 배경과 빨강, 노랑, 파랑 블록, 패들, 그리고 볼에 대한 그래픽 소스이다.

 

배경 이미지

 

 

프로그램 제작

 

패들 제어

 

블록 격파 게임에서 입력을 받아서 실제적으로 움직이는 것은 패들이다. 패들의 움직임에 따라서 게임의 법칙을 적용받고 있는 볼의 움직임이 바뀌게 된다.

 

입력을 받는 방법은 윈도우 시스템에서는 두가지가 있다. 윈도우 시스템의 이벤트를 받아서 처리하는 방법과 키보드의 현재 상태를 읽는 방법이다. 키보드 입력과 관련된 윈도우 이벤트를 WM_KEYDOWN, WM_KEYUP, WM_CHAR 등이 있다.

 

키보드의 현재 상태를 읽는 방법은 GetAsyncSelect 함수를 이용하는 방법이 있다. 원하는 키가 현재 눌려있는지 아닌지를 검사할 수 있다.

 

패들 오브젝트는 볼 오브젝트와 마찬가지로 위치와 움직이는 방향을 변수로 갖는다. 패들은 좌우로만 움직일 수 있으므로 가로축의 위치값만 가지고 있으면 된다.

 

float PaddlePos = 225;
float PaddleDir = 0;

 

왼쪽 커서키가 눌려져 있으면 PaddleDir-100으로 설정해서 왼쪽으로 움직이게 만들고 오른쪽 커서키가 눌려져 있으면 PaddleDir100으로 설정해서 오른쪽으로 움직이게 만든다. 현재 두개의 키 모두 눌리지 않았다면 PaddleDir0으로 설정해서 멈추게 한다.

 

//	키보드입력처리
if( GetAsyncKeyState(VK_LEFT) & 0x8000 ) PaddleDir = -100;
else if( GetAsyncKeyState(VK_RIGHT) & 0x8000 ) PaddleDir = 100;
else PaddleDir = 0;
PaddlePos += PaddleDir*diff;
if( PaddlePos < 50 ) PaddlePos = 50;
else if( PaddlePos > 400 ) PaddlePos = 400;

 

블록 처리

 

블록을 처리하기 위해서는 여기서는 BrickStatus라는 배열 변수를 설정하였다. 가로 13, 세로 9칸을 이용해서 다양한 무늬를 설정할 수 있도록 하였다. 0은 빈칸, 1부터 3까지는 각각 빨강, 노랑, 파랑의 색 블록을 지정한다. 블록에 기능을 지정할 수도 있다.

 

스테이지 파일을 이용해서 스테이지가 시작할 때마다 블록의 배치를 바꿀 수 있겠지만, 여기서는 배열 변수를 선언할 때, 초기화를 해주도록 하였다.

 

int BrickStatus[9][13] =
{
	{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 },
	{ 2, 2, 2, 1, 2, 2, 3, 1, 1, 2, 1, 1, 1 },
	{ 2, 2, 1, 1, 2, 2, 3, 1, 1, 2, 2, 1, 1 },
	{ 2, 1, 1, 1, 1, 1, 3, 2, 2, 2, 2, 2, 1 },
	{ 1, 1, 1, 1, 1, 1, 3, 2, 2, 2, 2, 2, 2 },
	{ 2, 1, 1, 1, 1, 1, 3, 2, 2, 2, 2, 2, 1 },
	{ 2, 2, 1, 1, 2, 2, 3, 1, 1, 2, 2, 1, 1 },
	{ 2, 2, 2, 1, 2, 2, 3, 1, 1, 2, 1, 1, 1 },
	{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }
};

 

물리 모델

 

공의 위치, 방향, 속도를 알고 있으면 공의 움직임을 표현할 수 있다. 일반적으로 위치와 속도를 결정하는데 문제점은 없다. 방향의 경우에는 여러 가지의 표현이 가능하다. 2차원에서는 방향을 각도로 표현할 수도 있고 벡터로 표현할 수도 있다. 나름대로의 방향 표시의 장단점이 있다. 각도로 표시하는 경우에는 방향을 중심점을 기준으로 바꾸기가 편리하다. 그에 비해서 벡터로 표시하는 경우에는 지속적인 움직임을 표현하기 편리하다.

 

본 교재에서의 게임은 벡터 방식으로 구현하도록 하였다. 방향 전환은 수직과 수평에 대해서만 반사를 표현하면 되기 때문에 간단한 부호 바꿈으로 처리할 수가 있다. D3DXVECTOR2 2차원 벡터를 표시하는 Direct3D용 변수타입이므로 이를 이용해서 처리하도록 하자.

 

공의 움직임을 표현하기 위해서는 두개의 벡터를 사용하여야 한다. 벡터는 방향과 크기가 있는 물리도구이므로 공의 현재 위치를 위해서는 별도의 벡터를 사용해야 한다. 공의 위치와 공의 초당 위치변화량을 각각의 벡터 변수로 선언하도록 한다.

 

DWORD LastTick = 0;
//	차이시간계산
if( LastTick == 0 ) LastTick = GetTickCount();
float diff = (GetTickCount() - LastTick)*0.001f;
LastTick = GetTickCount();
D3DXVECTOR2 BallPos;	//	볼의 위치
D3DXVECTOR2 BallDir;	//	볼의 이동량 (초당 픽셀)
//	diff 초동안볼의위치변환
BallPos += BallDir * diff;

 

충돌 검사는 모든 오브젝트에 대해서 검사해주어야 한다. 오브젝트별로 공 오브젝트와 겹친 부분이 있다면 충돌이 발생한 것으로 판정한다. 공의 속도가 빠르다면 충돌 판정 로직을 개선해주어야 하지만, 공의 속도가 프레임 당 몇 픽셀 이내라면 겹침 판정으로 충분하다.

 

//	세로벽충돌
if( BallPos.x < 0 || BallPos.x >= 320 )
	BallDir.x = -BallDir.x;
//	기로벽충돌
if( BallPos.y < 0 )
	BallDir.y = -BallDir.y;

 

블록과의 충돌검사를 위해서는 볼의 중심점에 대한 겹침 판정을 해주어야 한다. 해당 블록과 충돌할 때의 볼의 중심점은 다음 그림과 같다. 이에 대해서 겹침 판정을 하려면 미리 배열에 충돌 영역을 설정하는 것이 가장 효율이 좋을 것이지만, 여기서는 계산에 의해서 처리하도록 하였다.

 

벽돌의 겹침판정 범위

블록의 갯수가 많기 때문에 성능을 높이려면, 볼의 위치 근처의 블록을 검사하는 것이 좋겠지만 간단하게 처리하기 위해서 모든 블록에 대해서 충돌 검사를 하였다.

 

float dx1 = j*32+17-BallPos.x;
float dx2 = -32-dx1;
float dy1 = i*16+100-BallPos.y;
float dy2 = -16-dy1;
if( dx1 < 0 && dx2 < 0 && ((dy1 >= 0 && dy1 <= 8) || (dy2 >= 0 && dy2 <= 8)) )
{
	BallDir.y = -BallDir.y;
	BrickStatus[i][j] = 0;
}
if( dy1 < 0 && dy2 < 0 && ((dx1 >= 0 && dx1 <= 8) || (dx2 >= 0 && dx2 <= 8)) )
{
	BallDir.x = -BallDir.x;
	BrickStatus[i][j] = 0;
}
if( (dx1*dx1 + dy1*dy1) <= 64 || (dx1*dx1 + dy2*dy2) <= 64 ||
    (dx2*dx2 + dy1*dy1) <= 64 || (dx2*dx2 + dy2*dy2) <= 64 )
{
	BallDir = -BallDir;
	BrickStatus[i][j] = 0;
}

 

그림 그리기

 

그림을 그리기 위해서는 Direct3D에서 정점 버퍼를 생성하고 정점 버퍼를 이용하여 그림을 그려주어야 한다. 지난 시간에 배운 Direct3D의 정점버퍼 생성과 MakeVertices 함수와 DrawVertices 함수를 이용하여 그림을 그려보도록 한다.

 

//	정점버퍼생성
IDirect3DVertexBuffer9 *pBGVB, *pBrickVB[9][13], *pBallVB, *pPaddleVB;
pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, D3DFVF_CUSTOM, 
	D3DPOOL_DEFAULT, &pBGVB, NULL);
pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, D3DFVF_CUSTOM, 
	D3DPOOL_DEFAULT, &pBallVB, NULL);
pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, D3DFVF_CUSTOM, 
	D3DPOOL_DEFAULT, &pPaddleVB, NULL);
for( int i = 0 ; i < 9 ; i++ )
	for( int j = 0 ; j < 13 ; j++ )
		pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, 
			D3DFVF_CUSTOM, D3DPOOL_DEFAULT, &pBrickVB[i][j], NULL);

 

모든 정점버퍼를 생성했으면 MakeVertices 함수를 이용하여 초기 위치를 잡도록 한다. 볼과 패들을 제외하고는 움직이지 않는 오브젝트이므로 초기 설정된 위치를 그대로 유지하게 된다.

 

//	정점버퍼초기화
MakeVertices(pBGVB, 0, 0, 450, 520);
MakeVertices(pBallVB, 300, 400, 316, 416);
MakeVertices(pPaddleVB, 300, 490, 364, 522);
for( int i = 0 ; i < 9 ; i++ )
    for( int j = 0 ; j < 13 ; j++ )
        MakeVertices(pBrickVB[i][j], j*32+17, i*16+100, j*32+17+32, i*16+100+16);

 

텍스처 정보는 앞서 제작한 이미지들을 로딩하게 된다.

 

//	텍스처로딩
IDirect3DTexture9 *pBGTex, *pBrickTex[3], *pBallTex, *pPaddleTex;
D3DXCreateTextureFromFile(pD3DDev, L"bg.bmp", &pBGTex);
D3DXCreateTextureFromFile(pD3DDev, L"ball.bmp", &pBallTex);
D3DXCreateTextureFromFile(pD3DDev, L"paddle.bmp", &pPaddleTex);
D3DXCreateTextureFromFile(pD3DDev, L"brick-red.bmp", &pBrickTex[0]);
D3DXCreateTextureFromFile(pD3DDev, L"brick-yellow.bmp", &pBrickTex[1]);
D3DXCreateTextureFromFile(pD3DDev, L"brick-blue.bmp", &pBrickTex[2]);

 

움직이는 오브젝트인 볼과 패들은 게임데이터가 수정되면 그래픽 데이터라고 할 수 있는 정점 버퍼 내용도 수정해주어야 한다.

 

//	볼과패들위치설정
MakeVertices(pBallVB, BallPos.x-8, BallPos.y-8, BallPos.x+8, BallPos.y+8);
MakeVertices(pPaddleVB, PaddlePos-32, 490, PaddlePos+32, 522);

 

그림을 그려주는 부분을 DrawVertices 함수를 사용하였다.

 

//	그림그리기
DrawVertices(pD3DDev, pBGVB, pBGTex);
DrawVertices(pD3DDev, pBallVB, pBallTex);
DrawVertices(pD3DDev, pPaddleVB, pPaddleTex);
for( int i = 0 ; i < 9 ; i++ )
    for( int j = 0 ; j < 13 ; j++ )
        if( BrickStatus[i][j] )
           DrawVertices(pD3DDev, pBrickVB[i][j], pBrickTex[BrickStatus[i][j]-1]);

 

점수 기록

 

게임에서 빠질 수 없는 것이 점수를 기록하는 것이다. 점수는 간단하게 블록을 격파하면 10점씩 증가하도록 한다. 공과 블록이 충돌이 발생하면 점수를 증가시키도록 한다. 점수를 위한 변수는 지역변수로 nScore라는 이름으로 생성하였다.

 

점수를 화면에 뿌려주기 위해서는 ID3DXFont 객체를 생성해주어야 한다. 실제로 Direct3D 함수에는 폰트 관련된 부분이 없다. 실제 ID3DXFont 객체는 스프라이트라는 서피스에 필요할 때마다 글자를 새겨주는 방식이다. 성능 향상이 필요한 경우 복잡하지만 ID3DXFont 인터페이스와 비슷한 기능을 개발하여야 한다.

 

//	Create D3DXFont
ID3DXFont *pFont;
D3DXCreateFont(g_pD3DDev, 14, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET, 
	OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, 
	_T("Arial"), &pFont);

 

 

여기에서는 높이가 14이고, 볼드체이며 Arial이란 이름의 폰트를 사용하도록 하였다. 글씨체를 바꾸고 싶다면 해당 부분을 수정하면 된다.

 

ID3DXFont 객체는 문자열을 표시하는 방식에 있어서 윈도우즈 API와 유사한 방식을 사용한다.

 

//	Draw Text
RECT rt = { 350, 5, 450, 20 };
TCHAR str[128];
_stprintf(str, _T("SCORE : %05d"), nScore);
pFont->DrawText(NULL, str, _tcslen(str), &rt, DT_CENTER, 0xffffff00);

 

문자열이 표시될 공간은 직접 값으로 입력하였다. _stprintf 로 형식화된 문자열을 만들어주고 ID3DXFont::DrawText 메소드로 노란색으로 표시하였다.

 

사운드 출력

 

게임에서 사운드가 빠질 수는 없다. DirectX에 사운드 기능이 들어가 있지만 실제 Direct Sound API 를 사용하는 방법은 까다롭다. 컴포넌트 인터페이스에 익숙한 프로그래머가 아니라면 Direct Sound API를 이해하는데 많은 시간을 소요하게 된다.

 

이 교재에서는 간단한 형태의 사운드를 사용하도록 하였다. 윈도우즈에서 제공하는 멀티미디어 기능을 이용하면 간단하게 웨이브 파일을 출력할 수 있다.

 

윈도우즈 멀티미디어 API는 윈도우즈 어플리케이션이 사운드, 오디오, 미디 등을 플레이할 수 있도록 기능을 제공한다. 윈도우즈 멀티미디어 API를 사용하기 위해서는 mmsystem.h 라는 헤더 파일을 소스 파일에 포함시켜야 하며, 링크시에 winmm.lib 라이브러리 파일을 추가하여야 한다.

 

PlaySound API 함수는 파일, 리소스, 또는 메모리로부터 웨이브 오디오 데이터를 플레이해준다. PlaySound 함수의 원형은 다음과 같다.

 

BOOL PlaySound( LPCSTR pszSound,  HMODULE hmod, DWORD fdwSound );  

 

웨이브 파일을 플레이해주기 위해서는 pszSound에 웨이브 파일의 이름을 hmod에는 NULL을 설정해주어야 한다. fdwSound에는 SND_FILENAME을 설정하면 된다. PlaySound는 기본적으로 플레이가 끝날 때까지 기다리기 때문에 SND_ASYNC라는 fdwSound에 비트합으로 설정해줌으로써 플레이가 끝날 때까지 기다리지 않도록 한다.

 

 

 

 

728x90

'Lecture' 카테고리의 다른 글

슈팅게임 제작하기  (0) 2020.01.02
Brick Breaker 게임 제작하기 - 1  (0) 2019.12.24
Reversi 게임 제작하기 - 3  (1) 2019.12.22
Reversi 게임 제작하기 - 2  (1) 2019.12.19
Reversi 게임 제작하기 - 1  (0) 2019.12.19

댓글