강좌 시리즈:

.

지난 강좌에서 게임에 필요한 각 화면들과 화면들 사이의 전환 기능을 만들었습니다. 기본적인 게임 UI 틀은 완성이 된 셈입니다. 이제 본격적으로 게임 화면을 만들어 나갈 차례입니다.

이번 강좌에서 사용되는 소스는 GitHub에서 받을 수 있습니다. [Template/Template3] 폴더를 확인하시면 됩니다.

https://github.com/godstale/game-maker

러닝맨 게임은 횡 스크롤 액션 게임입니다. 주인공 캐릭터가 좌측 하단에서 달리면서(X축 위치는 고정) 다가오는 장애물을 뛰어 넘습니다. 장애물을 뛰어 넘을 때 캐릭터의 X축 위치는 고정이지만, Y축 위치가 변합니다. 캐릭터가 장애물은 넘지 못하고 닿게 되면 게임은 종료됩니다. 그리고 종종 나타나는 해골을 총을 쏴서 없앨 수 있습니다. 마찬가지로 해골도 총을 쏴서 없애지 않으면 캐릭터와 닿았을 때 게임이 종료됩니다.

runningman07

간단한 게임이지만 이걸 한 번에 코딩하기는 힘듭니다. 이번 강좌에서는 캐릭터가 달리는 애니메이션과 점프 움직임부터 구현해 보겠습니다.

.

캐릭터 애니메이션 리소스

캐릭터가 달리는 느낌을 만들기 위해 3개의 이미지(24×24)를 준비했습니다. 이 3개의 이미지를 배열에 넣어두고 2-1-2-3-2 순서대로 무한반복 시키면 캐릭터가 달리는 애니메이션이 됩니다. 일정한 간격으로 실행되는 게임 프레임 그리는 루틴에서 그려야 할 캐릭터 이미지의 index를 바꿔주면 되겠죠.

char_run1 char_run3 char_run2

그리고 캐릭터가 점프할 때도 점프 이미지를 그려줘야 합니다.

char_jump

캐릭터가 총을 쏠 때도 따로 이미지가 필요하고

char_fire

캐릭터가 장애물이나 해골에 닿아 종료될 때 사용할 이미지도 필요합니다.

char_die

이상의 이미지들은 코드 형태로 바뀌어서 bitmaps.cpp 파일에 들어가 있습니다. 이미지를 코드로 변경하는 방법은 [링크]를 참고하세요.

PROGMEM const unsigned char IMG_char_run1[] = { ... }
PROGMEM const unsigned char IMG_char_run2[] = { ... }
PROGMEM const unsigned char IMG_char_run3[] = { ... }
PROGMEM const unsigned char IMG_char_jump[] = { ... }
PROGMEM const unsigned char IMG_char_fire[] = { ... }
PROGMEM const unsigned char IMG_char_die[] = { ... }

그리고 이 이미지들은 다시 char_anim[] 배열에 순서대로 들어가 있습니다. 따라서 char_anim[] 배열을 이용해서 필요할 때 캐릭터 이미지를 꺼내 출력하면 됩니다.

PROGMEM const unsigned char* char_anim[] = {
    IMG_char_run1, IMG_char_run2, IMG_char_run3, IMG_char_jump, IMG_char_fire, IMG_char_die
};

.

게임 화면 코드 수정

이제 이미지들을 이용해서 화면에 그려보도록 하겠습니다. Template3.ino 파일에서 loop() 함수에 있는 게임 화면 코드를 수정하면 됩니다. else if (gameState == STATUS_PLAYING) {} 블록을 보시면 됩니다.

		else if (gameState == STATUS_PLAYING) { // If the game is playing
			// Run game engine
			checkInput();
			updateMove();
			checkCollision();
			
			// Draw game screen
			draw();
			
			// Exit condition
			if(bBut) {
				setResultMode();
			}
		}

굉장히 간단합니다. checkInput(), updateMove(), checkCollision(), draw() 함수가 차례대로 호출되고 B 버튼이 눌러지면 결과 화면으로 넘어가도록 작성되어 있습니다.

여기서 checkCollision() 함수는 캐릭터, 장애물, 해골, 총알 등이 서로 닿았는지 판별하는 함수인데 아직 구현하지 않고 함수 껍데기만 만들어 둔겁니다. 그래서 checkInput(), updateMove(), draw() 세 개의 함수를 추가해서 캐릭터 애니메이션과 점프 움직임을 구현하게 됩니다. loop() 함수 블록이 끝나는 지점을 보면 checkInput(), updateMove(), draw() 함수를 구현한 코드가 나옵니다.

.

사용자 입력 체크

점프 움직임을 구현하기 위해서는 먼저 사용자가 점프 버튼을 눌렀는지 확인부터 해야합니다. checkInput() 함수에서 이 작업을 처리합니다..

void checkInput() {
	if(up || aBut) {
		if(charStatus == CHAR_RUN) {
			charStatus = CHAR_JUMP;
			charJumpIndex = 0;
			charJumpDir = JUMP_STEP;
			prevPosX = CHAR_POS_X; prevPosY = CHAR_POS_Y;
		} else if(charStatus == CHAR_JUMP) {
			// Do nothing
		}
	}
}

조이스틱을 위 방향으로 올리거나 A 버튼을 누르면 charStatus 변수를 확인해서 캐릭터의 상태를 바꿔줍니다. 즉, 캐릭터가 달리기(기본 상태) 상태일 때 A 버튼이 눌러졌으면 점프 상태로 변경하고 Y축 방향 움직임을 바꿀 준비를 합니다.

.

점프 움직임 구현

checkInput() 함수에서는 사용자 입력을 체크해서 상태를 바꾸는 작업만 합니다. 매 프레임이 실행될 때 마다 캐릭터의 상태를 체크해서 움직임을 변경시켜주는 작업은 updateMove() 함수에서 해줍니다.

void updateMove() {
	if(charStatus == CHAR_JUMP) {
		charJumpIndex += charJumpDir;
		if(charJumpIndex >= JUMP_MAX) charJumpDir *= -1;
		// if jump ended
		if(charJumpIndex <= 0 && charJumpDir < 0) {
			charJumpDir = JUMP_STEP;
			charStatus = CHAR_RUN;
			display.fillRect(prevPosX, prevPosY, 24, 24, BLACK);	// delete previous character drawing
		}
	}
}

코드를 보면 캐릭터가 JUMP 상태일 때 Y 축방향으로 움직이도록 작성되어 있습니다. (캐릭터가 그려질 위치를 이동) 처음에는 위쪽으로 움직이다 JUMP_MAX 값을 넘어서면 다시 아래 방향으로 내려오도록 작성되어 있습니다. 그리고 점프가 종료되고 캐릭터가 다시 땅에 닿으면 CHAR_RUN 달리기 모드로 변경하고 기존에 그렸던 점프 이미지를 지워줍니다.

.

게임 화면 그리기

사용자 입력도 체크했고, 점프 상태일 때 캐릭터의 위치도 변경되도록 checkInput(), updateMove() 함수에서 처리해 줬습니다. 이제 현재 상태대로 화면을 업데이트 하도록 draw() 함수를 작성해주면 됩니다.

void draw() {
	// draw background
	if(drawBg) {
		display.clearDisplay();
		display.drawLine(0, 63, 127, 63, WHITE);
		drawBg = false;
	}
	
	// draw char
	if(charStatus == CHAR_RUN) {
		charAniIndex += charAniDir;
		if(charAniIndex >= RUN_IMAGE_MAX || charAniIndex <= 0) charAniDir *= -1;
		display.fillRect(CHAR_POS_X, CHAR_POS_Y, 24, 24, BLACK);
		display.drawBitmap(CHAR_POS_X, CHAR_POS_Y, (const unsigned char*)pgm_read_word(&(char_anim[charAniIndex])), 24, 24, WHITE);
	} else if(charStatus == CHAR_JUMP) {
		display.fillRect(prevPosX, prevPosY, 24, 24, BLACK);
		prevPosY = CHAR_POS_Y-charJumpIndex;
		display.drawBitmap(prevPosX, prevPosY, (const unsigned char*)pgm_read_word(&(char_anim[JUMP_IMAGE_INDEX])), 24, 24, WHITE);
	}
	
	// Show on screen
	display.display();
}

러닝맨 게임은 화면 전체의 움직임이 그리 큰 게임이 아닙니다. 그래서 캐릭터와 장애물, 해골 등의 오브젝트들이 움직일 때 이전에 그렸던 영역만큼만 지우고 새로 이미지를 그리도록 작성 했습니다. 즉 프레임을 그릴 때 화면에서 움직임이 있는 부분만 갱신합니다.

그래서 게임 배경화면이나 스코어 등의 화면 전체 레이아웃은 필요할 때만 그려줍니다. 이걸 알려주는 변수가 drawBg 변수입니다. 게임 화면에 들어온 직후처럼 화면 전체 업데이트가 필요하다면 drawBg 값을 true로 변경해주면 됩니다.

게임 배경에 대한 처리가 끝나면 캐릭터를 그려줍니다. 캐릭터를 그릴 때는 현재 상태(달리기 또는 점프)를 확인해서 다르게 처리합니다.

달리기 상태일 때는 3개의 달리기 이미지가 2-1-2-3-2 순서로 프레임마다 변경 될 수 있도록 해줍니다.

char_run2 char_run3 char_run1

점프 상태일 때는 이미지는 하나로 고정인데 Y축 좌표가 변화합니다. 그리고 이미지를 그리기 전에 이전에 그렸던 이미지를 지워줘야 하므로 현재 사용한 Y축 좌표는 계속 기억해 둬야 합니다. 이런 역할을 하는 변수가 prevPosY입니다.

화면 그리는 작업이 끝나면 display.display();를 호출해서 실제 화면을 갱신하도록 합니다.

.

전역 변수 추가 내용

캐릭터 움직임을 구현하기 위해 4개의 함수와 다수의 전역 변수를 추가했습니다. 추가된 코드는 다음과 같습니다.

//////////////////////////////////////////////////
// Game engine parameters
//////////////////////////////////////////////////

// Character parameters
#define CHAR_POS_X 20
#define CHAR_POS_Y 38

// Character status
#define CHAR_RUN 1
#define CHAR_JUMP 2
#define CHAR_FIRE 3
#define CHAR_DIE 4
int charStatus = CHAR_RUN;

// Run parameters
#define RUN_IMAGE_MAX 2
int charAniIndex = 1;
int charAniDir = 1;
boolean drawBg = true;

// Jump parameters
#define JUMP_MAX 20
#define JUMP_STEP 4
#define JUMP_IMAGE_INDEX 3
int charJumpIndex = 0;
int charJumpDir = JUMP_STEP;
int prevPosX = CHAR_POS_X;
int prevPosY = CHAR_POS_Y;


void checkInput();
void updateMove();
void checkCollision();
void draw();

.

테스트

소스코드를 아두이노에 올려 동작을 확인해보시죠. start game 메뉴를 이용해 게임화면에 들어가면 캐릭터가 뛰는 동작이 보이고, A 버튼을 눌렀을 때 점프도 해야 합니다. 그리고 B 버튼을 누르면 게임이 종료되어 결과 화면으로 넘어갑니다.

다음 강좌에서는 장애물을 생성해서 보여주고 캐릭터가 넘지 못했을 때 종료되도록 만들겠습니다. 그리고 캐릭터가 총을 쏠 수 있도록 할 예정입니다.