- 전체
- 게임 일반 (make game basics)
- 모바일 기획 및 디자인
- GameMaker Studio
- Unity3D
- Cocos2D
- 3D Engine OGRE
- 3D Engine irrlicht
- copperCube
- corona SDK
- Windows Basic Game
- BaaS (Mobile Backend)
- phnegap & cordova
- ionic & anguler
- parse (backend)
- firebase (backend)
- Game Backend Server / Opt
- web assembly
- Smart Makers
- pyGame
- 머드(MUD) 게임 만들기
- Xamarin(자마린)
- flutter (플루터 앱 개발)
- construct 2 / 3
게임 일반 (make game basics) (CGP) 12장 Snake 게임
2021.06.28 21:50
(CGP) 12장 Snake 게임
Snake 게임은 고전 게임이며 가장 친숙한 게임에 해당이 된다.
이미 11장에서 Snake 게임을 스테이지별로 다르게 진행할 수 있는 맵툴을 제작해 보았듯이 맵툴의 데이터가
실제 게임에 어떻게 적용되고 활용되는지를 프로그래밍을 통해 습득하는 것이 이장의 키포인트 이다.
유튜브 동영상 강의 주소
(1) http://youtu.be/tiNetLRLNws
(2) http://youtu.be/Kb2zd75n8iw
(3) http://youtu.be/2WDfa14IBNE
(4) http://youtu.be/JDO4nlqplfg
12.1 기획
■ 스토리
적 뱀을 피해 다니면서 제한 시간 안에 사방에 떨어져 있는 먹이를 모두 먹어라.
■ 게임방식
주인공 뱀은 다른 방향키를 누르기까지 현재 설정된 방향으로 계속 이동한다.
주인공 뱀과 적 캐릭터가 부딪히면 주인공 뱀의 꼬리는 하나씩 줄어들지만 먹이를 먹으면 하나씩 늘어난다.
■ 제한사항
각 스테이지마다 제한된 시간이 있으며 제한 시간 안에 먹이를 전부 먹어야만 다음 스테이지로 넘어간다. 기본 꼬리는 3개로 하며 꼬리가 없는 상태에서 적 뱀과 충돌하면 게임은 종료된다.
■ 기획화면
[그림 12-1] 게임 기획 화면
12.2 실행 화면
[그림 12-2] 게임 메인 화면
[그림 12-3] 스테이지 화면
[그림 12-4] 게임 진행 화면
[그림 12-5] 미션 실패 화면
[그림 12-6] 미션 성공 화면
[그림 12-7] 결과 화면
12.3 게임 제작 로드맵
Snake 게임의 제작 단계는 다음과 같이 6단계로 나눌 수 있다.
각 단계마다 일부는 이미 툴에서 제작한 코드를 재사용한다.
이 재사용되는 코드는 툴에서 이미 검증이 된 코드이므로 보다 안정적으로 사용할 수 있다.
이와 같이 툴과 게임은 서로 보완 관계를 가지고 있다.
[STEP 01]
[STEP 02]
[STEP 03]
[STEP 04]
[STEP 05]
[STEP 06]
[그림 12-8] 게임 제작 로드맵
12.4 단계별 프로그래밍
[STEP 01]
[그림 12-9] 1단계 제작 로드맵
■ Snake
- 속성
Snake 캐릭터는 머리와 꼬리라는 특이한 구조를 가지고 있다. Snake가 게임에서 소멸되는 경우는 꼬리가 적 캐릭터들로부터 공격을 받아 전부 소멸된 때이다.
Snake 생명은 ‘먹이’라는 것을 통해 연장되며 ‘먹이’는 Snake에게 꼬리를 하나씩 생성해 주는 아이템이 되지만 Snake 생명이 0이 되는 순간 게임을 종료하게 한다.
Snake 게임에서 방향키는 단순히 이동하려는 방향만 바꾸며 Snake는 현재 설정된 방향으로 계속 이동하는 특성이 있다. 그래서 Snake는 일정한 시간 간격으로 이동하는 속성이 있다.
Snake 꼬리는 이동하려는 꼬리 좌표를 다음 꼬리에 전달하며 이동한다.
꼬리의 증감은 단순히 최대 메모리를 미리 확보해 놓고 꼬리 개수만큼 일부 메모리를 사용한다. 이와 같은 사항을 정리하여 나타내면 아래 [표 12-1]과 같다.
① 생명 ② 좌표 ③ 이전 좌표 ④ 이동 방향 ⑤ 이동 시간 간격 ⑥ 이전 이동 시각 ⑦ 꼬리 정보 |
[표 12-1] Snake 속성
Snake가 이동하게 되면 꼬리 이동은 처음 머리의 좌표를 다음에 나오는 꼬리에게 전달하고 전달 받은 꼬리는 그 다음 꼬리에게 자신의 좌표를 전달하는 방식으로 이동한다. 아래 [그림 12-10]은 이와 같이 이동하는 꼬리의 좌표 경로를 나타낸다.
(이동 방향) |
(꼬리의 좌표 경로) |
[그림 12-9] 꼬리 좌표의 이동 경로
[표 12-1]의 꼬리 정보는 다음과 같다.
① 좌표 ② 이전 좌표 |
[표 12-2] 꼬리 정보
Snake의 꼬리 개수는 주인공의 생명과 밀접한 관계가 있으므로 최후 머리만 남았을 때 생명이 1이라면 아래 [식 12-1]을 통해 꼬리 개수를 구할 수 있다.
꼬리 개수 = 주인공의 생명 - 1 |
[식 12-1] 꼬리 개수
이 꼬리 개수는 전체 꼬리 중에서 일부를 사용하는 개수가 된다.
이제 Snake와 꼬리 속성을 다시 정의하면 아래 [소스 12-1]과 같다.
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT; typedef struct _POS { int nXl, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE; |
[소스 12-1] Snake 속성 정의
■ 키보드 처리
이동키 처리는 지금까지 제작해 온 키보드 처리와 동일하며 이동에 관련된 키를 정의하면 아래 [표 12-3]과 같다.
이동키 |
역할 |
← |
왼쪽으로 이동, 아스키 코드 값은 75 |
→ |
오른쪽으로 이동, 아스키 코드 값은 77 |
↑ |
위로 이동, 아스키 코드 값은 72 |
↓ |
아래로 이동, 아스키 코드 값은 80 |
[표 12-3] 이동키 정의
Snake 게임에서 Snake는 키 입력이 있든 없든 설정된 방향으로 계속 이동하며 위에서 정의한 키는 Snake의 이동 방향을 바꾼다.
[실습 예제 12-1]
[표 12-3]에 따라 Snake 머리가 [그림 12-11]과 같이 이동하게 게임 프레임워크 안에서 프로그래밍해 보자. Snake 머리는 한 번 이동 방향을 설정하면 다른 방향의 이동키가 들어올 때까지 계속 이동하는 특성이 있다.
[그림 12-11] 키 입력에 따른 Snake의 머리 이동
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
#include <stdio.h> #include <windows.h> #include <conio.h> #include <time.h> #include "Screen.h"
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _POS { int nX, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE;
SNAKE g_Snake;
void Init() { // Note: Snake 초기화 g_Snake.nLife = 1; g_Snake.sHead.nX = 30; g_Snake.sHead.nY = 10; g_Snake.MoveTime = 100; g_Snake.OldTime = clock(); g_Snake.nDirect = LEFT; }
void Update() { clock_t CurTime = clock();
if( CurTime - g_Snake.OldTime > g_Snake.MoveTime ) { g_Snake.OldTime = CurTime; switch( g_Snake.nDirect ) { case LEFT : if( g_Snake.sHead.nX - 2 > 1 ) g_Snake.sHead.nX -= 2; break; case RIGHT : if( g_Snake.sHead.nX + 2 < 60 ) g_Snake.sHead.nX += 2; break; case UP : if( g_Snake.sHead.nY - 1 > 1 ) g_Snake.sHead.nY--; break; case DOWN : if( g_Snake.sHead.nY + 1 < 20 ) g_Snake.sHead.nY++; break; } } }
void Render() { ScreenClear();
ScreenPrint( g_Snake.sHead.nX, g_Snake.sHead.nY, "●" );
ScreenFlipping(); }
void Release() { }
int main(void) { int nKey;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch();
switch( nKey ) { case 75: g_Snake.nDirect = LEFT; break; case 77: g_Snake.nDirect = RIGHT; break; case 72: g_Snake.nDirect = UP; break; case 80: g_Snake.nDirect = DOWN; break; } }
Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-2] Snake 이동
48행과 52행을 보면 Snake 머리의 이동 간격이 2인 것을 알 수 있다.
그 이유는 현재 Snake는 특수 문자인 블록 단위로 움직이게 하기 위해서이다.
■ 이동
키 입력과는 상관없이 Snake는 계속 이동하므로 Snake 머리는 최상위의 이동 좌표가 되며 이 좌표를 꼬리 쪽으로 전달시키면 꼬리가 머리를 따라오는 것과 같이 된다.
이제 머리와 꼬리가 연결되는 이동 부분을 살펴보자.
먼저 머리 좌표의 정보가 꼬리까지 전달되는 과정을 3단계로 나누면 아래와 같다.
1단계는 [그림 12-12]와 같이 모든 현재 좌표를 이전 좌표에 복사한다.
[그림 12-12] 1 단계
2단계는 모든 이전 좌표를 [그림 12-13]과 같이 현재 좌표에 복사한다. 아래 그림을 보면 머리를 제외한 모든 꼬리의 현재 좌표는 앞 꼬리의 이전 좌표로 바뀐다. 이로써 머리와 꼬리는 서로 이어진다.
[그림 12-13] 2 단계
3단계는 모든 좌표가 전달되었으므로 머리에 새로운 좌표를 적용한다.
[그림 12-14] 3 단계
[실습 예제 12-2]
3개의 꼬리를 가진 Sanke를 설정하고 머리의 이동에 따라 꼬리가 위의 3단계에 따라 이동하도록 [실습 예제 12-1]을 참고하면서 프로그래밍해 보자.
[그림 12-15] Snake 초기 이동 화면
[그림 12-16] Snake 상하좌우 이동 화면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
#include <stdio.h> #include <windows.h> #include <conio.h> #include <time.h> #include "Screen.h"
#define TAIL 3
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _POS { int nX, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE;
SNAKE g_sSnake;
void Move() { int i;
// Note: Step 1 . 모든 현재 좌표를 이전 좌표로 복사 g_sSnake.sHead.nOldX = g_sSnake.sHead.nX; g_sSnake.sHead.nOldY = g_sSnake.sHead.nY;
for( i = 0 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nOldX = g_sSnake.sTail[i].nX; g_sSnake.sTail[i].nOldY = g_sSnake.sTail[i].nY; }
// Note: Step 2. 이전 좌표를 현재 좌표로 복사 g_sSnake.sTail[0].nX = g_sSnake.sHead.nOldX; g_sSnake.sTail[0].nY = g_sSnake.sHead.nOldY;
for( i = 1 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nX = g_sSnake.sTail[i-1].nOldX; g_sSnake.sTail[i].nY = g_sSnake.sTail[i-1].nOldY; } }
void Init() { int i;
// Note: Snake 초기화 g_sSnake.nLife = 1; g_sSnake.sHead.nX = 30; g_sSnake.sHead.nY = 10; g_sSnake.sHead.nOldX = g_sSnake.sHead.nX; g_sSnake.sHead.nOldY = g_sSnake.sHead.nY; g_sSnake.MoveTime = 100; g_sSnake.OldTime = clock(); g_sSnake.nDirect = LEFT;
// Note: Snake 꼬리 초기화 for( i = 0 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nX = g_sSnake.sHead.nX + (i + 1)*2; // 2컬럼 간격 g_sSnake.sTail[i].nY = g_sSnake.sHead.nY; } }
void Update() { clock_t CurTime = clock();
if( CurTime - g_sSnake.OldTime > g_sSnake.MoveTime ) { g_sSnake.OldTime = CurTime; switch( g_sSnake.nDirect ) { case LEFT : if( g_sSnake.sHead.nX - 2 > 1 ) { Move(); g_sSnake.sHead.nX -= 2; } break;
case RIGHT : if( g_sSnake.sHead.nX + 2 < 60 ) { Move(); g_sSnake.sHead.nX += 2; } break;
case UP : if( g_sSnake.sHead.nY - 1 > 1 ) { Move(); g_sSnake.sHead.nY--; } break;
case DOWN : if( g_sSnake.sHead.nY + 1 < 20 ) { Move(); g_sSnake.sHead.nY++; } break; } } }
void Render() { int i;
ScreenClear();
ScreenPrint( g_sSnake.sHead.nX, g_sSnake.sHead.nY, "●" );
for( i = 0 ; i < TAIL ; i++ ) { ScreenPrint( g_sSnake.sTail[i].nX, g_sSnake.sTail[i].nY, "◆"); }
ScreenFlipping(); }
void Release() { }
int main(void) { int nKey;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch(); switch( nKey ) { case 75: g_sSnake.nDirect = LEFT; break; case 77: g_sSnake.nDirect = RIGHT; break; case 72: g_sSnake.nDirect = UP; break; case 80: g_sSnake.nDirect = DOWN; break; } }
Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-3] Snake의 이동
29행의 Move()함수는 [그림 12-12] 1단계와 [그림 12-13] 2단계를 코드를 구현한 것이다. 마지막 [그림 12-14] 3단계는 89행, 97행, 105행, 113행에서 실행하고 있다.
1, 2 단계는 단순히 현재 좌표를 이동해주면 되지만 3단계는 현재 Snake의 이동 방향에 따라 좌표를 증감해야 하기 때문에 3단계가 분리되어 있다.
그 외의 부분은 [실습 예제 12-1]과 동일하다.
[STEP 02]
[그림 12-17] 2단계 제작 로드맵
■ 적 캐릭터
적 캐릭터는 스스로 이동하며 맵과 충돌을 했을 때 임의의 방향으로 이동해야 한다.
이와 같은 적 캐릭터의 속성을 정리하면 아래 [표 12-4]와 같다.
① 생명 ② 위치 좌표 ③ 이동 방향 ④ 이동 시간 ⑤ 이전 이동 시각 |
[표 12-4] 적 캐릭터 속성
위의 속성을 보면 적 캐릭터가 자체적으로 이동하기 위한 이동 방향 속성 외에는 일반적인 적 캐릭터 속성과 동일하다. 위의 속성을 구조체로 정의하면 아래 [소스 12-4]와 같다.
typedef struct _ENEMY { int nLife; int nX, nY; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; } ENEMY; |
[소스 12-4] 적 캐릭터의 속성 정의
[실습 예제 12-3]
5개의 적 캐릭터를 생성하고 아래 [그림 12-18]과 같이 적 캐릭터가 임의의 좌표로 이동하게 프로그래밍해 보자. 적 캐릭터의 이동 방향 변경은 임의의 경계 영역과 충돌했을 때이며 적 캐릭터마다 이동 시간 간격을 다르게 설정하여 이동하게 하자.
[그림 12-18] 임의로 움직이는 적 캐릭터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
#include <stdio.h> #include <windows.h> #include <malloc.h> #include <time.h> #include "Screen.h"
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _ENEMY { int nLife; int nX, nY; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; } ENEMY;
ENEMY* g_pEnemy = NULL; int g_nEnemyCount = 5;
void Init() { int i;
// Note: 적 캐릭터 생성과 설정 g_pEnemy = (ENEMY*)malloc( sizeof( ENEMY ) * g_nEnemyCount ); srand( (unsigned int)time(NULL) ); // 난수 발생을 초기화
for( i = 0 ; i < g_nEnemyCount ; i++ ) { g_pEnemy[i].nLife = 1; g_pEnemy[i].nX = rand() % 49 + 1; // 최소 1에서 50까지 g_pEnemy[i].nY = rand() % 19 + 1; // 최소 1에서 20까지 g_pEnemy[i].nDirect = rand() % 4; g_pEnemy[i].MoveTime = rand() % 100 + 100; // 최소 100에서 299까지 g_pEnemy[i].OldTime = clock(); } }
void Update() { int i; clock_t CurTime = clock();
// Note: 데이터 갱신 for( i = 0 ; i < g_nEnemyCount ; i++ ) { if( g_pEnemy[i].nLife ) { if( CurTime - g_pEnemy[i].OldTime > g_pEnemy[i].MoveTime ) { g_pEnemy[i].OldTime = clock(); switch( g_pEnemy[i].nDirect ) { case LEFT : if( g_pEnemy[i].nX - 2 > 2 ) g_pEnemy[i].nX -= 2; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case RIGHT : if( g_pEnemy[i].nX + 2 < 40 ) g_pEnemy[i].nX += 2; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case UP : if( g_pEnemy[i].nY - 1 > 0 ) g_pEnemy[i].nY--; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case DOWN : if( g_pEnemy[i].nY + 1 < 21 ) g_pEnemy[i].nY++; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break; } } } } }
void Render() { int i; ScreenClear();
for( i = 0 ; i < g_nEnemyCount ; i++ ) { // Note: 이동 방향 출력 switch( g_pEnemy[i].nDirect ) { case LEFT: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "LEFT" ); break; case RIGHT: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "RIGHT" ); break; case UP: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "UP" ); break; case DOWN: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "DOWN" ); break; }
ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY, "◎" ); }
ScreenFlipping(); }
void Release() { free( g_pEnemy ); }
int main(void) { ScreenInit(); Init();
while( 1 ) { Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-5] 적 캐릭터의 이동
툴에서 결정되는 적 캐릭터의 개수는 스테이지마다 유동적이므로 적 캐릭터 변수를 18행과 같이 포인터 변수로 선언하며 26행과 같이 malloc() 함수를 이용하여 메모리를 생성한다.
59행, 69행, 73행, 80행은 충돌했을 때 rand() 함수를 이용하여 새로운 이동 방향을 설정하는 부분이다.
[STEP 03]
[그림 12-19] 3단계 제작 로드맵
■ 맵 구조 및 설정
아래의 맵 구조는 앞장의 맵툴 [소스 11-1]에서 이미 정의한 구조이다.
#define MAP_COL 29 #define MAP_ROW 22
typedef struct _STAGE_INFO { int nEnemyCount; // Note: 적 캐릭터의 개수 clock_t LimitTime; // Note: 스테이지의 제한 시간 int nEatCount; // Note: 먹이 개수 int nMap[MAP_ROW][MAP_COL]; // Note: 맵 정보 } STAGE_INFO;
char g_StateShape[4][3] = { "■", "♥", "⊙", "●" }; |
[소스 12-6] 스테이지의 전체 정보 정의
nMap[22][29] 배열 안에 저장되는 정보는 g_StateShape[4][3]에 나열된 특수 문자의 인덱스이다. nMap[22][29] 배열값 중에 -1은 공백으로 출력하고 그 외의 값은 g_StateShape[4][3]의 행 인덱스로 설정하여 아래 [그림 12-20]과 같이 출력할 수 있다.
[그림 12-20] 맵 정보 출력 화면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> #include <conio.h>
int g_nMap[6]; // 임시 맵 char g_StateShape[4][3] = { "■", "♥", "⊙", "●" };
int main(void) { int i;
// Note: 맵 초기화 g_nMap[0] = 0; g_nMap[1] = 1; g_nMap[2] = -1; g_nMap[3] = 2; g_nMap[4] = 3;
// Note: 맵 출력 for( i = 0 ; i < 5 ; i++ ) { if( g_nMap[i] == -1 ) printf( " " ); // 공백 출력 else printf( "%s", g_StateShape[ g_nMap[i] ] ); }
_getch(); return 0; } |
[소스 12-7] 맵 출력
참고로 일반적인 맵 구조에는 출력하고자 하는 작은 이미지의 인덱스가 저장된다. 이때 맵을 구성하는 작은 이미지를 타일(tile)이라고 하며 타일을 이용하면 다양한 출력 화면을 조합하여 만들 수 있으며 하나의 이미지를 여러 위치에 출력할 수 있어서 메모리도 절약된다. 타일을 이용한 대표적인 게임으로는 스타크래프트를 들 수 있다.
[그림 12-21] 타일을 이용한 맵 툴
위와 같은 타일 맵에도 장단점은 있다.
장점은 메모리 절약과 빠른 속도로 출력할 수 있는 장점이 있으며 단점으로는 자세한 맵을 표현하기가 어려운 단점도 있다. 하지만 타일 맵은 여전히 많이 사용되고 있으며 특히 땅, 벽 등과 같이 넓은 면을 구성할 때 많이 사용된다.
타일에 관련된 사항을 위의 [소스 12-7]에서도 찾아볼 수 있는데 g_StateShape[4][3] 안에 있는 특수 문자가 곧 타일이 된다. 그리고 각 타일에는 고유 아이디가 할당되어 이 아이디를 통해 출력하게 되는데 특수 문자가 저장된 g_StateShape[4][3] 배열은 배열이라는 특성 때문에 이미 인덱스가 할당되어 있다. 예를 들면 “♥”의 인덱스는 1이고, “●”의 인덱스는 3으로 중복되지 않는 값이 의미적으로 부여된 것이다. 이것은 타일의 고유 아이디와 같은 개념으로 사용된다.
■ 맵 읽기 및 출력
- 맵 읽기
맵 읽기 자체는 이미 맵 툴에서 제작한 내용이며 게임에서도 같은 방법으로 읽기를 한다.
하지만 게임에서는 읽은 데이터를 두 가지로 구분할 필요가 있다.
첫째, 먹이 위치의 값과 블록 위치의 값이다.
현재 맵 데이터에서 0, 1은 블록과 먹이를 나타내는 인덱스이다.
블록은 게임에서 배경 화면에 해당이 되므로 값이 변경되면 안 된다.
하지만 먹이는 -1로 변경될 수 있는데 그때는 주인공 Snake와 먹이가 서로 충돌했을 때이다.
둘째, 주인공 위치의 값과 적 캐릭터 위치의 값이다.
맵에는 주인공과 적 캐릭터에 관한 인덱스 2, 3으로 설정되어 있다.
이 인덱스를 통해 주인공과 적 캐릭터의 맵 좌표인 행과 열을 알아 낸 후에는 즉시 해당 맵 위치에 -1을 설정하여 빈 공간으로 설정한다.
이와 같이 하는 이유는 맵에서 2, 3은 단순히 주인공과 적 캐릭터의 맵 좌표인 행과 열을 알아내기 위한 것이기 때문이다.
이와 같이 맵 데이터를 구분한 이유는 출력할 때 배경 맵과 이동 개체의 인덱스를 실제 출력하는 맵에 저장하기 위함이다. 만약 이와 같은 방법을 사용하지 않고 파일에서 읽은 맵 데이터에 이동하는 개체의 인덱스를 저장하게 되면 이동 개체가 이동하는 모든 자취가 남아 배경과 개체를 구분할 수 없게 된다.
[그림 12-22] 출력 맵 구성
- 맵 출력
맵을 출력할 때에는 현재의 맵과 동일한 특성을 가진 출력용 맵을 생성하고 배경 맵을 출력 맵에 먼저 저장한 후에 각 이동 개체를 출력 맵에 저장하면 하나의 맵에 개체와 배경이 출력하게 된다. 참고로, 같은 메모리의 특성을 가진 메모리 내용을 복사할 때는 memcpy() 함수를 이용하면 쉽게 복사가 된다.
포함 헤더 |
<memory.h> 또는 <string.h> |
함수의 원형 |
void *memcpy( void *dest, const void *src, size_t count ); |
[표 12-5] memcpy() 함수
[STEP 04]
[그림 12-23] 4단계 제작 로드맵
■ Snake와 적 캐릭터 충돌
Snake 머리와 꼬리는 적 캐릭터의 충돌 대상이 된다. 충돌 체크는 적 캐릭터와 Snake 머리, 그리고 꼬리의 위치 값이 곧 행과 열이 되므로 이 값으로 충돌을 판단한다.
또한 충돌할 때 적 캐릭터는 소멸하며 Snake의 생명값이 감소하면 Snake의 꼬리 개수도 줄어들게 된다.
■ Snake, 적 캐릭터, 먹이, 경계 영역인 블록과 충돌
Snake가 먹이를 먹는 경우와 블록과 충돌하는 경우는 머리만 충돌 체크를 하면 된다. 왜냐하면 머리의 이동 방향에 따라 꼬리가 따라오므로 머리의 충돌이 없다면 당연히 먹이와 블록의 충돌도 없기 때문이다.
Snake 위치인 행과 열의 좌표는 먹이와 경계 영역을 나타내는 블록 위치와도 같다. 그래서 Snake의 행과 열을 nMap[Snake 행][Snake 열]로 설정하여 맵 배열의 값을 읽은 후 그 배열의 값이 0이면 블록, 1이면 먹이로 체크한다. 먹이와 충돌한 경우는 해당하는 nMap 배열에 -1을 넣어 출력에서 제외시킨다.
[실습 예제 12-4]
Snake, 블록, 먹이를 아래 [그림 12-24]와 같이 설정하고 방향키를 이용하여 Snake, 블록, 먹이가 충돌하게 프로그래밍해 보자. 단, Snake의 속성을 간단히 아래 [소스 12-8]과 같이 정의하고 방향키에 따라 직접 행과 열을 바꾸어 이동하도록 프로그래밍한다.
[그림 12-24] 맵, 먹이, Snake 충돌 체크
typedef struct _SNAKE { int nX, nY; } SNAKE; |
[소스 12-8] Snake 속성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
#include <stdio.h> #include <windows.h> #include <time.h> #include <conio.h> #include "Scㅇreen.h"
char g_StateShape[4][3] = { "■", "♥", "⊙", "●" }; int g_nMap[5][5] = { { 0, 0, 0, 0, 0 }, { 0, 1, -1, -1, 0 }, { 0, -1, -1, -1, 0 }, { 0, -1, 0, 1, 0 }, { 0, 0, 0, 0, 0 } };
typedef struct _SNAKE { int nX, nY; } SNAKE;
SNAKE g_Snake;
void Init() { // Note : Snake 초기화 g_Snake.nX = 2; g_Snake.nY = 2; }
void Update() { }
void Render() { int i, j; ScreenClear();
for( i = 0 ; i < 5 ; i++ ) { for( j = 0; j < 5 ; j++ ) { ScreenPrint( j*2, i, g_StateShape[ g_nMap[i][j] ] ); } }
ScreenPrint( g_Snake.nX * 2, g_Snake.nY, g_StateShape[3] ); ScreenFlipping(); }
void Release() { }
int main(void) { int nKey, nMapIndex;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch();
switch( nKey ) { case 75 : // 좌로 이동 nMapIndex = g_nMap[g_Snake.nY][g_Snake.nX - 1]; if( nMapIndex != 0 ) { g_Snake.nX--; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 77 : // 우로 이동 nMapIndex = g_nMap[g_Snake.nY][g_Snake.nX + 1]; if( nMapIndex != 0 ) { g_Snake.nX++; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 72 : // 위로 이동 nMapIndex = g_nMap[g_Snake.nY - 1][g_Snake.nX]; if( nMapIndex != 0 ) { g_Snake.nY--; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 80 : // 아래로 이동 nMapIndex = g_nMap[g_Snake.nY + 1][g_Snake.nX]; if( nMapIndex != 0 ) { g_Snake.nY++; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break; } }
Update(); Render(); } Release(); ScreenRelease(); return 0; } |
[소스 12-9] Snake의 이동과 먹이, 블록 충돌
8행에서부터 14행은 맵 데이터 부분으로 0은 블록을 나타내며 1은 먹이를 나타낸다.
68행에서부터 109행까지는 방향키에 따라 Sanke가 맵을 이동하게 하는 부분이다.
현재 맵은 행과 열로 이루어진 이차원 배열로 선언되어 있으므로 Snake가 이동한다는 것은 이차원 배열의 요소를 이동한다는 것이다.
43행과 47행은 행과 열을 출력 좌표로 변환하는 부분이다.
[STEP 05]
[그림 12-25] 5단계 제작 로드맵
■ 게임 스테이지 정보
Snake 게임의 스테이지 정보는 툴에서 제작한 [소스 11-1]과 같다.
① 적 캐릭터의 개수 ② 스테이지의 제한 시간 ③ 먹이 개수 ④ 맵 정보 |
[표 12-6] 스테이지 정보
#define MAP_COL 29 #define MAP_ROW 22
typedef struct _STAGE_INFO { int nEnemyCount; // Note: 적 캐릭터의 개수 clock_t LimitTime; // Note: 스테이지의 제한 시간 int nEatCount; // Note: 먹이 개수 int nMap[MAP_ROW][MAP_COL]; // Note: 맵 정보 } STAGE_INFO; |
[소스 12-10] 스테이지 전체 정보 정의
■ 게임 진행 제어와 기타
스테이지에 관련된 파일을 읽는 것은 이미 툴에서 완성된 내용이므로 이 코드를 그대로 사용하면 된다. 전체적인 게임 흐름은 앞서 제작한 게임과 모두 동일하므로 앞장의 내용을 참조하면서 전체 게임을 제작해 보자.
강의가 도움이 되셨습니까? 손가락 꾸욱 눌러주는 센스 ~~
[출처] https://nowcampus.tistory.com/entry/12%EC%9E%A5-Snake-%EA%B2%8C%EC%9E%84?category=655340
(CGP) 12장 Snake 게임
Snake 게임은 고전 게임이며 가장 친숙한 게임에 해당이 된다.
이미 11장에서 Snake 게임을 스테이지별로 다르게 진행할 수 있는 맵툴을 제작해 보았듯이 맵툴의 데이터가
실제 게임에 어떻게 적용되고 활용되는지를 프로그래밍을 통해 습득하는 것이 이장의 키포인트 이다.
유튜브 동영상 강의 주소
(1) http://youtu.be/tiNetLRLNws
(2) http://youtu.be/Kb2zd75n8iw
(3) http://youtu.be/2WDfa14IBNE
(4) http://youtu.be/JDO4nlqplfg
12.1 기획
■ 스토리
적 뱀을 피해 다니면서 제한 시간 안에 사방에 떨어져 있는 먹이를 모두 먹어라.
■ 게임방식
주인공 뱀은 다른 방향키를 누르기까지 현재 설정된 방향으로 계속 이동한다.
주인공 뱀과 적 캐릭터가 부딪히면 주인공 뱀의 꼬리는 하나씩 줄어들지만 먹이를 먹으면 하나씩 늘어난다.
■ 제한사항
각 스테이지마다 제한된 시간이 있으며 제한 시간 안에 먹이를 전부 먹어야만 다음 스테이지로 넘어간다. 기본 꼬리는 3개로 하며 꼬리가 없는 상태에서 적 뱀과 충돌하면 게임은 종료된다.
■ 기획화면
[그림 12-1] 게임 기획 화면
12.2 실행 화면
[그림 12-2] 게임 메인 화면
[그림 12-3] 스테이지 화면
[그림 12-4] 게임 진행 화면
[그림 12-5] 미션 실패 화면
[그림 12-6] 미션 성공 화면
[그림 12-7] 결과 화면
12.3 게임 제작 로드맵
Snake 게임의 제작 단계는 다음과 같이 6단계로 나눌 수 있다.
각 단계마다 일부는 이미 툴에서 제작한 코드를 재사용한다.
이 재사용되는 코드는 툴에서 이미 검증이 된 코드이므로 보다 안정적으로 사용할 수 있다.
이와 같이 툴과 게임은 서로 보완 관계를 가지고 있다.
[STEP 01]
[STEP 02]
[STEP 03]
[STEP 04]
[STEP 05]
[STEP 06]
[그림 12-8] 게임 제작 로드맵
12.4 단계별 프로그래밍
[STEP 01]
[그림 12-9] 1단계 제작 로드맵
■ Snake
- 속성
Snake 캐릭터는 머리와 꼬리라는 특이한 구조를 가지고 있다. Snake가 게임에서 소멸되는 경우는 꼬리가 적 캐릭터들로부터 공격을 받아 전부 소멸된 때이다.
Snake 생명은 ‘먹이’라는 것을 통해 연장되며 ‘먹이’는 Snake에게 꼬리를 하나씩 생성해 주는 아이템이 되지만 Snake 생명이 0이 되는 순간 게임을 종료하게 한다.
Snake 게임에서 방향키는 단순히 이동하려는 방향만 바꾸며 Snake는 현재 설정된 방향으로 계속 이동하는 특성이 있다. 그래서 Snake는 일정한 시간 간격으로 이동하는 속성이 있다.
Snake 꼬리는 이동하려는 꼬리 좌표를 다음 꼬리에 전달하며 이동한다.
꼬리의 증감은 단순히 최대 메모리를 미리 확보해 놓고 꼬리 개수만큼 일부 메모리를 사용한다. 이와 같은 사항을 정리하여 나타내면 아래 [표 12-1]과 같다.
① 생명 ② 좌표 ③ 이전 좌표 ④ 이동 방향 ⑤ 이동 시간 간격 ⑥ 이전 이동 시각 ⑦ 꼬리 정보 |
[표 12-1] Snake 속성
Snake가 이동하게 되면 꼬리 이동은 처음 머리의 좌표를 다음에 나오는 꼬리에게 전달하고 전달 받은 꼬리는 그 다음 꼬리에게 자신의 좌표를 전달하는 방식으로 이동한다. 아래 [그림 12-10]은 이와 같이 이동하는 꼬리의 좌표 경로를 나타낸다.
(이동 방향) |
(꼬리의 좌표 경로) |
[그림 12-9] 꼬리 좌표의 이동 경로
[표 12-1]의 꼬리 정보는 다음과 같다.
① 좌표 ② 이전 좌표 |
[표 12-2] 꼬리 정보
Snake의 꼬리 개수는 주인공의 생명과 밀접한 관계가 있으므로 최후 머리만 남았을 때 생명이 1이라면 아래 [식 12-1]을 통해 꼬리 개수를 구할 수 있다.
꼬리 개수 = 주인공의 생명 - 1 |
[식 12-1] 꼬리 개수
이 꼬리 개수는 전체 꼬리 중에서 일부를 사용하는 개수가 된다.
이제 Snake와 꼬리 속성을 다시 정의하면 아래 [소스 12-1]과 같다.
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT; typedef struct _POS { int nXl, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE; |
[소스 12-1] Snake 속성 정의
■ 키보드 처리
이동키 처리는 지금까지 제작해 온 키보드 처리와 동일하며 이동에 관련된 키를 정의하면 아래 [표 12-3]과 같다.
이동키 |
역할 |
← |
왼쪽으로 이동, 아스키 코드 값은 75 |
→ |
오른쪽으로 이동, 아스키 코드 값은 77 |
↑ |
위로 이동, 아스키 코드 값은 72 |
↓ |
아래로 이동, 아스키 코드 값은 80 |
[표 12-3] 이동키 정의
Snake 게임에서 Snake는 키 입력이 있든 없든 설정된 방향으로 계속 이동하며 위에서 정의한 키는 Snake의 이동 방향을 바꾼다.
[실습 예제 12-1]
[표 12-3]에 따라 Snake 머리가 [그림 12-11]과 같이 이동하게 게임 프레임워크 안에서 프로그래밍해 보자. Snake 머리는 한 번 이동 방향을 설정하면 다른 방향의 이동키가 들어올 때까지 계속 이동하는 특성이 있다.
[그림 12-11] 키 입력에 따른 Snake의 머리 이동
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
#include <stdio.h> #include <windows.h> #include <conio.h> #include <time.h> #include "Screen.h"
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _POS { int nX, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE;
SNAKE g_Snake;
void Init() { // Note: Snake 초기화 g_Snake.nLife = 1; g_Snake.sHead.nX = 30; g_Snake.sHead.nY = 10; g_Snake.MoveTime = 100; g_Snake.OldTime = clock(); g_Snake.nDirect = LEFT; }
void Update() { clock_t CurTime = clock();
if( CurTime - g_Snake.OldTime > g_Snake.MoveTime ) { g_Snake.OldTime = CurTime; switch( g_Snake.nDirect ) { case LEFT : if( g_Snake.sHead.nX - 2 > 1 ) g_Snake.sHead.nX -= 2; break; case RIGHT : if( g_Snake.sHead.nX + 2 < 60 ) g_Snake.sHead.nX += 2; break; case UP : if( g_Snake.sHead.nY - 1 > 1 ) g_Snake.sHead.nY--; break; case DOWN : if( g_Snake.sHead.nY + 1 < 20 ) g_Snake.sHead.nY++; break; } } }
void Render() { ScreenClear();
ScreenPrint( g_Snake.sHead.nX, g_Snake.sHead.nY, "●" );
ScreenFlipping(); }
void Release() { }
int main(void) { int nKey;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch();
switch( nKey ) { case 75: g_Snake.nDirect = LEFT; break; case 77: g_Snake.nDirect = RIGHT; break; case 72: g_Snake.nDirect = UP; break; case 80: g_Snake.nDirect = DOWN; break; } }
Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-2] Snake 이동
48행과 52행을 보면 Snake 머리의 이동 간격이 2인 것을 알 수 있다.
그 이유는 현재 Snake는 특수 문자인 블록 단위로 움직이게 하기 위해서이다.
■ 이동
키 입력과는 상관없이 Snake는 계속 이동하므로 Snake 머리는 최상위의 이동 좌표가 되며 이 좌표를 꼬리 쪽으로 전달시키면 꼬리가 머리를 따라오는 것과 같이 된다.
이제 머리와 꼬리가 연결되는 이동 부분을 살펴보자.
먼저 머리 좌표의 정보가 꼬리까지 전달되는 과정을 3단계로 나누면 아래와 같다.
1단계는 [그림 12-12]와 같이 모든 현재 좌표를 이전 좌표에 복사한다.
[그림 12-12] 1 단계
2단계는 모든 이전 좌표를 [그림 12-13]과 같이 현재 좌표에 복사한다. 아래 그림을 보면 머리를 제외한 모든 꼬리의 현재 좌표는 앞 꼬리의 이전 좌표로 바뀐다. 이로써 머리와 꼬리는 서로 이어진다.
[그림 12-13] 2 단계
3단계는 모든 좌표가 전달되었으므로 머리에 새로운 좌표를 적용한다.
[그림 12-14] 3 단계
[실습 예제 12-2]
3개의 꼬리를 가진 Sanke를 설정하고 머리의 이동에 따라 꼬리가 위의 3단계에 따라 이동하도록 [실습 예제 12-1]을 참고하면서 프로그래밍해 보자.
[그림 12-15] Snake 초기 이동 화면
[그림 12-16] Snake 상하좌우 이동 화면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
#include <stdio.h> #include <windows.h> #include <conio.h> #include <time.h> #include "Screen.h"
#define TAIL 3
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _POS { int nX, nY; int nOldX, nOldY; } POS;
typedef struct _SNAKE { int nLife; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; POS sHead; POS sTail[20]; } SNAKE;
SNAKE g_sSnake;
void Move() { int i;
// Note: Step 1 . 모든 현재 좌표를 이전 좌표로 복사 g_sSnake.sHead.nOldX = g_sSnake.sHead.nX; g_sSnake.sHead.nOldY = g_sSnake.sHead.nY;
for( i = 0 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nOldX = g_sSnake.sTail[i].nX; g_sSnake.sTail[i].nOldY = g_sSnake.sTail[i].nY; }
// Note: Step 2. 이전 좌표를 현재 좌표로 복사 g_sSnake.sTail[0].nX = g_sSnake.sHead.nOldX; g_sSnake.sTail[0].nY = g_sSnake.sHead.nOldY;
for( i = 1 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nX = g_sSnake.sTail[i-1].nOldX; g_sSnake.sTail[i].nY = g_sSnake.sTail[i-1].nOldY; } }
void Init() { int i;
// Note: Snake 초기화 g_sSnake.nLife = 1; g_sSnake.sHead.nX = 30; g_sSnake.sHead.nY = 10; g_sSnake.sHead.nOldX = g_sSnake.sHead.nX; g_sSnake.sHead.nOldY = g_sSnake.sHead.nY; g_sSnake.MoveTime = 100; g_sSnake.OldTime = clock(); g_sSnake.nDirect = LEFT;
// Note: Snake 꼬리 초기화 for( i = 0 ; i < TAIL ; i++ ) { g_sSnake.sTail[i].nX = g_sSnake.sHead.nX + (i + 1)*2; // 2컬럼 간격 g_sSnake.sTail[i].nY = g_sSnake.sHead.nY; } }
void Update() { clock_t CurTime = clock();
if( CurTime - g_sSnake.OldTime > g_sSnake.MoveTime ) { g_sSnake.OldTime = CurTime; switch( g_sSnake.nDirect ) { case LEFT : if( g_sSnake.sHead.nX - 2 > 1 ) { Move(); g_sSnake.sHead.nX -= 2; } break;
case RIGHT : if( g_sSnake.sHead.nX + 2 < 60 ) { Move(); g_sSnake.sHead.nX += 2; } break;
case UP : if( g_sSnake.sHead.nY - 1 > 1 ) { Move(); g_sSnake.sHead.nY--; } break;
case DOWN : if( g_sSnake.sHead.nY + 1 < 20 ) { Move(); g_sSnake.sHead.nY++; } break; } } }
void Render() { int i;
ScreenClear();
ScreenPrint( g_sSnake.sHead.nX, g_sSnake.sHead.nY, "●" );
for( i = 0 ; i < TAIL ; i++ ) { ScreenPrint( g_sSnake.sTail[i].nX, g_sSnake.sTail[i].nY, "◆"); }
ScreenFlipping(); }
void Release() { }
int main(void) { int nKey;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch(); switch( nKey ) { case 75: g_sSnake.nDirect = LEFT; break; case 77: g_sSnake.nDirect = RIGHT; break; case 72: g_sSnake.nDirect = UP; break; case 80: g_sSnake.nDirect = DOWN; break; } }
Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-3] Snake의 이동
29행의 Move()함수는 [그림 12-12] 1단계와 [그림 12-13] 2단계를 코드를 구현한 것이다. 마지막 [그림 12-14] 3단계는 89행, 97행, 105행, 113행에서 실행하고 있다.
1, 2 단계는 단순히 현재 좌표를 이동해주면 되지만 3단계는 현재 Snake의 이동 방향에 따라 좌표를 증감해야 하기 때문에 3단계가 분리되어 있다.
그 외의 부분은 [실습 예제 12-1]과 동일하다.
[STEP 02]
[그림 12-17] 2단계 제작 로드맵
■ 적 캐릭터
적 캐릭터는 스스로 이동하며 맵과 충돌을 했을 때 임의의 방향으로 이동해야 한다.
이와 같은 적 캐릭터의 속성을 정리하면 아래 [표 12-4]와 같다.
① 생명 ② 위치 좌표 ③ 이동 방향 ④ 이동 시간 ⑤ 이전 이동 시각 |
[표 12-4] 적 캐릭터 속성
위의 속성을 보면 적 캐릭터가 자체적으로 이동하기 위한 이동 방향 속성 외에는 일반적인 적 캐릭터 속성과 동일하다. 위의 속성을 구조체로 정의하면 아래 [소스 12-4]와 같다.
typedef struct _ENEMY { int nLife; int nX, nY; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; } ENEMY; |
[소스 12-4] 적 캐릭터의 속성 정의
[실습 예제 12-3]
5개의 적 캐릭터를 생성하고 아래 [그림 12-18]과 같이 적 캐릭터가 임의의 좌표로 이동하게 프로그래밍해 보자. 적 캐릭터의 이동 방향 변경은 임의의 경계 영역과 충돌했을 때이며 적 캐릭터마다 이동 시간 간격을 다르게 설정하여 이동하게 하자.
[그림 12-18] 임의로 움직이는 적 캐릭터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
#include <stdio.h> #include <windows.h> #include <malloc.h> #include <time.h> #include "Screen.h"
typedef enum _DIRECT { LEFT, RIGHT, UP, DOWN } DIRECT;
typedef struct _ENEMY { int nLife; int nX, nY; DIRECT nDirect; clock_t MoveTime; clock_t OldTime; } ENEMY;
ENEMY* g_pEnemy = NULL; int g_nEnemyCount = 5;
void Init() { int i;
// Note: 적 캐릭터 생성과 설정 g_pEnemy = (ENEMY*)malloc( sizeof( ENEMY ) * g_nEnemyCount ); srand( (unsigned int)time(NULL) ); // 난수 발생을 초기화
for( i = 0 ; i < g_nEnemyCount ; i++ ) { g_pEnemy[i].nLife = 1; g_pEnemy[i].nX = rand() % 49 + 1; // 최소 1에서 50까지 g_pEnemy[i].nY = rand() % 19 + 1; // 최소 1에서 20까지 g_pEnemy[i].nDirect = rand() % 4; g_pEnemy[i].MoveTime = rand() % 100 + 100; // 최소 100에서 299까지 g_pEnemy[i].OldTime = clock(); } }
void Update() { int i; clock_t CurTime = clock();
// Note: 데이터 갱신 for( i = 0 ; i < g_nEnemyCount ; i++ ) { if( g_pEnemy[i].nLife ) { if( CurTime - g_pEnemy[i].OldTime > g_pEnemy[i].MoveTime ) { g_pEnemy[i].OldTime = clock(); switch( g_pEnemy[i].nDirect ) { case LEFT : if( g_pEnemy[i].nX - 2 > 2 ) g_pEnemy[i].nX -= 2; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case RIGHT : if( g_pEnemy[i].nX + 2 < 40 ) g_pEnemy[i].nX += 2; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case UP : if( g_pEnemy[i].nY - 1 > 0 ) g_pEnemy[i].nY--; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break;
case DOWN : if( g_pEnemy[i].nY + 1 < 21 ) g_pEnemy[i].nY++; else g_pEnemy[i].nDirect = rand() % 4; // 방향 변경 break; } } } } }
void Render() { int i; ScreenClear();
for( i = 0 ; i < g_nEnemyCount ; i++ ) { // Note: 이동 방향 출력 switch( g_pEnemy[i].nDirect ) { case LEFT: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "LEFT" ); break; case RIGHT: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "RIGHT" ); break; case UP: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "UP" ); break; case DOWN: ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY - 1, "DOWN" ); break; }
ScreenPrint( g_pEnemy[i].nX, g_pEnemy[i].nY, "◎" ); }
ScreenFlipping(); }
void Release() { free( g_pEnemy ); }
int main(void) { ScreenInit(); Init();
while( 1 ) { Update(); Render(); }
Release(); ScreenRelease(); return 0; } |
[소스 12-5] 적 캐릭터의 이동
툴에서 결정되는 적 캐릭터의 개수는 스테이지마다 유동적이므로 적 캐릭터 변수를 18행과 같이 포인터 변수로 선언하며 26행과 같이 malloc() 함수를 이용하여 메모리를 생성한다.
59행, 69행, 73행, 80행은 충돌했을 때 rand() 함수를 이용하여 새로운 이동 방향을 설정하는 부분이다.
[STEP 03]
[그림 12-19] 3단계 제작 로드맵
■ 맵 구조 및 설정
아래의 맵 구조는 앞장의 맵툴 [소스 11-1]에서 이미 정의한 구조이다.
#define MAP_COL 29 #define MAP_ROW 22
typedef struct _STAGE_INFO { int nEnemyCount; // Note: 적 캐릭터의 개수 clock_t LimitTime; // Note: 스테이지의 제한 시간 int nEatCount; // Note: 먹이 개수 int nMap[MAP_ROW][MAP_COL]; // Note: 맵 정보 } STAGE_INFO;
char g_StateShape[4][3] = { "■", "♥", "⊙", "●" }; |
[소스 12-6] 스테이지의 전체 정보 정의
nMap[22][29] 배열 안에 저장되는 정보는 g_StateShape[4][3]에 나열된 특수 문자의 인덱스이다. nMap[22][29] 배열값 중에 -1은 공백으로 출력하고 그 외의 값은 g_StateShape[4][3]의 행 인덱스로 설정하여 아래 [그림 12-20]과 같이 출력할 수 있다.
[그림 12-20] 맵 정보 출력 화면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> #include <conio.h>
int g_nMap[6]; // 임시 맵 char g_StateShape[4][3] = { "■", "♥", "⊙", "●" };
int main(void) { int i;
// Note: 맵 초기화 g_nMap[0] = 0; g_nMap[1] = 1; g_nMap[2] = -1; g_nMap[3] = 2; g_nMap[4] = 3;
// Note: 맵 출력 for( i = 0 ; i < 5 ; i++ ) { if( g_nMap[i] == -1 ) printf( " " ); // 공백 출력 else printf( "%s", g_StateShape[ g_nMap[i] ] ); }
_getch(); return 0; } |
[소스 12-7] 맵 출력
참고로 일반적인 맵 구조에는 출력하고자 하는 작은 이미지의 인덱스가 저장된다. 이때 맵을 구성하는 작은 이미지를 타일(tile)이라고 하며 타일을 이용하면 다양한 출력 화면을 조합하여 만들 수 있으며 하나의 이미지를 여러 위치에 출력할 수 있어서 메모리도 절약된다. 타일을 이용한 대표적인 게임으로는 스타크래프트를 들 수 있다.
[그림 12-21] 타일을 이용한 맵 툴
위와 같은 타일 맵에도 장단점은 있다.
장점은 메모리 절약과 빠른 속도로 출력할 수 있는 장점이 있으며 단점으로는 자세한 맵을 표현하기가 어려운 단점도 있다. 하지만 타일 맵은 여전히 많이 사용되고 있으며 특히 땅, 벽 등과 같이 넓은 면을 구성할 때 많이 사용된다.
타일에 관련된 사항을 위의 [소스 12-7]에서도 찾아볼 수 있는데 g_StateShape[4][3] 안에 있는 특수 문자가 곧 타일이 된다. 그리고 각 타일에는 고유 아이디가 할당되어 이 아이디를 통해 출력하게 되는데 특수 문자가 저장된 g_StateShape[4][3] 배열은 배열이라는 특성 때문에 이미 인덱스가 할당되어 있다. 예를 들면 “♥”의 인덱스는 1이고, “●”의 인덱스는 3으로 중복되지 않는 값이 의미적으로 부여된 것이다. 이것은 타일의 고유 아이디와 같은 개념으로 사용된다.
■ 맵 읽기 및 출력
- 맵 읽기
맵 읽기 자체는 이미 맵 툴에서 제작한 내용이며 게임에서도 같은 방법으로 읽기를 한다.
하지만 게임에서는 읽은 데이터를 두 가지로 구분할 필요가 있다.
첫째, 먹이 위치의 값과 블록 위치의 값이다.
현재 맵 데이터에서 0, 1은 블록과 먹이를 나타내는 인덱스이다.
블록은 게임에서 배경 화면에 해당이 되므로 값이 변경되면 안 된다.
하지만 먹이는 -1로 변경될 수 있는데 그때는 주인공 Snake와 먹이가 서로 충돌했을 때이다.
둘째, 주인공 위치의 값과 적 캐릭터 위치의 값이다.
맵에는 주인공과 적 캐릭터에 관한 인덱스 2, 3으로 설정되어 있다.
이 인덱스를 통해 주인공과 적 캐릭터의 맵 좌표인 행과 열을 알아 낸 후에는 즉시 해당 맵 위치에 -1을 설정하여 빈 공간으로 설정한다.
이와 같이 하는 이유는 맵에서 2, 3은 단순히 주인공과 적 캐릭터의 맵 좌표인 행과 열을 알아내기 위한 것이기 때문이다.
이와 같이 맵 데이터를 구분한 이유는 출력할 때 배경 맵과 이동 개체의 인덱스를 실제 출력하는 맵에 저장하기 위함이다. 만약 이와 같은 방법을 사용하지 않고 파일에서 읽은 맵 데이터에 이동하는 개체의 인덱스를 저장하게 되면 이동 개체가 이동하는 모든 자취가 남아 배경과 개체를 구분할 수 없게 된다.
[그림 12-22] 출력 맵 구성
- 맵 출력
맵을 출력할 때에는 현재의 맵과 동일한 특성을 가진 출력용 맵을 생성하고 배경 맵을 출력 맵에 먼저 저장한 후에 각 이동 개체를 출력 맵에 저장하면 하나의 맵에 개체와 배경이 출력하게 된다. 참고로, 같은 메모리의 특성을 가진 메모리 내용을 복사할 때는 memcpy() 함수를 이용하면 쉽게 복사가 된다.
포함 헤더 |
<memory.h> 또는 <string.h> |
함수의 원형 |
void *memcpy( void *dest, const void *src, size_t count ); |
[표 12-5] memcpy() 함수
[STEP 04]
[그림 12-23] 4단계 제작 로드맵
■ Snake와 적 캐릭터 충돌
Snake 머리와 꼬리는 적 캐릭터의 충돌 대상이 된다. 충돌 체크는 적 캐릭터와 Snake 머리, 그리고 꼬리의 위치 값이 곧 행과 열이 되므로 이 값으로 충돌을 판단한다.
또한 충돌할 때 적 캐릭터는 소멸하며 Snake의 생명값이 감소하면 Snake의 꼬리 개수도 줄어들게 된다.
■ Snake, 적 캐릭터, 먹이, 경계 영역인 블록과 충돌
Snake가 먹이를 먹는 경우와 블록과 충돌하는 경우는 머리만 충돌 체크를 하면 된다. 왜냐하면 머리의 이동 방향에 따라 꼬리가 따라오므로 머리의 충돌이 없다면 당연히 먹이와 블록의 충돌도 없기 때문이다.
Snake 위치인 행과 열의 좌표는 먹이와 경계 영역을 나타내는 블록 위치와도 같다. 그래서 Snake의 행과 열을 nMap[Snake 행][Snake 열]로 설정하여 맵 배열의 값을 읽은 후 그 배열의 값이 0이면 블록, 1이면 먹이로 체크한다. 먹이와 충돌한 경우는 해당하는 nMap 배열에 -1을 넣어 출력에서 제외시킨다.
[실습 예제 12-4]
Snake, 블록, 먹이를 아래 [그림 12-24]와 같이 설정하고 방향키를 이용하여 Snake, 블록, 먹이가 충돌하게 프로그래밍해 보자. 단, Snake의 속성을 간단히 아래 [소스 12-8]과 같이 정의하고 방향키에 따라 직접 행과 열을 바꾸어 이동하도록 프로그래밍한다.
[그림 12-24] 맵, 먹이, Snake 충돌 체크
typedef struct _SNAKE { int nX, nY; } SNAKE; |
[소스 12-8] Snake 속성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
#include <stdio.h> #include <windows.h> #include <time.h> #include <conio.h> #include "Scㅇreen.h"
char g_StateShape[4][3] = { "■", "♥", "⊙", "●" }; int g_nMap[5][5] = { { 0, 0, 0, 0, 0 }, { 0, 1, -1, -1, 0 }, { 0, -1, -1, -1, 0 }, { 0, -1, 0, 1, 0 }, { 0, 0, 0, 0, 0 } };
typedef struct _SNAKE { int nX, nY; } SNAKE;
SNAKE g_Snake;
void Init() { // Note : Snake 초기화 g_Snake.nX = 2; g_Snake.nY = 2; }
void Update() { }
void Render() { int i, j; ScreenClear();
for( i = 0 ; i < 5 ; i++ ) { for( j = 0; j < 5 ; j++ ) { ScreenPrint( j*2, i, g_StateShape[ g_nMap[i][j] ] ); } }
ScreenPrint( g_Snake.nX * 2, g_Snake.nY, g_StateShape[3] ); ScreenFlipping(); }
void Release() { }
int main(void) { int nKey, nMapIndex;
ScreenInit(); Init();
while( 1 ) { if( _kbhit() ) { nKey = _getch();
switch( nKey ) { case 75 : // 좌로 이동 nMapIndex = g_nMap[g_Snake.nY][g_Snake.nX - 1]; if( nMapIndex != 0 ) { g_Snake.nX--; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 77 : // 우로 이동 nMapIndex = g_nMap[g_Snake.nY][g_Snake.nX + 1]; if( nMapIndex != 0 ) { g_Snake.nX++; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 72 : // 위로 이동 nMapIndex = g_nMap[g_Snake.nY - 1][g_Snake.nX]; if( nMapIndex != 0 ) { g_Snake.nY--; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break;
case 80 : // 아래로 이동 nMapIndex = g_nMap[g_Snake.nY + 1][g_Snake.nX]; if( nMapIndex != 0 ) { g_Snake.nY++; if( nMapIndex == 1 ) // 먹이 먹음 g_nMap[g_Snake.nY][g_Snake.nX] = -1; } break; } }
Update(); Render(); } Release(); ScreenRelease(); return 0; } |
[소스 12-9] Snake의 이동과 먹이, 블록 충돌
8행에서부터 14행은 맵 데이터 부분으로 0은 블록을 나타내며 1은 먹이를 나타낸다.
68행에서부터 109행까지는 방향키에 따라 Sanke가 맵을 이동하게 하는 부분이다.
현재 맵은 행과 열로 이루어진 이차원 배열로 선언되어 있으므로 Snake가 이동한다는 것은 이차원 배열의 요소를 이동한다는 것이다.
43행과 47행은 행과 열을 출력 좌표로 변환하는 부분이다.
[STEP 05]
[그림 12-25] 5단계 제작 로드맵
■ 게임 스테이지 정보
Snake 게임의 스테이지 정보는 툴에서 제작한 [소스 11-1]과 같다.
① 적 캐릭터의 개수 ② 스테이지의 제한 시간 ③ 먹이 개수 ④ 맵 정보 |
[표 12-6] 스테이지 정보
#define MAP_COL 29 #define MAP_ROW 22
typedef struct _STAGE_INFO { int nEnemyCount; // Note: 적 캐릭터의 개수 clock_t LimitTime; // Note: 스테이지의 제한 시간 int nEatCount; // Note: 먹이 개수 int nMap[MAP_ROW][MAP_COL]; // Note: 맵 정보 } STAGE_INFO; |
[소스 12-10] 스테이지 전체 정보 정의
■ 게임 진행 제어와 기타
스테이지에 관련된 파일을 읽는 것은 이미 툴에서 완성된 내용이므로 이 코드를 그대로 사용하면 된다. 전체적인 게임 흐름은 앞서 제작한 게임과 모두 동일하므로 앞장의 내용을 참조하면서 전체 게임을 제작해 보자.
강의가 도움이 되셨습니까? 손가락 꾸욱 눌러주는 센스 ~~
[출처] https://nowcampus.tistory.com/entry/12%EC%9E%A5-Snake-%EA%B2%8C%EC%9E%84?category=655340
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.