강좌 시리즈:

.

지난 강좌에서 장애물 만들어서 이동시키는 루틴과 캐릭터와의 충돌을 검사하는 루틴을 추가했습니다. 이번 강좌에서 추가할 해골의 움직임과 총알, 캐릭터와의 충돌 검사도 거의 유사하게 작성하면 됩니다.

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

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

.

이미지 리소스 추가

이번 강좌에서는 해골의 움직임을 넣기 위해 bitmaps.cpp, bitmaps.h 파일에 IMG_enemy[], IMG_enemy_die[] 배열을 추가했습니다.

enemy   enemy_die

이 두 배열은 해골의 bitmap 이미지를 코드로 바꾼 것입니다. 그리고 사용하기 편하도록 두 배열을 다시 enemy_anim[] 배열에 넣었습니다.

PROGMEM const unsigned char* enemy_anim[] = {
  IMG_enemy, IMG_enemy_die
};

.

해골과 총알의 움직임

updateMove() 함수가 화면상의 오브젝트 움직임을 업데이트 하는 함수입니다. 이 함수에 해골 생성과 움직임 업데이트 코드를 추가로 넣었습니다. 변경된 코드만 추려보면 아래와 같습니다.

	// Make enemy
	if(enemyCount < ENEMY_MAX && millis() > enemyTime) {
		// Make enemy
		for(int i=0; i<ENEMY_MAX; i++) {
			if(enemyX[i] < 1) {
				enemyX[i] = 127;
				enemyCount++;
				enemyTime = ENEMY_DELAY + getRandTime();	// Reserve next enemy
				break;
			}
		}
	}
	// Enemy move
	if(enemyCount > 0) {
		for(int i=0; i<ENEMY_MAX; i++) {
			if(enemyX[i] > 0) {
				enemyX[i] -= ENEMY_MOVE;
				if(enemyX[i] < OBSTACLE_DEL_THRESHOLD) {
					// clear last drawing
					display.fillRect(enemyX[i] + ENEMY_MOVE, ENEMY_POS_Y, ENEMY_WIDTH, ENEMY_HEIGHT, BLACK);
					
					// delete enemy
					enemyX[i] = 0;
					enemyCount--;
				}
			}
		}
	}

현재 생성된 해골이 없으면 getRandTime() 함수를 이용해 일정 시간 뒤에 해골을 만들도록 예약합니다. 시간이 흘러 해골 생성 시간이 되면 해골을 만들어 enemyX[] 배열에 위치 값을 업데이트 합니다. 해골이 캐릭터와 닿거나 총알에 파괴되면 해골이 화면에서 삭제됩니다.

매 프레임 업데이트 할 때 마다 생성된 해골의 움직임도 업데이트 해줍니다. 해골의 위치를 나타내는 enemyX[] 배열의 값이 ENEMY_MOVE 만큼 작아집니다. 그래서 마치 왼쪽으로 움직이는 것처럼 보이게 됩니다. 당연하게도 해골의 움직임을 매번 업데이트하기 위해서는 앞서 그렸던 해골 영역을 지우고 변경된 위치에 다시 그려야 합니다.

.

해골과 총알, 캐릭터의 충돌 검사

해골과 총알, 캐릭터의 충돌했는지 검사하는 함수는 checkCollision()입니다. 이미 총알과 해골의 충돌을 검사하는 루틴은 지난 강좌에서 작성 했습니다. 여기서는 추가로 해골과 캐릭터의 충돌을 검사해주면 됩니다.

	// check bullet touch
	if(bulletCount > 0) {
		for(int i=0; i<BULLET_MAX; i++) {
			if(bulletX[i] < 1) continue;
			for(int j=0; j<ENEMY_MAX; j++) {
				if(enemyX[j] < 1) continue;
				if(enemyX[j] < bulletX[i] + BULLET_WIDTH) {
					// Bullet touched enemy. delete bullet and enemy
					bulletX[i] = 0;
					bulletCount--;
					gameScore += ENEMY_KILL_SCORE;
					display.drawLine(prevBulletPosX[i], BULLET_POS_Y, prevBulletPosX[i] + BULLET_WIDTH, BULLET_POS_Y, BLACK);	// Delete previous drawing
					
					enemyX[j] = -1;		// To draw broken enemy image, set this -1
				}
			}
		}
	}
	
	// check enemy touch
	if(enemyCount > 0) {
		for(int i=0; i<ENEMY_MAX; i++) {
			if(enemyX[i] > 0) {
				if(enemyX[i] <= prevPosX + CHAR_WIDTH) {
					// Character touched enemy. End game
					charStatus = CHAR_DIE;
					break;
				}
			}
		}
	}

캐릭터의 X 위치는 고정이므로 해골의 움직임을 나타내는 enemyX[] 배열의 값이 캐릭터 위치까지 도달했는지만 검사하면 됩니다. 충돌한다면 캐릭터의 상태를 나타내는 charStatus 변수에 CHAR_DIE 값을 넣어줍니다. 그러면 캐릭터 이미지가 변경되면서 게임이 종료됩니다.

.

draw() 함수 수정

draw() 함수에 아직 포함되지 않은 캐릭터 총쏘기 이미지, 해골 이미지, 총알 이미지 등을 넣어줍니다. 변경된 부분은 아래와 같습니다.

	} else if(charStatus == CHAR_FIRE) {
		prevPosY = CHAR_POS_Y;
		display.fillRect(CHAR_POS_X, prevPosY, CHAR_WIDTH, CHAR_HEIGHT, BLACK);
		display.drawBitmap(CHAR_POS_X, prevPosY, (const unsigned char*)pgm_read_word(&(char_anim[FIRE_IMAGE_INDEX])), CHAR_WIDTH, CHAR_HEIGHT, WHITE);
		charStatus = CHAR_RUN;
	}

	...

	// draw enemy
	if(enemyCount > 0) {
		for(int i=0; i<ENEMY_MAX; i++) {
			if(enemyX[i] == -1) {
				// Enemy dead image
				display.fillRect(prevEnemyPosX[i], ENEMY_POS_Y, ENEMY_WIDTH, ENEMY_HEIGHT, BLACK);	// clear previous drawing
				display.drawBitmap(prevEnemyPosX[i], ENEMY_POS_Y, (const unsigned char*)pgm_read_word(&(enemy_anim[ENEMY_DIE_IMAGE_INDEX])), ENEMY_WIDTH, ENEMY_HEIGHT, WHITE);
				enemyX[i] = -2;
			}
			else if(enemyX[i] == -2) {
				// Clear enemy drawing
				display.fillRect(prevEnemyPosX[i], ENEMY_POS_Y, ENEMY_WIDTH, ENEMY_HEIGHT, BLACK);	// clear previous drawing
				enemyX[i] = 0;
				enemyCount--;
			}
			else if(enemyX[i] > 0) {
				// Enemy running image
				display.fillRect(prevEnemyPosX[i], ENEMY_POS_Y, ENEMY_WIDTH, ENEMY_HEIGHT, BLACK);	// clear previous drawing
				display.drawBitmap(enemyX[i], ENEMY_POS_Y, (const unsigned char*)pgm_read_word(&(enemy_anim[ENEMY_RUN_IMAGE_INDEX])), ENEMY_WIDTH, ENEMY_HEIGHT, WHITE);
				prevEnemyPosX[i] = enemyX[i];
			}
		}
	}
	
	// Draw bullet
	if(bulletCount > 0) {
		for(int i=0; i<BULLET_MAX; i++) {
			if(bulletX[i] < 1) continue;
			if(bulletX[i] > BULLET_START_X) {
				// Delete previous drawing
				display.drawLine(prevBulletPosX[i], BULLET_POS_Y, bulletX[i], BULLET_POS_Y, BLACK);
			}
			display.drawLine(bulletX[i], BULLET_POS_Y, bulletX[i] + BULLET_WIDTH, BULLET_POS_Y, WHITE);
			prevBulletPosX[i] = bulletX[i];
		}
	}

해골 이미지는 enemyX[] 배열이 가진 값에 따라 그릴 이미지가 틀려집니다. 총알과 충돌했을 때 파괴되는 이미지를 그려야 할 때가 있기 때문입니다. 즉, 해골의 움직임 뿐 아니라 총알과의 충돌 상태를 모두 enemyX[] 배열로 표시합니다.

총알의 움직임은 bulletX[] 배열로 표시됩니다.

추가로 draw() 함수에서는 점수도 표시하도록 수정되어 있습니다. 아래 코드가 점수를 게임 화면에 표시하는 코드입니다. 장애물을 넘거나 해골을 파괴하면 점수가 올라가는데 이때 점수 영역도 업데이트 됩니다.

	// Draw score
	if(prevGameScore != gameScore) {
		int tempS = gameScore+(int)((millis() - startTime) / 1000);
		int margin = 120 - getOffset(tempS);
		display.fillRect(margin, 1, 127, 9, BLACK);
		display.setCursor(margin, 1);
		display.print(tempS);
		prevGameScore = gameScore;
	}

.

게임 점수 기록하기

게임 점수가 최고점을 기록하면 EEPROM에 기록해서 재부팅 후에도 유지되도록 해줄 수 있습니다. 먼저 아두이노에 전원이 들어올 때 처음 실행되는 setup() 함수에서 EEPROM으로부터 최고 점수를 읽어오는 코드를 추가해야 합니다.

	// Read high score from eeprom
	while (!eeprom_is_ready()); // Wait for EEPROM to be ready
	cli();
	gameHighScore = eeprom_read_word((uint16_t*)eepromAddr);
	sei();
	if(gameHighScore < 0 || gameHighScore > 65500) gameHighScore = 0;

그리고 게임이 종료되고 게임 결과 화면이 표시될 때 최고 점수와 현재 점수를 비교합니다. 최고 점수를 갱신한 경우는 EEPROM에 기록합니다.

		else if (gameState == STATUS_RESULT) {  // Draw a Game Over screen w/ score
			gameScore += (int)((millis() - startTime) / 1000);
			if (gameScore > gameHighScore) { 
				gameHighScore = gameScore; 
				cli();
				eeprom_write_word((uint16_t*)eepromAddr, gameHighScore);
				sei();
			}  // Update game score

EEPROM 기록을 위해 아래와 같은 코드가 Template5.ino 파일 최상단 영역에 추가되었습니다. EEPROM 제어를 위해 필요한 헤더 파일과 전역 변수들입니다.

#include <avr/interrupt.h>
#include <avr/eeprom.h>

...
// System
int eepromAddr = 0;
...
int prevGameScore = 0;

.

테스트

앞서 설명한 총알과 해골의 움직임, 충돌, 점수와 관련된 코드 외에도 작게 수정한 부분들이 존재합니다. 마음에 안드시는 부분이 있다면 직접 여기저기 수정해 보시길 바랍니다.

이 소스코드를 아두이노에 올려서 테스트 해보세요. 정상적으로 게임이 진행되며 얼추 횡스크롤 액션 게임같은 느낌이 날 것입니다. 에러가 발생하는 부분 없이 잘 동작하는지 충분히 확인해보세요.

runningman07

이상으로 아두이노에 올릴 8비트 게임 만들기 강좌를 마치겠습니다.

.

마치며

어릴 적 즐겼던 8비트 게임을 아두이노로 다시 즐길 수 있다는 것은 정말 즐거운 일입니다. PC/모바일 게임과는 비할 수 없을 정도로 단순하지만 그게 8비트 게임의 매력이기도 합니다. 그리고 어릴 적 추억을 담겨 더 끌리기도 합니다.

비록 이런 게임들을 직접 만드는 작업이 쉽진 않지만 앞선 강좌들과 예제코드를 활용하면 직접 8비트 게임을 만드는데 적지 않게 도움이 될 것입니다. 게임기도 직접 만들어보시고 게임도 직접 제작해서 즐겨보세요. 값비싼 전용 게임기가 부럽지 않습니다.