(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 "Screen.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 게임 고전 게임이며 가장 친숙한 게임에 해당이 된다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

이미 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 "Screen.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

 

 

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
108 [Xamarin] Xamarin.Forms. Android 실행/ 디버깅시에 에뮬리이터 배포오류 Why am I getting this error in Xamarin.Forms using Visual Studio? file 졸리운_곰 2021.12.01 22
107 [Xamarin] Visual Studio 2019 를 설치하고 Xmarin.forms 빌드시 에러 : I am just download and start Visual Studio (Xamarin Project). But there is an ERROR NU1101. file 졸리운_곰 2021.12.01 53
106 [게임개발] How to Create Smarter NPCs in Games file 졸리운_곰 2021.08.31 23
105 (CGP)16장. 탱크 게임 file 졸리운_곰 2021.06.28 802
104 (CGP) 15장. 탱크 맵툴 만들기 file 졸리운_곰 2021.06.28 225
103 (CGP) 14장 Sogo 게임 file 졸리운_곰 2021.06.28 21
102 (CGP)13장. 패턴 뷰어 file 졸리운_곰 2021.06.28 19
» (CGP) 12장 Snake 게임 file 졸리운_곰 2021.06.28 23
100 (CGP) 11장 Snake 게임 툴 만들기 file 졸리운_곰 2021.06.28 51
99 (CGP) 10장. 하트담기 게임 file 졸리운_곰 2021.06.28 25
98 (CGP) 9장. 하트 툴 만들기 file 졸리운_곰 2021.06.28 24
97 (CGP) 8장. 벽돌깨기 게임 file 졸리운_곰 2021.06.28 48
96 (CGP)7장. 짝 맞추기 게임 file 졸리운_곰 2021.06.28 131
95 (CGP) 6장. 두더지 잡기 게임 file 졸리운_곰 2021.06.28 113
94 (CGP) 5장. 슛골인 게임 file 졸리운_곰 2021.06.27 151
93 (CGP) 4장. 사운드 file 졸리운_곰 2021.06.27 78
92 (CGP) 3장. 게임의 기본 구조 file 졸리운_곰 2021.06.27 126
91 (CGP) 2장. 함수 file 졸리운_곰 2021.06.27 36
90 (CGP) 1장. C언어 file 졸리운_곰 2021.06.27 60
89 (CGP) 0장. C를 이용한 게임프로그래밍 강좌를 시작하기 전에 file 졸리운_곰 2021.06.27 173
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED