본문 바로가기
Lecture

슈팅게임 제작하기

by NoxLuna 2020. 1. 2.
반응형

슈팅 게임

 

슈팅 게임은 고전적으로 아케이드 게임에서 가장 많이 개발된 장르이다. 슈팅 게임은 단순하면서도 액션감이 높아서 현재까지도 많은 슈팅 게임이 오락실용 게임으로 개발되고 있다.

 

여기서는 간단한 형태의 슈팅게임을 제작해보도록 한다.

 

shooting game screenshot

 

그래픽 소스는 스타크래프트에 나오는 레이스와 뮤탈리스크를 가져왔다. 배경은 포토샵에서 간단하게 점을 찍어 제작해보았다. 적기는 무작위로 생성되어서 밑으로 내려온다. 레이스에서 발사할 수 있는 총알의 갯수는 한 화면에 4개로 한정지었다.

 

게임 데이터 변수

 

게임 데이터는 움직이는 객체마다 위치를 가지고 있어야 한다. 배경은 스크롤이 되기 때문에 현재 어디까지 스크롤되고 있는지 변수로 지정되어야 한다. 플레이어 기체인 레이스의 경우 현재 위치를 저장할 변수가 필요하다. 적기와 총알의 경우에는 위치 변수값 이외에도 현재 화면에 출력되어 있는지를 알 수 있도록 별도의 변수를 설정하였다.

 

	//	게임데이터
	D3DXVECTOR2 PlayerPos = D3DXVECTOR2( 200, 500 );
	float Scroll = 0;
	int FireKeyStatus = 0;
	D3DXVECTOR2 EnemyPos[20];
	int EnemyStatus[20];
	D3DXVECTOR2 BulletPos[4];
	int BulletStatus[4];

	//	게임데이터초기화
	ZeroMemory(EnemyStatus, sizeof(EnemyStatus));
	ZeroMemory(BulletStatus, sizeof(BulletStatus));

 

PlayerPos는 플레이어기체의 위치값이다. Scroll은 배경이 얼만큼 스크롤되었는지를 저장하고 있다. EnemyPos는 적기의 위치값을 가지고 있으며, 20기의 적기가 화면상에 존재할 수 있다. EnemyStatus는 적기의 상태값으로 1이면 현재 생성되어 있어서 위치 이동 및 충돌 검사에 사용된다. 마찬가지로 총알의 경우에도 생성되었을 수도 또는 아닐 수도 있기 때문에 BulletPos라는 변수와 BulletStatus라는 변수를 사용하고 있다.

 

변수의 수를 줄이기 위해서는 EnemyStatus 대신에 EnemyPos에 절대 나올 수 없는 값을 설정할 수 있지만, 여기서는 각각의 변수 역할을 명확하게 사용하도록 한다. EnemyStatusBulletStatusZeroMemory 함수를 이용하여 0으로 초기화하였다.

 

FireKeyStatus 변수는 키보드 입력 처리에서 설명할 키보드 트리거 검사를 위한 변수이다. 이 값은 기본적으로 0으로 초기화하였다.

 

그래픽 데이터 생성

 

Direct3D에서 그래픽 데이터는 정점, 텍스처 등이 존재한다. 각각의 그래픽 요소들은 모두 하나씩의 정점이 필요하다. 게임 데이터에서 20기의 적기와 4개의 총알이 존재할 수 있고, 플레이어기, 그리고 배경화면이 모두 그래픽 요소들이다. 이를 위해 정점 버퍼를 생성하도록 한다. 또한 텍스처도 필요한 그래픽 데이터에 맞게 생성한다.

 

//	정점버퍼생성
IDirect3DVertexBuffer9 *pBGVB, *pPlayerVB, *pEnemyVB[20], *pBulletVB[4];
pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, D3DFVF_CUSTOM, 
	D3DPOOL_DEFAULT, &pBGVB, NULL);
pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, D3DFVF_CUSTOM, 
	D3DPOOL_DEFAULT, &pPlayerVB, NULL);
for( int i = 0 ; i < 20 ; i++ )
	pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, 
		D3DFVF_CUSTOM, D3DPOOL_DEFAULT, &pEnemyVB[i], NULL);
for( int i = 0 ; i < 4 ; i++ )
	pD3DDev->CreateVertexBuffer(4*sizeof(CustomVertex), 0, 
		D3DFVF_CUSTOM, D3DPOOL_DEFAULT, &pBulletVB[i], NULL);

//	텍스처로딩
IDirect3DTexture9 *pBGTex, *pPlayerTex, *pEnemyTex, *pBulletTex;
D3DXCreateTextureFromFile(pD3DDev, L"bg.bmp", &pBGTex);
D3DXCreateTextureFromFile(pD3DDev, L"player.bmp", &pPlayerTex);
D3DXCreateTextureFromFile(pD3DDev, L"enemy.bmp", &pEnemyTex);
D3DXCreateTextureFromFile(pD3DDev, L"bullet.bmp", &pBulletTex);

 

텍스처에서 사용하는 그림 파일들은 모두 포토샵으로 작업해서 필요한 부분에 알파값을 빼주어야 한다. 그렇게 하지 않으면 사각형 전체가 배경을 가리게 되어서 부자연스러운 게임이 될 것이다.

 

키보드 입력 처리

 

키보드 입력은 기본적으로 현재 키보드의 상태를 알아야 하는 경우와 트리거 정보가 필요한 경우가 있다. 윈도우즈의 기본 입력 시스템인 WM_KEYDOWNWM_KEYUP 메시지를 입력한 경우에는 트리거 정보를 알기는 편하지만, 키보드 상태를 알기 위해서는 추가 변수가 필요하다. GetAsyncKeyState 함수를 이용하는 경우에는 키보드 상태를 알기는 편하지만 트리거 정보를 알기 위해서는 추가 변수가 필요하다. 여기서는 GetAsyncKeyState 함수를 사용하므로 트리거 정보가 필요한 총알 발사를 위해서 추가 변수를 지정하였다.

 

//	차이시간계산
if( LastTick == 0 ) LastTick = GetTickCount();
float diff = (GetTickCount() - LastTick)*0.001f;
LastTick = GetTickCount();
//	키보드입력처리
if( GetAsyncKeyState(VK_LEFT) & 0x8000 )
	PlayerPos.x -= diff*80;
else if( GetAsyncKeyState(VK_RIGHT) & 0x8000 )
	PlayerPos.x += diff*80;
if( GetAsyncKeyState(VK_UP) & 0x8000 )
	PlayerPos.y -= diff*80;
else if( GetAsyncKeyState(VK_DOWN) & 0x8000 )
	PlayerPos.y += diff*80;

 

커서의 왼쪽키와 오른쪽키는 동시에 눌렸어도 처리는 하나만 될 수 있도록 else 구문을 사용하였다. 그에 비해서 왼쪽키와 윗키는 서로 같이 눌려도 처리가 될 수 있도록 하였다.

 

//	키보드입력에따른총알발사
if( !(GetAsyncKeyState(' ') & 0x8000) )
	FireKeyStatus = 0;
else if( FireKeyStatus == 0 )
{
	FireKeyStatus = 1;
	<파트 : 총알 발사>
}

 

FireKeyStatus는 처음 0으로 설정되어 있다. 현재 스페이스 키를 검사해서 눌려져 있지 않다면 FireKeyStauts0으로 설정한다. 그렇지 않고 현재 FireKeyStatus 의 값이 0이라는 것은 전 프레임에는 스페이스 키가 눌려져 있지 않았는데, 이번 프레임에서 눌러졌음을 의미한다. 즉 트리거 이벤트가 생긴 것이므로 이 때 총알을 발사하는 로직을 실행한다.

 

총알 발사 및 적기 생성

 

총알 발사는 키보드 입력부에서 바로 처리하도록 하였다. 총알 발사에는 제약점이 있는데 한 화면에 보이는 총알의 갯수이다. 이는 적기 생성에도 똑같이 적용된다. 결국 두개의 로직은 다루는 오브젝트만 틀릴 뿐 동일한 로직이다.

 

<파트 : 총알 발사>
for( int i = 0 ; i < 4 ; i++ )
	if( BulletStatus[i] == 0 )
	{
		BulletStatus[i] = 1;
		BulletPos[i].x = PlayerPos.x;
		BulletPos[i].y = PlayerPos.y-20;
		break
	}
<파트 : 적기 생성>
//	적기생성
if( rand()%100 < 2 )
{
	for( int i = 0 ; i < 20 ; i++ )
		if( EnemyStatus[i] == 0 )
		{
			EnemyStatus[i] = 1;
			EnemyPos[i].x = rand()%300 + 50;
			EnemyPos[i].y = -50;
			break
		}
}

 

적기 생성은 확률에 의해서 생성할 것인가 말 것인가 결정한다. rand 함수는 무작위 값을 생성하는 함수로 이 값을 100으로 나눈 나머지는 0부터 99까지의 유사 무작위 값이 발생한다. 이 경우 매 프레임마다 2% 확률로 적기가 생성된다.

 

총알 및 적기 이동

 

총알은 위로 움직이게 되고 적기는 아래로 움직이게 된다. 패턴 방식에 따라서 적기의 이동을 조절할 수 있지만, 여기서는 교육용 게임이므로 구현치 않았다.

 

총알과 적기는 모두 이동을 해서 화면 밖으로 나가면 자동 소멸시킴으로써, 그 다음 필요할 때 해당 리소스를 사용할 수 있게 해준다.

 

for( int i = 0 ; i < 4 ; i++ )
	if( BulletStatus[i] )
	{
		BulletPos[i].y -= 120*diff;
		if( BulletPos[i].y < -10 )
			BulletStatus[i] = 0;
	}
for( int i = 0 ; i < 20 ; i++ )
	if( EnemyStatus[i] )
	{
		EnemyPos[i].y += 80*diff;
		if( EnemyPos[i].y > 620 )
			EnemyStatus[i] = 0;
	}

 

총알과 적기 충돌

 

총알과 적기 충돌은 사각형 겹침 판정을 사용하였다. 총알, 적기 모두 중심점을 기본 피봇으로 설정하였으므로 차이값이 두 사각형 길이의 평균이 되면 겹쳤다고 판단한다. 보다 정밀한 겹침 판정을 할려면 매스킹 기법을 이용할 수 있다.

 

for( int i = 0 ; i < 4 ; i++ )
{
	if( BulletStatus[i] == 0 )
		continue
	for( int j = 0 ; j < 20 ; j++ )
		if( EnemyStatus[j] && 
			fabs(BulletPos[i].x - EnemyPos[j].x) < 30 && 
			fabs(BulletPos[i].y - EnemyPos[j].y) < 25 )
		{
			EnemyStatus[j] = 0;
			BulletStatus[i] = 0;
			break
		}
}

 

각각의 총알에 대해서 적기를 검사하였다. fabs 함수는 float 값에 대해서 절대값을 반환해준다. fabs( a - b ) 는 결과적으로 ab의 차이값을 얻어온다. 적기와 총알이 충돌하게 되면 적기와 총알의 상태값인 EnemyStatusBulletStatus의 값을 0으로 설정해준다. 점수 계산을 하고자 한다면 이 부분에 점수값을 증가시켜주면 될 것이다.

 

그래픽 데이터 업데이트

 

게임 데이터에 대한 모든 작업이 완료되었으므로 이제 그래픽 데이터를 업데이트하고 그려주면 된다. MakeVertices 함수를 이용하여 그래픽 데이터를 수정하도록 한다. 이 함수는 배경 스크롤을 위해서 텍스처 위치 값을 받을 수 있도록 하였다.

 

void MakeVertices(IDirect3DVertexBuffer9 *pVB, int left, int top, int right, 
	int bottom, float tleft=0, float ttop=0, float tright=1, float tbottom=1)
{
	CustomVertex *pVerts;
	pVB->Lock(0, 4*sizeof(CustomVertex), (void **)&pVerts, 0);
	pVerts[0].pos = D3DXVECTOR4(left, top, 0, 1);
	pVerts[0].tex1 = D3DXVECTOR2(tleft, ttop);
	pVerts[1].pos = D3DXVECTOR4(right, top, 0, 1);
	pVerts[1].tex1 = D3DXVECTOR2(tright, ttop);
	pVerts[2].pos = D3DXVECTOR4(left, bottom, 0, 1);
	pVerts[2].tex1 = D3DXVECTOR2(tleft, tbottom);
	pVerts[3].pos = D3DXVECTOR4(right, bottom, 0, 1);
	pVerts[3].tex1 = D3DXVECTOR2(tright, tbottom);
	pVB->Unlock();
}

 

 

MakeVertices 함수늘 대부분 텍스처 한장을 꽉 채우는 경우를 예상해서 C++에서 사용할 수 있는 파라미터 기본값을 넣어주었다.

 

//	그래픽데이터업데이트
MakeVertices(pBGVB, 0, 0, 400, 600, 0, 0.5-Scroll, 1, 1-Scroll);
MakeVertices(pPlayerVB, PlayerPos.x-20, PlayerPos.y-20, PlayerPos.x+20, 
	PlayerPos.y+20);
for( int i = 0 ; i < 20 ; i++ )
	if( EnemyStatus[i] )
		MakeVertices(pEnemyVB[i], EnemyPos[i].x-20, EnemyPos[i].y-20, 
			EnemyPos[i].x+20, EnemyPos[i].y+20);
for( int i = 0 ; i < 4 ; i++ )
	if( BulletStatus[i] )
		MakeVertices(pBulletVB[i], BulletPos[i].x-20, BulletPos[i].y-20, 
			BulletPos[i].x+5, BulletPos[i].y+5);

 

그래픽 데이터 표현

 

그래픽 데이터는 DrawVertices 함수를 이용하여 표현하도록 한다.

 

//	그림그리기
DrawVertices(pD3DDev, pBGVB, pBGTex);
DrawVertices(pD3DDev, pPlayerVB, pPlayerTex);
for( int i = 0 ; i < 20 ; i++ )
	if( EnemyStatus[i] )
		DrawVertices(pD3DDev, pEnemyVB[i], pEnemyTex);
for( int i = 0 ; i < 4 ; i++ )
	if( BulletStatus[i] )
		DrawVertices(pD3DDev, pBulletVB[i], pBulletTex);
728x90

'Lecture' 카테고리의 다른 글

Brick Breaker 게임 제작하기 - 2  (0) 2019.12.26
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

댓글