게임 메이커: 아두이노 게임기 만들기 PART6 – 장애물 움직임과 충돌 구현
강좌 시리즈:
.
지난 시간까지 게임의 큰 틀도 만들고 게임 화면에서의 캐릭터 움직임도 만들었습니다. 이제 좀 더 나아가 B 버튼을 누르면 캐릭터가 총을 쏘고, 총알의 움직임이도록 구현해 보겠습니다. 그리고 장애물도 랜덤하게 생성해서 캐릭터가 장애물에 부딪히면 게임이 종료되도록 해보겠습니다.
이번 강좌에서 사용되는 소스는 GitHub에서 받을 수 있습니다. [Template/Template4] 폴더를 확인하시면 됩니다.
https://github.com/godstale/game-maker
이번 강좌에서 가장 중요한 부분은 장애물을 생성하고 충돌을 감지하는 부분입니다. 이 부분을 어떻게 구현할지 나름대로 생각을 한번 해보신 뒤, 아래 내용을 확인해보시는 것이 좋겠습니다.
.
게임 종료 조건 구현
이전 강좌의 소스에서는 B 버튼을 눌렀을 때 강제로 게임 종료 화면으로 바뀌도록 했습니다. 이제는 진짜 게임답게 캐릭터가 장애물에 부딪힐 때 종료하도록 구현할 것입니다. 따라서 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(charStatus == CHAR_DIE) { setResultMode(); } }
charStatus 변수가 CHAR_DIE 일 때 게임 결과 화면으로 넘어가도록 수정했습니다. 나머지 장애물 생성과 캐릭터가 장애물과 충돌했는지 확인하는 부분은 updateMove(), checkCollision(), draw() 함수 안에서 처리합니다. 그리고 추가로 캐릭터가 총을 쏠 수 있도록 처리도 같이 해주도록 하겠습니다.
.
총 쏘기 구현
checkInput() 함수 안에서 B 버튼이 눌러졌는지 확인한 뒤 버튼이 눌러진 경우 총알을 하나 생성합니다.
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 } } else if(bBut) { if(charStatus == CHAR_RUN) { if(bulletCount < BULLET_MAX) { for(int i=0; i<BULLET_MAX; i++) { if(bulletX[i] < 1) { bulletX[i] = BULLET_START_X - BULLET_MOVE; charStatus = CHAR_FIRE; bulletCount++; } } } } } }
BULLET_MAX 값을 1로 제한해 뒀기 때문에 총알은 화면상에 하나만 생성됩니다. 그리고 캐릭터가 CHAR_RUN 상태일 때만 생성할 수 있습니다. 총알을 생성할 때 캐릭터 이미지도 총 쏘는 이미지로 바꾸기 위해 charStatus 변수를 CHAR_FIRE로 변경했습니다. 총알의 X 좌표는 캐릭터 위치부터 매 프레임 값을 증가시키기 때문에 오른쪽으로 이동합니다. 다음 강좌에서 해골 움직임을 추가하면 해골과의 충돌 감지도 추가할 것입니다.
.
장애물 생성
updateMove() 함수 안에서 장애물과 관련된 처리를 해줍니다.
void updateMove() { // Character move 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, CHAR_WIDTH, CHAR_HEIGHT, BLACK); // delete previous character drawing } } // Make obstacle if(obstacleCount < OBSTACLE_MAX && millis() > obstacleTime) { // Make obstacle for(int i=0; i<OBSTACLE_MAX; i++) { if(obstacleX[i] < 1) { obstacleX[i] = 127; obstacleCount++; obstacleTime = getRandTime(); // Reserve next obstacle break; } } } // Obstacle move if(obstacleCount > 0) { for(int i=0; i<OBSTACLE_MAX; i++) { if(obstacleX[i] > 0) { obstacleX[i] -= OBSTACLE_MOVE; if(obstacleX[i] < OBSTACLE_DEL_THRESHOLD) { // clear last drawing display.fillRect(obstacleX[i] + OBSTACLE_MOVE, OBS_POS_Y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT, BLACK); // delete obstacle obstacleX[i] = 0; obstacleCount--; } } } } // Bullet move if(bulletCount > 0) { for(int i=0; i<BULLET_MAX; i++) { if(bulletX[i] > WIDTH - BULLET_MOVE) { // Bullet touched end of screen. delete bullet bulletX[i] = 0; bulletCount--; } else if(bulletX[i] > 0) { bulletX[i] += BULLET_MOVE; } } } }
캐릭터의 점프 움직임을 처리한 뒤, 현재 장애물이 몇 개 예약되어 있는지를 확인하고 장애물을 새로 생성합니다.
장애물을 생성하는 규칙은 간단합니다. 현재 장애물 개수를 파악해서 장애물 수가 MAX(3개) 보다 작으면 다음 장애물이 나타날 시간을 getRandTime() 함수로 정합니다. 그리고 다음 장애물이 나타날 시간이 되면 장애물을 화면에 표시합니다. 다시 다음 장애물이 나타날 시간을 getRandTime() 함수로 정해둡니다. 화면에 표시되는 장애물 수를 확인해서 이 작업을 반복합니다. 그러면 화면에 장애물 간격이 랜덤하게 계속 생성됩니다. 일단 장애물이 생성되면 obstacleX[] 배열에 127 값이 셋팅됩니다. 장애물이 삭제되면 이 값이 0으로 바뀝니다.
장애물을 표시할 시간이 되면 화면 오른쪽 끝에서부터 장애물을 표시하고 매 프레임이 반복될 때 OBSTACLE_MOVE 만큼 왼쪽으로 이동합니다. obstacleX[] 배열의 값이 점점 작아지게 됩니다. 물론 장애물이 매 프레임 이동하므로 이전에 그린 장애물을 지우고 왼쪽으로 이동한 위치에 다시 그려야 합니다. 그리고 화면 왼쪽 끝에 도달하면 장애물은 삭제해야 합니다. 장애물이 삭제되면 다음 장애물이 언제 나올지 랜덤한 시간을 정해줘야겠죠.
총알의 움직임도 마찬가지입니다. 캐릭터 위치로부터 BULLET_MOVE 만큼 오른쪽으로 계속 이동하다가 화면 오른쪽 끝에 닿으면 총알이 삭제됩니다. 총알의 위치를 표시하는 bulletX[] 배열의 값으로 움직임을 컨트롤 합니다.
.
충돌 판단
checkCollision() 함수를 본격적으로 구현해줘야 합니다. 이 함수는 캐릭터와 장애물, 캐릭터와 해골, 총알과 해골의 충돌을 감지해서 게임을 종료시키거나 해골을 없애는 역할을 합니다.
void checkCollision() { // check obstacle touch if(obstacleCount > 0) { for(int i=0; i<OBSTACLE_MAX; i++) { if(obstacleX[i] > 0) { if(prevPosY + CHAR_HEIGHT >= OBS_POS_Y && obstacleX[i] < prevPosX + CHAR_WIDTH - CHAR_COLLISION_MARGIN && obstacleX[i] + OBSTACLE_WIDTH > prevPosX + CHAR_COLLISION_MARGIN) { // Character stepped on obstacle. End game charStatus = CHAR_DIE; break; } } } } // 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--; enemyX[j] = -1; enemyCount--; } } } } }
캐릭터와 장애물의 충돌을 감지하기 위해서는 매 프레임마다 캐릭터 영역(24×24)이 obstacleX[] 배열에 담긴 값과 겹치는지 확인하면 됩니다. 만약 영역이 겹친다면 점프로 피하지 못한 상태이므로 charStatus 변수를 CHAR_DIE 로 변경해서 게임을 종료합니다.
총알도 마찬가지로 enemyX[] 배열과 겹치는지 확인해서 겹치는 경우 해골과 총알을 없애면 됩니다. 해골의 움직임을 컨트롤하기 위한 배열인 enemyX[] 가 소스에 포함되어 있긴 하지만 이번 강좌에서 해골을 생성하진 않습니다. 충돌 검사만 합니다.
.
draw() 함수에서 장애물 그리기
장애물과 총알을 생성하고 프레임마다 X축 좌표 업데이트도 해주도록 구현해 뒀으므로 이제 화면에 그려주기만 하면 됩니다. draw() 함수에서 아래 부분이 추가되었습니다.
// draw obstacle if(obstacleCount > 0) { for(int i=0; i<OBSTACLE_MAX; i++) { if(obstacleX[i] > 0) { display.fillRect(obstacleX[i] + OBSTACLE_MOVE, OBS_POS_Y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT, BLACK); // clear previous drawing display.fillRect(obstacleX[i], OBS_POS_Y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT, WHITE); } } } if(charStatus == CHAR_DIE) { // Start game end drawing for(int i=prevPosY; i<=CHAR_POS_Y; i++) { display.fillRect(CHAR_POS_X, i, CHAR_WIDTH, CHAR_HEIGHT, BLACK); display.drawBitmap(CHAR_POS_X, i, (const unsigned char*)pgm_read_word(&(char_anim[DIE_IMAGE_INDEX])), CHAR_WIDTH, CHAR_HEIGHT, WHITE); display.display(); } delay(400); return; }
장애물을 컨트롤하는 배열인 obstacleX[] 배열을 검사해서 0이 아닌 경우(장애물이 생성된 경우 0보다 큰 값을 가짐) 장애물을 화면에 그려줍니다. 이미지를 사용하지 않고 작은 사각형으로 바닥에 표시됩니다.
장애물이 캐릭터와 닿아 게임이 종료된 경우, 즉, charStatus 변수가 CHAR_DIE 값을 가지는 경우는 캐릭터 이미지를 바꿔줍니다.
.
전역 변수와 함수 추가
장애물, 총알, 해골의 움직임에 필요한 전역 변수들을 미리 정의해 뒀습니다. 아래 코드는 이전 강좌에서 추가된 부분만 정리한 것입니다.
#define FIRE_IMAGE_INDEX 4 #define DIE_IMAGE_INDEX 5 // Object parameters #define OBSTACLE_MAX 3 #define OBSTACLE_MOVE 3 #define OBSTACLE_WIDTH 3 #define OBSTACLE_HEIGHT 4 #define OBS_POS_Y 59 #define OBSTACLE_DEL_THRESHOLD 5 int obstacleX[] = {0, 0, 0}; int obstacleCount = 0; unsigned long obstacleTime; #define ENEMY_MAX 1 #define ENEMY_MOVE 2 #define ENEMY_WIDTH 16 #define ENEMY_POS_Y 40 #define ENEMY_START_X 128 int enemyX[] = {0}; int enemyCount = 0; unsigned long enemyTime; #define BULLET_MAX 1 #define BULLET_MOVE 3 #define BULLET_WIDTH 3 #define BULLET_POS_Y 48 #define BULLET_START_X 44 int bulletX[] = {0}; int bulletCount = 0; unsigned long bulletTime; unsigned long getRandTime();
게임 진행에 필요한 변수들이 추가되면 게임 화면에 진입하기 전에 이 변수들을 초기화 해줘야 합니다. 따라서 게임 화면에 들어갈 때 호출되는 setGameMode() 함수 안에서 변수 초기화 코드가 추가되어야 합니다.
void setGameMode() { gameState = STATUS_PLAYING; drawBg = true; gameScore = 0; gameEnd = false; charAniIndex = 0; charAniDir = 1; charStatus = CHAR_RUN; bulletCount = 0; delay(300); for(int i=0; i<OBSTACLE_MAX; i++) obstacleX[i] = 0; obstacleCount = 0; obstacleTime = millis() + 2000; startTime = millis(); }
Template4.ino 파일 가장 아래쪽에 장애물 생성 시간을 랜덤하게 잡아주는 getRandTime() 함수가 구현되어 있습니다.
unsigned long getRandTime() { return millis() + 100 * random(12, 30); }
추가로 게임 결과 화면으로 이동할 때 스코어 계산을 하는 루틴도 추가되었습니다. 스코어는 단순하게 게임이 진행된 시간(초)을 점수로 계산합니다. 이를 위해 게임 시작할 때 시간 값을 startTime 변수에 기록해 둡니다.
.
테스트
이상의 소스를 업로드 후 게임을 실행해보면 캐릭터가 점프를 통해 장애물을 넘으며 게임을 진행할 수 있습니다. 그리고 캐릭터가 장애물에 닿으면 게임이 종료되고 결과를 보여준 뒤, 다시 메뉴 화면으로 돌아옵니다.
이제 게임의 75% 이상이 구현된 것이라 봐도 무방합니다. 다음 강좌에서 해골의 움직임과 총알의 움직임을 구현해서 게임 화면을 마무리 해 보도록 하겠습니다. 그리고 게임의 완성도를 높이기 위해 소소한 부분들을 보완해 보도록 하겠습니다.