(CGP)16장. 탱크 게임

이전 장에서 제작한 맵툴을 적용하여 탱크 게임을 완성해 보자. 

 

파이팅유튜브 동영상 강의 주소 

(1) http://youtu.be/4H0NBeVEPiw

(2) http://youtu.be/PDjbIgMXkFU

(3) http://youtu.be/BijrM0UUey4

(4) http://youtu.be/7IzWyykVT1k

 


 

16.1 기획

 

■ 스토리

 

2900년 세계 5차 대전이 발생했다. 적들은 늦은 밤 12시를 기점으로 아군의 진영으로 침투하였다. 아군은 사력을 다해 적군을 섬멸하고 아군 보스를 지켜야 한다.

 

■ 게임 방식

 

주인공 탱크는 방향키로 이동하며 ‘s' 키는 대포를 발사한다.

전체 맵은 블록과 방호벽으로 구성되며 블록은 적과 주인공의 대포로 파괴되지만 방호벽은 파괴되지 않는다. 게임에서 미션 실패는 주인공 탱크가 보호하는 아군 보스가 적으로부터 공격을 받거나 주인공 탱크의 생명값이 0이 되면 미션은 실패된다.

 

■ 제한 사항

 

아군 탱크의 총알을 5발로 제한하되 일정한 간격으로 발사되게 한다.  

적 탱크가 지능적으로 이동과 공격을 하도록 인공지능 요소를 첨가 할 수 있겠지만 전체 게임의 구성에 집중하기 위해서 적 탱크의 지능적인 이동과 공격은 임의로 동작하게 한다.

적 캐릭터는 스테이지당 최대 30개까지 생성할 수 있다.

 

■ 기획 화면

 

[그림 16-1] 게임 기획 화면

 

 

 


16.2 실행 화면

  

[그림 16-2] 초기 메인 화면

 

[그림 16-3] 스테이지 화면

 

[그림 16-4] 게임 진행 화면

 

[그림 16-5] 미션 성공 화면

 

[그림 16-6] 미션 실패 화면

 

[그림 16-7] 결과 화면

 

 


16.3 게임 제작 로드맵

 

툴에서 제작된 구조를 적용하여 각 단계별로 프로그래밍해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

[STEP 06]

 

[STEP 07]

 

[그림 16-8] 게임 제작 로드맵

 

 


16.4 단계별 프로그래밍

 

[STEP 01]

 

[그림 16-9] 1단계 제작 로드맵

 

■ 주인공 구조 설계 및 설정

 

- 속성

 

주인공 탱크는 생명력, 좌표, 이동 시간 간격, 포탄 발사 시간 간격, 이전 이동 시각, 이전 포탄 발사 시각, 이동 방향이라는 속성을 가진다.

대부분의 캐릭터는 좌표와 생명력을 가지고 있지만 이동을 하는 경우에는 언제 이동시킬 것인지를 결정하기 위한 이동 시간 간격이 있어야 한다.

또한 이동 시간 간격은 시간 차이를 이용하므로 이전에 이동한 시각 정보가 있어야 한다. 또한 주인공 탱크에서 발사하는 포탄은 키 입력에 따라 무한으로 발사되는 것이 아니므로 발사 시간 간격이란 속성과 이전 포탄의 발사 시각 정보가 있어야 한다.

이와 같은 사항은 이미 앞서 제작한 모든 게임에 포함된 속성임을 기억하자.

 

주인공 탱크 속성에 포탄에 관련된 내용을 포함하지 않은 것은 포탄이라는 것을 탱크와는 별개로 다루기 위한 것이다. 이것은 차후에 학습하게 될 C++의 객체의 개념이기도 하다.

이것은 주인공 탱크와 포탄의 독립성을 유지해 주고 서로 요청에 의해서 고유 기능을 하도록 하기 위함이다. 이와 같은 방법은 윈도우 프로그래밍에서 메시지를 이용하는 방법의 기초가 된다.

 

포탄은 4개의 방향(상하좌우)을 가지고 있다.

포탄의 발사 방향은 주인공 탱크의 이동 방향과 같으며 이것은 이미 Sogo게임과 벽돌깨기 게임에서도 이미 살펴본 내용이다.

주인공 탱크의 이동 방향은 방향키가 입력되었을 때 결정되며 이 방향은 포탄도 동일하게

사용하는 이동 방향이 된다.

전체 구조는 [소스 16-1]과 같다.

 

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;          // 이동 좌표

        int nLife;            // 생명력

        int nMoveTime;      // 이동 시간 간격

        int nFireTime;       // 포탄 발사 시간 간격

        int nOldMoveTime;   // 이전 이동 시간

        int nOldFireTime;    // 이전 포탄 발사 시간 

        DIRECT nDirect;   // 이동 방향

} PLAYER;

 

[소스 16-1] 주인공 탱크 속성을 정의

 

■ 키보드 처리

 

주인공 탱크의 키 처리에서 중요한 점은 방향키에 의해 주인공 탱크의 이동 방향이 결정된다는 것이다. 주인공 탱크의 키 처리 구조는 [소스 16-2]와 같다.

 

  

[그림 16-10] 키 상태 출력

 

 

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

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

#include <conio.h>

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

PLAYER g_Player;

 

int main(void)

{

    int nKey;

    clock_t nCurTime;

    g_Player.nMoveTime = 500;

    char *pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };

 

    while(1)

    {

          if( _kbhit() )

        {

            nKey = _getch();

            nCurTime = clock();

 

            switch( nKey )

            {

             case 72: // 위쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = UP;

                    g_Player.nY--;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 80: // 아래쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = DOWN;

                    g_Player.nY++;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 75: // 왼쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = LEFT;

                    g_Player.nX--;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 77: // 오른쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = RIGHT;

                    g_Player.nX++;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             }

        }

 

        system( "cls" );

        printf( "%s 상태 %d %d", pState[g_Player.nDirect],

                                                 g_Player.nX, g_Player.nY );

    }

        return 0;

}

 

[소스 16-2] 키 입력 처리

 

[실습 예제 16-1]

 

주인공 탱크(‘◈’)가 키 입력에 따라 이동하는 프로그래밍을 작성해 보자. 단, 화면 하단에는 주인공 탱크의 이동 상태를 [소스 16-3]과 같이 출력되게 하자.

 

[그림 16-11] 주인공 탱크 출력

 

 

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

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

PLAYER g_Player;

char *g_pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };


void Init()

{

     g_Player.nMoveTime = 100;    

     g_Player.nX = 15;

     g_Player.nY = 10;

}

 

void Update()

{

}

 

void Render()

{

      char str[100];

      ScreenClear();

 

      // Note: 출력 코드

      ScreenPrint( g_Player.nX*2, g_Player.nY, "◈" );

      sprintf( str, "%s 상태 %d %d", g_pState[g_Player.nDirect],

                                               g_Player.nX, g_Player.nY );

      ScreenPrint( 10, 20, str );

      

     ScreenFlipping();

}

 

int main(void)

{

    int nKey;

    clock_t nCurTime;

  

    ScreenInit();

    Init();

 

    while(1)

    {

        if( _kbhit() )

        {

            nKey = _getch();

              nCurTime = clock();

            

            switch( nKey )

            {

            case 72: // 위쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = UP;

                       g_Player.nY--;

                       g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 80: // 아래쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = DOWN;

                       g_Player.nY++;

                       g_Player.nOldMoveTime = nCurTime;

                   }

                   break;

             case 75: // 왼쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = LEFT;

                       g_Player.nX--;

                       g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

             case 77: // 오른쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = RIGHT;

                       g_Player.nX++;

                       g_Player.nOldMoveTime = nCurTime; 

                        }

                    break;

            }

        }

 

        Update();               

        Render();                

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-3] 주인공 탱크 이동

 

 


[STEP 02]

  

[그림 16-12] 2단계 제작 로드맵

 

■ 포탄 구조 설계 및 설정

 

포탄은 주인공 탱크와 적 탱크에 공통으로 적용되는 아이템이므로 주인공 탱크와 적 탱크에 종속적인 관계로 설계하기 보다는 독립적으로 설계하는 것이 효율적이다.

주인공 탱크와 적 탱크가 포탄을 발사할 때가 되면 포탄 버퍼에 위치와 방향만 설정해 주면  포탄은 일정한 이동 시간 간격으로 이동 방향에 따라 위치를 변경하게 된다.

 

- 속성

 

포탄은 기본적으로 생명과 좌표 속성을 가진다.

포탄의 이동 방향은 탱크가 포탄을 발사할 때 결정되며 탱크의 이동 방향이 곧 포탄의 이동 방향이 된다. 또한 포탄은 스스로 이동해야 하므로 이동 시간 간격과 이전 이동 시각이란 속성이 있어야 한다.

여기서는 모든 포탄의 이동 시간 간격을 같게 설정하므로 포탄 속성에는 이전 이동 시각만 있으면 된다. 이와 같은 사항을 정의하면 [소스 16-4]와 같다.

 

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        unsigned int nOldMoveTime;

} BULLET;

 

BULLET g_sEnemyBullet[100];

BULLET g_sPlayerBullet[5];

 

[소스 16-4] 포탄 속성 정의

 

- 주인공 탱크의 포탄 탑재 및 경계 영역 처리

 

주인공 탱크에 포탄을 탑재한다는 것은 포탄 버퍼 중에서 사용되지 않는 포탄에 주인공 탱크의 이동 방향과 초기 좌표를 설정하는 것을 말하며 적 탱크에도 동일하게 적용된다.

포탄의 이동 좌표는 맵의 행과 열에 일대일 대응되므로 맵의 크기에게 따라서 경계 영역이 설정된다. 이 부분은 12장 Snake 게임에서도 살펴본 내용이다. 

 

[실습 예제 16-2]

 

[실습 예제 16-1]의 주인공 탱크에 포탄을 탑재하고 ‘s'키가 입력되면 최대 5발까지 발사되도록 프로그래밍을 해보자.

 

[그림 16-13] 포탄 발사 및 이동

 

 

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

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

#define MAP_COL   23

#define MAP_ROW   18

#define BULLET_MOVE_TIME 80

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        int nOldMoveTime;

} BULLET;

 

PLAYER g_Player;

BULLET g_sPlayerBullet[5];

 

char *g_pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };

 

void Init()

{

     g_Player.nMoveTime = 100;    

     g_Player.nX = 15;

     g_Player.nY = 17;

}

 

void Update()

{

     int i;

     clock_t nCurTime = clock();

 

     for( i = 0 ; i < 5 ; i++ )

     {

        if( g_sPlayerBullet[i].nLife == 1 )

        { 

           // Note: 총알 이동

           if( nCurTime - g_sPlayerBullet[i].nOldMoveTime >= BULLET_MOVE_TIME )

           {

              switch( g_sPlayerBullet[i].nDirect )

              {

                case UP :

                        g_sPlayerBullet[i].nY -= 1;

                        break;

                case DOWN:

                        g_sPlayerBullet[i].nY += 1;

                        break;

                case LEFT:

                        g_sPlayerBullet[i].nX -= 1;

                        break;

                case RIGHT:

                        g_sPlayerBullet[i].nX += 1;

                        break;

              }         

                g_sPlayerBullet[i].nOldMoveTime = nCurTime;

          }                     

                        

          // Note: 경계 영역 충돌

          if( g_sPlayerBullet[i].nX < 0 || g_sPlayerBullet[i].nX > MAP_COL - 1 ||

              g_sPlayerBullet[i].nY > MAP_ROW - 1 || g_sPlayerBullet[i].nY < 0 )

          {

            g_sPlayerBullet[i].nLife = 0;                          

          }

          }

     }

}

 

void Render()

{

     char str[100];

     int i;

     ScreenClear();

 

     // Note: 렌더링 코드

     ScreenPrint( g_Player.nX*2, g_Player.nY, "◈" );

 

     // Note: 주인공 총알 발사

     for( i = 0 ; i < 5 ; i++ )

     {

        if( g_sPlayerBullet[i].nLife == 1 )

        {

           if( g_sPlayerBullet[i].nDirect == UP ||

                             g_sPlayerBullet[i].nDirect == DOWN )

              ScreenPrint(g_sPlayerBullet[i].nX*2, g_sPlayerBullet[i].nY, "┃" );

           else

              ScreenPrint(g_sPlayerBullet[i].nX*2, g_sPlayerBullet[i].nY, "━" );

        }

     }

     sprintf( str, "%s 상태 %d %d", g_pState[g_Player.nDirect], g_Player.nX,

                                      g_Player.nY );

     ScreenPrint( 10, 20, str );                

     ScreenFlipping();

}

 

int main(void)

{

    int nKey, i;

    clock_t nCurTime;

  

    ScreenInit();

    Init();

 

    while(1)

    {

        if( _kbhit() )

        {

           nKey = _getch();

           nCurTime = clock();

 

           switch( nKey )

           {

            case 72: // 위쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = UP;

                      g_Player.nY--;

                      g_Player.nOldMoveTime = nCurTime;

                   }

                   break;

            case 80: // 아래쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = DOWN;

                      g_Player.nY++;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 75: // 왼쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = LEFT;

                      g_Player.nX--;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 77: // 오른쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = RIGHT;

                      g_Player.nX++;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 's' : // 스페이스

                   if( nCurTime - g_Player.nOldFireTime >= g_Player.nFireTime )

                   {

                      for( i = 0 ; i < 5 ; i++ )

                      {

                        if( g_sPlayerBullet[i].nLife == 0 )

                        {

                           g_sPlayerBullet[i].nLife = 1 ;

                           g_sPlayerBullet[i].nDirect = g_Player.nDirect;

                           // Note: 초기 위치 설정

                          g_sPlayerBullet[i].nX = g_Player.nX;

                          g_sPlayerBullet[i].nY = g_Player.nY;

                          // Note: 방향에 따른 초기 좌표 재조정

                          switch( g_sPlayerBullet[i].nDirect )

                          {

                           case UP :

                                g_sPlayerBullet[i].nY -= 1;

                                  break;

                           case DOWN:

                                g_sPlayerBullet[i].nY += 1;

                                break;

                           case LEFT:

                                g_sPlayerBullet[i].nX -= 1;

                                break;

                           case RIGHT:

                                g_sPlayerBullet[i].nX += 1;

                                break;

                          }                       

                           g_Player.nOldFireTime = g_sPlayerBullet[i].nOldMoveTime

                                      =  nCurTime;                           

                          break;

                        }

                      }

                  }

                  break;

           }

        }               

        Update();               

        Render();                

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-5] 포탄을 발사하는 주인공 탱크

 

[STEP 03]

  

[그림 16-14] 3단계 제작 로드맵

 

■ 맵 구조 설계 및 설정

 

맵 구조는 15장의 2단계에서 이미 설명을 했지만 간단히 살펴보면 다음과 같다.

맵이 가로, 세로의 고정 크기를 가지므로 이차원 배열로 선언할 수 있다.

또한 이차원 배열의 행과 열은 출력하려는 맵과 캐릭터의 초기 좌표가 되며 배열값은 출력 문자의 인덱스가 된다. 맵의 값은 0부터 5까지의 값으로 설정되며 값의 의미는 [표 16-1]과 같다.

 

 

 

맵 정보

0

1

2

3

4

5

의미

공백

블록

방호벽

적 탱크 생성 위치

보스 위치

아군 탱크

출력 문자

 

 

[표 16-1] 출력 문자와 인덱스

 

 

#define MAP_COL   23

#define MAP_ROW   18

 

int g_nMap[MAP_ROW][MAP_COL];

 

[소스 16-6] 맵 선언

 

■ 맵 로딩 및 출력

 

맵 로딩과 출력은 이미 14장에서 살펴본 내용이다.

툴에서는 읽기와 저장, 그리고 출력이 있었지만 게임에서는 읽기와 출력만 있으면 된다.

 

STEP 04

 

[그림 16-15] 4단계 제작 로드맵

 

■ 적 탱크 구조 설계 및 설정

 

- 속성

 

적 탱크의 데이터 구조는 툴에서 일부가 정해지고 나머지는 실제 게임에서 결정된다.

툴에서 살펴 본 적 탱크의 속성은 타입 인덱스, 생성할 초기 위치 인덱스, 출현 시간이 있었다. 이들 데이터는 툴에서 설정하는 고정 데이터이다.

 

게임에서 추가되는 속성은 실제 게임을 진행하기 위한 속성으로 이동 좌표, 상태 전이, 생명, 이동 방향, 이동 시간 간격, 포탄 발사 시간 간격, 출현 시간, 이전 이동 시각, 이전 포탄 발사 시각과 같은 속성이 추가된다.

상태 전이를 제외한 대부분의 속성이 주인공 탱크와 동일하다.

 

주인공 탱크와 적 탱크 속성 중에서 유일하게 다른 점은 주인공 탱크는 플레이어에 의해 이동하지만 적군 탱크는 스스로 움직여야 한다는 것이다.

스스로 움직이기 위해서는 움직이고자 하는 방향을 결정할 수 있는 근거가 있어야 하며 그것에 따라 적 탱크는 다양하게 이동하면서 포탄을 발사하게 된다.

전체적인 속성을 정의하면 [소스 16-7]과 같다.

 

 

typedef enum _STATE { STOP, RUN, ROTATION } STATE;  // 상태 전이값

 

typedef struct _ENEMY_TYPE

{       

        int nLife;              // 생명력

        int nMoveTime;   // 이동 시간 간격

        int nFireTime;       // 포탄 발사 시간 간격 

} ENEMY_TYPE;

 

typedef struct _ENEMY

{

        int nTypeIndex;            // 타입 인덱스

        int nPosIndex;             // 생성 초기 위치 인덱스

        int nX, nY;                   // 이동 좌표

        STATE nState;              // 상태 전이

        int nLife;                          // 생명

        int nDirect;               // 이동 방향

        int nMoveTime;             // 이동 시간 간격

        int nFireTime;                 // 포탄 발사 시간 간격

        int nAppearanceTime;          // 출현 시간

        int nOldTime;                  // 이전 이동 시각

        int nOldFireTime;              // 포탄 발사 이전 시각

} ENEMY;

 

[소스 16-7] 적 탱크 속성

 

- 상태 전이

 

상태 전이는 8장 벽돌깨기 게임의 3단계에서 이미 설명한 내용이지만 간단히 살펴보면 다음과 같다.

상태 전이는 적 탱크가 지능적으로 움직이는 것과 같이 흉내 내기 위한 방법이다. 상태 전이의 기본적인 동작은 항상 현재의 상태에서 외부 입력 값에 따라 다음 상태가 결정된다.

그래서 상태 전이는 이차원 배열을 이용할 수 있으며 이를 통해 전이된 결과값을 쉽게 얻을 수 있다.

 

현재 적 탱크의 상태 전이는 STOP, RUN, ROTATION으로 3가지이다.

STOP은 적 탱크가 출현 이전 상태 또는 파괴된 상태를 말하며 RUN은 이동 상태를 나타낸다. ROTATION이 적 탱크의 상태 전이의 가장 핵심이며 이 부분이 적 탱크의 지능을 결정한다.

 

적 탱크에서 지능이라는 것은 주인공 탱크의 움직임을 파악한 후에 이동 방향을 결정하는 것을 말하지만 여기서는 간단히 적 탱크가 블록, 방호벽, 경계 영역과 충돌했을 때 rand()함수에 의해 이동 방향을 바꾸는 것을 말한다.

아래 [소스 16-8]은 이와 같은 사항을 코드로 옮긴 것이다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

case ROTATION :

              while( 1 )

              {

                nTemp = rand() % 4;

                if( nTemp == g_Enemy[i].nDirect ) // 같은 방형이 나오면 바꾼다.

                    continue;

                else

                {       

                    g_Enemy[i].nDirect = nTemp;

                    g_Enemy[i].nState = RUN;

                    g_Enemy[i].nOldTime = nCurTime;

                    break;

                }

               }

               break;

 

[소스 16-8] ROTATION 상태

 

rand() 함수를 사용할 때 주의할 점은 [소스 16-8]의 5행, 6행과 같이 현재의 방향과 같을 때는 다른 방향이 나올 때까지 반복문을 실행시키기 위해 6행에 continue를 사용하고 있다. 만약 이 부분이 코딩되어 있지 않으면 적 탱크가 구석에 몰릴 경우 빠져 나오지 못하게 된다.

 

■ 이동 처리

 

적 탱크는 상태 전이값이 RUN 상태일 때 좌표 증감에 의해 이동하게 된다.

그리고 적 탱크가 RUN 상태일 때는 현재 생명이 0이 아니므로 포탄 발사와 이동을 동시에 할 수 있는 상태를 말한다.

 

RUN 상태일 때 항상 체크해야 하는 부분은 ROTATION 상태로 변경할 수 있는 상황인지를 파악하는 것이다. 즉 이동하기 전에 이동하려는 좌표에 블록과 방호벽이 있는지를 파악해야 한다. 이와 같은 사항은 아래 [소스 16-9]의 9행부터 11행까지, 그리고 20행부터  22행까지, 31행부터 33행까지, 42행부터 44행까지 잘 나와 있다.

 

 

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

switch( g_Enemy[i].nState )

{

case RUN : // 이동 중 벽과 충돌 여부를 파악

        if( nCurTime - g_Enemy[i].nOldTime > g_Enemy[i].nMoveTime )

        {

           switch( g_Enemy[i].nDirect )

           {

              case UP :   // 경계 영역 처리와 블록과 방호벽에 충돌하면 방향 전환

                      if( g_Enemy[i].nY - 1 < 0 ||

                               g_nMap[ g_Enemy[i].nY - 1][g_Enemy[i].nX] == 1 ||

                               g_nMap[ g_Enemy[i].nY - 1 ][g_Enemy[i].nX] == 2 )

                      { 

                        g_Enemy[i].nState = ROTATION;

                      }else{

                        g_Enemy[i].nY--;

                        g_Enemy[i].nOldTime = nCurTime;

                      }

                      break;

              case DOWN :

                     if( g_Enemy[i].nY + 1 > MAP_ROW - 1 ||

                            g_nMap[ g_Enemy[i].nY + 1 ][g_Enemy[i].nX] == 1 ||

                            g_nMap[ g_Enemy[i].nY + 1 ][g_Enemy[i].nX] == 2 )

                     {  

                        g_Enemy[i].nState = ROTATION;

                     }else{

                        g_Enemy[i].nY++;

                        g_Enemy[i].nOldTime = nCurTime;

                     }

                     break;

              case LEFT :

                     if( g_Enemy[i].nX - 1 < 0 ||

                            g_nMap[g_Enemy[i].nY][g_Enemy[i].nX-1] == 1 ||

                            g_nMap[g_Enemy[i].nY][g_Enemy[i].nX-1] == 2 )

                     {  

                        g_Enemy[i].nState = ROTATION;

                     }else{

                        g_Enemy[i].nX--;

                          g_Enemy[i].nOldTime = nCurTime;

                     }

                     break;

               case RIGHT:

                 if( g_Enemy[i].nX + 1 > MAP_COL - 1 ||

                        g_nMap[g_Enemy[i].nY][g_Enemy[i].nX+1] == 1 ||

                        g_nMap[g_Enemy[i].nY][g_Enemy[i].nX+1] == 2 )

                 {      

                    g_Enemy[i].nState = ROTATION;

                 }else{

                    g_Enemy[i].nX++;

                    g_Enemy[i].nOldTime = nCurTime;

                 }

                 break;

          }

        }                                       

        break;

 

[소스 16-9] 적 탱크의 이동 처리

 

■ 적 탱크에 포탄 탑재

 

적 탱크에 포탄을 탑재하는 방식은 주인공 탱크와 동일하다.

중요한 것은 충분한 포탄 버퍼의 크기와 발사되지 않는 포탄을 검색하여 적 탱크의 이동 방향과 좌표를 포탄에 설정하는 것이다.

이에 대한 실제 코드는 아래 [소스 16-10]과 같다.

 

 

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

for( i = 0 ; i < g_nEnemyIndex ; i++ )

{

     if( g_Enemy[i].nState == RUN )

     {

        if( nCurTime - g_Enemy[i].nOldFireTime > g_Enemy[i].nFireTime )

        {

            // 100개의 포탄에서 사용할 수 있는 포탄을 찾는 부분이다.

            for( j = 0 ; j < 100 ; j++ )

            {

                if( g_sEnemyBullet[j].nLife == 0 )

                {

                  g_sEnemyBullet[j].nDirect = g_Enemy[i].nDirect;

            

                    switch( g_Enemy[i].nDirect )

                  {

                  case LEFT:

                            g_sEnemyBullet[j].nX = g_Enemy[i].nX-1;

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                        break;

                  case RIGHT:

                          g_sEnemyBullet[j].nX = g_Enemy[i].nX + 1;

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                        break;

                  case UP:

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY - 1;

                        g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                        break;

                  case DOWN:

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY + 1;

                        g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                        break;

                  }

                  g_sEnemyBullet[j].nLife = 1;

                  g_sEnemyBullet[j].nOldMoveTime = nCurTime;

                      g_Enemy[i].nOldFireTime = nCurTime;

                  break;

                  }

               }

            }

        }

     }

}

 

[소스 16-10] 적 탱크에 포탄 탑재

 

[실습 예제 16-3]

 

아래 [그림 16-16]과 같이 화면에 30개의 적 탱크가 임의의 위치에서 생성되어 이동하는 프로그램을 작성해 보자. 단, 경계 영역과 포탄 이동 처리가 되도록 한다.

 

[그림 16-16] 적 탱크의 이동과 포탄 출력

 

 

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

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define BULLET_MOVE_TIME 100

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

typedef enum _STATE { STOP, RUN, ROTATION } STATE;

 

typedef struct _ENEMY

{

        int nTypeIndex;            // 타입 인덱스

        int nPosIndex;             // 생성 초기 위치 인덱스

        int nX, nY;                   // 이동 좌표

        STATE nState;              // 상태 전이

        int nLife;                          // 생명

        int nDirect;               // 이동 방향

        int nMoveTime;             // 이동 시간 간격

        int nFireTime;                 // 포탄 발사 시간 간격

        int nAppearanceTime;          // 출현 시간

        int nOldTime;                  // 이전 이동 시각

        int nOldFireTime;              // 포탄 발사 이전 시각

} ENEMY;

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        int nOldMoveTime;

} BULLET;

 

int     g_nEnemyIndex = 0;

unsigned int g_nStartTime;

BULLET g_sEnemyBullet[100];

ENEMY g_Enemy[30];

 

void Init()

{

    int i;

    for( i = 0 ; i < 30 ; i++ )

    {

        g_Enemy[i].nAppearanceTime = 800*(i+1);

        g_Enemy[i].nDirect = rand() % 4;

        g_Enemy[i].nFireTime = 700;

        g_Enemy[i].nMoveTime = 400;

        g_Enemy[i].nState = STOP;

        g_Enemy[i].nX = rand() % 35;

        g_Enemy[i].nY = rand() % 23;

        g_Enemy[i].nLife = 1;

    }   

}

 

void Update()

{

     int nCurTime, nPassTime;

     int i, j, nTemp;

 

     nCurTime = clock();

     nPassTime = nCurTime - g_nStartTime;

 

     // Note: 적 탱크 출현

     for( i = g_nEnemyIndex ; i < 30 ; i++ )

     {          

        if( g_Enemy[i].nState == STOP )

        {

           if( g_Enemy[i].nAppearanceTime <= nPassTime )

           {

              g_Enemy[i].nState = RUN;

              g_nEnemyIndex++;

           }else{

               break;

           }

        }

      }

 

     // Note: 적 탱크 이동

     for( i = 0 ; i < g_nEnemyIndex ; i++ )

     {

        if( g_Enemy[i].nLife == 1 )

        {

           switch( g_Enemy[i].nState )

           {

           case RUN :

                     if( nCurTime - g_Enemy[i].nOldTime > g_Enemy[i].nMoveTime )

                     {

                        switch( g_Enemy[i].nDirect )

                        {

                        case UP :   // 경계 영역 처리

                                if( g_Enemy[i].nY - 1 < 0 )

                                {       

                                    g_Enemy[i].nState = ROTATION;

                                }else{

                                    g_Enemy[i].nY--;

                                    g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case DOWN :

                                if( g_Enemy[i].nY + 1 > 23 )

                                {       

                                    g_Enemy[i].nState = ROTATION;

                                }else{

                                    g_Enemy[i].nY++;

                                    g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case LEFT :

                                if( g_Enemy[i].nX - 1 < 1 )

                                {       

                                   g_Enemy[i].nState = ROTATION;

                                }else{

                                   g_Enemy[i].nX--;

                                   g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case RIGHT:

                                if( g_Enemy[i].nX + 1 > 39 )

                                {       

                                   g_Enemy[i].nState = ROTATION;

                                }else{

                                   g_Enemy[i].nX++;

                                   g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        }

                     }                                  

                     break;

             case ROTATION :

                       while( 1 )

                     {

                        nTemp = rand() % 4;

                        if( nTemp == g_Enemy[i].nDirect )

                           continue;

                        else

                        {       

                           g_Enemy[i].nDirect = nTemp;

                           g_Enemy[i].nState = RUN;

                           g_Enemy[i].nOldTime = nCurTime;

                           break;

                        }

                      }

                      break;

          }

        }

     }

 

     // Note: 포탄 발사

     for( i = 0 ; i < g_nEnemyIndex ; i++ )

     {

        if( g_Enemy[i].nState == RUN )

        {

            if( nCurTime - g_Enemy[i].nOldFireTime > g_Enemy[i].nFireTime )

            {

               // 100개의 총알에서 찾는 부분이다.

               for( j = 0 ; j < 100 ; j++ )

               {

                if( g_sEnemyBullet[j].nLife == 0 )

                {

                   g_sEnemyBullet[j].nDirect = g_Enemy[i].nDirect;

                       switch( g_Enemy[i].nDirect )

                   {

                    case LEFT:

                            g_sEnemyBullet[j].nX = g_Enemy[i].nX - 1;

                            g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                            break;

                    case RIGHT:

                               g_sEnemyBullet[j].nX = g_Enemy[i].nX + 1;

                             g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                             break;

                    case UP:

                               g_sEnemyBullet[j].nY = g_Enemy[i].nY - 1;

                             g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                             break;

                    case DOWN:

                             g_sEnemyBullet[j].nY = g_Enemy[i].nY + 1;

                             g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                             break;

                   }

                   g_sEnemyBullet[j].nLife = 1;

                   g_sEnemyBullet[j].nOldMoveTime = nCurTime;

                       g_Enemy[i].nOldFireTime = nCurTime;

                   break;

                }

               }

            }

        }

     }

 

     // Note: 포탄 이동

     for( i = 0 ; i < 100 ; i++ )

     {

        if( g_sEnemyBullet[i].nLife == 1 )

        {       // 포탄의 이동 시간을 120으로 고정

            if( nCurTime - g_sEnemyBullet[i].nOldMoveTime > 120 )

            {

               switch( g_sEnemyBullet[i].nDirect )

               {

               case UP:

                       if( g_sEnemyBullet[i].nY - 1 < 0 )

                       {

                        g_sEnemyBullet[i].nLife = 0;

                        continue;

                       }else{

                        g_sEnemyBullet[i].nY--;

                       }

                       break;

                case DOWN:

                       if( g_sEnemyBullet[i].nY + 1 > 23 )

                       {

                        g_sEnemyBullet[i].nLife = 0;

                        continue;

                       }else{

                        g_sEnemyBullet[i].nY++;

                       }

                       break;

                case LEFT:

                        if( g_sEnemyBullet[i].nX - 1 < 1 )

                        {

                            g_sEnemyBullet[i].nLife = 0;

                            continue;

                        }else{

                            g_sEnemyBullet[i].nX--;

                        }

                        break;

                case RIGHT:

                        if( g_sEnemyBullet[i].nX + 1 > 39 )

                        {

                           g_sEnemyBullet[i].nLife = 0;

                           continue;

                        }else{

                           g_sEnemyBullet[i].nX++;

                        }

                        break;

                }

                g_sEnemyBullet[i].nOldMoveTime = nCurTime;

            }

        }

     }

}

 

void Render()

{

     int i;    

 

     ScreenClear();

        

      // Note: 렌더링 부분 & 적 탱크 총알 발사

     for( i = 0 ;  i < 100 ; i++ )

     {

         if( g_sEnemyBullet[i].nLife == 1 )

         {

             switch( g_sEnemyBullet[i].nDirect )

             {

              case UP :

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "↑");

                  break;

              case DOWN :

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "↓");

                 break;

              case LEFT:

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "←");

                  break;

              case RIGHT:

                ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "→");

                break;

             }

         }

      }

 

      for( i = 0 ; i < g_nEnemyIndex ; i++ )

      {

          if( g_Enemy[i].nState != STOP )

          {

              ScreenPrint( g_Enemy[i].nX*2, g_Enemy[i].nY, "★" );                   

          }

      } 

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

    ScreenInit();

    Init();

    

    g_nStartTime = clock();

 

    while( 1 )

    {           

        Update();               

        Render();               

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-11] 상태 전이에 의한 적 탱크 이동 및 포탄 발사

 

 


[STEP 05]

 

[그림 16-17] 5단계 제작 로드맵

 

■ 충돌 체크

 

충돌 체크는 [그림 16-17]과 같이 나눌 수 있다.

충돌을 체크하기 위해서 모든 캐릭터의 좌표를 이동한 후에 충돌 체크를 하면 충돌은 했지만 어긋난 경우가 발생된다.

그래서 각 캐릭터를 이동하기 전에 이동할 좌표로 먼저 충돌 체크를 한 후에 이동을 결정하면 충돌 체크에서 제외되는 경우를 막을 수 있다.

 

위 [그림 16-17]에서 주인공 탱크를 예로 들면 먼저 주인공 탱크의 이동할 좌표를 계산한  후에 적 탱크, 적 포탄, 경계 영역, 방호벽, 블록까지 충돌을 체크하고 득점과 연결하면 된다. 이 부분은 다른 캐릭터에도 동일하게 적용된다.

 

 


STEP 06

 

[그림 16-18] 6단계 제작 로드맵

 

■ 게임 스테이지 정보

 

탱크 게임의 스테이지 정보는 툴에서 이미 제작된 정보를 동일하게 읽어 적용한다.

실제 진행 데이터는 게임이 실행되면서 만들어지지만 초기 셋팅에 관한 정보는 툴에서 만들어져 적용된다. 특히 스테이지 정보는 유동적인 정보가 아니라 고정적인 정보이므로 더욱 툴에 의존적이다. 스테이지 정보를 나열하면 아래 [표 16-2]와 같다.

 

 

① 적 캐릭터수

② 적 출현 위치 개수

③ 적 출현 위치 포인터

④ 보스 초기 위치

⑤ 주인공 탱크 초기 위치

⑥ 적 탱크 종류 개수

 

[표 16-2] 스테이지 정보

 

[표 16-2]를 보면 적 출현 위치 개수는 툴에서 최대 10개까지 유동적으로 설정해 줄 수 있도록 하였다. 적 출현 위치의 개수가 유동적이라는 것은 유동적으로 메모리가 생성과 해제를 해야 하므로 적 출현 위치를 저장하는 변수형은 포인터가 된다.

위의 스테이지 정보를 구조체로 정의하면 아래 [소스 16-12]와 같다.

 

 

typedef struct _STAGE

{

        int nEnemyCount;         

        int nEnemyPositionCount;   

        POS_XY *pEnemyPosition;  

        int nBossX, nBossY;       

        int nPlayerX, nPlayerY;    

        int nEnemyTypeCount;      

} STAGE;

 

[소스 16-12] 스테이지 정의

 

■ 게임 진행 정보

 

탱크 게임의 진행 상태는 앞서 제작한 12장의 Snake 게임과 동일하다.

여기까지 살펴본 내용을 기반으로 마지막 탱크 게임을 완성해 보자.

 

1장부터 시작해서 16장까지 여러 게임과 툴을 제작해 보았으므로 이제 윈도우 프로그래밍을 할 때가 되었다.

그 이유는 이미 우리는 윈도우 프로그래밍 또는 DirectX, OpenGL을 이용하여 프로그래밍 할 수 있는 프레임워크를 3장을 통해 살펴보았고 대부분의 게임을 프레임워크 환경 안에서 제작했기 때문이다.

 

같은 환경에서 다양한 게임을 작성해 본 경험은 팀 단위로 같은 프로그래밍 환경 속에서 개발할 때 많은 도움이 될 것이다.

이제 여러분이 DirectX 또는 OpenGL과 같은 SDK를 이용하여 프로그래밍을 할 때 무엇을, 어떤 함수에 프로그래밍해야 하는가를 알 수 있는 것은 우리가 제작한 게임 프레임워크의 구조가 SDK에서 제공하는 프레임워크와 유사하기 때문이다.

물론 이 구조는 게임을 제작하기 위한 스마트폰에도 적용될 수 있다.

 

결국 여러분은 ‘C를 이용한 게임 프로그래밍’ 과정을 통해 게임이라는 도구를 가지고 다양한 환경에서 프로그래밍할 수 있는 능력을 갖추게 되었다.

여러분은 게임을 기획부터 제작까지 해보았으므로 이 경험이 앞으로 개발하게 되는 게임을 시작부터 어떤 식으로 진행해야 할지를 알게 하는 로드맵이 될 것이다.  

 

 

 

좋은하루강의가 도움이 되셨습니까? 손가락 꾸욱 눌러주는 센스 ~~ 

[출처] https://nowcampus.tistory.com/entry/CGP16%EC%9E%A5-%ED%83%B1%ED%81%AC-%EA%B2%8C%EC%9E%84?category=655340

 

 

 

 

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

 

(CGP)16장. 탱크 게임

이전 장에서 제작한 맵툴을 적용하여 탱크 게임을 완성해 보자. 

 

파이팅유튜브 동영상 강의 주소 

(1) http://youtu.be/4H0NBeVEPiw

(2) http://youtu.be/PDjbIgMXkFU

(3) http://youtu.be/BijrM0UUey4

(4) http://youtu.be/7IzWyykVT1k

 


 

16.1 기획

 

■ 스토리

 

2900년 세계 5차 대전이 발생했다. 적들은 늦은 밤 12시를 기점으로 아군의 진영으로 침투하였다. 아군은 사력을 다해 적군을 섬멸하고 아군 보스를 지켜야 한다.

 

■ 게임 방식

 

주인공 탱크는 방향키로 이동하며 ‘s' 키는 대포를 발사한다.

전체 맵은 블록과 방호벽으로 구성되며 블록은 적과 주인공의 대포로 파괴되지만 방호벽은 파괴되지 않는다. 게임에서 미션 실패는 주인공 탱크가 보호하는 아군 보스가 적으로부터 공격을 받거나 주인공 탱크의 생명값이 0이 되면 미션은 실패된다.

 

■ 제한 사항

 

아군 탱크의 총알을 5발로 제한하되 일정한 간격으로 발사되게 한다.  

적 탱크가 지능적으로 이동과 공격을 하도록 인공지능 요소를 첨가 할 수 있겠지만 전체 게임의 구성에 집중하기 위해서 적 탱크의 지능적인 이동과 공격은 임의로 동작하게 한다.

적 캐릭터는 스테이지당 최대 30개까지 생성할 수 있다.

 

■ 기획 화면

 

[그림 16-1] 게임 기획 화면

 

 

 


16.2 실행 화면

  

[그림 16-2] 초기 메인 화면

 

[그림 16-3] 스테이지 화면

 

[그림 16-4] 게임 진행 화면

 

[그림 16-5] 미션 성공 화면

 

[그림 16-6] 미션 실패 화면

 

[그림 16-7] 결과 화면

 

 


16.3 게임 제작 로드맵

 

툴에서 제작된 구조를 적용하여 각 단계별로 프로그래밍해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

[STEP 06]

 

[STEP 07]

 

[그림 16-8] 게임 제작 로드맵

 

 


16.4 단계별 프로그래밍

 

[STEP 01]

 

[그림 16-9] 1단계 제작 로드맵

 

■ 주인공 구조 설계 및 설정

 

- 속성

 

주인공 탱크는 생명력, 좌표, 이동 시간 간격, 포탄 발사 시간 간격, 이전 이동 시각, 이전 포탄 발사 시각, 이동 방향이라는 속성을 가진다.

대부분의 캐릭터는 좌표와 생명력을 가지고 있지만 이동을 하는 경우에는 언제 이동시킬 것인지를 결정하기 위한 이동 시간 간격이 있어야 한다.

또한 이동 시간 간격은 시간 차이를 이용하므로 이전에 이동한 시각 정보가 있어야 한다. 또한 주인공 탱크에서 발사하는 포탄은 키 입력에 따라 무한으로 발사되는 것이 아니므로 발사 시간 간격이란 속성과 이전 포탄의 발사 시각 정보가 있어야 한다.

이와 같은 사항은 이미 앞서 제작한 모든 게임에 포함된 속성임을 기억하자.

 

주인공 탱크 속성에 포탄에 관련된 내용을 포함하지 않은 것은 포탄이라는 것을 탱크와는 별개로 다루기 위한 것이다. 이것은 차후에 학습하게 될 C++의 객체의 개념이기도 하다.

이것은 주인공 탱크와 포탄의 독립성을 유지해 주고 서로 요청에 의해서 고유 기능을 하도록 하기 위함이다. 이와 같은 방법은 윈도우 프로그래밍에서 메시지를 이용하는 방법의 기초가 된다.

 

포탄은 4개의 방향(상하좌우)을 가지고 있다.

포탄의 발사 방향은 주인공 탱크의 이동 방향과 같으며 이것은 이미 Sogo게임과 벽돌깨기 게임에서도 이미 살펴본 내용이다.

주인공 탱크의 이동 방향은 방향키가 입력되었을 때 결정되며 이 방향은 포탄도 동일하게

사용하는 이동 방향이 된다.

전체 구조는 [소스 16-1]과 같다.

 

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;          // 이동 좌표

        int nLife;            // 생명력

        int nMoveTime;      // 이동 시간 간격

        int nFireTime;       // 포탄 발사 시간 간격

        int nOldMoveTime;   // 이전 이동 시간

        int nOldFireTime;    // 이전 포탄 발사 시간 

        DIRECT nDirect;   // 이동 방향

} PLAYER;

 

[소스 16-1] 주인공 탱크 속성을 정의

 

■ 키보드 처리

 

주인공 탱크의 키 처리에서 중요한 점은 방향키에 의해 주인공 탱크의 이동 방향이 결정된다는 것이다. 주인공 탱크의 키 처리 구조는 [소스 16-2]와 같다.

 

  

[그림 16-10] 키 상태 출력

 

 

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

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

#include <conio.h>

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

PLAYER g_Player;

 

int main(void)

{

    int nKey;

    clock_t nCurTime;

    g_Player.nMoveTime = 500;

    char *pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };

 

    while(1)

    {

          if( _kbhit() )

        {

            nKey = _getch();

            nCurTime = clock();

 

            switch( nKey )

            {

             case 72: // 위쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = UP;

                    g_Player.nY--;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 80: // 아래쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = DOWN;

                    g_Player.nY++;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 75: // 왼쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = LEFT;

                    g_Player.nX--;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             case 77: // 오른쪽

                if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                {

                    g_Player.nDirect = RIGHT;

                    g_Player.nX++;

                    g_Player.nOldMoveTime = nCurTime;

                    }

                break;

             }

        }

 

        system( "cls" );

        printf( "%s 상태 %d %d", pState[g_Player.nDirect],

                                                 g_Player.nX, g_Player.nY );

    }

        return 0;

}

 

[소스 16-2] 키 입력 처리

 

[실습 예제 16-1]

 

주인공 탱크(‘◈’)가 키 입력에 따라 이동하는 프로그래밍을 작성해 보자. 단, 화면 하단에는 주인공 탱크의 이동 상태를 [소스 16-3]과 같이 출력되게 하자.

 

[그림 16-11] 주인공 탱크 출력

 

 

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

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

PLAYER g_Player;

char *g_pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };


void Init()

{

     g_Player.nMoveTime = 100;    

     g_Player.nX = 15;

     g_Player.nY = 10;

}

 

void Update()

{

}

 

void Render()

{

      char str[100];

      ScreenClear();

 

      // Note: 출력 코드

      ScreenPrint( g_Player.nX*2, g_Player.nY, "◈" );

      sprintf( str, "%s 상태 %d %d", g_pState[g_Player.nDirect],

                                               g_Player.nX, g_Player.nY );

      ScreenPrint( 10, 20, str );

      

     ScreenFlipping();

}

 

int main(void)

{

    int nKey;

    clock_t nCurTime;

  

    ScreenInit();

    Init();

 

    while(1)

    {

        if( _kbhit() )

        {

            nKey = _getch();

              nCurTime = clock();

            

            switch( nKey )

            {

            case 72: // 위쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = UP;

                       g_Player.nY--;

                       g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 80: // 아래쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = DOWN;

                       g_Player.nY++;

                       g_Player.nOldMoveTime = nCurTime;

                   }

                   break;

             case 75: // 왼쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = LEFT;

                       g_Player.nX--;

                       g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

             case 77: // 오른쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                       g_Player.nDirect = RIGHT;

                       g_Player.nX++;

                       g_Player.nOldMoveTime = nCurTime; 

                        }

                    break;

            }

        }

 

        Update();               

        Render();                

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-3] 주인공 탱크 이동

 

 


[STEP 02]

  

[그림 16-12] 2단계 제작 로드맵

 

■ 포탄 구조 설계 및 설정

 

포탄은 주인공 탱크와 적 탱크에 공통으로 적용되는 아이템이므로 주인공 탱크와 적 탱크에 종속적인 관계로 설계하기 보다는 독립적으로 설계하는 것이 효율적이다.

주인공 탱크와 적 탱크가 포탄을 발사할 때가 되면 포탄 버퍼에 위치와 방향만 설정해 주면  포탄은 일정한 이동 시간 간격으로 이동 방향에 따라 위치를 변경하게 된다.

 

- 속성

 

포탄은 기본적으로 생명과 좌표 속성을 가진다.

포탄의 이동 방향은 탱크가 포탄을 발사할 때 결정되며 탱크의 이동 방향이 곧 포탄의 이동 방향이 된다. 또한 포탄은 스스로 이동해야 하므로 이동 시간 간격과 이전 이동 시각이란 속성이 있어야 한다.

여기서는 모든 포탄의 이동 시간 간격을 같게 설정하므로 포탄 속성에는 이전 이동 시각만 있으면 된다. 이와 같은 사항을 정의하면 [소스 16-4]와 같다.

 

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        unsigned int nOldMoveTime;

} BULLET;

 

BULLET g_sEnemyBullet[100];

BULLET g_sPlayerBullet[5];

 

[소스 16-4] 포탄 속성 정의

 

- 주인공 탱크의 포탄 탑재 및 경계 영역 처리

 

주인공 탱크에 포탄을 탑재한다는 것은 포탄 버퍼 중에서 사용되지 않는 포탄에 주인공 탱크의 이동 방향과 초기 좌표를 설정하는 것을 말하며 적 탱크에도 동일하게 적용된다.

포탄의 이동 좌표는 맵의 행과 열에 일대일 대응되므로 맵의 크기에게 따라서 경계 영역이 설정된다. 이 부분은 12장 Snake 게임에서도 살펴본 내용이다. 

 

[실습 예제 16-2]

 

[실습 예제 16-1]의 주인공 탱크에 포탄을 탑재하고 ‘s'키가 입력되면 최대 5발까지 발사되도록 프로그래밍을 해보자.

 

[그림 16-13] 포탄 발사 및 이동

 

 

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

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

#define MAP_COL   23

#define MAP_ROW   18

#define BULLET_MOVE_TIME 80

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

 

typedef struct _PLAYER

{

        int nX, nY;

        int nLife;

        int nMoveTime;

        int nOldMoveTime;

        int nOldFireTime;

        int nFireTime;

        DIRECT nDirect;  

} PLAYER;

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        int nOldMoveTime;

} BULLET;

 

PLAYER g_Player;

BULLET g_sPlayerBullet[5];

 

char *g_pState[4] = { "위쪽", "아래쪽", "왼쪽", "오른쪽" };

 

void Init()

{

     g_Player.nMoveTime = 100;    

     g_Player.nX = 15;

     g_Player.nY = 17;

}

 

void Update()

{

     int i;

     clock_t nCurTime = clock();

 

     for( i = 0 ; i < 5 ; i++ )

     {

        if( g_sPlayerBullet[i].nLife == 1 )

        { 

           // Note: 총알 이동

           if( nCurTime - g_sPlayerBullet[i].nOldMoveTime >= BULLET_MOVE_TIME )

           {

              switch( g_sPlayerBullet[i].nDirect )

              {

                case UP :

                        g_sPlayerBullet[i].nY -= 1;

                        break;

                case DOWN:

                        g_sPlayerBullet[i].nY += 1;

                        break;

                case LEFT:

                        g_sPlayerBullet[i].nX -= 1;

                        break;

                case RIGHT:

                        g_sPlayerBullet[i].nX += 1;

                        break;

              }         

                g_sPlayerBullet[i].nOldMoveTime = nCurTime;

          }                     

                        

          // Note: 경계 영역 충돌

          if( g_sPlayerBullet[i].nX < 0 || g_sPlayerBullet[i].nX > MAP_COL - 1 ||

              g_sPlayerBullet[i].nY > MAP_ROW - 1 || g_sPlayerBullet[i].nY < 0 )

          {

            g_sPlayerBullet[i].nLife = 0;                          

          }

          }

     }

}

 

void Render()

{

     char str[100];

     int i;

     ScreenClear();

 

     // Note: 렌더링 코드

     ScreenPrint( g_Player.nX*2, g_Player.nY, "◈" );

 

     // Note: 주인공 총알 발사

     for( i = 0 ; i < 5 ; i++ )

     {

        if( g_sPlayerBullet[i].nLife == 1 )

        {

           if( g_sPlayerBullet[i].nDirect == UP ||

                             g_sPlayerBullet[i].nDirect == DOWN )

              ScreenPrint(g_sPlayerBullet[i].nX*2, g_sPlayerBullet[i].nY, "┃" );

           else

              ScreenPrint(g_sPlayerBullet[i].nX*2, g_sPlayerBullet[i].nY, "━" );

        }

     }

     sprintf( str, "%s 상태 %d %d", g_pState[g_Player.nDirect], g_Player.nX,

                                      g_Player.nY );

     ScreenPrint( 10, 20, str );                

     ScreenFlipping();

}

 

int main(void)

{

    int nKey, i;

    clock_t nCurTime;

  

    ScreenInit();

    Init();

 

    while(1)

    {

        if( _kbhit() )

        {

           nKey = _getch();

           nCurTime = clock();

 

           switch( nKey )

           {

            case 72: // 위쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = UP;

                      g_Player.nY--;

                      g_Player.nOldMoveTime = nCurTime;

                   }

                   break;

            case 80: // 아래쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = DOWN;

                      g_Player.nY++;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 75: // 왼쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = LEFT;

                      g_Player.nX--;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 77: // 오른쪽

                   if( nCurTime - g_Player.nOldMoveTime >= g_Player.nMoveTime )

                   {

                      g_Player.nDirect = RIGHT;

                      g_Player.nX++;

                      g_Player.nOldMoveTime = nCurTime;

                       }

                   break;

            case 's' : // 스페이스

                   if( nCurTime - g_Player.nOldFireTime >= g_Player.nFireTime )

                   {

                      for( i = 0 ; i < 5 ; i++ )

                      {

                        if( g_sPlayerBullet[i].nLife == 0 )

                        {

                           g_sPlayerBullet[i].nLife = 1 ;

                           g_sPlayerBullet[i].nDirect = g_Player.nDirect;

                           // Note: 초기 위치 설정

                          g_sPlayerBullet[i].nX = g_Player.nX;

                          g_sPlayerBullet[i].nY = g_Player.nY;

                          // Note: 방향에 따른 초기 좌표 재조정

                          switch( g_sPlayerBullet[i].nDirect )

                          {

                           case UP :

                                g_sPlayerBullet[i].nY -= 1;

                                  break;

                           case DOWN:

                                g_sPlayerBullet[i].nY += 1;

                                break;

                           case LEFT:

                                g_sPlayerBullet[i].nX -= 1;

                                break;

                           case RIGHT:

                                g_sPlayerBullet[i].nX += 1;

                                break;

                          }                       

                           g_Player.nOldFireTime = g_sPlayerBullet[i].nOldMoveTime

                                      =  nCurTime;                           

                          break;

                        }

                      }

                  }

                  break;

           }

        }               

        Update();               

        Render();                

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-5] 포탄을 발사하는 주인공 탱크

 

[STEP 03]

  

[그림 16-14] 3단계 제작 로드맵

 

■ 맵 구조 설계 및 설정

 

맵 구조는 15장의 2단계에서 이미 설명을 했지만 간단히 살펴보면 다음과 같다.

맵이 가로, 세로의 고정 크기를 가지므로 이차원 배열로 선언할 수 있다.

또한 이차원 배열의 행과 열은 출력하려는 맵과 캐릭터의 초기 좌표가 되며 배열값은 출력 문자의 인덱스가 된다. 맵의 값은 0부터 5까지의 값으로 설정되며 값의 의미는 [표 16-1]과 같다.

 

 

 

맵 정보

0

1

2

3

4

5

의미

공백

블록

방호벽

적 탱크 생성 위치

보스 위치

아군 탱크

출력 문자

 

 

[표 16-1] 출력 문자와 인덱스

 

 

#define MAP_COL   23

#define MAP_ROW   18

 

int g_nMap[MAP_ROW][MAP_COL];

 

[소스 16-6] 맵 선언

 

■ 맵 로딩 및 출력

 

맵 로딩과 출력은 이미 14장에서 살펴본 내용이다.

툴에서는 읽기와 저장, 그리고 출력이 있었지만 게임에서는 읽기와 출력만 있으면 된다.

 

STEP 04

 

[그림 16-15] 4단계 제작 로드맵

 

■ 적 탱크 구조 설계 및 설정

 

- 속성

 

적 탱크의 데이터 구조는 툴에서 일부가 정해지고 나머지는 실제 게임에서 결정된다.

툴에서 살펴 본 적 탱크의 속성은 타입 인덱스, 생성할 초기 위치 인덱스, 출현 시간이 있었다. 이들 데이터는 툴에서 설정하는 고정 데이터이다.

 

게임에서 추가되는 속성은 실제 게임을 진행하기 위한 속성으로 이동 좌표, 상태 전이, 생명, 이동 방향, 이동 시간 간격, 포탄 발사 시간 간격, 출현 시간, 이전 이동 시각, 이전 포탄 발사 시각과 같은 속성이 추가된다.

상태 전이를 제외한 대부분의 속성이 주인공 탱크와 동일하다.

 

주인공 탱크와 적 탱크 속성 중에서 유일하게 다른 점은 주인공 탱크는 플레이어에 의해 이동하지만 적군 탱크는 스스로 움직여야 한다는 것이다.

스스로 움직이기 위해서는 움직이고자 하는 방향을 결정할 수 있는 근거가 있어야 하며 그것에 따라 적 탱크는 다양하게 이동하면서 포탄을 발사하게 된다.

전체적인 속성을 정의하면 [소스 16-7]과 같다.

 

 

typedef enum _STATE { STOP, RUN, ROTATION } STATE;  // 상태 전이값

 

typedef struct _ENEMY_TYPE

{       

        int nLife;              // 생명력

        int nMoveTime;   // 이동 시간 간격

        int nFireTime;       // 포탄 발사 시간 간격 

} ENEMY_TYPE;

 

typedef struct _ENEMY

{

        int nTypeIndex;            // 타입 인덱스

        int nPosIndex;             // 생성 초기 위치 인덱스

        int nX, nY;                   // 이동 좌표

        STATE nState;              // 상태 전이

        int nLife;                          // 생명

        int nDirect;               // 이동 방향

        int nMoveTime;             // 이동 시간 간격

        int nFireTime;                 // 포탄 발사 시간 간격

        int nAppearanceTime;          // 출현 시간

        int nOldTime;                  // 이전 이동 시각

        int nOldFireTime;              // 포탄 발사 이전 시각

} ENEMY;

 

[소스 16-7] 적 탱크 속성

 

- 상태 전이

 

상태 전이는 8장 벽돌깨기 게임의 3단계에서 이미 설명한 내용이지만 간단히 살펴보면 다음과 같다.

상태 전이는 적 탱크가 지능적으로 움직이는 것과 같이 흉내 내기 위한 방법이다. 상태 전이의 기본적인 동작은 항상 현재의 상태에서 외부 입력 값에 따라 다음 상태가 결정된다.

그래서 상태 전이는 이차원 배열을 이용할 수 있으며 이를 통해 전이된 결과값을 쉽게 얻을 수 있다.

 

현재 적 탱크의 상태 전이는 STOP, RUN, ROTATION으로 3가지이다.

STOP은 적 탱크가 출현 이전 상태 또는 파괴된 상태를 말하며 RUN은 이동 상태를 나타낸다. ROTATION이 적 탱크의 상태 전이의 가장 핵심이며 이 부분이 적 탱크의 지능을 결정한다.

 

적 탱크에서 지능이라는 것은 주인공 탱크의 움직임을 파악한 후에 이동 방향을 결정하는 것을 말하지만 여기서는 간단히 적 탱크가 블록, 방호벽, 경계 영역과 충돌했을 때 rand()함수에 의해 이동 방향을 바꾸는 것을 말한다.

아래 [소스 16-8]은 이와 같은 사항을 코드로 옮긴 것이다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

case ROTATION :

              while( 1 )

              {

                nTemp = rand() % 4;

                if( nTemp == g_Enemy[i].nDirect ) // 같은 방형이 나오면 바꾼다.

                    continue;

                else

                {       

                    g_Enemy[i].nDirect = nTemp;

                    g_Enemy[i].nState = RUN;

                    g_Enemy[i].nOldTime = nCurTime;

                    break;

                }

               }

               break;

 

[소스 16-8] ROTATION 상태

 

rand() 함수를 사용할 때 주의할 점은 [소스 16-8]의 5행, 6행과 같이 현재의 방향과 같을 때는 다른 방향이 나올 때까지 반복문을 실행시키기 위해 6행에 continue를 사용하고 있다. 만약 이 부분이 코딩되어 있지 않으면 적 탱크가 구석에 몰릴 경우 빠져 나오지 못하게 된다.

 

■ 이동 처리

 

적 탱크는 상태 전이값이 RUN 상태일 때 좌표 증감에 의해 이동하게 된다.

그리고 적 탱크가 RUN 상태일 때는 현재 생명이 0이 아니므로 포탄 발사와 이동을 동시에 할 수 있는 상태를 말한다.

 

RUN 상태일 때 항상 체크해야 하는 부분은 ROTATION 상태로 변경할 수 있는 상황인지를 파악하는 것이다. 즉 이동하기 전에 이동하려는 좌표에 블록과 방호벽이 있는지를 파악해야 한다. 이와 같은 사항은 아래 [소스 16-9]의 9행부터 11행까지, 그리고 20행부터  22행까지, 31행부터 33행까지, 42행부터 44행까지 잘 나와 있다.

 

 

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

switch( g_Enemy[i].nState )

{

case RUN : // 이동 중 벽과 충돌 여부를 파악

        if( nCurTime - g_Enemy[i].nOldTime > g_Enemy[i].nMoveTime )

        {

           switch( g_Enemy[i].nDirect )

           {

              case UP :   // 경계 영역 처리와 블록과 방호벽에 충돌하면 방향 전환

                      if( g_Enemy[i].nY - 1 < 0 ||

                               g_nMap[ g_Enemy[i].nY - 1][g_Enemy[i].nX] == 1 ||

                               g_nMap[ g_Enemy[i].nY - 1 ][g_Enemy[i].nX] == 2 )

                      { 

                        g_Enemy[i].nState = ROTATION;

                      }else{

                        g_Enemy[i].nY--;

                        g_Enemy[i].nOldTime = nCurTime;

                      }

                      break;

              case DOWN :

                     if( g_Enemy[i].nY + 1 > MAP_ROW - 1 ||

                            g_nMap[ g_Enemy[i].nY + 1 ][g_Enemy[i].nX] == 1 ||

                            g_nMap[ g_Enemy[i].nY + 1 ][g_Enemy[i].nX] == 2 )

                     {  

                        g_Enemy[i].nState = ROTATION;

                     }else{

                        g_Enemy[i].nY++;

                        g_Enemy[i].nOldTime = nCurTime;

                     }

                     break;

              case LEFT :

                     if( g_Enemy[i].nX - 1 < 0 ||

                            g_nMap[g_Enemy[i].nY][g_Enemy[i].nX-1] == 1 ||

                            g_nMap[g_Enemy[i].nY][g_Enemy[i].nX-1] == 2 )

                     {  

                        g_Enemy[i].nState = ROTATION;

                     }else{

                        g_Enemy[i].nX--;

                          g_Enemy[i].nOldTime = nCurTime;

                     }

                     break;

               case RIGHT:

                 if( g_Enemy[i].nX + 1 > MAP_COL - 1 ||

                        g_nMap[g_Enemy[i].nY][g_Enemy[i].nX+1] == 1 ||

                        g_nMap[g_Enemy[i].nY][g_Enemy[i].nX+1] == 2 )

                 {      

                    g_Enemy[i].nState = ROTATION;

                 }else{

                    g_Enemy[i].nX++;

                    g_Enemy[i].nOldTime = nCurTime;

                 }

                 break;

          }

        }                                       

        break;

 

[소스 16-9] 적 탱크의 이동 처리

 

■ 적 탱크에 포탄 탑재

 

적 탱크에 포탄을 탑재하는 방식은 주인공 탱크와 동일하다.

중요한 것은 충분한 포탄 버퍼의 크기와 발사되지 않는 포탄을 검색하여 적 탱크의 이동 방향과 좌표를 포탄에 설정하는 것이다.

이에 대한 실제 코드는 아래 [소스 16-10]과 같다.

 

 

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

for( i = 0 ; i < g_nEnemyIndex ; i++ )

{

     if( g_Enemy[i].nState == RUN )

     {

        if( nCurTime - g_Enemy[i].nOldFireTime > g_Enemy[i].nFireTime )

        {

            // 100개의 포탄에서 사용할 수 있는 포탄을 찾는 부분이다.

            for( j = 0 ; j < 100 ; j++ )

            {

                if( g_sEnemyBullet[j].nLife == 0 )

                {

                  g_sEnemyBullet[j].nDirect = g_Enemy[i].nDirect;

            

                    switch( g_Enemy[i].nDirect )

                  {

                  case LEFT:

                            g_sEnemyBullet[j].nX = g_Enemy[i].nX-1;

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                        break;

                  case RIGHT:

                          g_sEnemyBullet[j].nX = g_Enemy[i].nX + 1;

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                        break;

                  case UP:

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY - 1;

                        g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                        break;

                  case DOWN:

                        g_sEnemyBullet[j].nY = g_Enemy[i].nY + 1;

                        g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                        break;

                  }

                  g_sEnemyBullet[j].nLife = 1;

                  g_sEnemyBullet[j].nOldMoveTime = nCurTime;

                      g_Enemy[i].nOldFireTime = nCurTime;

                  break;

                  }

               }

            }

        }

     }

}

 

[소스 16-10] 적 탱크에 포탄 탑재

 

[실습 예제 16-3]

 

아래 [그림 16-16]과 같이 화면에 30개의 적 탱크가 임의의 위치에서 생성되어 이동하는 프로그램을 작성해 보자. 단, 경계 영역과 포탄 이동 처리가 되도록 한다.

 

[그림 16-16] 적 탱크의 이동과 포탄 출력

 

 

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

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define BULLET_MOVE_TIME 100

 

typedef enum _DIRECT { UP, DOWN, LEFT, RIGHT } DIRECT;

typedef enum _STATE { STOP, RUN, ROTATION } STATE;

 

typedef struct _ENEMY

{

        int nTypeIndex;            // 타입 인덱스

        int nPosIndex;             // 생성 초기 위치 인덱스

        int nX, nY;                   // 이동 좌표

        STATE nState;              // 상태 전이

        int nLife;                          // 생명

        int nDirect;               // 이동 방향

        int nMoveTime;             // 이동 시간 간격

        int nFireTime;                 // 포탄 발사 시간 간격

        int nAppearanceTime;          // 출현 시간

        int nOldTime;                  // 이전 이동 시각

        int nOldFireTime;              // 포탄 발사 이전 시각

} ENEMY;

 

typedef struct _BULLET

{

        int nX, nY;

        int nLife;

        DIRECT nDirect;

        int nOldMoveTime;

} BULLET;

 

int     g_nEnemyIndex = 0;

unsigned int g_nStartTime;

BULLET g_sEnemyBullet[100];

ENEMY g_Enemy[30];

 

void Init()

{

    int i;

    for( i = 0 ; i < 30 ; i++ )

    {

        g_Enemy[i].nAppearanceTime = 800*(i+1);

        g_Enemy[i].nDirect = rand() % 4;

        g_Enemy[i].nFireTime = 700;

        g_Enemy[i].nMoveTime = 400;

        g_Enemy[i].nState = STOP;

        g_Enemy[i].nX = rand() % 35;

        g_Enemy[i].nY = rand() % 23;

        g_Enemy[i].nLife = 1;

    }   

}

 

void Update()

{

     int nCurTime, nPassTime;

     int i, j, nTemp;

 

     nCurTime = clock();

     nPassTime = nCurTime - g_nStartTime;

 

     // Note: 적 탱크 출현

     for( i = g_nEnemyIndex ; i < 30 ; i++ )

     {          

        if( g_Enemy[i].nState == STOP )

        {

           if( g_Enemy[i].nAppearanceTime <= nPassTime )

           {

              g_Enemy[i].nState = RUN;

              g_nEnemyIndex++;

           }else{

               break;

           }

        }

      }

 

     // Note: 적 탱크 이동

     for( i = 0 ; i < g_nEnemyIndex ; i++ )

     {

        if( g_Enemy[i].nLife == 1 )

        {

           switch( g_Enemy[i].nState )

           {

           case RUN :

                     if( nCurTime - g_Enemy[i].nOldTime > g_Enemy[i].nMoveTime )

                     {

                        switch( g_Enemy[i].nDirect )

                        {

                        case UP :   // 경계 영역 처리

                                if( g_Enemy[i].nY - 1 < 0 )

                                {       

                                    g_Enemy[i].nState = ROTATION;

                                }else{

                                    g_Enemy[i].nY--;

                                    g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case DOWN :

                                if( g_Enemy[i].nY + 1 > 23 )

                                {       

                                    g_Enemy[i].nState = ROTATION;

                                }else{

                                    g_Enemy[i].nY++;

                                    g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case LEFT :

                                if( g_Enemy[i].nX - 1 < 1 )

                                {       

                                   g_Enemy[i].nState = ROTATION;

                                }else{

                                   g_Enemy[i].nX--;

                                   g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        case RIGHT:

                                if( g_Enemy[i].nX + 1 > 39 )

                                {       

                                   g_Enemy[i].nState = ROTATION;

                                }else{

                                   g_Enemy[i].nX++;

                                   g_Enemy[i].nOldTime = nCurTime;

                                }

                                break;

                        }

                     }                                  

                     break;

             case ROTATION :

                       while( 1 )

                     {

                        nTemp = rand() % 4;

                        if( nTemp == g_Enemy[i].nDirect )

                           continue;

                        else

                        {       

                           g_Enemy[i].nDirect = nTemp;

                           g_Enemy[i].nState = RUN;

                           g_Enemy[i].nOldTime = nCurTime;

                           break;

                        }

                      }

                      break;

          }

        }

     }

 

     // Note: 포탄 발사

     for( i = 0 ; i < g_nEnemyIndex ; i++ )

     {

        if( g_Enemy[i].nState == RUN )

        {

            if( nCurTime - g_Enemy[i].nOldFireTime > g_Enemy[i].nFireTime )

            {

               // 100개의 총알에서 찾는 부분이다.

               for( j = 0 ; j < 100 ; j++ )

               {

                if( g_sEnemyBullet[j].nLife == 0 )

                {

                   g_sEnemyBullet[j].nDirect = g_Enemy[i].nDirect;

                       switch( g_Enemy[i].nDirect )

                   {

                    case LEFT:

                            g_sEnemyBullet[j].nX = g_Enemy[i].nX - 1;

                            g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                            break;

                    case RIGHT:

                               g_sEnemyBullet[j].nX = g_Enemy[i].nX + 1;

                             g_sEnemyBullet[j].nY = g_Enemy[i].nY;

                             break;

                    case UP:

                               g_sEnemyBullet[j].nY = g_Enemy[i].nY - 1;

                             g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                             break;

                    case DOWN:

                             g_sEnemyBullet[j].nY = g_Enemy[i].nY + 1;

                             g_sEnemyBullet[j].nX = g_Enemy[i].nX;

                             break;

                   }

                   g_sEnemyBullet[j].nLife = 1;

                   g_sEnemyBullet[j].nOldMoveTime = nCurTime;

                       g_Enemy[i].nOldFireTime = nCurTime;

                   break;

                }

               }

            }

        }

     }

 

     // Note: 포탄 이동

     for( i = 0 ; i < 100 ; i++ )

     {

        if( g_sEnemyBullet[i].nLife == 1 )

        {       // 포탄의 이동 시간을 120으로 고정

            if( nCurTime - g_sEnemyBullet[i].nOldMoveTime > 120 )

            {

               switch( g_sEnemyBullet[i].nDirect )

               {

               case UP:

                       if( g_sEnemyBullet[i].nY - 1 < 0 )

                       {

                        g_sEnemyBullet[i].nLife = 0;

                        continue;

                       }else{

                        g_sEnemyBullet[i].nY--;

                       }

                       break;

                case DOWN:

                       if( g_sEnemyBullet[i].nY + 1 > 23 )

                       {

                        g_sEnemyBullet[i].nLife = 0;

                        continue;

                       }else{

                        g_sEnemyBullet[i].nY++;

                       }

                       break;

                case LEFT:

                        if( g_sEnemyBullet[i].nX - 1 < 1 )

                        {

                            g_sEnemyBullet[i].nLife = 0;

                            continue;

                        }else{

                            g_sEnemyBullet[i].nX--;

                        }

                        break;

                case RIGHT:

                        if( g_sEnemyBullet[i].nX + 1 > 39 )

                        {

                           g_sEnemyBullet[i].nLife = 0;

                           continue;

                        }else{

                           g_sEnemyBullet[i].nX++;

                        }

                        break;

                }

                g_sEnemyBullet[i].nOldMoveTime = nCurTime;

            }

        }

     }

}

 

void Render()

{

     int i;    

 

     ScreenClear();

        

      // Note: 렌더링 부분 & 적 탱크 총알 발사

     for( i = 0 ;  i < 100 ; i++ )

     {

         if( g_sEnemyBullet[i].nLife == 1 )

         {

             switch( g_sEnemyBullet[i].nDirect )

             {

              case UP :

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "↑");

                  break;

              case DOWN :

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "↓");

                 break;

              case LEFT:

                 ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "←");

                  break;

              case RIGHT:

                ScreenPrint(g_sEnemyBullet[i].nX*2, g_sEnemyBullet[i].nY, "→");

                break;

             }

         }

      }

 

      for( i = 0 ; i < g_nEnemyIndex ; i++ )

      {

          if( g_Enemy[i].nState != STOP )

          {

              ScreenPrint( g_Enemy[i].nX*2, g_Enemy[i].nY, "★" );                   

          }

      } 

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

    ScreenInit();

    Init();

    

    g_nStartTime = clock();

 

    while( 1 )

    {           

        Update();               

        Render();               

    }

    Release();

    ScreenRelease();

    return 0;

}

 

[소스 16-11] 상태 전이에 의한 적 탱크 이동 및 포탄 발사

 

 


[STEP 05]

 

[그림 16-17] 5단계 제작 로드맵

 

■ 충돌 체크

 

충돌 체크는 [그림 16-17]과 같이 나눌 수 있다.

충돌을 체크하기 위해서 모든 캐릭터의 좌표를 이동한 후에 충돌 체크를 하면 충돌은 했지만 어긋난 경우가 발생된다.

그래서 각 캐릭터를 이동하기 전에 이동할 좌표로 먼저 충돌 체크를 한 후에 이동을 결정하면 충돌 체크에서 제외되는 경우를 막을 수 있다.

 

위 [그림 16-17]에서 주인공 탱크를 예로 들면 먼저 주인공 탱크의 이동할 좌표를 계산한  후에 적 탱크, 적 포탄, 경계 영역, 방호벽, 블록까지 충돌을 체크하고 득점과 연결하면 된다. 이 부분은 다른 캐릭터에도 동일하게 적용된다.

 

 


STEP 06

 

[그림 16-18] 6단계 제작 로드맵

 

■ 게임 스테이지 정보

 

탱크 게임의 스테이지 정보는 툴에서 이미 제작된 정보를 동일하게 읽어 적용한다.

실제 진행 데이터는 게임이 실행되면서 만들어지지만 초기 셋팅에 관한 정보는 툴에서 만들어져 적용된다. 특히 스테이지 정보는 유동적인 정보가 아니라 고정적인 정보이므로 더욱 툴에 의존적이다. 스테이지 정보를 나열하면 아래 [표 16-2]와 같다.

 

 

① 적 캐릭터수

② 적 출현 위치 개수

③ 적 출현 위치 포인터

④ 보스 초기 위치

⑤ 주인공 탱크 초기 위치

⑥ 적 탱크 종류 개수

 

[표 16-2] 스테이지 정보

 

[표 16-2]를 보면 적 출현 위치 개수는 툴에서 최대 10개까지 유동적으로 설정해 줄 수 있도록 하였다. 적 출현 위치의 개수가 유동적이라는 것은 유동적으로 메모리가 생성과 해제를 해야 하므로 적 출현 위치를 저장하는 변수형은 포인터가 된다.

위의 스테이지 정보를 구조체로 정의하면 아래 [소스 16-12]와 같다.

 

 

typedef struct _STAGE

{

        int nEnemyCount;         

        int nEnemyPositionCount;   

        POS_XY *pEnemyPosition;  

        int nBossX, nBossY;       

        int nPlayerX, nPlayerY;    

        int nEnemyTypeCount;      

} STAGE;

 

[소스 16-12] 스테이지 정의

 

■ 게임 진행 정보

 

탱크 게임의 진행 상태는 앞서 제작한 12장의 Snake 게임과 동일하다.

여기까지 살펴본 내용을 기반으로 마지막 탱크 게임을 완성해 보자.

 

1장부터 시작해서 16장까지 여러 게임과 툴을 제작해 보았으므로 이제 윈도우 프로그래밍을 할 때가 되었다.

그 이유는 이미 우리는 윈도우 프로그래밍 또는 DirectX, OpenGL을 이용하여 프로그래밍 할 수 있는 프레임워크를 3장을 통해 살펴보았고 대부분의 게임을 프레임워크 환경 안에서 제작했기 때문이다.

 

같은 환경에서 다양한 게임을 작성해 본 경험은 팀 단위로 같은 프로그래밍 환경 속에서 개발할 때 많은 도움이 될 것이다.

이제 여러분이 DirectX 또는 OpenGL과 같은 SDK를 이용하여 프로그래밍을 할 때 무엇을, 어떤 함수에 프로그래밍해야 하는가를 알 수 있는 것은 우리가 제작한 게임 프레임워크의 구조가 SDK에서 제공하는 프레임워크와 유사하기 때문이다.

물론 이 구조는 게임을 제작하기 위한 스마트폰에도 적용될 수 있다.

 

결국 여러분은 ‘C를 이용한 게임 프로그래밍’ 과정을 통해 게임이라는 도구를 가지고 다양한 환경에서 프로그래밍할 수 있는 능력을 갖추게 되었다.

여러분은 게임을 기획부터 제작까지 해보았으므로 이 경험이 앞으로 개발하게 되는 게임을 시작부터 어떤 식으로 진행해야 할지를 알게 하는 로드맵이 될 것이다.  

 

 

 

좋은하루강의가 도움이 되셨습니까? 손가락 꾸욱 눌러주는 센스 ~~ 

[출처] https://nowcampus.tistory.com/entry/CGP16%EC%9E%A5-%ED%83%B1%ED%81%AC-%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
» (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
101 (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