(CGP) 14장 Sogo 게임

Sogo 게임 가장 기본적인 슈팅게임에 해당이 됩니다.

이 장에서는 슈팅 게임을 이해하고 각종 캐릭터를 스토리에 따라 출력하며 앞서 제작한 패턴을 적용하여 프로그래밍하는 것이 가장 중요합니다.

 

굿잡  유튜브 동영상 강의 주소

(1) http://youtu.be/gajEzUe4_8U

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

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

(4) http://youtu.be/qRVBh_HvJ-g

(5) http://youtu.be/HXURI5yOh58

(6) http://youtu.be/NyJLnsm2d7U

(7) http://youtu.be/9cM-pqN1wb8

(8) http://youtu.be/HMqprZ2ncuU

 

 

 


 

14.1 기획

 

■ 스토리

 

외계인들이 물과 식량을 구하기 위해 지구를 침공했다. 외계인들을 물리치고 세계의 평화를 지키자.

 

■ 게임방식

 

주인공은 좌우키를 이용하여 이동할 수 있으며, 다수의 적 캐릭터는 다양한 패턴으로 공격을 한다. 적 캐릭터가 모두 출현한 후에 보스 캐릭터와 주인공 간의 대화 내용이 나오고 보스와 대결하게 된다.

 

■ 제한사항

 

기본적으로 방향키로 좌우 이동을 하며 s키는 미사일 발사키이다.

주인공 미사일은 한 화면에 최대 5발 이상 발사되지 못하게 제안하며 적 캐릭터와 보스는 앞장에서 제작한 패턴 데이터를 이용하여 이동한다.

 

■ 기획화면

 

 

기본적 게임 진행 화면 )

( 보스 대화 화면 )

( 주인공 대화 화면 )

( 보스와 대전 화면 )

 

[그림 14-1] 기획 화면

 

 

 


 

14.2 실행 화면

 

[그림 14-2] 초기 화면

 

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

 

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

 

[그림 14-5] 대화2 화면

 

[그림 14-6] 보스와 대결 화면

 

 

[그림 14-7] 미션 실패 화면

 

14.3 게임 제작 로드맵

 

앞장에서 제작한 게임은 주로 적 캐릭터와 주인공에 대한 프로그래밍이었지만 여기에서는 보스 캐릭터와 주인공 간의 대화를 출력하거나 다양한 패턴이 적용된 적 캐릭터를 추가하여 제작하게 된다. 또한 Sogo 게임에서 적 캐릭터와 보스는 이동 패턴이라는 요소를 통해 이동하므로 이 부분을 집중하여 살펴보도록 하자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

[STEP 06]

 

[STEP 07]

 

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

 

 

 


 

14.4 단계별 프로그래밍

 

 

[STEP 01]

  

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

 

■ 주인공

 

- 속성

 

슈팅 게임의 주인공 속성은 앞장에서 제작한 슛 골인 게임과 유사하지만 슈팅 게임이라는 특성 때문에 몇 가지 추가되는 속성이 있다.

먼저 주인공 속성을 나열하면 아래 [표 14-1]과 같으며 이에 대한 내용을 자세히 살펴보면 다음과 같다.

 

 

① 생명

② 이동 좌표

③ 이동 시간 간격

④ 이전 이동 시각

⑤ 미사일 발사 시간 간격

⑥ 이전 미사일 발사 시각

 

[표 14-1] 주인공 속성

 

첫째, 생명이다.

생명은 주인공이 게임을 계속할 것인지 아니면 종료할 것인지를 결정하게 하는 중요한 속성이다. 기본적으로 3이 설정되며 적 캐릭터로부터 3번의 공격을 받아 생명값이 0이 되면 게임은 종료하게 된다.

 

둘째, 이동 좌표이다.

이 게임에서 이동 좌표는 캐릭터의 맨 좌측 좌표를 말하며 출력할 때도 이 좌표를 기준으로 출력한다.

 

셋째, 이동 시간 간격이다.

이 속성은 일정한 간격으로 이동하기 위한 속성이다.

만일 이 부분이 없다면 방향키가 눌려지는 속도만큼 주인공 캐릭터는 이동하게 된다.

 

넷째, 이전 이동 시각이다.

시간 차이를 계산하기 위해서는 현재 시각과 이전 이동 시각이 있어야 한다.

 

다섯째, 미사일 발사 시간 간격이다.

미사일이 발사되는 간격을 일정하게 하기 위한 속성이다.

 

여섯째, 이전 미사일 발사 시각이다.

미사일 발사도 이동과 동일하게 시간 차이를 이용하므로 이 속성이 필요하다.

 

여기까지 설명한 속성을 정의하면 아래 [소스 14-1]과 같다.

 

 

typedef struct _PLAYER

{

       int  nLife;             // 생명

       int nX, nY;            // 이동 좌표

       clock_t MoveTime;     // 이동 시간 간격

       clock_t OldTime;   // 이전 이동 시각

       clock_t FireTime;      // 미사일 발사 시간 간격

       clock_t OldFireTime;   // 이전 미사일 발사 시각

} PLAYER;

 

PLAYER g_Player;

 

[소스 14-1] 주인공 속성 정의

 

■ 미사일

 

- 속성

 

미사일은 주인공과 적 캐릭터가 같이 사용하는 속성으로 아래 [표 14-2]와 같다.

 

 

① 생명

② 이동 좌표

③ 이전 이동 시각

 

[표 14-2] 미사일 속성

 

위의 속성은 주인공 속성과 유사하지만 자세히 살펴보면 이동 시간 간격이라는 속성이 제외된 것을 알 수 있다.

이 속성이 제외된 이유는 이 게임에서 사용되는 모든 미사일은 같은 이동 시간 간격을 사용하므로 미사일마다 이 속성을 정의할 필요가 없기 때문이다.

그래서 이 속성은 전역 변수 또는 #define과 같은 전처리어를 사용하여 공용값으로 처리함으로써 불필요한 속성으로 인한 메모리 낭비를 줄이게 된다.

 

 

typedef struct _MISSILE

{

      int      nLife;    

      int      nX, nY;

      clock_t  OldTime;

} MISSILE;

 

[소스 14-2] 미사일 속성 정의

 

주인공 미사일은 한 화면에 나타낼 수 있는 최대 미사일 개수가 5개로 기획에서 제한되어 있으므로 아래 [소스 14-2]와 같이 선언된다.

 

 

#define HERO_MISSILE  5

 

MISSILE g_PlayerMissile[ HERO_MISSILE ];

 

[소스 14-3] 주인공 캐릭터의 미사일 선언

 

위와 같이 미사일을 배열로 선언함으로써 최대 사용할 수 있는 미사일 개수는 5개가 되며 미사일 발사키가 입력되면 위의 배열 중에서 사용하지 않는 배열을 찾아 배열의 nLife를 1로 설정한다.

중요한 것은 이와 같은 배열 선언만으로도 그 사용 범위를 제한할 수 있다는 것이다.

 

[실습 예제 14-1]

 

좌우 방향키에 의해 이동하는 주인공을 게임 프레임워크에 적용하여 아래 [그림 14-10]과 같이 출력되게 프로그래밍해 보자. 그리고 s 키가 입력되면 주인공 미사일이 발사되도록 한다. 단 미사일은 한 화면에 최대 5발까지 출력할 수 있다.

[그림 14-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

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define HERO_MISSILE  5

#define MISSILE_MOVE_TIME 100

 

typedef struct _PLAYER

{

       int  nLife;

       int nX, nY;

       clock_t MoveTime;

       clock_t OldTime;   

       clock_t FireTime;

       clock_t OldFireTime;

} PLAYER;

 

typedef struct _MISSILE

{

      int      nLife;     

      int      nX, nY;

      clock_t  OldTime;

} MISSILE;

 

PLAYER g_Player;

MISSILE g_PlayerMissile[ HERO_MISSILE ];

 

void Init()

{

        g_Player.nLife = 3;

        g_Player.nX = 30;

        g_Player.nY = 20;

        g_Player.OldTime = clock();

        g_Player.MoveTime = 100;

        g_Player.FireTime = 50;

        g_Player.OldFireTime = clock();

}

 

void Update()

{

     int i;

     clock_t CurTime = clock();

     // 주인공 미사일 관련

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

     {

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

        {   // 주인공 미사일 이동

            if( CurTime - g_PlayerMissile[i].OldTime > MISSILE_MOVE_TIME )

            {

                g_PlayerMissile[i].OldTime = CurTime;

                if( g_PlayerMissile[i].nY - 1 > 1 )

                  g_PlayerMissile[i].nY--;

                else

                  g_PlayerMissile[i].nLife = 0;

             }

         }

     }

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

     {

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

            ScreenPrint( g_PlayerMissile[i].nX, g_PlayerMissile[i].nY, "$" );

     }

 

     ScreenPrint( g_Player.nX, g_Player.nY, "#-@-#" );

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, i;

    clock_t CurTime;

 

    ScreenInit();

    Init();

        

    while( 1 )

    {

         CurTime = clock();

 

         if( _kbhit() )

         {

           nKey = _getch();

 

           switch( nKey )

           {

            case 75 : // 왼쪽

                      if( CurTime - g_Player.OldTime > g_Player.MoveTime )

                    {

                        g_Player.OldTime = CurTime;

                            if( g_Player.nX - 1 > 1 )

                        {

                          g_Player.nX--;

                          break;

                         }

                     }

                     break;

 

              case 77 : // 오른쪽

                      if( CurTime - g_Player.OldTime > g_Player.MoveTime )

                      {

                        g_Player.OldTime = CurTime;

                        if( g_Player.nX + 1 < 59 )

                        {

                            g_Player.nX++;

                            break;

                        }

                      }                                 

                      break;

 

              case 's' : // 미사일 발사

                      if( CurTime - g_Player.OldFireTime >  g_Player.FireTime )

                      {

                        g_Player.OldFireTime = CurTime;

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

                        {

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

                             {

                                 g_PlayerMissile[i].nLife = 1;

                                g_PlayerMissile[i].nX = g_Player.nX + 2;

                                g_PlayerMissile[i].nY = g_Player.nY - 1;

                                g_PlayerMissile[i].OldTime = clock();

                                break;

                               }

                            }

                        }

                        break;

                }

        }

 

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 14-4] 주인공 캐릭터와 미사일 출력

 

 

 


 

[STEP 02]

  

[그림 14-11] 2단계 제작 로드맵

 

■ 적 캐릭터

 

- 속성

 

기본적으로 적 캐릭터는 주인공은 유사하지만 몇 가지 차이점이 있다.

 

첫째, 출현 시점에 관한 사항이다.

주인공은 게임 실행과 동시에 출현되지만 적 캐릭터는 지정된 시점에 출현되야 한다. 그러므로 주인공과는 달리 출현 시점이라는 속성이 있어야 하며 이 출현 시점은 오름차순으로 정렬된 시간을 적용한다.

[그림 14-12] 출현 시점

 

둘째, 미사일 발사에 관한 사항이다.

주인공 미사일은 플레이어에 의해 발사가 결정되지만 적 캐릭터는 일정한 시간 간격으로 발사된다. 즉 주인공 미사일은 키 입력에 따라 발사되지만 적 캐릭터는 스스로 발사되어야 하는 것을 의미한다. 그래서 적 캐릭터의 미사일 발사에 관한 방법은 [그림 14-13]과 같이 미사일을 관리하는 버퍼를 두고 미사일 발사 시간이 되면 이 버퍼에 미사일 발사를 설정하도록 하여 발사되게 한다. 이것은 적 캐릭터가 일일이 미사일에 관여하는 것이 아니라 미사일을 담당하는 버퍼에게 미사일 발사와 이동을 맡긴다는 의미가 된다.

[그림 14-13] 미사일 버퍼

 

적 캐릭터의 일반적인 속성과 위에서 설명한 속성을 나열하면 아래 [표 14-3]과 같다.

 

 

① 생명 정보 ( 0: 소멸 상태, 1: 생존 상태 )

② 좌표 정보

③ 캐릭터 타입 정보

④ 출현 시각

⑤ 이동 시간 간격

⑥ 이전 이동 시각

⑦ 미사일 발사 시간 간격

⑧ 이전 미사일 발사 시각

 

[표 14-3] 적 캐릭터 속성

 

위 속성 중에 세 번째 속성인 캐릭터 타입 정보는 적 캐릭터를 출력하기 위한 특수 문자를 지정하기 위한 부분으로 11장 Snake 맵 툴에서 인덱스로 먹이 또는 적, 블록 등을 출력한 것과 같은 것이다. 또한 일곱 번째 속성인 미사일 발사 시간 간격과 그 다음 이전 미사일 발사 시각은 일정한 시간 간격으로 미사일 발사를 버퍼에 설정하기 위한 속성이다.

 

위의 사항을 구조체로 정의하면 아래 [소스 14-5]와 같다.

 

 

typedef struct _ENEMY

{

       int  nLife;

       int  nX, nY;

       int  nType;

       clock_t AppearTime;      

       clock_t MoveTime;

       clock_t OldMoveTime;     

       clock_t MissileFireTime; 

       clock_t MissileOldTime;    

} ENEMY;

 

[소스 14-5] 적 캐릭터 속성 정의

 

■ 적 캐릭터 미사일과 미사일 버퍼

 

적 캐릭터 미사일과 주인공 미사일은 상당히 유사하지만 처리 부분에서 약간의 차이점이 있다. 주인공 미사일은 5개로 한정되어 있어서 배열로 선언이 가능했지만 적 캐릭터 미사일의 개수는 알 수 없으므로 최대한 처리할 수 있는 크기의 미사일 버퍼가 있어야 한다.

그리고 적 캐릭터 속성의 발사 시점이 되면 미사일 버퍼에서 사용하지 않는 미사일을 찾아 발사 정보를 설정한다.

미사일 출력은 적 캐릭터 정보를 통해 출력하는 것이 아니라 미사일 버퍼 안에서 살아 있는 미사일을 찾아 출력한다.

이 부분은 주인공 캐릭터가 최대 5개의 미사일 중에서 사용하지 않는 미사일을 찾아 발사상태로 만드는 것과 같은 원리이다.

 

미사일에 관한 구조체와 미사일 버퍼를 살펴보면 아래 [소스 14-6]과 같다.

 

 

typedef struct _ENEMY_MISSILE

{

       int     nLife;

       int     nX, nY;

       clock_t MoveTime;

       clock_t OldMoveTime;     

} ENEMY_MISSILE;

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

 

[소스 14-6] 미사일 속성

 

[실습 예제 14-2]

 

임의의 출현 시간 간격을 가지고 이동하는 적 캐릭터 5개와 최대 100개까지의 미사일을 화면에 출력할 수 있는 미사일 버퍼를 만들고 적 캐릭터와 미사일이 일정한 시간 간격으로 출력되도록 프로그래밍해 보자. 단, 적 캐릭터 타입은 ♨, ◆, ☎, ▣ 이다.

 

[그림 14-14] 적 캐릭터와 미사일

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define MISSILE_MOVE_TIME 100

 

typedef struct _ENEMY

{

        int  nLife;

        int  nX, nY;

        int  nType;

        clock_t AppearTime;      

        clock_t MoveTime;

        clock_t OldMoveTime;     

        clock_t MissileFireTime; 

        clock_t MissileOldTime;    

} ENEMY;

 

typedef struct _ENEMY_MISSILE

{

        int     nLife;

        int     nX, nY;

        clock_t MoveTime;

        clock_t OldMoveTime;     

} ENEMY_MISSILE;

 

ENEMY g_Enemy[5];

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

char g_EnemyType[4][3] = { "♨", "◆", "☎", "▣" };

clock_t g_StartTime;

int g_nEnemyIndex; // 적 캐릭터 출현 인덱스

 

void Init()

{

     int i = 0;

 

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

     {

        g_Enemy[i].nLife = 0;

        g_Enemy[i].nX = rand() % 55 + 5;

        g_Enemy[i].nY = 1;

        g_Enemy[i].nType = i % 4;

        g_Enemy[i].AppearTime = 1000 * ( i + 1 ); // 출현 시간을 오름차순으로 설정

        g_Enemy[i].MoveTime = 400;

        g_Enemy[i].OldMoveTime = clock();

        g_Enemy[i].MissileFireTime = rand() % 500 + 1000;

        g_Enemy[i].MissileOldTime = clock();               

     }

     g_nEnemyIndex = 0;

}

 

void Update()

{

     int i, j;

     clock_t CurTime = clock();

 

     // Note: 출현 ( 출현 시간의 데이터는 오름차순으로 정렬되어 있다 )

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

     {

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

        {

            if( CurTime - g_StartTime >= g_Enemy[i].AppearTime )

            {

                g_Enemy[i].nLife = 1;

                g_nEnemyIndex++;

            }else{                                                      

                break;

            }

        }

     }

 

     // Note:  데이터 업데이트

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

     {

        if( g_Enemy[i].nLife )

        {

            // Note: 적 캐릭터 업데이트

            if( CurTime - g_Enemy[i].OldMoveTime > g_Enemy[i].MoveTime )

            {

                g_Enemy[i].OldMoveTime = CurTime;

                g_Enemy[i].nY++;

                 if( g_Enemy[i].nY > 20 )

                 {

                   g_Enemy[i].nLife = 0;

                   continue;

                }

             }

 

             // Note: 적 미사일 발사 하는 부분

             if(CurTime - g_Enemy[i].MissileOldTime > g_Enemy[i].MissileFireTime)

             {

                g_Enemy[i].MissileOldTime = CurTime;

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

                {

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

                   {

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

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

                       g_EnemyMissile[j].nLife = 1;

                       g_EnemyMissile[j].MoveTime = rand() % 100 + 80;

                       g_EnemyMissile[j].OldMoveTime = CurTime;

                       break;

                    }   

                 }

              }

        }

     }

 

     // Note: 미사일 업데이트

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

     {

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

        {

           if( CurTime - g_EnemyMissile[i].OldMoveTime >

                                         g_EnemyMissile[i].MoveTime )

           {

               g_EnemyMissile[i].OldMoveTime = CurTime;

               if( g_EnemyMissile[i].nY + 1 > 20 )

                 g_EnemyMissile[i].nLife = 0;

               else

                 g_EnemyMissile[i].nY += 1;

           }

        }

     }

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

      { 

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

        {

            ScreenPrint( g_EnemyMissile[i].nX, g_EnemyMissile[i].nY, "↓" );

        }

      }

 

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

      {

        if( g_Enemy[i].nLife )

        {

           ScreenPrint( g_Enemy[i].nX, g_Enemy[i].nY, g_EnemyType[g_Enemy[i].nType] ); 

        }

      }

 

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    g_StartTime = clock();

 

    while( 1 )

    {

        Update();

        Render();       

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 14-7] 적 캐릭터와 미사일

 

33행의 g_nEnemyIndex 변수는 최종 출현된 적 캐릭터의 인덱스를 나타낸다.

이 변수의 용도는 다음과 같다.

 

첫째, 60행과 같이 이미 출현한 적 캐릭터는 출현 검색에서 제외시키기 위해 사용된다.

둘째, 75행과 같이 대기 상태의 적 캐릭터는 업데이트 대상에서 제외시키기 위해 사용된다.

셋째, 142행과 같이 대기 상태의 적 캐릭터를 출력에서 제외시키기 위해 사용된다.

 

45행은 출현 시간을 오름차순으로 설정하고 있다.

이 설정에 따라 64행 이상에서 게임이 시작된 시간과 현재의 시간 차이로 출현을 결정하고 있다. 출현을 시킨다는 것은 적 캐릭터의 생명을 1로 설정하는 것을 의미한다.

 

적 캐릭터 미사일을 설정하는 부분은 92행에서부터 107행까지가 된다.

적 캐릭터의 미사일 발사가 미사일 발사 시간 간격에 의해 결정되면 현재 미사일 버퍼에서 소멸 상태의 미사일을 찾아 활성화시켜 발사하게 되는데 이 부분은 95행부터 106행까지가 된다.

 

 

 


 

[STEP 03]

  

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

 

■ 패턴 설계

 

패턴 구조는 13장에서 툴을 제작할 때 살펴본 아래 [소스 14-8] 코드를 동일하게 사용한다.

 

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표      

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO g_PatInfo;

 

[소스 14-8] 패턴 속성

 

■ 패턴을 적 캐릭터에 적용

 

13장 [실습 예제 13-1]을 통해 하나의 패턴을 한 캐릭터에 적용하여 출력하는 소스를 제작해 보았다. 이것을 기반으로 이제 여러 개의 패턴 중에서 하나의 패턴을 여러 캐릭터에 적용하여 이동되게 프로그래밍해 보자.

 

[그림 14-16] 적 캐릭터의 패턴 선택

 

먼저 패턴 구조는 확정되어 있으므로 개수가 다른 다수의 패턴은 포인터 또는 배열로 다룰 수 있다. 선언 형식에서 포인터는 메모리 생성과 해제가 자유롭지만 배열은 그렇지 않다.

만약 파일로부터 읽는 패턴 개수가 항상 일정하다면 배열 형식을 사용할 수 있겠지만 그렇지않으면 포인터를 사용하는 것이 프로그래밍의 자유도를 높일 수 있다.

 

여러 개의 패턴 파일을 읽어 패턴을 생성하면 생성 과정에서 이미 아이디가 암시적으로 할당된다. 그 이유는 순차적으로 읽혀진 패턴 데이터는 인덱스를 통해 접근하게 되는데 이때  이 인덱스는 그 패턴의 고유 아이디가 된다.

그러면 이와 같은 내용을 간단히 코드로 작성해 보면 아래 [소스 14-9]와 같다.

 

[그림 14-17] 여러 개의 패턴 읽기 및 메모리가 생성된 결과

 

 

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

#include <stdio.h>

#include <time.h>

#include <malloc.h>

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표         

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO *g_PatInfo;

 

int main(void)

{

    int nFileCount, i, j;

    FILE *fp;

    char *strFileName[3] = { "pat1.txt", "pat2.txt", "pat3.txt" };

        

    nFileCount = sizeof( strFileName ) / sizeof( char * );

    g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * nFileCount ); 

 

    // 패턴 파일 읽기

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

    {

        fp = fopen( strFileName[i], "r" );

        

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

            fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                                  &g_PatInfo[i].pPat[j].nStep,

                                  &g_PatInfo[i].pPat[j].MoveTime,

                                  &g_PatInfo[i].pPat[j].nDist );

            fclose( fp );    

    }

 

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

    {

        printf( "파일 : %s \n", strFileName[i] );

        printf( "%d\n", g_PatInfo[i].nCount );

        printf( "%d %d\n", g_PatInfo[i].nX0, g_PatInfo[i].nY0 );

        printf( "[ 이동 경로 ] \n" );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

        {

             printf( "%d %d %d %d\n", g_PatInfo[i].pPat[j].nDirect,

                      g_PatInfo[i].pPat[j].nStep,g_PatInfo[i].pPat[j].MoveTime,

                      g_PatInfo[i].pPat[j].nDist );

        }

 

        printf( "\n" );

    }

 

    // 메모리 해제

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

         free( g_PatInfo[i].pPat );

 

    free( g_PatInfo );

 

    return 0;

}

 

[소스 14-9] 여러 개의 패턴 읽기 및 메모리 생성

 

위의 소스에서 유심히 살펴봐야 하는 부분은 메모리 해제 부분이다.

68행과 69행을 보면 다수의 이동 경로를 저장하기 위한 메모리를 패턴 개수만큼 해제한 후에 최종적으로 패턴 자체를 메모리에서 해제하고 있다.

즉 malloc()로 할당한 메모리 개수만큼 free()도 동일하게 있어야 한다.

 

이제 위의 패턴을 사용하기 위해 추가되는 적 캐릭터의 속성을 살펴보면 다음과 같다.

 

첫째, 적 캐릭터는 여러 개의 패턴 중에서 하나를 선택해야 하므로 패턴 인덱스를 저장할 수 있어야 한다. 

 

둘째, 하나의 패턴 안에 순차적으로 있는 여러 개의 이동 경로 중에서 현재 이동 경로의 인덱스를 저장할 수 있어야 한다.

이 부분은 13장 패턴 뷰어 프로그램의 [실습 예제 13-1]에서 이동 경로 구간을 구분하기 한 인덱스 g_nIndex과 같은 것이다.

 

셋째, 현재 이동 스텝을 저장할 수 있어야 한다.

적 캐릭터는 이동 스텝만큼 이동하여 목표 좌표까지 도달하게 되므로 현재 스텝을 개별적으로 저장해야 한다.

이동 경로마다 있는 스텝은 최종 스텝수이므로 이 값과 현재 이동 스텝을 비교하여 다음 이동 경로로 진행할 것인지를 결정한다.

 

여기까지 살펴본 적 캐릭터 추가 속성을 나열하면 아래 [표 14-3]과 같다.

 

 

① 패턴 형태

② 이동 경로의 인덱스

③ 현재 스텝

 

[표 14-3] 적 캐릭터의 추가 속성 

 

이제까지 살펴본 추가 속성을 적 캐릭터 속성에 포함시켜 정의하면 아래 [소스 14-10]과 같다.

 

 

typedef struct _ENEMY

{

       int  nLife;

       int  nX, nY;

       int  nType;

       clock_t AppearTime;      

       clock_t MoveTime;

       clock_t OldMoveTime;     

       clock_t MissileFireTime; 

       clock_t MissileOldTime;

 

         // 추가된 속성

         int nPatType; // 패턴 형태

         int nPatIndex; // 이동 경로 인덱스

         int nPatStep; // 현재 스텝       

} ENEMY;

 

[소스 14-10] 추가된 적 캐릭터 속성

 

[실습 예제 14-3]

 

[소스 14-9]의 패턴 읽기 소스와 추가된 적 캐릭터 속성을 이용하여 적 캐릭터 3개가 패턴에 따라 이동되도록 프로그래밍해 보자.

 

[그림 14-18] 패턴에 의해 이동하는 적 캐릭터(1)

 

[그림 14-19] 패턴에 의해 이동하는 적 캐릭터(2)

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

 

typedef struct _ENEMY

{

        int  nLife;

        int  nX, nY;

        int  nType;

        clock_t AppearTime;      

        clock_t MoveTime;

        clock_t OldMoveTime;     

        clock_t MissileFireTime; 

        clock_t MissileOldTime;

 

          // 추가된 속성

          int nPatType; // 패턴 형태

          int nPatIndex; // 이동 경로 인덱스

          int nPatStep; // 현재 스텝       

} ENEMY;

 

typedef struct _ENEMY_MISSILE

{

        int     nLife;

        int     nX, nY;

        clock_t MoveTime;

        clock_t OldMoveTime;     

} ENEMY_MISSILE;

 

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO *g_PatInfo;

char g_EnemyType[4][3] = { "♨", "◆", "☎", "▣" };

ENEMY g_Enemy[3];

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

clock_t g_StartTime;

int g_nEnemyIndex; // 적 캐릭터 출현 인덱스

int g_nEnemyCount; // 적 캐릭터 수

char *g_strFileName[3] = { "pat1.txt", "pat2.txt", "pat3.txt" };

 

void Init()

{

     int i, nFileCount, j;

     FILE *fp;

    

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

     g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * nFileCount );

 

     // 패턴 파일 읽기

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

     {

        fp = fopen( g_strFileName[i], "r" );

                

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

             fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                     &g_PatInfo[i].pPat[j].nStep, &g_PatInfo[i].pPat[j].MoveTime,

                          &g_PatInfo[i].pPat[j].nDist );                           

        fclose( fp );    

      } 

     

     g_nEnemyCount = 3; // 적 캐릭터 수

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

     {

        g_Enemy[i].nLife = 0;

        g_Enemy[i].nType = i % 4;

        g_Enemy[i].AppearTime = 1000 * ( i + 1 ); // 출현 시간을 오름차순으로 설정        

        g_Enemy[i].MissileFireTime = rand() % 500 + 1000;

        g_Enemy[i].MissileOldTime = clock();                       

        g_Enemy[i].nPatType = i;

        g_Enemy[i].nPatIndex = 0;

        g_Enemy[i].nPatStep = -1;

        g_Enemy[i].nX = g_PatInfo[ g_Enemy[i].nPatType ].nX0;

        g_Enemy[i].nY = g_PatInfo[ g_Enemy[i].nPatType ].nY0;

        g_Enemy[i].MoveTime = g_PatInfo[ g_Enemy[i].nPatType ].pPat[0].MoveTime;

        g_Enemy[i].OldMoveTime = clock();

     } 

 

      g_nEnemyIndex = 0; // 출현 캐릭터의 인덱스 범위

      g_StartTime = clock();

}

 

void Update()

{        

     int i, j;

     int nSignX, nSignY;    

     clock_t CurTime = clock();

 

     // Note: 출현 ( 출현 시간의 데이터는 오름차순으로 정렬되어 있다 )

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

     {

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

        {

           if( CurTime - g_StartTime >= g_Enemy[i].AppearTime )

           {

               g_Enemy[i].nLife = 1;

               g_nEnemyIndex++;

            }else{                                                      

               break;

            }

        }

     }  

 

     // Note:  데이터 업데이트

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

     {   

        if( g_Enemy[i].nLife )

        {

           // Note: 적 캐릭터 업데이트

           if( CurTime - g_Enemy[i].OldMoveTime > g_Enemy[i].MoveTime )

           {

              g_Enemy[i].OldMoveTime = CurTime;

              g_Enemy[i].nPatStep++;

 

              // 다음 패턴 인덱스로 진행

              if( g_Enemy[i].nPatStep ==

              g_PatInfo[ g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].nStep )

              {

                g_Enemy[i].nPatIndex++;

              if( g_Enemy[i].nPatIndex == g_PatInfo[ g_Enemy[i].nPatType ].nCount )

                {

                    g_Enemy[i].nLife = 0;

                    continue; //패턴이동이 종료되어 다음 캐릭터로 넘어가기

                }else{

                    g_Enemy[i].MoveTime =

           g_PatInfo[ g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].MoveTime;

            g_Enemy[i].nPatStep = 0;  

            }

     }

              

    switch( g_PatInfo[g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].nDirect )

            {

             case UP:

                    nSignX = 0;

                    nSignY = -1;

                    break;

             case UP_RIGHT:

                    nSignX = 1;

                    nSignY = -1;

                    break;

             case RIGHT:

                    nSignX = 1;

                    nSignY = 0;

                    break;

             case DOWN_RIGHT:

                    nSignX = 1;

                   nSignY = 1;

                   break;

              case DOWN:

                   nSignX = 0;

                   nSignY = 1;

                   break;

              case DOWN_LEFT:

                   nSignX = -1;

                   nSignY = 1;

                   break;

              case LEFT:

                   nSignX = -1;

                   nSignY = 0;

                   break;

              case UP_LEFT:

                   nSignX = -1;

                   nSignY = -1;

                   break;

             }

     g_Enemy[i].nX = g_Enemy[i].nX +

         nSignX*g_PatInfo[g_Enemy[i].nPatType].pPat[ g_Enemy[i].nPatIndex ].nDist;

        g_Enemy[i].nY = g_Enemy[i].nY +

        nSignY*g_PatInfo[g_Enemy[i].nPatType].pPat[ g_Enemy[i].nPatIndex ].nDist;

 

             // Note: 경계 영역 처리

             if( g_Enemy[i].nX < 1 || g_Enemy[i].nX > 78 || g_Enemy[i].nY > 20 )

             {

                g_Enemy[i].nLife = 0;

                continue;

              }

           }

                                

           // Note: 적 미사일 발사 하는 부분 ( 캐릭터가 살아 있는 때만 가능 )

           if( CurTime - g_Enemy[i].MissileOldTime > g_Enemy[i].MissileFireTime )

           {

               g_Enemy[i].MissileOldTime = CurTime;

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

               {

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

                  {

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

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

                      g_EnemyMissile[j].nLife = 1;

                      g_EnemyMissile[j].MoveTime = rand() % 100 + 80;

                      g_EnemyMissile[j].OldMoveTime = CurTime;

                      break;

                   }    

                }

           }

          }

     }

         

     // Note: 미사일 업데이트

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

     {

        if( g_EnemyMissile[i].nLife )

        {

           if( CurTime - g_EnemyMissile[i].OldMoveTime >

                                 g_EnemyMissile[i].MoveTime )

           {

               g_EnemyMissile[i].OldMoveTime = CurTime;

               if( g_EnemyMissile[i].nY + 1 > 20 )

                 g_EnemyMissile[i].nLife = 0;

               else

                 g_EnemyMissile[i].nY++;

            }

         } 

     }

}

 

void Render()

{

     int i;

     ScreenClear();

        

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

     {  

        if( g_EnemyMissile[i].nLife )

        {                       

           ScreenPrint( g_EnemyMissile[i].nX, g_EnemyMissile[i].nY, "↓" );

        }

     }

 

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

     {

        if( g_Enemy[i].nLife )

        {

           ScreenPrint( g_Enemy[i].nX, g_Enemy[i].nY,

             g_EnemyType[g_Enemy[i].nType] );

        }

     }

 

     ScreenFlipping();

}

 

void Release()

{

     int i, nFileCount;

 

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

 

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

        free( g_PatInfo[i].pPat );

        

     free( g_PatInfo );

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    while( 1 )

    {

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 14-11] 패턴이 적용된 적 캐릭터

 

위의 소스는 앞서 제작한 검증된 소스의 일부분들이 조합되어 완성된 것을 알 수 있다.

128행부터 198행까지는 적 캐릭터에 패턴을 적용하는 부분이며 201행에서부터 216행까지는 미사일 발사 설정에 관한 부분이다. 그리고 221행에서부터 234행은 미사일 이동을 처리하는 부분이다. 이 부분은 [실습 예제 14-2]의 [소스 14-7] 75행부터 125행까지 미리 제작해본 내용이다.

 

 


[STEP 04]

  

[그림 14-20] 4단계 제작 로드맵

 

■ 보스

 

- 보스 속성과 패턴

 

보스는 적 캐릭터와 동일한 속성을 가지고 있지만 몇 가지 차이점이 있다.

 

첫째, 적 캐릭터는 공격을 받으면 바로 소멸되지만 보스는 한 번의 공격이 아닌 여러 번의 공격을 받아야 소멸된다.

 

둘째, 적 캐릭터는 패턴이 한번 적용되지만 보스는 소멸될 때까지 다양한 패턴이 반복된다.

 

셋째, 보스는 경계 영역에 대한 충돌 체크를 할 필요가 없다.

이미 패턴으로 만들어진 이동 경로는 경계 영역과 충돌되지 않게 만들어지기 때문에 충돌 체크가 필요 없다.

 

아래 [그림 14-21]은 보스와 적 캐릭터간의 패턴 차이를 나타낸 것이다.

 

(적 캐릭터 패턴)

(보스 패턴)

 

[그림 14-21] 적 캐릭터와 보스 패턴

 

위의 [그림 14-21] 보스 패턴에서 주의해야 하는 점은, 패턴이 끝나고 새로운 패턴이 시작될 때 이전 끝난 좌표와 새로 시작하는 좌표가 동일한 위치에서 시작해야 한다.

그렇지 않으면 엉뚱한 좌표에서 보스가 갑자기 출현되어 경로의 연결이 끊어져 나타는 것 같이 보이게 된다.

 

이제 보스 속성을 나열하면 아래 [표 14-4]와 같다.

 

 

① 생명

② 좌표

③ 패턴 형태

④ 현재 스텝

⑤ 이동 경로의 인덱스

⑥ 이동 시간 간격

⑦ 이전 이동 시각

 

[표 14-4] 보스 속성

 

 

typedef struct _BOSS

{

       int  nLife;

       int  nX, nY;        

         int nPatType; // 패턴 형태

         int nPatStep; // 현재 스텝 

         int nPatIndex; // 이동 경로 인덱스

         clock_t MoveTime; // 이동 시간 간격

         clock_t OldTime;  // 이전 이동 시각

} BOSS;

 

[소스 14-12] 보스 속성 정의

 

[실습 예제 14-4]

 

[그림 14-22]와 같이 보스 캐릭터가 아래 [그림 14-23]과 [그림 14-24]와 같은 패턴 2개를 반복하여 실행되게 프로그래밍해 보자.

그리고 [그림 14-25]와 [그림 14-26] 패턴 데이터를 작성한 후에 패턴 뷰어에서 확인해보자. 특히 [그림 14-26] 패턴 데이터 중에서 이동 거리가 0인 것은 지정된 스텝만큼 정지 상태로 있게 한다.

 

[그림 14-22] 패턴이 적용된 보스

 

[그림 14-23] 보스 패턴(1)

[그림 14-24] 보스 패턴(2)

 

 

[그림 14-25] 패턴(1) 파일 : boss_pat1.txt

[그림 14-26] 패턴(2) 파일 : boss_pat2.txt

 

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;    // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime;  // 이동 시간 간격

        int nDist;         // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표

        PAT *pPat;       // 패턴

} PAT_INFO;

 

typedef struct _BOSS

{

        int  nLife;

       int  nX, nY;        

         int nPatType; // 패턴 형태

         int nPatStep; // 현재 스텝 

         int nPatIndex; // 이동 경로 인덱스

         clock_t MoveTime; // 이동 시간 간격

         clock_t OldTime;  // 이전 이동 시각

} BOSS;

 

PAT_INFO *g_PatInfo;

BOSS g_Boss;

char *g_strFileName[2] = { "boss_pat1.txt", "boss_pat2.txt" };

int g_nPatCount;

 

void Init()

{

     int i, j;

     FILE *fp;

     

     g_nPatCount = sizeof( g_strFileName ) / sizeof( char * );

     g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * g_nPatCount );

 

     // 패턴 파일 읽기

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

     {

        fp = fopen( g_strFileName[i], "r" );               

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

             fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                    &g_PatInfo[i].pPat[j].nStep, &g_PatInfo[i].pPat[j].MoveTime,

                           &g_PatInfo[i].pPat[j].nDist );                          

       fclose( fp );    

      } 

 

     // Note: 보스 설정

     g_Boss.nLife = 10;

     g_Boss.nPatIndex = 0;

     g_Boss.nPatStep = -1;

     g_Boss.nPatType = 0;

     g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

     g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

     g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

     g_Boss.OldTime = clock();

}

 

void Update()

{

     int nSignX, nSignY;

     clock_t CurTime = clock();

 

     if( g_Boss.nLife )

     {

        if( CurTime - g_Boss.OldTime > g_Boss.MoveTime )

        {

            g_Boss.OldTime = CurTime;

            g_Boss.nPatStep++;

 

           if( g_Boss.nPatStep ==

                  g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].nStep )

           {

               g_Boss.nPatIndex++;

               if( g_Boss.nPatIndex == g_PatInfo[ g_Boss.nPatType ].nCount )

               {

                 g_Boss.nPatType++;

                 g_Boss.nPatType = g_Boss.nPatType % g_nPatCount; //패턴 반복

                 g_Boss.nPatIndex = 0;

                 g_Boss.nPatStep = -1;

                 g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

                 g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

                 g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

                 g_Boss.OldTime = clock();                                 

                }else{

                 g_Boss.MoveTime =

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].MoveTime;

                 g_Boss.nPatStep = 0;

                }

           }

          

           switch( g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDirect )

           {

            case UP:

                    nSignX = 0;

                    nSignY = -1;

                    break;

            case UP_RIGHT:

                      nSignX = 1;

                    nSignY = -1;

                    break;

            case RIGHT:

                      nSignX = 1;

                    nSignY = 0;

                    break;

            case DOWN_RIGHT:

                    nSignX = 1;

                    nSignY = 1;

                    break;

            case DOWN:

                      nSignX = 0;

                    nSignY = 1;

                    break;

            case DOWN_LEFT:

                    nSignX = -1;

                    nSignY = 1;

                    break;

            case LEFT:

                      nSignX = -1;

                    nSignY = 0;

                    break;

            case UP_LEFT:

                      nSignX = -1;

                    nSignY = -1;

                    break;

             }

      

       g_Boss.nX =

    g_Boss.nX + nSignX*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

       g_Boss.nY =

    g_Boss.nY + nSignY*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

        }

    }

}

 

 

void Render()

{

     char string[100];

     ScreenClear();

 

     ScreenPrint( g_Boss.nX, g_Boss.nY  ,  " /| ━ // ━ |\");

     ScreenPrint( g_Boss.nX, g_Boss.nY+1, "//q ●    ● p\");

     ScreenPrint( g_Boss.nX, g_Boss.nY+2, "  (┗┻━┻┛)");

 

     sprintf( string, "PatType:%d nPatIndex:%d ",  g_Boss.nPatType,

                                                        g_Boss.nPatIndex );

     ScreenPrint( g_Boss.nX - 10, g_Boss.nY + 4, string );

     sprintf( string, "x : %d y : %d", g_Boss.nX, g_Boss.nY );

     ScreenPrint( g_Boss.nX - 10, g_Boss.nY + 5, string );

 

     ScreenFlipping();

}

 

void Release()

{

     int nFileCount, i;

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

 

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

     {

        free( g_PatInfo[i].pPat );

      }

 

      free( g_PatInfo );

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    while( 1 )

    {

          Update();

          Render();      

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 14-13]패턴이 적용된 보스 이동

 

위의 소스는 [실습 예제 14-3]의 [소스 14-11]과 유사한 내용들이 대부분이다.

왜냐하면 적 캐릭터와 보스는 패턴에 의해 제어되기 때문이다.

91행부터 98행까지는 보스의 패턴이 바뀔 때마다 초기화되는 부분이다.

특히 92행에서 % 연산자를 사용하는 이유는 패턴 파일의 인덱스를 반복하기 위해서이다. 현재 패턴 파일은 2개이며 파일 인덱스는 0, 1을 가지므로 92행의 % 2는 0과 1을 반복적으로 가지게 한다.

 

■ 보스 미사일

  

보스 미사일과 적 캐릭터 미사일의 차이점을 살펴보자. 적 캐릭터 미사일은 한 번에 한 개씩 미사일을 발사하지만, 보스는 미사일을 한 번에 3발 이상을 발사하게 된다.

이때 사방으로 펴져 나가는 미사일의 이동은 앞서 살펴본 이동 방향을 이용하면 쉽게 이동시킬 수 있다.

 

- 속성

 

보스 미사일 구조는 ‘미사일 전체에 적용되는 정보’와 ‘개별 미사일에 적용되는 정보’로 나눌 수 있다. 전체 정보로는 3개의 미사일을 한꺼번에 발사하기 위한 발사 시간 간격과 이동 시간 간격, 이전 발사 시각이 있다. 개별 미사일은 생명, 좌표, 이전 이동 시각, 이동 방향 이 있다. 이를 구분하여 나열하면 아래 [표 14-5]와 같다.

 

 

전체 정보

개별 정보

① 이동 시간 간격

② 발사 시간 간격

③ 이전 발사 시각

 

① 생명

② 이동 좌표

③ 이동 방향

④ 이전 이동 시각

 

[표 14-5] 보스 미사일 속성

 

보스 미사일이 화면에 최대로 출력될 수 있는 개수를 30개로 한정하고 위의 속성을 구조체로 정의하면 다음과 같다.

 

 

 typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT } DIRECT;

 

typedef struct _BOSS_MISSILE

{

       int nLife;

       DIRECT  nDirect;

       int nX, nY;

       clock_t OldMoveTime;

} BOSS_MISSILE;

 

typedef struct _BOSS_MISSILE_INFO

{

       clock_t MoveTime;

       clock_t FireTime;

       clock_t OldFireTime;

       BOSS_MISSILE sMissile[30];

} BOSS_MISSILE_INFO;

 

BOSS_MISSILE_INFO g_sBossMissile;

 

[소스 14-14] 보스 미사일 속성 정의

 

위에 정의된 구조체를 보면 BOSS_MISSILE이 BOSS_MISSILE_INFO 안에 있는 것을 알 수 있다. 물론 이 둘을 따로 정의하고 선언해도 상관없지만 위와 같이 정의한 것은 둘의 관계가 종속적이라는 것을 나타내는 것이다.

그러므로 미사일의 개별 속성은 BOSS_MISSILE_INFO 구조체를 통해서만 접근할 수 있으며, 전체 속성에 따라 전체 개별 속성은 영향을 받는다. 최대 출력할 수 있는 미사일의 개수가 30개이므로 sMissile[30]으로 선언된다.

 

■ 출력

 

이제 [실습 예제 14-4]의 [소스 14-13]에 다음과 같은 사항을 추가해 보자.

 

첫째, 방향에 의해 이동하는 부분을 추가해 보자.

둘째, 3개의 미사일이 일정한 시간 간격으로 한꺼번에 발사되는 부분을 추가해 보자.

셋째, 미사일이 경계 영역과 충돌하면 소멸되게 추가해 보자.

 

보스는 경계 영역과 충돌 체크를 할 필요가 없는 것은 보스는 패턴에 의해 이동하므로 절대 경계 영역과 충돌하지 않는다. 그 이유는 이미 충돌 되지 않게 이동 경로를 패턴에 설정해 놓았기 때문이다.

 

먼저, 미사일의 방향은 기본적으로 [그림 14-27]과 같이 세 가지이며 이 방향은 패턴의 8방향 중에서 3방향과 같다.

 

[그림 14-27] 보스 미사일의 이동 방향

 

미사일 설정과 이동을 하기 위해 [실습 예제 14-4]의 [소스 14-13] Update() 함수 대신  아래 [소스 14-15]의 함수로 대치하면 [그림 14-28]과 같은 출력을 얻게 되며 아래 소스 3행부터 76행까지는 [소스 14-13]의 78행부터 147행까지와 같다.

 

[그림 14-27] 보스 미사일 출력 화면

 

 

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

void Update()

{

     int nSignX, nSignY, nFireMissileCount, i;

     clock_t CurTime = clock();

 

     if( g_Boss.nLife )

     {

        if( CurTime - g_Boss.OldTime > g_Boss.MoveTime )

        {

              g_Boss.OldTime = CurTime;

              g_Boss.nPatStep++;

 

              if( g_Boss.nPatStep ==

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].nStep )

              {

                g_Boss.nPatIndex++;

 

                if( g_Boss.nPatIndex == g_PatInfo[ g_Boss.nPatType ].nCount )

                {

                    g_Boss.nPatType++;

                    g_Boss.nPatType = g_Boss.nPatType % g_nPatCount; // 패턴 반복

 

                    g_Boss.nPatIndex = 0;

                    g_Boss.nPatStep = -1;

                    g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

                    g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

                  g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

                  g_Boss.OldTime = clock();

                 }else{

                    g_Boss.MoveTime =

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].MoveTime;

                    g_Boss.nPatStep = 0;

                 }

               }

 

           switch( g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDirect )

               {

                case UP:

                       nSignX = 0;

                       nSignY = -1;

                       break;

                case UP_RIGHT:

                       nSignX = 1;

                       nSignY = -1;

                       break;

                case RIGHT:

                       nSignX = 1;

                       nSignY = 0;

                       break;

                case DOWN_RIGHT:

                       nSignX = 1;

                       nSignY = 1;

                       break;

                case DOWN:

                       nSignX = 0;

                       nSignY = 1;

                       break;

                case DOWN_LEFT:

                       nSignX = -1;

                       nSignY = 1;

                       break;

                case LEFT:

                       nSignX = -1;

                       nSignY = 0;

                       break;

                case UP_LEFT:

                       nSignX = -1;

                       nSignY = -1;

                       break;

                }

 

                g_Boss.nX =

    g_Boss.nX + nSignX*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

                g_Boss.nY =

    g_Boss.nY + nSignY*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

        }

 

        if( CurTime - g_sBossMissile.OldFireTime > g_sBossMissile.FireTime )

        {

            g_sBossMissile.OldFireTime = CurTime;

            nFireMissileCount = 0;

            for( i = 0 ; i < 30 ; i++ )    // 30개의 미사일을 검색하는 부분

            {

                 if( g_sBossMissile.sMissile[i].nLife == 0 )

                 {

                   g_sBossMissile.sMissile[i].nX = g_Boss.nX + 6;

                   g_sBossMissile.sMissile[i].nY = g_Boss.nY + 2;

                   g_sBossMissile.sMissile[i].nLife = 1;

                     g_sBossMissile.sMissile[i].nDIRECT =

                               DOWN_LEFT - nFireMissileCount; // 방향설정: 5, 4, 3

                   g_sBossMissile.sMissile[i].OldMoveTime = CurTime;

                   nFireMissileCount++;

 

           if( nFireMissileCount > 2 ) // 3발 이상 발사 되지 못하게 하는 부분

                       break;

                }

            }

        }

     }

 

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

     {

        if( g_sBossMissile.sMissile[i].nLife )

        {

           if( CurTime - g_sBossMissile.sMissile[i].OldMoveTime >

                                                g_sBossMissile.MoveTime )

            {

                g_sBossMissile.sMissile[i].OldMoveTime = CurTime;

                switch( g_sBossMissile.sMissile[i].nDIRECT )

                {

                 case DOWN_RIGHT :

                                  nSignX = 1;

                                  nSignY = 1;

                                  break;

                case DOWN :

                                  nSignX = 0;

                                  nSignY = 1;

                                  break;

                case DOWN_LEFT :

                                  nSignX = -1;

                                  nSignY = 1;

                                  break;

                }

              g_sBossMissile.sMissile[i].nX = g_sBossMissile.sMissile[i].nX + nSignX;

              g_sBossMissile.sMissile[i].nY = g_sBossMissile.sMissile[i].nY + nSignY;

             }

                

             // Note: 경계 영역 충돌 체크

         if( g_sBossMissile.sMissile[i].nX < 2 ||

                   g_sBossMissile.sMissile[i].nX + 1 > 78 ||

                   g_sBossMissile.sMissile[i].nY > 23 )

         {

             g_sBossMissile.sMissile[i].nLife = 0;

          }

        }

     }

}

 

[소스 14-14] 패턴이 적용된 보스의 미사일 이동

 

78행부터 98행까지는 일정 시간 간격으로 미사일이 발사되게 하는 부분이다. 미사일 발사를 설정할 때에는 앞서 살펴본 것과 같이 미사일 버퍼에 설정하며 이와 같은 부분은 82행부터 97행까지 나열되어 있다.

 

미사일 이동에 관한 내용은 101행부터 127행까지가 된다.

 

 


[STEP 05]

 

[그림 14-29] 5단계 제작 로드맵

 

■ 충돌 체크

 

위의 [그림 14-29]를 보면 충돌 체크는 크게 적에 관한 충돌과 보스에 관한 충돌로 나눌 수 있다. 그래서 RUNNING 상태에서는 적에 관한 충돌 체크를 하고 BOSS_WAR 상태에서는 보스에 관한 충돌 체크를 한다.

 

- RUNNING 상태에서의 충돌 체크

 

RUNNING 상태에서 충돌 대상은 주인공, 주인공 미사일, 적 캐릭터, 적 미사일이다. 충돌 관계를 표로 나타내면 아래 [표 14-6]과 같다.

 

 

충돌 주체

충돌 대상

적 캐릭터

주인공 캐릭터, 주인공 미사일

적 미사일

주인공 캐릭터, 주인공 미사일

주인공 캐릭터

적 캐릭터, 적 미사일

주인공 미사일

적 캐릭터, 적 미사일

 

[표 14-6] RUNNING 상태에서 충돌

 

위의 충돌 체크 외에 경계 영역에 대한 충돌 체크가 있는데 이 체크는 각 충돌 주체를 이동시킨 후에 바로 체크하는 것이 프로그램을 단순화시키는데 도움이 된다.

아래의 [표 14-7]을 보면 캐릭터의 크기가 일정하지 않으므로 크기에 따라 충돌 영역은 달라진다.

 

 

주인공

적 캐릭터

적 미사일

주인공 미사일

<-^->

♨  ◆  ☎  ▣

 ^

[가로 길이 = 5]

[가로 길이 = 2]

[가로 길이 = 2]

[가로 길이 = 1]

 

[표 14-7] 캐릭터 크기

 

충돌 영역에 따른 충돌 상황은 아래 [그림 14-30]부터 [그림 14-32]까지이므로 [식 14-1]을 이용하면 충돌을 판단할 수 있다.

 

[그림 14-30] 충돌 예1

 

[그림 14-31] 충돌 예2

 

[그림 14-32] 비충돌

 

 

x1 <= x4 && x2 >= x3 && y1 <= y4 && y2 >= y3

 

[식 14-1] 충돌 판정

 

캐릭터의 크기마다 달라지는 충돌 영역은 아래 [그림 14-33]과 같은 툴을 통해 설정하는 경우가 대부분이다.

 

[그림 14-33] 툴에서 캐릭터의 충돌 영역을 설정

위의 [식 14-1]을 함수로 만들면 아래 [소스 14-16]과 같다.

 

 

int Collision( int nX1, int nY1, int nX2, int nY2, int nX3, int nY3, int nX4, int nY4 )

{

        if( nX1 <= nX4 && nX2 >= nX3 && nY1 <= nY4 && nY2 >= nY3 )

           return 1;  // 충돌

        else

           return 0;  // 비충돌

}

 

[소스 14-16] 충돌 체크 함수

 

위의 함수는 2D 게임 제작에서 주로 사용되는 충돌체크 함수이며 충돌 체크를 하는 시점은  각 개체를 이동시킨 후가 된다. 즉, 한 번 이동 좌표가 변경되면 충돌 대상과 한 번씩 충돌 체크를 한다.

 

 

이동 → 충돌 체크 → 이동 → 충돌 체크

 

[표 14-7] 충돌 체크 과정

 

- BOSS_WAR 상태에서의 충돌 체크

  

BOSS_WAR 상태에서는 보스, 보스 미사일, 주인공, 주인공 미사일만 충돌 체크한다. 이들의 충돌 관계를 간단히 표로 나타내면 다음과 같다.

 

 

충돌 주체

충돌 대상

보스 캐릭터

주인공 미사일

보스 미사일

주인공 캐릭터, 주인공 미사일

주인공 캐릭터

보스 미사일

주인공 미사일

보스 캐릭터, 보스 미사일

 

[표 14-8] 충돌 관계

 

위의 표에서 보스 캐릭터와 주인공 간의 충돌이 빠져 있는 이유는 보스 패턴을 만들 때 이미 주인공 캐릭터와 충돌하지 않게 만들기 때문이다.

보스는 [그림 14-34]와 같이 앞의 캐릭터와 약간 다른 크기를 가진다.

 

[그림 14-34] 가로 길이 16이고 세로 길이가 3인 보스 캐릭터

 

보스 충돌이 앞의 충돌 체크와 약간의 차이를 보이는 부분은 ‘경계 영역에 대한 처리 부분’이다. 보스는 패턴에 의해 계속적으로 이동하므로 경계 영역을 넘을 수 없도록 되어 있으나 보스 미사일은 경계 영역을 넘을 수가 있기 때문에 경계 영역의 충돌 체크는 보스 미사일에만 적용하면 된다. 주인공도 이동 영역이 경계 영역 안에서만 움직이도록 되어 있으므로 주인공 미사일만 y 좌표에 대한 경계 영역만 충돌 체크하면 된다.

 

 


[STEP 06]

 

[그림 14-35] 6단계 제작 로드맵

 

■ 게임 스테이지 정보

 

Sogo 게임은 보스 캐릭터를 소멸시켜야 스테이지가 끝난다. 각 스테이지는 적 캐릭터의 개수, 이동 속도, 패턴, 보스의 HP, 보스의 무기를 달리하여 스테이지별 차별성을 둘 수 있지만, 이 장에서는 적 캐릭터의 수와 보스 생명값만을 스테이지에 적용한다.

 

 

typedef struct _STAGE_INFO

{

        int nEnemyCount;

        int nBossLife;

} STAGE_INFO;

 

[소스 14-17] 스테이지 정보 정의

 

■ 게임 진행 정보

 

Sogo 게임에서 게임 진행 상태는 PLAYER_TALK와 BOSS_TALK, 그리고 BOSS_WAR라는 상태가 추가되었을 뿐 앞서 살펴본 내용과 동일하다.

PALYER_TALK와 BOOS_TALK 경우는 주인공과 보스 간의 대화가 화면에 일시적으로 출력되도록 하는 상태값이며 BOSS_WAR는 주인공과 보스가 대전하는 상태를 말한다.

 

 

상태

설명

초기 상태 (INIT)

게임 변수의 초기화 및 사운드 초기화

준비 상태 (READY)

스테이지 정보의 출력

게임 진행 상태 (RUNNING)

게임 진행 및 중지 상태로 전이

주인공의 대화 출력 (PLAYER_TALK)

주인공의 대화 내용 출력

보스의 대화 출력 (BOSS_TALK)

보스의 대화 출력, BOSS_TALK에서 다음 상태인 BOSS_WAR로 넘어가면 보스가 출현하게 된다.

보스와의 대결 (BOSS_WAR)

주인공, 미사일과 보스, 미사일과 충돌 체크 및 게임 진행

중지 상태(STOP)

미션 성공과 실패를 판단

미션 성공 상태 (SUCCESS)

미션 성공

미션 실패 상태 (FAILED)

미션 실패

결과 출력 및 종료 상태 (RESULT)

게임 종료 및 종료

 

[표 14-8] 게임 진행 상태

 

 

typedef enum _GAME_STATE { INIT, READY, RUNNING, PLAYER_TALK, BOSS_TALK, BOSS_WAR,

                                STOP, SUCCESS, FAILED, RESULT } GAME_STATE;

 

[소스 14-18] enum형

 

여기까지 슈팅 게임의 가장 기본적인 형태의 게임을 살펴보았으며 패턴 데이터를 적용하여 적 캐릭터와 보스의 이동을 제어해 보았다. 이제 이를 기반으로 전체 프로그램을 제작해 보자.

 

 

 

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

 

[출처] https://nowcampus.tistory.com/entry/14%EC%9E%A5-Sogo-%EA%B2%8C%EC%9E%84?category=655340

 

 

 

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

 

 

(CGP) 14장 Sogo 게임

Sogo 게임 가장 기본적인 슈팅게임에 해당이 됩니다.

이 장에서는 슈팅 게임을 이해하고 각종 캐릭터를 스토리에 따라 출력하며 앞서 제작한 패턴을 적용하여 프로그래밍하는 것이 가장 중요합니다.

 

굿잡  유튜브 동영상 강의 주소

(1) http://youtu.be/gajEzUe4_8U

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

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

(4) http://youtu.be/qRVBh_HvJ-g

(5) http://youtu.be/HXURI5yOh58

(6) http://youtu.be/NyJLnsm2d7U

(7) http://youtu.be/9cM-pqN1wb8

(8) http://youtu.be/HMqprZ2ncuU

 

 

 


 

14.1 기획

 

■ 스토리

 

외계인들이 물과 식량을 구하기 위해 지구를 침공했다. 외계인들을 물리치고 세계의 평화를 지키자.

 

■ 게임방식

 

주인공은 좌우키를 이용하여 이동할 수 있으며, 다수의 적 캐릭터는 다양한 패턴으로 공격을 한다. 적 캐릭터가 모두 출현한 후에 보스 캐릭터와 주인공 간의 대화 내용이 나오고 보스와 대결하게 된다.

 

■ 제한사항

 

기본적으로 방향키로 좌우 이동을 하며 s키는 미사일 발사키이다.

주인공 미사일은 한 화면에 최대 5발 이상 발사되지 못하게 제안하며 적 캐릭터와 보스는 앞장에서 제작한 패턴 데이터를 이용하여 이동한다.

 

■ 기획화면

 

 

기본적 게임 진행 화면 )

( 보스 대화 화면 )

( 주인공 대화 화면 )

( 보스와 대전 화면 )

 

[그림 14-1] 기획 화면

 

 

 


 

14.2 실행 화면

 

[그림 14-2] 초기 화면

 

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

 

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

 

[그림 14-5] 대화2 화면

 

[그림 14-6] 보스와 대결 화면

 

 

[그림 14-7] 미션 실패 화면

 

14.3 게임 제작 로드맵

 

앞장에서 제작한 게임은 주로 적 캐릭터와 주인공에 대한 프로그래밍이었지만 여기에서는 보스 캐릭터와 주인공 간의 대화를 출력하거나 다양한 패턴이 적용된 적 캐릭터를 추가하여 제작하게 된다. 또한 Sogo 게임에서 적 캐릭터와 보스는 이동 패턴이라는 요소를 통해 이동하므로 이 부분을 집중하여 살펴보도록 하자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

[STEP 06]

 

[STEP 07]

 

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

 

 

 


 

14.4 단계별 프로그래밍

 

 

[STEP 01]

  

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

 

■ 주인공

 

- 속성

 

슈팅 게임의 주인공 속성은 앞장에서 제작한 슛 골인 게임과 유사하지만 슈팅 게임이라는 특성 때문에 몇 가지 추가되는 속성이 있다.

먼저 주인공 속성을 나열하면 아래 [표 14-1]과 같으며 이에 대한 내용을 자세히 살펴보면 다음과 같다.

 

 

① 생명

② 이동 좌표

③ 이동 시간 간격

④ 이전 이동 시각

⑤ 미사일 발사 시간 간격

⑥ 이전 미사일 발사 시각

 

[표 14-1] 주인공 속성

 

첫째, 생명이다.

생명은 주인공이 게임을 계속할 것인지 아니면 종료할 것인지를 결정하게 하는 중요한 속성이다. 기본적으로 3이 설정되며 적 캐릭터로부터 3번의 공격을 받아 생명값이 0이 되면 게임은 종료하게 된다.

 

둘째, 이동 좌표이다.

이 게임에서 이동 좌표는 캐릭터의 맨 좌측 좌표를 말하며 출력할 때도 이 좌표를 기준으로 출력한다.

 

셋째, 이동 시간 간격이다.

이 속성은 일정한 간격으로 이동하기 위한 속성이다.

만일 이 부분이 없다면 방향키가 눌려지는 속도만큼 주인공 캐릭터는 이동하게 된다.

 

넷째, 이전 이동 시각이다.

시간 차이를 계산하기 위해서는 현재 시각과 이전 이동 시각이 있어야 한다.

 

다섯째, 미사일 발사 시간 간격이다.

미사일이 발사되는 간격을 일정하게 하기 위한 속성이다.

 

여섯째, 이전 미사일 발사 시각이다.

미사일 발사도 이동과 동일하게 시간 차이를 이용하므로 이 속성이 필요하다.

 

여기까지 설명한 속성을 정의하면 아래 [소스 14-1]과 같다.

 

 

typedef struct _PLAYER

{

       int  nLife;             // 생명

       int nX, nY;            // 이동 좌표

       clock_t MoveTime;     // 이동 시간 간격

       clock_t OldTime;   // 이전 이동 시각

       clock_t FireTime;      // 미사일 발사 시간 간격

       clock_t OldFireTime;   // 이전 미사일 발사 시각

} PLAYER;

 

PLAYER g_Player;

 

[소스 14-1] 주인공 속성 정의

 

■ 미사일

 

- 속성

 

미사일은 주인공과 적 캐릭터가 같이 사용하는 속성으로 아래 [표 14-2]와 같다.

 

 

① 생명

② 이동 좌표

③ 이전 이동 시각

 

[표 14-2] 미사일 속성

 

위의 속성은 주인공 속성과 유사하지만 자세히 살펴보면 이동 시간 간격이라는 속성이 제외된 것을 알 수 있다.

이 속성이 제외된 이유는 이 게임에서 사용되는 모든 미사일은 같은 이동 시간 간격을 사용하므로 미사일마다 이 속성을 정의할 필요가 없기 때문이다.

그래서 이 속성은 전역 변수 또는 #define과 같은 전처리어를 사용하여 공용값으로 처리함으로써 불필요한 속성으로 인한 메모리 낭비를 줄이게 된다.

 

 

typedef struct _MISSILE

{

      int      nLife;    

      int      nX, nY;

      clock_t  OldTime;

} MISSILE;

 

[소스 14-2] 미사일 속성 정의

 

주인공 미사일은 한 화면에 나타낼 수 있는 최대 미사일 개수가 5개로 기획에서 제한되어 있으므로 아래 [소스 14-2]와 같이 선언된다.

 

 

#define HERO_MISSILE  5

 

MISSILE g_PlayerMissile[ HERO_MISSILE ];

 

[소스 14-3] 주인공 캐릭터의 미사일 선언

 

위와 같이 미사일을 배열로 선언함으로써 최대 사용할 수 있는 미사일 개수는 5개가 되며 미사일 발사키가 입력되면 위의 배열 중에서 사용하지 않는 배열을 찾아 배열의 nLife를 1로 설정한다.

중요한 것은 이와 같은 배열 선언만으로도 그 사용 범위를 제한할 수 있다는 것이다.

 

[실습 예제 14-1]

 

좌우 방향키에 의해 이동하는 주인공을 게임 프레임워크에 적용하여 아래 [그림 14-10]과 같이 출력되게 프로그래밍해 보자. 그리고 s 키가 입력되면 주인공 미사일이 발사되도록 한다. 단 미사일은 한 화면에 최대 5발까지 출력할 수 있다.

[그림 14-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

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define HERO_MISSILE  5

#define MISSILE_MOVE_TIME 100

 

typedef struct _PLAYER

{

       int  nLife;

       int nX, nY;

       clock_t MoveTime;

       clock_t OldTime;   

       clock_t FireTime;

       clock_t OldFireTime;

} PLAYER;

 

typedef struct _MISSILE

{

      int      nLife;     

      int      nX, nY;

      clock_t  OldTime;

} MISSILE;

 

PLAYER g_Player;

MISSILE g_PlayerMissile[ HERO_MISSILE ];

 

void Init()

{

        g_Player.nLife = 3;

        g_Player.nX = 30;

        g_Player.nY = 20;

        g_Player.OldTime = clock();

        g_Player.MoveTime = 100;

        g_Player.FireTime = 50;

        g_Player.OldFireTime = clock();

}

 

void Update()

{

     int i;

     clock_t CurTime = clock();

     // 주인공 미사일 관련

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

     {

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

        {   // 주인공 미사일 이동

            if( CurTime - g_PlayerMissile[i].OldTime > MISSILE_MOVE_TIME )

            {

                g_PlayerMissile[i].OldTime = CurTime;

                if( g_PlayerMissile[i].nY - 1 > 1 )

                  g_PlayerMissile[i].nY--;

                else

                  g_PlayerMissile[i].nLife = 0;

             }

         }

     }

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

     {

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

            ScreenPrint( g_PlayerMissile[i].nX, g_PlayerMissile[i].nY, "$" );

     }

 

     ScreenPrint( g_Player.nX, g_Player.nY, "#-@-#" );

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, i;

    clock_t CurTime;

 

    ScreenInit();

    Init();

        

    while( 1 )

    {

         CurTime = clock();

 

         if( _kbhit() )

         {

           nKey = _getch();

 

           switch( nKey )

           {

            case 75 : // 왼쪽

                      if( CurTime - g_Player.OldTime > g_Player.MoveTime )

                    {

                        g_Player.OldTime = CurTime;

                            if( g_Player.nX - 1 > 1 )

                        {

                          g_Player.nX--;

                          break;

                         }

                     }

                     break;

 

              case 77 : // 오른쪽

                      if( CurTime - g_Player.OldTime > g_Player.MoveTime )

                      {

                        g_Player.OldTime = CurTime;

                        if( g_Player.nX + 1 < 59 )

                        {

                            g_Player.nX++;

                            break;

                        }

                      }                                 

                      break;

 

              case 's' : // 미사일 발사

                      if( CurTime - g_Player.OldFireTime >  g_Player.FireTime )

                      {

                        g_Player.OldFireTime = CurTime;

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

                        {

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

                             {

                                 g_PlayerMissile[i].nLife = 1;

                                g_PlayerMissile[i].nX = g_Player.nX + 2;

                                g_PlayerMissile[i].nY = g_Player.nY - 1;

                                g_PlayerMissile[i].OldTime = clock();

                                break;

                               }

                            }

                        }

                        break;

                }

        }

 

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 14-4] 주인공 캐릭터와 미사일 출력

 

 

 


 

[STEP 02]

  

[그림 14-11] 2단계 제작 로드맵

 

■ 적 캐릭터

 

- 속성

 

기본적으로 적 캐릭터는 주인공은 유사하지만 몇 가지 차이점이 있다.

 

첫째, 출현 시점에 관한 사항이다.

주인공은 게임 실행과 동시에 출현되지만 적 캐릭터는 지정된 시점에 출현되야 한다. 그러므로 주인공과는 달리 출현 시점이라는 속성이 있어야 하며 이 출현 시점은 오름차순으로 정렬된 시간을 적용한다.

[그림 14-12] 출현 시점

 

둘째, 미사일 발사에 관한 사항이다.

주인공 미사일은 플레이어에 의해 발사가 결정되지만 적 캐릭터는 일정한 시간 간격으로 발사된다. 즉 주인공 미사일은 키 입력에 따라 발사되지만 적 캐릭터는 스스로 발사되어야 하는 것을 의미한다. 그래서 적 캐릭터의 미사일 발사에 관한 방법은 [그림 14-13]과 같이 미사일을 관리하는 버퍼를 두고 미사일 발사 시간이 되면 이 버퍼에 미사일 발사를 설정하도록 하여 발사되게 한다. 이것은 적 캐릭터가 일일이 미사일에 관여하는 것이 아니라 미사일을 담당하는 버퍼에게 미사일 발사와 이동을 맡긴다는 의미가 된다.

[그림 14-13] 미사일 버퍼

 

적 캐릭터의 일반적인 속성과 위에서 설명한 속성을 나열하면 아래 [표 14-3]과 같다.

 

 

① 생명 정보 ( 0: 소멸 상태, 1: 생존 상태 )

② 좌표 정보

③ 캐릭터 타입 정보

④ 출현 시각

⑤ 이동 시간 간격

⑥ 이전 이동 시각

⑦ 미사일 발사 시간 간격

⑧ 이전 미사일 발사 시각

 

[표 14-3] 적 캐릭터 속성

 

위 속성 중에 세 번째 속성인 캐릭터 타입 정보는 적 캐릭터를 출력하기 위한 특수 문자를 지정하기 위한 부분으로 11장 Snake 맵 툴에서 인덱스로 먹이 또는 적, 블록 등을 출력한 것과 같은 것이다. 또한 일곱 번째 속성인 미사일 발사 시간 간격과 그 다음 이전 미사일 발사 시각은 일정한 시간 간격으로 미사일 발사를 버퍼에 설정하기 위한 속성이다.

 

위의 사항을 구조체로 정의하면 아래 [소스 14-5]와 같다.

 

 

typedef struct _ENEMY

{

       int  nLife;

       int  nX, nY;

       int  nType;

       clock_t AppearTime;      

       clock_t MoveTime;

       clock_t OldMoveTime;     

       clock_t MissileFireTime; 

       clock_t MissileOldTime;    

} ENEMY;

 

[소스 14-5] 적 캐릭터 속성 정의

 

■ 적 캐릭터 미사일과 미사일 버퍼

 

적 캐릭터 미사일과 주인공 미사일은 상당히 유사하지만 처리 부분에서 약간의 차이점이 있다. 주인공 미사일은 5개로 한정되어 있어서 배열로 선언이 가능했지만 적 캐릭터 미사일의 개수는 알 수 없으므로 최대한 처리할 수 있는 크기의 미사일 버퍼가 있어야 한다.

그리고 적 캐릭터 속성의 발사 시점이 되면 미사일 버퍼에서 사용하지 않는 미사일을 찾아 발사 정보를 설정한다.

미사일 출력은 적 캐릭터 정보를 통해 출력하는 것이 아니라 미사일 버퍼 안에서 살아 있는 미사일을 찾아 출력한다.

이 부분은 주인공 캐릭터가 최대 5개의 미사일 중에서 사용하지 않는 미사일을 찾아 발사상태로 만드는 것과 같은 원리이다.

 

미사일에 관한 구조체와 미사일 버퍼를 살펴보면 아래 [소스 14-6]과 같다.

 

 

typedef struct _ENEMY_MISSILE

{

       int     nLife;

       int     nX, nY;

       clock_t MoveTime;

       clock_t OldMoveTime;     

} ENEMY_MISSILE;

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

 

[소스 14-6] 미사일 속성

 

[실습 예제 14-2]

 

임의의 출현 시간 간격을 가지고 이동하는 적 캐릭터 5개와 최대 100개까지의 미사일을 화면에 출력할 수 있는 미사일 버퍼를 만들고 적 캐릭터와 미사일이 일정한 시간 간격으로 출력되도록 프로그래밍해 보자. 단, 적 캐릭터 타입은 ♨, ◆, ☎, ▣ 이다.

 

[그림 14-14] 적 캐릭터와 미사일

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

#define MISSILE_MOVE_TIME 100

 

typedef struct _ENEMY

{

        int  nLife;

        int  nX, nY;

        int  nType;

        clock_t AppearTime;      

        clock_t MoveTime;

        clock_t OldMoveTime;     

        clock_t MissileFireTime; 

        clock_t MissileOldTime;    

} ENEMY;

 

typedef struct _ENEMY_MISSILE

{

        int     nLife;

        int     nX, nY;

        clock_t MoveTime;

        clock_t OldMoveTime;     

} ENEMY_MISSILE;

 

ENEMY g_Enemy[5];

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

char g_EnemyType[4][3] = { "♨", "◆", "☎", "▣" };

clock_t g_StartTime;

int g_nEnemyIndex; // 적 캐릭터 출현 인덱스

 

void Init()

{

     int i = 0;

 

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

     {

        g_Enemy[i].nLife = 0;

        g_Enemy[i].nX = rand() % 55 + 5;

        g_Enemy[i].nY = 1;

        g_Enemy[i].nType = i % 4;

        g_Enemy[i].AppearTime = 1000 * ( i + 1 ); // 출현 시간을 오름차순으로 설정

        g_Enemy[i].MoveTime = 400;

        g_Enemy[i].OldMoveTime = clock();

        g_Enemy[i].MissileFireTime = rand() % 500 + 1000;

        g_Enemy[i].MissileOldTime = clock();               

     }

     g_nEnemyIndex = 0;

}

 

void Update()

{

     int i, j;

     clock_t CurTime = clock();

 

     // Note: 출현 ( 출현 시간의 데이터는 오름차순으로 정렬되어 있다 )

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

     {

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

        {

            if( CurTime - g_StartTime >= g_Enemy[i].AppearTime )

            {

                g_Enemy[i].nLife = 1;

                g_nEnemyIndex++;

            }else{                                                      

                break;

            }

        }

     }

 

     // Note:  데이터 업데이트

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

     {

        if( g_Enemy[i].nLife )

        {

            // Note: 적 캐릭터 업데이트

            if( CurTime - g_Enemy[i].OldMoveTime > g_Enemy[i].MoveTime )

            {

                g_Enemy[i].OldMoveTime = CurTime;

                g_Enemy[i].nY++;

                 if( g_Enemy[i].nY > 20 )

                 {

                   g_Enemy[i].nLife = 0;

                   continue;

                }

             }

 

             // Note: 적 미사일 발사 하는 부분

             if(CurTime - g_Enemy[i].MissileOldTime > g_Enemy[i].MissileFireTime)

             {

                g_Enemy[i].MissileOldTime = CurTime;

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

                {

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

                   {

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

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

                       g_EnemyMissile[j].nLife = 1;

                       g_EnemyMissile[j].MoveTime = rand() % 100 + 80;

                       g_EnemyMissile[j].OldMoveTime = CurTime;

                       break;

                    }   

                 }

              }

        }

     }

 

     // Note: 미사일 업데이트

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

     {

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

        {

           if( CurTime - g_EnemyMissile[i].OldMoveTime >

                                         g_EnemyMissile[i].MoveTime )

           {

               g_EnemyMissile[i].OldMoveTime = CurTime;

               if( g_EnemyMissile[i].nY + 1 > 20 )

                 g_EnemyMissile[i].nLife = 0;

               else

                 g_EnemyMissile[i].nY += 1;

           }

        }

     }

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

      { 

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

        {

            ScreenPrint( g_EnemyMissile[i].nX, g_EnemyMissile[i].nY, "↓" );

        }

      }

 

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

      {

        if( g_Enemy[i].nLife )

        {

           ScreenPrint( g_Enemy[i].nX, g_Enemy[i].nY, g_EnemyType[g_Enemy[i].nType] ); 

        }

      }

 

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    g_StartTime = clock();

 

    while( 1 )

    {

        Update();

        Render();       

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 14-7] 적 캐릭터와 미사일

 

33행의 g_nEnemyIndex 변수는 최종 출현된 적 캐릭터의 인덱스를 나타낸다.

이 변수의 용도는 다음과 같다.

 

첫째, 60행과 같이 이미 출현한 적 캐릭터는 출현 검색에서 제외시키기 위해 사용된다.

둘째, 75행과 같이 대기 상태의 적 캐릭터는 업데이트 대상에서 제외시키기 위해 사용된다.

셋째, 142행과 같이 대기 상태의 적 캐릭터를 출력에서 제외시키기 위해 사용된다.

 

45행은 출현 시간을 오름차순으로 설정하고 있다.

이 설정에 따라 64행 이상에서 게임이 시작된 시간과 현재의 시간 차이로 출현을 결정하고 있다. 출현을 시킨다는 것은 적 캐릭터의 생명을 1로 설정하는 것을 의미한다.

 

적 캐릭터 미사일을 설정하는 부분은 92행에서부터 107행까지가 된다.

적 캐릭터의 미사일 발사가 미사일 발사 시간 간격에 의해 결정되면 현재 미사일 버퍼에서 소멸 상태의 미사일을 찾아 활성화시켜 발사하게 되는데 이 부분은 95행부터 106행까지가 된다.

 

 

 


 

[STEP 03]

  

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

 

■ 패턴 설계

 

패턴 구조는 13장에서 툴을 제작할 때 살펴본 아래 [소스 14-8] 코드를 동일하게 사용한다.

 

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표      

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO g_PatInfo;

 

[소스 14-8] 패턴 속성

 

■ 패턴을 적 캐릭터에 적용

 

13장 [실습 예제 13-1]을 통해 하나의 패턴을 한 캐릭터에 적용하여 출력하는 소스를 제작해 보았다. 이것을 기반으로 이제 여러 개의 패턴 중에서 하나의 패턴을 여러 캐릭터에 적용하여 이동되게 프로그래밍해 보자.

 

[그림 14-16] 적 캐릭터의 패턴 선택

 

먼저 패턴 구조는 확정되어 있으므로 개수가 다른 다수의 패턴은 포인터 또는 배열로 다룰 수 있다. 선언 형식에서 포인터는 메모리 생성과 해제가 자유롭지만 배열은 그렇지 않다.

만약 파일로부터 읽는 패턴 개수가 항상 일정하다면 배열 형식을 사용할 수 있겠지만 그렇지않으면 포인터를 사용하는 것이 프로그래밍의 자유도를 높일 수 있다.

 

여러 개의 패턴 파일을 읽어 패턴을 생성하면 생성 과정에서 이미 아이디가 암시적으로 할당된다. 그 이유는 순차적으로 읽혀진 패턴 데이터는 인덱스를 통해 접근하게 되는데 이때  이 인덱스는 그 패턴의 고유 아이디가 된다.

그러면 이와 같은 내용을 간단히 코드로 작성해 보면 아래 [소스 14-9]와 같다.

 

[그림 14-17] 여러 개의 패턴 읽기 및 메모리가 생성된 결과

 

 

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

#include <stdio.h>

#include <time.h>

#include <malloc.h>

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표         

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO *g_PatInfo;

 

int main(void)

{

    int nFileCount, i, j;

    FILE *fp;

    char *strFileName[3] = { "pat1.txt", "pat2.txt", "pat3.txt" };

        

    nFileCount = sizeof( strFileName ) / sizeof( char * );

    g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * nFileCount ); 

 

    // 패턴 파일 읽기

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

    {

        fp = fopen( strFileName[i], "r" );

        

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

            fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                                  &g_PatInfo[i].pPat[j].nStep,

                                  &g_PatInfo[i].pPat[j].MoveTime,

                                  &g_PatInfo[i].pPat[j].nDist );

            fclose( fp );    

    }

 

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

    {

        printf( "파일 : %s \n", strFileName[i] );

        printf( "%d\n", g_PatInfo[i].nCount );

        printf( "%d %d\n", g_PatInfo[i].nX0, g_PatInfo[i].nY0 );

        printf( "[ 이동 경로 ] \n" );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

        {

             printf( "%d %d %d %d\n", g_PatInfo[i].pPat[j].nDirect,

                      g_PatInfo[i].pPat[j].nStep,g_PatInfo[i].pPat[j].MoveTime,

                      g_PatInfo[i].pPat[j].nDist );

        }

 

        printf( "\n" );

    }

 

    // 메모리 해제

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

         free( g_PatInfo[i].pPat );

 

    free( g_PatInfo );

 

    return 0;

}

 

[소스 14-9] 여러 개의 패턴 읽기 및 메모리 생성

 

위의 소스에서 유심히 살펴봐야 하는 부분은 메모리 해제 부분이다.

68행과 69행을 보면 다수의 이동 경로를 저장하기 위한 메모리를 패턴 개수만큼 해제한 후에 최종적으로 패턴 자체를 메모리에서 해제하고 있다.

즉 malloc()로 할당한 메모리 개수만큼 free()도 동일하게 있어야 한다.

 

이제 위의 패턴을 사용하기 위해 추가되는 적 캐릭터의 속성을 살펴보면 다음과 같다.

 

첫째, 적 캐릭터는 여러 개의 패턴 중에서 하나를 선택해야 하므로 패턴 인덱스를 저장할 수 있어야 한다. 

 

둘째, 하나의 패턴 안에 순차적으로 있는 여러 개의 이동 경로 중에서 현재 이동 경로의 인덱스를 저장할 수 있어야 한다.

이 부분은 13장 패턴 뷰어 프로그램의 [실습 예제 13-1]에서 이동 경로 구간을 구분하기 한 인덱스 g_nIndex과 같은 것이다.

 

셋째, 현재 이동 스텝을 저장할 수 있어야 한다.

적 캐릭터는 이동 스텝만큼 이동하여 목표 좌표까지 도달하게 되므로 현재 스텝을 개별적으로 저장해야 한다.

이동 경로마다 있는 스텝은 최종 스텝수이므로 이 값과 현재 이동 스텝을 비교하여 다음 이동 경로로 진행할 것인지를 결정한다.

 

여기까지 살펴본 적 캐릭터 추가 속성을 나열하면 아래 [표 14-3]과 같다.

 

 

① 패턴 형태

② 이동 경로의 인덱스

③ 현재 스텝

 

[표 14-3] 적 캐릭터의 추가 속성 

 

이제까지 살펴본 추가 속성을 적 캐릭터 속성에 포함시켜 정의하면 아래 [소스 14-10]과 같다.

 

 

typedef struct _ENEMY

{

       int  nLife;

       int  nX, nY;

       int  nType;

       clock_t AppearTime;      

       clock_t MoveTime;

       clock_t OldMoveTime;     

       clock_t MissileFireTime; 

       clock_t MissileOldTime;

 

         // 추가된 속성

         int nPatType; // 패턴 형태

         int nPatIndex; // 이동 경로 인덱스

         int nPatStep; // 현재 스텝       

} ENEMY;

 

[소스 14-10] 추가된 적 캐릭터 속성

 

[실습 예제 14-3]

 

[소스 14-9]의 패턴 읽기 소스와 추가된 적 캐릭터 속성을 이용하여 적 캐릭터 3개가 패턴에 따라 이동되도록 프로그래밍해 보자.

 

[그림 14-18] 패턴에 의해 이동하는 적 캐릭터(1)

 

[그림 14-19] 패턴에 의해 이동하는 적 캐릭터(2)

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

 

typedef struct _ENEMY

{

        int  nLife;

        int  nX, nY;

        int  nType;

        clock_t AppearTime;      

        clock_t MoveTime;

        clock_t OldMoveTime;     

        clock_t MissileFireTime; 

        clock_t MissileOldTime;

 

          // 추가된 속성

          int nPatType; // 패턴 형태

          int nPatIndex; // 이동 경로 인덱스

          int nPatStep; // 현재 스텝       

} ENEMY;

 

typedef struct _ENEMY_MISSILE

{

        int     nLife;

        int     nX, nY;

        clock_t MoveTime;

        clock_t OldMoveTime;     

} ENEMY_MISSILE;

 

typedef struct _PAT

{

        DIRECT nDirect;  // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime; // 이동 시간 간격

        int nDist;          // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표

        PAT *pPat;      // 패턴

} PAT_INFO;

 

PAT_INFO *g_PatInfo;

char g_EnemyType[4][3] = { "♨", "◆", "☎", "▣" };

ENEMY g_Enemy[3];

ENEMY_MISSILE   g_EnemyMissile[100];   // 미사일 버퍼

clock_t g_StartTime;

int g_nEnemyIndex; // 적 캐릭터 출현 인덱스

int g_nEnemyCount; // 적 캐릭터 수

char *g_strFileName[3] = { "pat1.txt", "pat2.txt", "pat3.txt" };

 

void Init()

{

     int i, nFileCount, j;

     FILE *fp;

    

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

     g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * nFileCount );

 

     // 패턴 파일 읽기

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

     {

        fp = fopen( g_strFileName[i], "r" );

                

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

 

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

             fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                     &g_PatInfo[i].pPat[j].nStep, &g_PatInfo[i].pPat[j].MoveTime,

                          &g_PatInfo[i].pPat[j].nDist );                           

        fclose( fp );    

      } 

     

     g_nEnemyCount = 3; // 적 캐릭터 수

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

     {

        g_Enemy[i].nLife = 0;

        g_Enemy[i].nType = i % 4;

        g_Enemy[i].AppearTime = 1000 * ( i + 1 ); // 출현 시간을 오름차순으로 설정        

        g_Enemy[i].MissileFireTime = rand() % 500 + 1000;

        g_Enemy[i].MissileOldTime = clock();                       

        g_Enemy[i].nPatType = i;

        g_Enemy[i].nPatIndex = 0;

        g_Enemy[i].nPatStep = -1;

        g_Enemy[i].nX = g_PatInfo[ g_Enemy[i].nPatType ].nX0;

        g_Enemy[i].nY = g_PatInfo[ g_Enemy[i].nPatType ].nY0;

        g_Enemy[i].MoveTime = g_PatInfo[ g_Enemy[i].nPatType ].pPat[0].MoveTime;

        g_Enemy[i].OldMoveTime = clock();

     } 

 

      g_nEnemyIndex = 0; // 출현 캐릭터의 인덱스 범위

      g_StartTime = clock();

}

 

void Update()

{        

     int i, j;

     int nSignX, nSignY;    

     clock_t CurTime = clock();

 

     // Note: 출현 ( 출현 시간의 데이터는 오름차순으로 정렬되어 있다 )

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

     {

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

        {

           if( CurTime - g_StartTime >= g_Enemy[i].AppearTime )

           {

               g_Enemy[i].nLife = 1;

               g_nEnemyIndex++;

            }else{                                                      

               break;

            }

        }

     }  

 

     // Note:  데이터 업데이트

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

     {   

        if( g_Enemy[i].nLife )

        {

           // Note: 적 캐릭터 업데이트

           if( CurTime - g_Enemy[i].OldMoveTime > g_Enemy[i].MoveTime )

           {

              g_Enemy[i].OldMoveTime = CurTime;

              g_Enemy[i].nPatStep++;

 

              // 다음 패턴 인덱스로 진행

              if( g_Enemy[i].nPatStep ==

              g_PatInfo[ g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].nStep )

              {

                g_Enemy[i].nPatIndex++;

              if( g_Enemy[i].nPatIndex == g_PatInfo[ g_Enemy[i].nPatType ].nCount )

                {

                    g_Enemy[i].nLife = 0;

                    continue; //패턴이동이 종료되어 다음 캐릭터로 넘어가기

                }else{

                    g_Enemy[i].MoveTime =

           g_PatInfo[ g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].MoveTime;

            g_Enemy[i].nPatStep = 0;  

            }

     }

              

    switch( g_PatInfo[g_Enemy[i].nPatType ].pPat[ g_Enemy[i].nPatIndex ].nDirect )

            {

             case UP:

                    nSignX = 0;

                    nSignY = -1;

                    break;

             case UP_RIGHT:

                    nSignX = 1;

                    nSignY = -1;

                    break;

             case RIGHT:

                    nSignX = 1;

                    nSignY = 0;

                    break;

             case DOWN_RIGHT:

                    nSignX = 1;

                   nSignY = 1;

                   break;

              case DOWN:

                   nSignX = 0;

                   nSignY = 1;

                   break;

              case DOWN_LEFT:

                   nSignX = -1;

                   nSignY = 1;

                   break;

              case LEFT:

                   nSignX = -1;

                   nSignY = 0;

                   break;

              case UP_LEFT:

                   nSignX = -1;

                   nSignY = -1;

                   break;

             }

     g_Enemy[i].nX = g_Enemy[i].nX +

         nSignX*g_PatInfo[g_Enemy[i].nPatType].pPat[ g_Enemy[i].nPatIndex ].nDist;

        g_Enemy[i].nY = g_Enemy[i].nY +

        nSignY*g_PatInfo[g_Enemy[i].nPatType].pPat[ g_Enemy[i].nPatIndex ].nDist;

 

             // Note: 경계 영역 처리

             if( g_Enemy[i].nX < 1 || g_Enemy[i].nX > 78 || g_Enemy[i].nY > 20 )

             {

                g_Enemy[i].nLife = 0;

                continue;

              }

           }

                                

           // Note: 적 미사일 발사 하는 부분 ( 캐릭터가 살아 있는 때만 가능 )

           if( CurTime - g_Enemy[i].MissileOldTime > g_Enemy[i].MissileFireTime )

           {

               g_Enemy[i].MissileOldTime = CurTime;

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

               {

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

                  {

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

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

                      g_EnemyMissile[j].nLife = 1;

                      g_EnemyMissile[j].MoveTime = rand() % 100 + 80;

                      g_EnemyMissile[j].OldMoveTime = CurTime;

                      break;

                   }    

                }

           }

          }

     }

         

     // Note: 미사일 업데이트

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

     {

        if( g_EnemyMissile[i].nLife )

        {

           if( CurTime - g_EnemyMissile[i].OldMoveTime >

                                 g_EnemyMissile[i].MoveTime )

           {

               g_EnemyMissile[i].OldMoveTime = CurTime;

               if( g_EnemyMissile[i].nY + 1 > 20 )

                 g_EnemyMissile[i].nLife = 0;

               else

                 g_EnemyMissile[i].nY++;

            }

         } 

     }

}

 

void Render()

{

     int i;

     ScreenClear();

        

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

     {  

        if( g_EnemyMissile[i].nLife )

        {                       

           ScreenPrint( g_EnemyMissile[i].nX, g_EnemyMissile[i].nY, "↓" );

        }

     }

 

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

     {

        if( g_Enemy[i].nLife )

        {

           ScreenPrint( g_Enemy[i].nX, g_Enemy[i].nY,

             g_EnemyType[g_Enemy[i].nType] );

        }

     }

 

     ScreenFlipping();

}

 

void Release()

{

     int i, nFileCount;

 

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

 

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

        free( g_PatInfo[i].pPat );

        

     free( g_PatInfo );

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    while( 1 )

    {

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 14-11] 패턴이 적용된 적 캐릭터

 

위의 소스는 앞서 제작한 검증된 소스의 일부분들이 조합되어 완성된 것을 알 수 있다.

128행부터 198행까지는 적 캐릭터에 패턴을 적용하는 부분이며 201행에서부터 216행까지는 미사일 발사 설정에 관한 부분이다. 그리고 221행에서부터 234행은 미사일 이동을 처리하는 부분이다. 이 부분은 [실습 예제 14-2]의 [소스 14-7] 75행부터 125행까지 미리 제작해본 내용이다.

 

 


[STEP 04]

  

[그림 14-20] 4단계 제작 로드맵

 

■ 보스

 

- 보스 속성과 패턴

 

보스는 적 캐릭터와 동일한 속성을 가지고 있지만 몇 가지 차이점이 있다.

 

첫째, 적 캐릭터는 공격을 받으면 바로 소멸되지만 보스는 한 번의 공격이 아닌 여러 번의 공격을 받아야 소멸된다.

 

둘째, 적 캐릭터는 패턴이 한번 적용되지만 보스는 소멸될 때까지 다양한 패턴이 반복된다.

 

셋째, 보스는 경계 영역에 대한 충돌 체크를 할 필요가 없다.

이미 패턴으로 만들어진 이동 경로는 경계 영역과 충돌되지 않게 만들어지기 때문에 충돌 체크가 필요 없다.

 

아래 [그림 14-21]은 보스와 적 캐릭터간의 패턴 차이를 나타낸 것이다.

 

(적 캐릭터 패턴)

(보스 패턴)

 

[그림 14-21] 적 캐릭터와 보스 패턴

 

위의 [그림 14-21] 보스 패턴에서 주의해야 하는 점은, 패턴이 끝나고 새로운 패턴이 시작될 때 이전 끝난 좌표와 새로 시작하는 좌표가 동일한 위치에서 시작해야 한다.

그렇지 않으면 엉뚱한 좌표에서 보스가 갑자기 출현되어 경로의 연결이 끊어져 나타는 것 같이 보이게 된다.

 

이제 보스 속성을 나열하면 아래 [표 14-4]와 같다.

 

 

① 생명

② 좌표

③ 패턴 형태

④ 현재 스텝

⑤ 이동 경로의 인덱스

⑥ 이동 시간 간격

⑦ 이전 이동 시각

 

[표 14-4] 보스 속성

 

 

typedef struct _BOSS

{

       int  nLife;

       int  nX, nY;        

         int nPatType; // 패턴 형태

         int nPatStep; // 현재 스텝 

         int nPatIndex; // 이동 경로 인덱스

         clock_t MoveTime; // 이동 시간 간격

         clock_t OldTime;  // 이전 이동 시각

} BOSS;

 

[소스 14-12] 보스 속성 정의

 

[실습 예제 14-4]

 

[그림 14-22]와 같이 보스 캐릭터가 아래 [그림 14-23]과 [그림 14-24]와 같은 패턴 2개를 반복하여 실행되게 프로그래밍해 보자.

그리고 [그림 14-25]와 [그림 14-26] 패턴 데이터를 작성한 후에 패턴 뷰어에서 확인해보자. 특히 [그림 14-26] 패턴 데이터 중에서 이동 거리가 0인 것은 지정된 스텝만큼 정지 상태로 있게 한다.

 

[그림 14-22] 패턴이 적용된 보스

 

[그림 14-23] 보스 패턴(1)

[그림 14-24] 보스 패턴(2)

 

 

[그림 14-25] 패턴(1) 파일 : boss_pat1.txt

[그림 14-26] 패턴(2) 파일 : boss_pat2.txt

 

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT,

                          LEFT, UP_LEFT } DIRECT;

typedef struct _PAT

{

        DIRECT nDirect;    // 이동 방향

        int nStep;         // 스텝

        clock_t MoveTime;  // 이동 시간 간격

        int nDist;         // 이동 거리

} PAT;

 

typedef struct _PAT_INFO

{

        int nCount;      // 패턴 개수

        int nX0, nY0;    // 시작 좌표

        PAT *pPat;       // 패턴

} PAT_INFO;

 

typedef struct _BOSS

{

        int  nLife;

       int  nX, nY;        

         int nPatType; // 패턴 형태

         int nPatStep; // 현재 스텝 

         int nPatIndex; // 이동 경로 인덱스

         clock_t MoveTime; // 이동 시간 간격

         clock_t OldTime;  // 이전 이동 시각

} BOSS;

 

PAT_INFO *g_PatInfo;

BOSS g_Boss;

char *g_strFileName[2] = { "boss_pat1.txt", "boss_pat2.txt" };

int g_nPatCount;

 

void Init()

{

     int i, j;

     FILE *fp;

     

     g_nPatCount = sizeof( g_strFileName ) / sizeof( char * );

     g_PatInfo = (PAT_INFO *)malloc( sizeof( PAT_INFO ) * g_nPatCount );

 

     // 패턴 파일 읽기

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

     {

        fp = fopen( g_strFileName[i], "r" );               

        fscanf( fp, "%d\n", &g_PatInfo[i].nCount );

        fscanf( fp, "%d %d\n", &g_PatInfo[i].nX0, &g_PatInfo[i].nY0 );

        g_PatInfo[i].pPat = (PAT*)malloc( sizeof( PAT ) * g_PatInfo[i].nCount );

        for( j = 0 ; j < g_PatInfo[i].nCount ; j++ )

             fscanf( fp, "%d %d %d %d\n", &g_PatInfo[i].pPat[j].nDirect,

                    &g_PatInfo[i].pPat[j].nStep, &g_PatInfo[i].pPat[j].MoveTime,

                           &g_PatInfo[i].pPat[j].nDist );                          

       fclose( fp );    

      } 

 

     // Note: 보스 설정

     g_Boss.nLife = 10;

     g_Boss.nPatIndex = 0;

     g_Boss.nPatStep = -1;

     g_Boss.nPatType = 0;

     g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

     g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

     g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

     g_Boss.OldTime = clock();

}

 

void Update()

{

     int nSignX, nSignY;

     clock_t CurTime = clock();

 

     if( g_Boss.nLife )

     {

        if( CurTime - g_Boss.OldTime > g_Boss.MoveTime )

        {

            g_Boss.OldTime = CurTime;

            g_Boss.nPatStep++;

 

           if( g_Boss.nPatStep ==

                  g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].nStep )

           {

               g_Boss.nPatIndex++;

               if( g_Boss.nPatIndex == g_PatInfo[ g_Boss.nPatType ].nCount )

               {

                 g_Boss.nPatType++;

                 g_Boss.nPatType = g_Boss.nPatType % g_nPatCount; //패턴 반복

                 g_Boss.nPatIndex = 0;

                 g_Boss.nPatStep = -1;

                 g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

                 g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

                 g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

                 g_Boss.OldTime = clock();                                 

                }else{

                 g_Boss.MoveTime =

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].MoveTime;

                 g_Boss.nPatStep = 0;

                }

           }

          

           switch( g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDirect )

           {

            case UP:

                    nSignX = 0;

                    nSignY = -1;

                    break;

            case UP_RIGHT:

                      nSignX = 1;

                    nSignY = -1;

                    break;

            case RIGHT:

                      nSignX = 1;

                    nSignY = 0;

                    break;

            case DOWN_RIGHT:

                    nSignX = 1;

                    nSignY = 1;

                    break;

            case DOWN:

                      nSignX = 0;

                    nSignY = 1;

                    break;

            case DOWN_LEFT:

                    nSignX = -1;

                    nSignY = 1;

                    break;

            case LEFT:

                      nSignX = -1;

                    nSignY = 0;

                    break;

            case UP_LEFT:

                      nSignX = -1;

                    nSignY = -1;

                    break;

             }

      

       g_Boss.nX =

    g_Boss.nX + nSignX*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

       g_Boss.nY =

    g_Boss.nY + nSignY*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

        }

    }

}

 

 

void Render()

{

     char string[100];

     ScreenClear();

 

     ScreenPrint( g_Boss.nX, g_Boss.nY  ,  " /| ━ // ━ |\");

     ScreenPrint( g_Boss.nX, g_Boss.nY+1, "//q ●    ● p\");

     ScreenPrint( g_Boss.nX, g_Boss.nY+2, "  (┗┻━┻┛)");

 

     sprintf( string, "PatType:%d nPatIndex:%d ",  g_Boss.nPatType,

                                                        g_Boss.nPatIndex );

     ScreenPrint( g_Boss.nX - 10, g_Boss.nY + 4, string );

     sprintf( string, "x : %d y : %d", g_Boss.nX, g_Boss.nY );

     ScreenPrint( g_Boss.nX - 10, g_Boss.nY + 5, string );

 

     ScreenFlipping();

}

 

void Release()

{

     int nFileCount, i;

     nFileCount = sizeof( g_strFileName ) / sizeof( char * );

 

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

     {

        free( g_PatInfo[i].pPat );

      }

 

      free( g_PatInfo );

}

 

int main(void)

{

    ScreenInit();

    Init();

 

    while( 1 )

    {

          Update();

          Render();      

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 14-13]패턴이 적용된 보스 이동

 

위의 소스는 [실습 예제 14-3]의 [소스 14-11]과 유사한 내용들이 대부분이다.

왜냐하면 적 캐릭터와 보스는 패턴에 의해 제어되기 때문이다.

91행부터 98행까지는 보스의 패턴이 바뀔 때마다 초기화되는 부분이다.

특히 92행에서 % 연산자를 사용하는 이유는 패턴 파일의 인덱스를 반복하기 위해서이다. 현재 패턴 파일은 2개이며 파일 인덱스는 0, 1을 가지므로 92행의 % 2는 0과 1을 반복적으로 가지게 한다.

 

■ 보스 미사일

  

보스 미사일과 적 캐릭터 미사일의 차이점을 살펴보자. 적 캐릭터 미사일은 한 번에 한 개씩 미사일을 발사하지만, 보스는 미사일을 한 번에 3발 이상을 발사하게 된다.

이때 사방으로 펴져 나가는 미사일의 이동은 앞서 살펴본 이동 방향을 이용하면 쉽게 이동시킬 수 있다.

 

- 속성

 

보스 미사일 구조는 ‘미사일 전체에 적용되는 정보’와 ‘개별 미사일에 적용되는 정보’로 나눌 수 있다. 전체 정보로는 3개의 미사일을 한꺼번에 발사하기 위한 발사 시간 간격과 이동 시간 간격, 이전 발사 시각이 있다. 개별 미사일은 생명, 좌표, 이전 이동 시각, 이동 방향 이 있다. 이를 구분하여 나열하면 아래 [표 14-5]와 같다.

 

 

전체 정보

개별 정보

① 이동 시간 간격

② 발사 시간 간격

③ 이전 발사 시각

 

① 생명

② 이동 좌표

③ 이동 방향

④ 이전 이동 시각

 

[표 14-5] 보스 미사일 속성

 

보스 미사일이 화면에 최대로 출력될 수 있는 개수를 30개로 한정하고 위의 속성을 구조체로 정의하면 다음과 같다.

 

 

 typedef enum _DIRECT { UP, UP_RIGHT, RIGHT, DOWN_RIGHT, DOWN, DOWN_LEFT, LEFT, UP_LEFT } DIRECT;

 

typedef struct _BOSS_MISSILE

{

       int nLife;

       DIRECT  nDirect;

       int nX, nY;

       clock_t OldMoveTime;

} BOSS_MISSILE;

 

typedef struct _BOSS_MISSILE_INFO

{

       clock_t MoveTime;

       clock_t FireTime;

       clock_t OldFireTime;

       BOSS_MISSILE sMissile[30];

} BOSS_MISSILE_INFO;

 

BOSS_MISSILE_INFO g_sBossMissile;

 

[소스 14-14] 보스 미사일 속성 정의

 

위에 정의된 구조체를 보면 BOSS_MISSILE이 BOSS_MISSILE_INFO 안에 있는 것을 알 수 있다. 물론 이 둘을 따로 정의하고 선언해도 상관없지만 위와 같이 정의한 것은 둘의 관계가 종속적이라는 것을 나타내는 것이다.

그러므로 미사일의 개별 속성은 BOSS_MISSILE_INFO 구조체를 통해서만 접근할 수 있으며, 전체 속성에 따라 전체 개별 속성은 영향을 받는다. 최대 출력할 수 있는 미사일의 개수가 30개이므로 sMissile[30]으로 선언된다.

 

■ 출력

 

이제 [실습 예제 14-4]의 [소스 14-13]에 다음과 같은 사항을 추가해 보자.

 

첫째, 방향에 의해 이동하는 부분을 추가해 보자.

둘째, 3개의 미사일이 일정한 시간 간격으로 한꺼번에 발사되는 부분을 추가해 보자.

셋째, 미사일이 경계 영역과 충돌하면 소멸되게 추가해 보자.

 

보스는 경계 영역과 충돌 체크를 할 필요가 없는 것은 보스는 패턴에 의해 이동하므로 절대 경계 영역과 충돌하지 않는다. 그 이유는 이미 충돌 되지 않게 이동 경로를 패턴에 설정해 놓았기 때문이다.

 

먼저, 미사일의 방향은 기본적으로 [그림 14-27]과 같이 세 가지이며 이 방향은 패턴의 8방향 중에서 3방향과 같다.

 

[그림 14-27] 보스 미사일의 이동 방향

 

미사일 설정과 이동을 하기 위해 [실습 예제 14-4]의 [소스 14-13] Update() 함수 대신  아래 [소스 14-15]의 함수로 대치하면 [그림 14-28]과 같은 출력을 얻게 되며 아래 소스 3행부터 76행까지는 [소스 14-13]의 78행부터 147행까지와 같다.

 

[그림 14-27] 보스 미사일 출력 화면

 

 

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

void Update()

{

     int nSignX, nSignY, nFireMissileCount, i;

     clock_t CurTime = clock();

 

     if( g_Boss.nLife )

     {

        if( CurTime - g_Boss.OldTime > g_Boss.MoveTime )

        {

              g_Boss.OldTime = CurTime;

              g_Boss.nPatStep++;

 

              if( g_Boss.nPatStep ==

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].nStep )

              {

                g_Boss.nPatIndex++;

 

                if( g_Boss.nPatIndex == g_PatInfo[ g_Boss.nPatType ].nCount )

                {

                    g_Boss.nPatType++;

                    g_Boss.nPatType = g_Boss.nPatType % g_nPatCount; // 패턴 반복

 

                    g_Boss.nPatIndex = 0;

                    g_Boss.nPatStep = -1;

                    g_Boss.nX = g_PatInfo[ g_Boss.nPatType ].nX0;

                    g_Boss.nY = g_PatInfo[ g_Boss.nPatType ].nY0;

                  g_Boss.MoveTime = g_PatInfo[ g_Boss.nPatType ].pPat[0].MoveTime;

                  g_Boss.OldTime = clock();

                 }else{

                    g_Boss.MoveTime =

                     g_PatInfo[ g_Boss.nPatType ].pPat[g_Boss.nPatIndex].MoveTime;

                    g_Boss.nPatStep = 0;

                 }

               }

 

           switch( g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDirect )

               {

                case UP:

                       nSignX = 0;

                       nSignY = -1;

                       break;

                case UP_RIGHT:

                       nSignX = 1;

                       nSignY = -1;

                       break;

                case RIGHT:

                       nSignX = 1;

                       nSignY = 0;

                       break;

                case DOWN_RIGHT:

                       nSignX = 1;

                       nSignY = 1;

                       break;

                case DOWN:

                       nSignX = 0;

                       nSignY = 1;

                       break;

                case DOWN_LEFT:

                       nSignX = -1;

                       nSignY = 1;

                       break;

                case LEFT:

                       nSignX = -1;

                       nSignY = 0;

                       break;

                case UP_LEFT:

                       nSignX = -1;

                       nSignY = -1;

                       break;

                }

 

                g_Boss.nX =

    g_Boss.nX + nSignX*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

                g_Boss.nY =

    g_Boss.nY + nSignY*g_PatInfo[ g_Boss.nPatType ].pPat[ g_Boss.nPatIndex ].nDist;

        }

 

        if( CurTime - g_sBossMissile.OldFireTime > g_sBossMissile.FireTime )

        {

            g_sBossMissile.OldFireTime = CurTime;

            nFireMissileCount = 0;

            for( i = 0 ; i < 30 ; i++ )    // 30개의 미사일을 검색하는 부분

            {

                 if( g_sBossMissile.sMissile[i].nLife == 0 )

                 {

                   g_sBossMissile.sMissile[i].nX = g_Boss.nX + 6;

                   g_sBossMissile.sMissile[i].nY = g_Boss.nY + 2;

                   g_sBossMissile.sMissile[i].nLife = 1;

                     g_sBossMissile.sMissile[i].nDIRECT =

                               DOWN_LEFT - nFireMissileCount; // 방향설정: 5, 4, 3

                   g_sBossMissile.sMissile[i].OldMoveTime = CurTime;

                   nFireMissileCount++;

 

           if( nFireMissileCount > 2 ) // 3발 이상 발사 되지 못하게 하는 부분

                       break;

                }

            }

        }

     }

 

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

     {

        if( g_sBossMissile.sMissile[i].nLife )

        {

           if( CurTime - g_sBossMissile.sMissile[i].OldMoveTime >

                                                g_sBossMissile.MoveTime )

            {

                g_sBossMissile.sMissile[i].OldMoveTime = CurTime;

                switch( g_sBossMissile.sMissile[i].nDIRECT )

                {

                 case DOWN_RIGHT :

                                  nSignX = 1;

                                  nSignY = 1;

                                  break;

                case DOWN :

                                  nSignX = 0;

                                  nSignY = 1;

                                  break;

                case DOWN_LEFT :

                                  nSignX = -1;

                                  nSignY = 1;

                                  break;

                }

              g_sBossMissile.sMissile[i].nX = g_sBossMissile.sMissile[i].nX + nSignX;

              g_sBossMissile.sMissile[i].nY = g_sBossMissile.sMissile[i].nY + nSignY;

             }

                

             // Note: 경계 영역 충돌 체크

         if( g_sBossMissile.sMissile[i].nX < 2 ||

                   g_sBossMissile.sMissile[i].nX + 1 > 78 ||

                   g_sBossMissile.sMissile[i].nY > 23 )

         {

             g_sBossMissile.sMissile[i].nLife = 0;

          }

        }

     }

}

 

[소스 14-14] 패턴이 적용된 보스의 미사일 이동

 

78행부터 98행까지는 일정 시간 간격으로 미사일이 발사되게 하는 부분이다. 미사일 발사를 설정할 때에는 앞서 살펴본 것과 같이 미사일 버퍼에 설정하며 이와 같은 부분은 82행부터 97행까지 나열되어 있다.

 

미사일 이동에 관한 내용은 101행부터 127행까지가 된다.

 

 


[STEP 05]

 

[그림 14-29] 5단계 제작 로드맵

 

■ 충돌 체크

 

위의 [그림 14-29]를 보면 충돌 체크는 크게 적에 관한 충돌과 보스에 관한 충돌로 나눌 수 있다. 그래서 RUNNING 상태에서는 적에 관한 충돌 체크를 하고 BOSS_WAR 상태에서는 보스에 관한 충돌 체크를 한다.

 

- RUNNING 상태에서의 충돌 체크

 

RUNNING 상태에서 충돌 대상은 주인공, 주인공 미사일, 적 캐릭터, 적 미사일이다. 충돌 관계를 표로 나타내면 아래 [표 14-6]과 같다.

 

 

충돌 주체

충돌 대상

적 캐릭터

주인공 캐릭터, 주인공 미사일

적 미사일

주인공 캐릭터, 주인공 미사일

주인공 캐릭터

적 캐릭터, 적 미사일

주인공 미사일

적 캐릭터, 적 미사일

 

[표 14-6] RUNNING 상태에서 충돌

 

위의 충돌 체크 외에 경계 영역에 대한 충돌 체크가 있는데 이 체크는 각 충돌 주체를 이동시킨 후에 바로 체크하는 것이 프로그램을 단순화시키는데 도움이 된다.

아래의 [표 14-7]을 보면 캐릭터의 크기가 일정하지 않으므로 크기에 따라 충돌 영역은 달라진다.

 

 

주인공

적 캐릭터

적 미사일

주인공 미사일

<-^->

♨  ◆  ☎  ▣

 ^

[가로 길이 = 5]

[가로 길이 = 2]

[가로 길이 = 2]

[가로 길이 = 1]

 

[표 14-7] 캐릭터 크기

 

충돌 영역에 따른 충돌 상황은 아래 [그림 14-30]부터 [그림 14-32]까지이므로 [식 14-1]을 이용하면 충돌을 판단할 수 있다.

 

[그림 14-30] 충돌 예1

 

[그림 14-31] 충돌 예2

 

[그림 14-32] 비충돌

 

 

x1 <= x4 && x2 >= x3 && y1 <= y4 && y2 >= y3

 

[식 14-1] 충돌 판정

 

캐릭터의 크기마다 달라지는 충돌 영역은 아래 [그림 14-33]과 같은 툴을 통해 설정하는 경우가 대부분이다.

 

[그림 14-33] 툴에서 캐릭터의 충돌 영역을 설정

위의 [식 14-1]을 함수로 만들면 아래 [소스 14-16]과 같다.

 

 

int Collision( int nX1, int nY1, int nX2, int nY2, int nX3, int nY3, int nX4, int nY4 )

{

        if( nX1 <= nX4 && nX2 >= nX3 && nY1 <= nY4 && nY2 >= nY3 )

           return 1;  // 충돌

        else

           return 0;  // 비충돌

}

 

[소스 14-16] 충돌 체크 함수

 

위의 함수는 2D 게임 제작에서 주로 사용되는 충돌체크 함수이며 충돌 체크를 하는 시점은  각 개체를 이동시킨 후가 된다. 즉, 한 번 이동 좌표가 변경되면 충돌 대상과 한 번씩 충돌 체크를 한다.

 

 

이동 → 충돌 체크 → 이동 → 충돌 체크

 

[표 14-7] 충돌 체크 과정

 

- BOSS_WAR 상태에서의 충돌 체크

  

BOSS_WAR 상태에서는 보스, 보스 미사일, 주인공, 주인공 미사일만 충돌 체크한다. 이들의 충돌 관계를 간단히 표로 나타내면 다음과 같다.

 

 

충돌 주체

충돌 대상

보스 캐릭터

주인공 미사일

보스 미사일

주인공 캐릭터, 주인공 미사일

주인공 캐릭터

보스 미사일

주인공 미사일

보스 캐릭터, 보스 미사일

 

[표 14-8] 충돌 관계

 

위의 표에서 보스 캐릭터와 주인공 간의 충돌이 빠져 있는 이유는 보스 패턴을 만들 때 이미 주인공 캐릭터와 충돌하지 않게 만들기 때문이다.

보스는 [그림 14-34]와 같이 앞의 캐릭터와 약간 다른 크기를 가진다.

 

[그림 14-34] 가로 길이 16이고 세로 길이가 3인 보스 캐릭터

 

보스 충돌이 앞의 충돌 체크와 약간의 차이를 보이는 부분은 ‘경계 영역에 대한 처리 부분’이다. 보스는 패턴에 의해 계속적으로 이동하므로 경계 영역을 넘을 수 없도록 되어 있으나 보스 미사일은 경계 영역을 넘을 수가 있기 때문에 경계 영역의 충돌 체크는 보스 미사일에만 적용하면 된다. 주인공도 이동 영역이 경계 영역 안에서만 움직이도록 되어 있으므로 주인공 미사일만 y 좌표에 대한 경계 영역만 충돌 체크하면 된다.

 

 


[STEP 06]

 

[그림 14-35] 6단계 제작 로드맵

 

■ 게임 스테이지 정보

 

Sogo 게임은 보스 캐릭터를 소멸시켜야 스테이지가 끝난다. 각 스테이지는 적 캐릭터의 개수, 이동 속도, 패턴, 보스의 HP, 보스의 무기를 달리하여 스테이지별 차별성을 둘 수 있지만, 이 장에서는 적 캐릭터의 수와 보스 생명값만을 스테이지에 적용한다.

 

 

typedef struct _STAGE_INFO

{

        int nEnemyCount;

        int nBossLife;

} STAGE_INFO;

 

[소스 14-17] 스테이지 정보 정의

 

■ 게임 진행 정보

 

Sogo 게임에서 게임 진행 상태는 PLAYER_TALK와 BOSS_TALK, 그리고 BOSS_WAR라는 상태가 추가되었을 뿐 앞서 살펴본 내용과 동일하다.

PALYER_TALK와 BOOS_TALK 경우는 주인공과 보스 간의 대화가 화면에 일시적으로 출력되도록 하는 상태값이며 BOSS_WAR는 주인공과 보스가 대전하는 상태를 말한다.

 

 

상태

설명

초기 상태 (INIT)

게임 변수의 초기화 및 사운드 초기화

준비 상태 (READY)

스테이지 정보의 출력

게임 진행 상태 (RUNNING)

게임 진행 및 중지 상태로 전이

주인공의 대화 출력 (PLAYER_TALK)

주인공의 대화 내용 출력

보스의 대화 출력 (BOSS_TALK)

보스의 대화 출력, BOSS_TALK에서 다음 상태인 BOSS_WAR로 넘어가면 보스가 출현하게 된다.

보스와의 대결 (BOSS_WAR)

주인공, 미사일과 보스, 미사일과 충돌 체크 및 게임 진행

중지 상태(STOP)

미션 성공과 실패를 판단

미션 성공 상태 (SUCCESS)

미션 성공

미션 실패 상태 (FAILED)

미션 실패

결과 출력 및 종료 상태 (RESULT)

게임 종료 및 종료

 

[표 14-8] 게임 진행 상태

 

 

typedef enum _GAME_STATE { INIT, READY, RUNNING, PLAYER_TALK, BOSS_TALK, BOSS_WAR,

                                STOP, SUCCESS, FAILED, RESULT } GAME_STATE;

 

[소스 14-18] enum형

 

여기까지 슈팅 게임의 가장 기본적인 형태의 게임을 살펴보았으며 패턴 데이터를 적용하여 적 캐릭터와 보스의 이동을 제어해 보았다. 이제 이를 기반으로 전체 프로그램을 제작해 보자.

 

 

 

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

 

[출처] https://nowcampus.tistory.com/entry/14%EC%9E%A5-Sogo-%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
» (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 50
99 (CGP) 10장. 하트담기 게임 file 졸리운_곰 2021.06.28 25
98 (CGP) 9장. 하트 툴 만들기 file 졸리운_곰 2021.06.28 22
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