게임 메이커: 아두이노 게임기 만들기 PART7 – 총알과 해골 움직임, 마무리
강좌 시리즈:
.
지난 강좌에서 장애물 만들어서 이동시키는 루틴과 캐릭터와의 충돌을 검사하는 루틴을 추가했습니다. 이번 강좌에서 추가할 해골의 움직임과 총알, 캐릭터와의 충돌 검사도 거의 유사하게 작성하면 됩니다.
이번 강좌에서 사용되는 소스는 GitHub에서 받을 수 있습니다. [Template/Template5] 폴더를 확인하시면 됩니다.
https://github.com/godstale/game-maker
.
이미지 리소스 추가
이번 강좌에서는 해골의 움직임을 넣기 위해 bitmaps.cpp, bitmaps.h 파일에 IMG_enemy[], IMG_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;
.
테스트
앞서 설명한 총알과 해골의 움직임, 충돌, 점수와 관련된 코드 외에도 작게 수정한 부분들이 존재합니다. 마음에 안드시는 부분이 있다면 직접 여기저기 수정해 보시길 바랍니다.
이 소스코드를 아두이노에 올려서 테스트 해보세요. 정상적으로 게임이 진행되며 얼추 횡스크롤 액션 게임같은 느낌이 날 것입니다. 에러가 발생하는 부분 없이 잘 동작하는지 충분히 확인해보세요.
이상으로 아두이노에 올릴 8비트 게임 만들기 강좌를 마치겠습니다.
.
마치며
어릴 적 즐겼던 8비트 게임을 아두이노로 다시 즐길 수 있다는 것은 정말 즐거운 일입니다. PC/모바일 게임과는 비할 수 없을 정도로 단순하지만 그게 8비트 게임의 매력이기도 합니다. 그리고 어릴 적 추억을 담겨 더 끌리기도 합니다.
비록 이런 게임들을 직접 만드는 작업이 쉽진 않지만 앞선 강좌들과 예제코드를 활용하면 직접 8비트 게임을 만드는데 적지 않게 도움이 될 것입니다. 게임기도 직접 만들어보시고 게임도 직접 제작해서 즐겨보세요. 값비싼 전용 게임기가 부럽지 않습니다.