(CGP) 8장. 벽돌깨기 게임

벽돌깨기 게임에서는 개체의 속성 정의와 상태전이에 따른 공의 이동밯향의 결정이 중요한다.

특히 여기서의 상태전이는 인공지능의 한 영역에 해당이 되며 구현방법이 단순하지만 결과가 상당히

 

 

다양함을 알 수 있다.

 

슈퍼맨 유튜브 동영상 강의 주소

 

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

(2) http://youtu.be/8ALFzKqrgZE

(3) http://youtu.be/iTjp53UXJOY
(4) http://youtu.be/2s0mXDthke8

(5) http://youtu.be/4JY3lXR_u8Y

(6) http://youtu.be/5fLLgtEfR0s

(7) http://youtu.be/MbYmAiAxYtQ

 

 

  


01 기획

 

■ 스토리

 

제한 시간 안에 화면에 출력된 모든 벽돌을 맞추어라.

 

■ 게임 방식

 

게임을 시작하면 막대기와 공이 같이 움직인다. 공을 발사한 후에는 막대기로 공을 받아 튕기게 하거나 i 키를 이용하여 공을 잡아 다시 발사하는 방법으로 벽돌을 맞춘다. 이때 사용하는 키는 j(좌), l(우), k(공 발사), i(공 잡기)이다.

공을 잡았을 때는 입력키 1, 2, 3에 의해 차례대로 왼쪽, 위쪽, 오른쪽으로 발사 방향을 결정한다.

 

■ 제한 사항

 

공은 3개까지 제공되며 스테이지 난이도는 벽돌 개수와 제한 시간으로 조절한다.

 

■ 기획 화면

 

[그림 8-1] 기획 화면

 

 


02 실행 화면

 

[그림 8-2] 게임 메인 화면

 

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

 

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

 

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

 

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

 

[그림 8-7] 게임 종료 화면

 

 


03 게임 제작 로드맵

 

벽돌 깨기 게임은 슛 골인 게임과 유사한 부분이 많으므로 [그림 8-8]의 제작 로드맵을 참고하면서 제작해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

[그림 8-8] 제작 로드맵

 

 


04 단계별 프로그래밍

 

 


STEP 01

 

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

 

■ 공

 

벽돌깨기 게임의 공을 슈팅 게임과 비교한다면 공은 총알과 같은 역할을 한다.

슈팅 게임에서 총알은 적과 충돌하거나 경계영역에 닿으면 사라지지만 벽돌깨기 게임에서 공은 막대기로 받지 못하는 경우를 제외하고는 항상 게임 안에 존재한다.

또한 이 게임은 공을 벽돌 또는 경계영역과 충돌시켜 게임을 진행하므로 공의 이동 방향은 게임의 중요한 요소가 된다.

 

- 속성

 

공의 기본적인 속성은 좌표, 생명, 이동 시간의 간격, 이전 이동 시각을 들 수 있다. 공은 좌표를 따라 출력과 이동을 하게 되며 막대기가 공을 3번 놓치게 되면 게임이 종료되므로 생명이라는 속성이 있다.

또한 공의 이동 속도를 조절하기 위해 이동 시간의 간격과 이전 이동 시각은 반드시 있어야 하는 속성 중 하나이다.

 

이와 같은 기본 속성 외에 추가되는 속성은 바로 공의 이동 방향이다. 앞서 설명했지만 공이 막대기와 충돌하면 공의 방향에 따라 적절한 방향으로 튕겨 나가야 한다.

이를 구현하기 위해서 공의 이동 방향을 [그림 8-10]과 같이 정의한다.

비록 [그림 8-10]에서 공의 전체 이동 방향은 여섯 방향이지만 이것을 더욱 세분화한다면 공은 더욱 다양한 방향으로 이동하게 된다.

 

[그림 8-10] 공의 이동 방향

 

 

① 공 상태 ( 준비 상태, 이동 상태 )

② 생명

③ 좌표

④ 이동 방향

⑤ 이동 시간 간격

⑥ 이전 이동 시각

 

[표 8-1] 공 속성

 

위의 속성 중에 공 상태에 대해서 살펴보자.

공은 두 가지 상태를 가진다.

첫째, 준비 상태이다.

이 상태에서 공은 막대기와 동일한 방향으로 좌우 이동을 같이 하며 플레이어가 공을 잡은 상태가 된다. 그래서 공은 플레이어에 의해 다시 발사될 수 있는 상태가 된다.

 

둘째, 이동 상태이다.

이 상태는 현재 공의 이동 방향에 따라 공의 좌표를 증감하는 상태를 말하며 벽돌과 경계 영역과의 충돌을 체크하며 다음 이동 방향을 결정하기 위한 상태이다.

 

6방향에 대한 enum형과 공의 속성을 구조체로 정의하면 다음과 같다.

 

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

 

typedef struct _BALL

{

        int nReady; // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;   // 생명

          int  nX, nY; // 좌표

        DIRECT  nDirect; // 이동 방향

        clock_t  MoveTime; // 이동 시간 간격

        clock_t  OldTime; // 이전 이동 시각

} BALL;

 

[소스 8-1] 공 속성 정의

 

■ 막대기

 

 

- 속성

 

막대기는 블록 3개를 연결하여 구성하며, 3개의 블록은 x 좌표만 차이가 있을 뿐 y 좌표는 모두 동일하다. 그러므로 막대기는 아래와 같은 속성을 가진다.

 

 

① x 좌표 3개

② y 좌표

 

[표 8-2] 막대기 속성

 

막대기 속성 중에서 x 좌표 3개는 모두 동일한 데이터형을 가지므로 배열로 선언한다.

배열로 선언된 x 좌표값은 반복문과 인덱스를 통해 쉽게 읽을 수 있으므로 공과 충돌 체크할 때 유용하게 사용된다.

이와 같은 사항을 구조체로 정의하면 아래와 같다.

 

typedef struct _BAR

{

       int nX[3];

       int nY;         

} BAR;

 

[소스 8-2] 막대기 속성 정의

 

■ 키보드 처리

 

막대기의 이동과 공 잡기와 발사에 대한 키를 정의하면 다음과 같다.

 

 

이동키

역할

j

왼쪽으로 이동

l

오른쪽으로 이동

k

공 발사 ( 공 이동 시작 )

i

공 잡기 (막대기와 같이 이동하는 상태 )

 

[표 8-3] 키 정의

 

위의 키 입력에 따른 동작을 살펴보자.

j와 l 키는 좌우로 이동하는 키이므로 막대기의 x 좌표를 증감해주면 간단히 해결된다.

이에 대한 처리는 앞서 제작한 게임에서도 많이 다루었던 부분이다.

 

k 키는 공을 발사하는 키로 사용되는데 공이 발사된다는 것은 공의 이동을 의미한다.

j와 l키 입력이 있을 때와 k 키 입력이 있을 때 공과 막대기의 이동에는 어떤 차이점은 무엇일까? j와 l키 입력은 플레이어에 의해 막대기 이동을 시키지만 k 키 입력은 공 스스로가 일정한 속도로 이동을 해야 한다. 그래서 k 키가 입력되면 이 곳에서 공의 방향과 이전 이동 시각에 대한 초기화, 공의 상태전이를 통해 공이 스스로 이동할 수 있게 설정해야 한다.

 

i 키는 공을 잡게 하는 키 입력이다. 공을 잡는다는 것은 공과 막대기가 충돌했다는 것이며  또한 공의 상태를 준비 상태로 전이시켜 막대기의 x 좌표 증감값만큼 공의 좌표를 증감해야 한다는 것을 의미한다.

 

[실습 예제 8-1]

 

위의 [표 8-3]에 정의한 키가 입력되면 아래 그림과 같이 출력되도록 게임 프레임워크 안에서 프로그래밍해 보자.

 

[그림 8-11] 키 입력에 따른 메시지 출력(1)

 

[그림 8-12] 키 입력에 따른 메시지 출력(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

#include <stdio.h>

#include <conio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

char *g_strMessage[] = { "왼쪽 이동", "오른쪽 이동", "공 발사", "공 잡기" };

int g_nIndex = -1;  // 출력 메시지 인덱스, -1일 때 메시지는 출력에서 제외

 

void Init()

{

}

 

void Update()

{

}

 

void Render()

{       

     ScreenClear();

        

     if( g_nIndex >= 0 )

        ScreenPrint( 10, 10, g_strMessage[g_nIndex] );

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        nKey = _getch();

        switch( nKey )

        {

        case 'j' :      

                g_nIndex = 0;

                break;

        case 'l' :

                g_nIndex = 1;

                break;

        case 'k' :

                g_nIndex = 2;

                break;

        case 'i' :

                g_nIndex = 3;

                break;

        }

 

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 8-3] 키 입력에 따른 출력

 

■ 이동 및 출력

 

- 공의 이동 및 출력

 

공은 아래 [그림 8-13]과 같이 총 6방향으로 이동한다.

 

 [그림 8-13] 공의 이동 방향

 

각 이동 방향으로 이동하기 위해서는 방향에 따른 x, y 좌표의 증감이 필요하다.

아래의 [표 8-4]는 방향에 따른 좌표의 증감을 나타낸다.

 

 

이동 방향

증감 부분

0

y 값 감소

1

x 값 증가, y 값 감소

2

x 값 증가, y 값 증가

3

y 값 증가

4

x 값 감소, y 값 증가

5

x 값 감소, y 값 감소

 

[표 8-4] 이동 방향과 증감

 

위의 [표 8-4]에 따른 증감과 이동 시간 간격은 공의 속도를 결정하는 요소가 된다.

 

[실습 예제 8-2]

 

숫자 0, 1, 2, 3, 4, 5 키가 입력되면 위의 [표 8-4]의 증감에 따라 공이 이동하게 프로그래밍해 보자. 그리고 경계 영역과 공이 닿으면 공은 준비 상태가 되며 f 키는 이동 상태가 된다. 

 

      

[그림 8-14] 준비 상태의 공

 

[그림 8-15] 우측 상단으로 이동하는 공 (LEFT_TOP)

 

[그림 8-16] 좌측 하단으로 이동하는 공 (RIGHT_DOWN)

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int nReady;          // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;            // 생명

          int  nX, nY;         // 좌표

        DIRECT  nDirect;     // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

BALL g_sBall;

 

void Init()

{   // Note: 공 초기화

     g_sBall.nX = 38;

     g_sBall.nY = 12;

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.nReady = 1;

     g_sBall.MoveTime = 50;

}

 

void Update()

{

     clock_t CurTime = clock();

 

      if( g_sBall.nReady == 0 )

      {

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

        {

            g_sBall.OldTime = CurTime;

        

            switch( g_sBall.nDirect )

            {

              case TOP :

                         g_sBall.nY--;

                         break;

              case LEFT_TOP :

                         g_sBall.nX++;

                         g_sBall.nY--;

                         break;

              case LEFT_DOWN :

                         g_sBall.nX++;

                         g_sBall.nY++;

                         break;

              case DOWN :

                         g_sBall.nY++;

                         break;

              case RIGHT_DOWN :

                         g_sBall.nX--;

                         g_sBall.nY++;

                         break;

              case RIGHT_TOP :

                         g_sBall.nX--;

                         g_sBall.nY--;

                         break;

              }

         }

        

         // Note: 경계 영역과 충돌

    if( g_sBall.nX < 0 || g_sBall.nX > 78 ||  g_sBall.nY > 24 || g_sBall.nY < 0 )

   {

             g_sBall.nReady = 1;

             g_sBall.nX = 38;

             g_sBall.nY = 12;

             g_sBall.nDirect = TOP; // 기본 상태

          }

     }

}

 

void Render()

{

     ScreenClear();

        

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

         if( _kbhit() )

        {

           nKey = _getch();

                        

           if( nKey == 'f' )

           {

               g_sBall.nReady = 0;  // 이동 상태

            }else{

               nDirect = nKey - '0';  // 0에서부터 5까지의 정수형으로 변환

               if( nDirect >= 0 && nDirect <= 5 )

               {

                 g_sBall.nReady = 1;

                 g_sBall.nX = 38;

                 g_sBall.nY = 12;

                 g_sBall.nDirect = nDirect;

                 g_sBall.OldTime = clock();

                }

            }

         }

          

           Update();

           Render();       

        }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-4] 키 입력에 따른 공의 이동

 

43행에서부터 64행까지는 공의 이동 방향에 따른 증감을 나타내며 [표 8-4]를 코드로 옮긴 것이다.

 

69행을 보면 경계 영역과 공의 충돌 조건식이 나와 있다. 가로 0에서부터 79컬럼이며 세로 0에서부터 24컬럼이다.

특히 오른쪽 경계 영역 충돌을 체크할 때 g_sBall.nX > 78로 한 것은 공은 특수 문자로 2컬럼의 크기를 가지기 때문이다.

 

- 막대기 속성과 이동

 

막대기 이동은 일반적인 이동과 같이 좌우 증감에 의해 이동한다.

여기서도 막대기는 특수 문자로 출력되므로 문자당 2컬럼의 크기를 가지고 좌표를 초기화할 때 크기 부분을 고려해야 한다.

또한 키 입력이 있을 때마다 즉각적으로 반응하는 것이 아니라 일정한 시간 간격에 의해 반응하여 스테이지마다 게임성을 다르게 할 수도 있다.

이와 같은 내용은 이미 앞서 제작한 슛 골인 게임에서 골대와 골대 라인을 구현할 때 다루었던 내용이다. 여기까지 설명한 막대기의 속성을 정리하고 구조체로 정의하면 아래 [표 8-5]와 같다. 

 

 

① x 좌표 3개

② y좌표

③ 이전 이동 시각

④ 이동 시간 간격

 

[표 8-5] 막대기 속성

 

 

typedef struct _BAR

{

      int nX[3];

      int nY;            

      clock_t OldTime;

      clock_t MoveTime;

} BAR;

 

[소스 8-5] 막대기의 속성 정의

 

[실습 예제 8-3]

 

아래 [그림 8-17]과 같이 막대기를 구성하는 3개의 블록이 j키와 l키에 의해 좌우로 이동하는 프로그램을 작성해 보자. 여기서 경계영역은 ( 0, 0 )에서부터 ( 79, 24 )로 한다.

 

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

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _BAR

{

      int nX[3];

      int nY;            

      clock_t OldTime;

      clock_t MoveTime;

} BAR;

 

BAR  g_sBar;

 

void Init()

{

     g_sBar.nY = 20;

     g_sBar.nX[0] = 30;

     g_sBar.nX[1] = 32;

     g_sBar.nX[2] = 34;

     g_sBar.MoveTime = 100;

     g_sBar.OldTime= clock();

}

 

void Update()

{

}

 

void Render()

{

     int i;

     ScreenClear();

        

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

     {

        ScreenPrint( g_sBar.nX[i], g_sBar.nY, "▣" );

     }

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

    clock_t CurTime;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

            nKey = _getch();

 

            switch( nKey )

            {

             case 'j' :

                   CurTime = clock();

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

                   {

                       g_sBar.OldTime = CurTime;

                       if( g_sBar.nX[0] > 0 ) // 경계 영역 충돌 체크

                       {

                         g_sBar.nX[0]--;

                         g_sBar.nX[1]--;

                         g_sBar.nX[2]--;         

                       }

                     }

                     break;

              case 'l' :

                     CurTime = clock();

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

                     {                                  

                        g_sBar.OldTime = CurTime;

                        if( g_sBar.nX[2] <= 77 )  // 경계 영역 체크

                        {

                            g_sBar.nX[0]++;

                            g_sBar.nX[1]++;

                            g_sBar.nX[2]++;              

                        }

                      }

                      break;

              }

          }

 

          Update();

          Render();      

     }

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-6] 막대기 이동

 

68행과 81행은 좌우 경계 영역과 충돌 체크를 하기 위한 조건식이다.

68행에서 블록의 g_sBar.nX[0] 좌표값은 블록의 출력 기준 좌표이고 경계 영역과 충돌이 되는 끝 좌표에 해당이 되므로 g_sBar.nX[0]로 왼쪽 경계 영역과 충돌 체크를 하면 된다.

반면 81행의 g_sBar.nX[2] 좌표값은 블록의 출력 기준 좌표일 뿐 실제 경계 영역과 충돌되는 끝 좌표는 g_sBar.nX[2] + 1의 좌표가 된다.

그래서 g_sBar.nX[2]와 경계 영역을 비교할 때에는 이런 부분을 고려한 조건식을 적용해야 한다. 81행 조건식의 경우 g_sBar.nX[2]의 값이 77일 때 85행에 의해 g_sBar.nX[2]는 78이되고 g_sBar.nX[2] + 1은 79 가 되므로 경계영역 이상의 값으로는 증가하지 않는다.

 

- 키 입력에 따른 막대기 이동과 공 잡기

 

앞에서 공 이동과 키 입력에 따른 막대기의 이동 소스까지 살펴보았다. 이 두 소스를 이용하면 키 입력에 따른 막대기의 이동과 공을 출력할 수 있다.

 

키 입력에 따라 공을 잡을 수 있는 경우는 아래 [그림 8-18]과 같이 공이 막대기의 x 좌표 범위 안에 있고 막대기의 y좌표 보다 공이 1 적은 위치에 있을 때이다.

[그림 8-18] 공 잡기

 

[실습 예제 8-4]

 

앞에서 살펴본 [실습 8-2]와 [실습 8-3]을 합치면 막대기와 공이 이동하면서 공을 발사를 할 수 있는 프로그램을 제작할 수 있다. 여기에서 j 와 l 키는 막대기의 좌우 이동이며 k 키는 공 발사, 그리고 경계 영역과 충돌하면 공은 준비 상태가 된다.

그리고 0, 1, 2, 3, 4, 5 키로 공의 방향이 결정되는 프로그램을 작성해 보자.

 

[그림 8-19] 준비 상태

 

[그림 8-20] LEFT_TOP 방향으로 이동

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int nReady;          // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;            // 생명

        int  nX, nY;         // 좌표

        DIRECT  nDirect;     // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

typedef struct _BAR

{

        int nX[3];

        int nY;          

        clock_t OldTime;

        clock_t MoveTime;

} BAR;

 

BAR  g_sBar;

BALL g_sBall;

 

void Init()

{

     g_sBar.nY = 13;

     g_sBar.nX[0] = 30;

     g_sBar.nX[1] = 32;

     g_sBar.nX[2] = 34;

     g_sBar.MoveTime = 80;

     g_sBar.OldTime= clock();

 

     g_sBall.nX = g_sBar.nX[1];

     g_sBall.nY = g_sBar.nY - 1;

     g_sBall.nReady = 1; // 준비 상태

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.MoveTime = 100;

}

 

void Update()

{

     clock_t CurTime = clock();

 

     if( g_sBall.nReady == 0 )

     {

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

        {

            g_sBall.OldTime = CurTime;

                

            switch( g_sBall.nDirect )

            {

             case TOP :

                      g_sBall.nY--;

                      break;

             case LEFT_TOP :

                      g_sBall.nX++;

                      g_sBall.nY--;

                      break;

             case LEFT_DOWN :

                      g_sBall.nX++;

                      g_sBall.nY++;

                      break;

             case DOWN :

                      g_sBall.nY++;

                      break;

             case RIGHT_DOWN :

                      g_sBall.nX--;

                      g_sBall.nY++;

                      break;

             case RIGHT_TOP :

                      g_sBall.nX--;

                      g_sBall.nY--;

                      break;

             }

         }

        

         // Note: 경계 영역과 충돌

     if( g_sBall.nX < 0 || g_sBall.nX > 78 ||  g_sBall.nY > 24 || g_sBall.nY < 0 )

         {

             g_sBall.nReady = 1;

             g_sBall.nX = g_sBar.nX[1];

             g_sBall.nY = g_sBar.nY - 1;                                   

             g_sBall.nDirect = TOP; // 기본 상태

          }

      }else{

             g_sBall.nX = g_sBar.nX[1];

           g_sBall.nY = g_sBar.nY  - 1;

      }   

}

 

void Render()

{

     int i;

     ScreenClear();

 

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

     {

        ScreenPrint( g_sBar.nX[i], g_sBar.nY, "▣" );

     }

 

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

    clock_t CurTime;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

 

           switch( nKey )

           {

           case 'k' :

                 g_sBall.nReady = 0;  // 이동 상태

                 g_sBall.OldTime = clock();

                 break;

           case 'j' :

                 CurTime = clock();

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

                 {

                     g_sBar.OldTime = CurTime;

                     if( g_sBar.nX[0] > 0 ) // 경계 영역 충돌 체크

                     {

                        g_sBar.nX[0]--;

                        g_sBar.nX[1]--;

                        g_sBar.nX[2]--;          

                      }

                   }

                   break;

            case 'l' :

                     CurTime = clock();

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

                   {                                    

                        g_sBar.OldTime = CurTime;

                        if( g_sBar.nX[2] <= 77 )  // 경계 영역 체크

                        {

                          g_sBar.nX[0]++;

                          g_sBar.nX[1]++;

                          g_sBar.nX[2]++;         

                        }

                      }

                      break;

             case '0' :

             case '1' :

             case '2' :

             case '3' :

             case '4' :

             case '5' :

                   nDirect = nKey - '0';  // 0에서부터 5까지의 정수형으로 변환

                   g_sBall.nReady = 1;

                   g_sBall.nX = g_sBar.nX[1];

                   g_sBall.nY = g_sBar.nY - 1;

                   g_sBall.nDirect = nDirect;

                   g_sBall.OldTime = clock();

                   break;

             }

        }  

 

        Update();

        Render();       

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-7] 이동하는 공과 막대기

 

 


STEP 02

 

[그림 8-21] 2단계 제작 로드맵

 

 

■ 벽돌

 

- 속성

 

이제까지 제작해온 캐릭터들을 살펴보자.

슛 골인 게임에서 공과 골대는 게임이 종료될 때까지 언제나 살아 있는 캐릭터였다.

짝 맞추기 게임에서 카드 30개도 항상 고정적으로 출현하는 캐릭터였으며 두더지 잡기 게임의 두더지도 언제나 살아 있었다.

하지만 이번 벽돌 깨기 게임에서 벽돌은 공과 충돌함과 동시에 소멸 상태를 유지해야 한다.

그래서 벽돌 속성에는 생명이라는 속성을 두어 소멸 상태와 살아 있는 상태를 구분한다.

여기서 소멸 상태란 출력에서 제외되는 상태를 말하며 살아 있는 상태는 그 반대의 상태를 말한다. 이 속성을 생명이라고 하며 이 생명에는 살아 있는 상태값과 소멸 상태값을 저장한다.

이와 같은 사항을 속성으로 정의하면 다음과 같다.

 

 

① 생명

② 좌표 ( x, y )

 

[표 8-6] 벽돌 속성

 

 

typedef struct _BLOCK

{

        int  nLife;

        int  nX, nY;

} BLOCK;

 

[소스 8-8] 벽돌 속성 정의

 

■ 벽돌 좌표 생성 및 출력

 

벽돌은 스테이지마다 중복되지 않는 임의의 좌표를 받아 설정해야 한다. 중복되지 않게 좌표를 생성하여 설정하는 방법은 앞서 제작한 게임에서도 다루었던 내용이다.

물론 rand() 함수를 이용하는 방법 외에도 다양한 방법이 있겠지만 벽돌깨기 게임에서는  이 방법을 이용한다.

 

[실습 예제 8-5]

 

30개의 벽돌이 중복되지 않게 임의의 개수만큼 출력되도록 게임 프레임워크 안에서 프로그래밍해 보자. 임의의 좌표는 rand() 함수를 이용하고 출력 좌표 범위는 ( 0, 0 )에서부터 ( 79, 10 )까지로 한다.

아래의 [그림 8-22]는 30개의 벽돌 중에서 20개만을 출력한 예이다.

 

[그림 8-22] 30개의 벽돌 중에서 20개만 출력

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _BLOCK

{

       int  nLife;

       int  nX, nY;

} BLOCK;

 

BLOCK g_sBlock[30];

 

int Search( int nEnd, int nX, int nY )

{

     int i;

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

     {

        if( g_sBlock[i].nY == nY )

        {

           if( g_sBlock[i].nX == nX || ( g_sBlock[i].nX + 1 ) == nX ||

                g_sBlock[i].nX == nX + 1 || (g_sBlock[i].nX + 1) == nX + 1 )

                return 1; // 같은 것이 있으면

        }

      }

      return 0; // 같은 것이 없으면

}

 

void SetBlock( int nBlockCount )

{

     int nX, nY, i;

 

     srand( (unsigned int )time(NULL) );

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

     {

        g_sBlock[i].nLife = 1;

        

        while( 1 )

        {              

             nX = rand() % 79; // 0 ~ 78 범위 안에서

             nY = rand() % 11; // 0 ~ 10 까지의 범위

                

               if( Search( i, nX, nY ) == 0 )

               {

                   g_sBlock[i].nX = nX;

                   g_sBlock[i].nY = nY;

                   break;

               }    

         }

     }

}

 

void Init()

{

     SetBlock( 20 );

}

 

void Update()

{

}

 

void Render()

{

     int i;

     ScreenClear();

 

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

     {

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

           ScreenPrint( g_sBlock[i].nX, g_sBlock[i].nY, "■" );

     }

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

         Update();

         Render();        

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 8-9] 벽돌의 좌표 설정 및 출력

 

14행의 Search()함수는 중복되는 좌표가 현재의 벽돌 안에 있는지를 조사하는 함수이다.

첫 번째 매개변수인 nEnd는 좌표 조사를 하는 벽돌 배열의 끝 인덱스를 지정한다.

 

38행의 반복문은 47행의 break를 만나기까지 무한 반복하며 이 무한 반복을 종료하게 만드는 조건은 바로 43행에서 중복되지 않는 좌표를 구했을 때이다.

 

 


STEP 03

 

[그림 8-23] 3단계 제작 로드맵

 

■ 충돌

 

벽돌 깨기 게임에서 출력과 이동은 앞에서 제작한 게임과 유사하지만 공이 경계 영역 또는 벽돌과 충돌하면 공은 다른 방향으로 튕겨 나가야 한다.

이때 공이 다른 방향으로 튕겨나가도록 방향에 대한 상태 전이가 있어야 한다.

 

- 상태 전이

 

상태 전이란 현재 상태가 외부 요소에 의해 바뀌는 것을 말한다. 예를 들면, 주인공 캐릭터가 몹의 공격 영역 안에 들어오면 대부분의 몹은 공격 상태로 바뀌는데 이것을 상태 전이라고 한다. 상태 전이를 만드는 방법에는 여러 가지가 있지만 그 중 가장 일반적으로 이차원 배열을 많이 활용한다. 이에 대한 사항은 아래에 소개되는 공의 이동 방향에서 살펴보도록 하자.

 

- 공의 이동 방향과 상태 전이표 활용

 

공의 이동 방향이 바뀔 때는 경계 영역과 충돌하거나 벽돌과 충돌했을 때이다.

경계 영역 또는 벽돌과 충돌 했을 때 이동 방향의 설정은 둘 다 같으므로 여기서는 경계 영역에 대한 사항만 설명하겠다.

 

먼저 공이 충돌했을 때 이동 방향에 따라 튕기는 방향을 살펴보면 [그림 8-24]와 같다.

 

[그림 8-24] 공의 이동 방향과 튕기는 방향

 

상태 전이는 앞서 설명했듯이 현재의 상태가 외부의 상태에 의해서 변화되는 것이다.

그렇다면 위의 [그림 8-24]에서 현재 공의 상태가 0 상태일 때 외부의 상태는 과연 무엇일까? 바로 경계 영역이 된다. 경계 영역은 현재 위쪽, 아래쪽, 왼쪽, 오른쪽의 경계 영역을 가지고 있다. 공이 0 상태일 때 만날 수 있는 경계 영역은 위쪽 경계 영역외에는 없다.

즉 공이 0 상태에서 위쪽 경계 영역을 만나면 다음 상태인 3 상태로 전이를 해주면 공은 튕기게 된다. 이때 경계 영역의 인덱스를 시계방향으로 위쪽은 0, 오른쪽은 1, 아래쪽은 2, 왼쪽은 3으로 한다. 경계 영역의 인덱스를 행으로 하고 공의 상태를 열로 할 때 이차원 배열을 도표로 나타내면 아래 [그림 8-25]와 같다.

 

[그림 8-25] 상태 전이표

 

먼저 간단한 예로 경계 영역 인덱스가 1일 때 공의 이동 방향 상태 전이를 생각해 보자.

위의 [그림 8-24]를 보면 공의 이동 방향 인덱스가 1일 때 튕기는 방향은 5이다.

이 사항을 상태전이표에 적용하면 다음과 같다.

 

[그림 8-26] 상태전이 적용

 

이와 같은 적용 방법을 상태 전이표에 전부 적용하면 아래와 같다.

 

[그림 8-27] 공의 이동 방향과 튕기는 방향이 적용된 이차원 배열

 

위와 같이 도표를 만들다 보면 경우에 따라 방향이 정해질 수 없는 경우가 있다.

이 경우에는 현재 상태 전이표에 의미 없는 값을 넣어 이동에서 제외시킨다.

아래의 [그림 8-28]을 보면 -1이 설정된 것을 볼 수 있다.

 

 

[그림 8-28] -1이 적용된 상태전이표

 

이제 위의 [그림 8-28]의 상태 전이표를 사용해 보자.

현재 공이 1 상태이고 경계영역 1과 충돌 했을 때 다음 상태는 경계영역 인덱스인 행과 공의 이동 방향 인덱스인 열로 [표 8-7]에 의해 다음 상태를 구할 수 있다.

 

 

다음 상태 = array[ 경계영역 인덱스 ][ 이동 방향 인덱스 ];

 

[표 8-7] 다음 상태 전이

 

[실습 예제 8-7]

 

[실습 예제 8-2]의 소스를 이용하여 경계영역과 충돌하면 [그림 8-28] 상태 전이표에 의해 공이 계속적으로 이동하는 프로그램을 작성해 보자.

또한 f키는 공 발사키로 사용하고 0, 1, 2, 3, 4, 5 키는 공의 방향을 설정하는 키로 사용한다. 그리고 공의 초기화는 r키로 설정하며 경계영역은 ( 0, 0 )에서부터 ( 79, 24 )까지로 한다.

 

[그림 8-29] 공의 초기 상태

 

[그림 8-30] 경계영역과 충돌

 

[그림 8-31] 튕기는 공

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int     nReady;     // 1 이면 준비 상태, 0이면 이동 상태

        int     nHP;        // 생명

          int    nX, nY;     // 좌표

        DIRECT   nDirect;    // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

BALL g_sBall;

int g_StateTable[4][6] = { // 벽과 충돌하게 되면 방향 상태

        {  3,  2, -1, -1, -1,  4 },

        { -1,  5,  4, -1, -1, -1 },

        { -1, -1,  1,  0,  5, -1 },

        { -1, -1, -1, -1,  2,  1 }

};

 

void Init()

{   // Note: 공 초기화

     g_sBall.nX = 38;

     g_sBall.nY = 12;

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.nReady = 1;

     g_sBall.MoveTime = 30;

}

 

// Note: 충돌 채크 함수   

int Collision( int nX, int nY )

{

        // Note: 위쪽 

    if( nY < 0 )

    {

       g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];           

       return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

     return 0;

}

 

void Update()

{

     clock_t CurTime = clock();

 

     if( g_sBall.nReady == 0 )

     {

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

        {

            g_sBall.OldTime = CurTime;

        

            switch( g_sBall.nDirect )

            {

             case TOP :

                      if( Collision( g_sBall.nX, g_sBall.nY - 1 ) == 0 )

                          g_sBall.nY--;                                                        break;

             case LEFT_TOP :

                      if( Collision( g_sBall.nX + 1, g_sBall.nY - 1 ) == 0 )

                      {

                          g_sBall.nX++;

                          g_sBall.nY--;

                      }

                      break;

             case LEFT_DOWN :

                      if( Collision( g_sBall.nX + 1, g_sBall.nY + 1 ) == 0 )

                      {

                         g_sBall.nX++;

                         g_sBall.nY++;

                      }

                       break;

             case DOWN : 

                      if( Collision( g_sBall.nX, g_sBall.nY + 1 ) == 0 )

                          g_sBall.nY++;

                       break;

             case RIGHT_DOWN :

                       if( Collision( g_sBall.nX - 1, g_sBall.nY + 1 ) == 0 )

                       {

                         g_sBall.nX--;

                         g_sBall.nY++;

                       }

                       break;

             case RIGHT_TOP :

                        if( Collision( g_sBall.nX - 1, g_sBall.nY - 1 ) == 0 )

                        {

                         g_sBall.nX--;

                         g_sBall.nY--;

                        }

                         break;

             }

        }

     }

}

 

void Render()

{

     ScreenClear();

        

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

            nKey = _getch();

 

            switch( nKey )

            {

             case 'f' : // 공 발사

                     g_sBall.nReady = 0;

                     break;

             case 'r' :  // 초기화

                     Init();

                     break;

             case '0' : // 공의 방향 설정

             case '1' :

             case '2' :

             case '3' :

             case '4' :

             case '5' :                  

                     g_sBall.nReady = 1;

                     g_sBall.nX = 38;

                     g_sBall.nY = 12;

                     g_sBall.nDirect = nKey - '0';

                     g_sBall.OldTime = clock();

                     break;

             }

                }               

          

          Update();

          Render();      

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-10] 튕기는 공

 

20행에서부터 25행까지는 [그림 8-28]의 배열을 실제 코드로 옮긴 부분으로 이 부분이 얼마나 더 세부적으로 나눠지는가에 따라 공은 더욱 사실적으로 이동한다.

 

38행의 Collision()함수는 좌표를 입력 받아 4개의 경계 영역과 충돌을 체크하는 함수이다.

충돌 순서는 경계영역의 인덱스인 0, 1, 2, 3의 순서로 공과 충돌 체크를 한다.

43행, 50행, 57행, 63행은 충돌 체크와 함께 상태 전이를 하는 부분으로 [표 8-7]의 상태 전이가 적용되어 있다.

 

82행, 86행, 93행, 100행, 104행, 111행의 조건식은 충돌 체크를 해서 충돌이 않되었을 때 이동하는 조건식이다.

 

- 공과 블록 충돌

 

 

공은 경계영역인 벽과도 충돌하지만 벽돌과 충돌하여 점수를 얻는다. 공이 벽돌과  충돌하면 벽돌은 소멸되지만 공은 튕겨져 이동을 해야 한다. 이때 벽돌의 충돌 경계는 상하만 존재하고 좌우 경계는 존재하지 않으며 공도 [그림 8-13]과 같이 좌우 이동을 하지 않으므로 충돌할 때 상하에 대해 튕기는 방향만 설정하면 된다.

 

 

[그림 8-32] 벽돌과 충돌하는 공의 이동 방향

 

[그림 8-32]와 같이 벽돌과 충돌한 공의 튕기는 방향을 상태 전이로 만들면 아래와 같다.

 

[그림 8-33] 상태전이 배열

 

앞서 제작한 공과 경계영역은 이차원 배열이었지만 [그림 8-33]의 경우 일차원 배열로 되어 있다. 그 이유는 벽돌 하나로 충돌되었을 때 튕기는 방향인 [그림 8-32]의 모든 방향을 정의할 수 있기 때문이다. 그래서 위의 배열값은 다음 상태를 나타내며 배열 인덱스는 공의 이동 방향을 나타낸다.

예를 들어 공이 2 방향이고 벽돌과 충돌했다면 다음 상태는 아래 [표 8-8]에 의해 1이 된다.

 

 

다음 상태 = array[ 공의 이동 방향 ];

 

[표 8-8] 다음 상태전이

 

[실습 예제 8-8]

 

[실습 예제 8-5]에서 30개의 벽돌 중에서 20개를 출력하는 프로그램을 작성해 보았다.

그리고 [실습 예제 8-7]에서는 경계 영역과 공이 충돌했을 때 공이 튕기는 프로그램도 작성해 보았다. 이제 이 두 예제를 합쳐서 공이 벽돌과 충돌하면 벽돌은 소멸되고 공이 튕기도록 프로그래밍해 보자.

공 방향은 0, 1, 2, 3, 4, 5로 초기 설정할 수 있으며 f는 공 발사, r 키는 공의 초기화인 준비 상태이다.

 

[그림 8-34] 초기 화면

 

[그림 8-35] 벽돌 깨기 화면(1)

 

 

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

int g_nBlockCount;  // 벽돌의 개수

 

int Collision( int nX, int nY )

{       

   int i, nCount = 0; // 충돌 개수

  

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

   {

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

        {

            if( g_sBlock[i].nY == nY )

            {

               if( ( g_sBlock[i].nX == nX || ( g_sBlock[i].nX + 1 ) == nX ) ||

                 (g_sBlock[i].nX == (nX+1) || ( g_sBlock[i].nX + 1 ) == (nX+1) ) )

               {

                 g_sBall.nDirect = g_BlockState[ g_sBall.nDirect ];

                  g_sBlock[i].nLife = 0;

                  nCount++;                               

                }

             }                  

         }

    }

 

    // 충돌 체크

    if( nCount != 0 )

        return 1;

 

    // Note: 위쪽 

    if( nY < 0 )

    {

        g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

 

     return 0;

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

     {

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

           ScreenPrint( g_sBlock[i].nX, g_sBlock[i].nY, "■" );

     }

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

[소스 8-11] 충돌 체크 함수

 

기본 소스는 [실습 예제 8-7]을 기본으로 하고 [실습 예제 8-5]에서 SetBlock()함수만을 기본 소스에 포함하면 벽돌과 공이 출력된다.

위의 소스는 벽돌과 공의 충돌 그리고 경계 영역의 충돌을 처리하는 함수로서 기존 [실습 예제 8-7]의 Collision()함수에 벽돌 충돌 부분인 7행부터 22행까지의 내용을 추가한 형태이다. 1행의 g_nBlockCount 변수는 출력하려는 벽돌 개수를 저장하는 변수이며 이 변수값 만큼 출력 함수인 Render() 함수에서 출력한다.

 

5행의 nCount 변수는 공이 충돌한 벽돌 개수를 나타낸다.

충돌한 벽돌 개수는 7행의 반복문을 이용하여 충돌한 벽돌을 찾아 nCount 값을 증가시키며 이 변수값에 따라 충돌 판정이 25행과 26행에 의해 결정된다.

이와 같이 하는 이유는 공이 아래와 같이 동시에 두 개의 벽돌과 충돌할 수 있기 때문에 처음부터 모든 벽돌과의 충돌 가능성을 찾는 것이다.

 

[그림 8-36] 공이 두 개의 벽돌과 충돌

 

- 공과 막대기 충돌

 

공과 막대기가 충돌하는 경우는 아래 [그림 3-37]과 같으며 막대기와 공의 충돌에 따른 이동 방향은 벽돌과 같으므로 벽돌과 막대기는 상태 전이 배열을 같이 공유할 수 있다.

 

[그림 8-37] 공과 막대기 충돌 예

 

[그림 8-38] 벽돌 상태와 공유되는 막대기의 상태

 

[실습 예제 8-9]

 

[실습 예제 8-4]에서 제작한 프로그램에 막대기와 공이 충돌하면 상태 배열에 따라 공이 튕기도록 프로그래밍해 보자.

이 프로그램에서 j 와 l 키는 막대기의 좌우 이동이며 k 키는 공 발사, u 키는 공 잡기가 된다. 또한 공의 방향은 0, 1, 2, 3, 4, 5 키로 공의 방향이 결정되도록 한다.

 

[그림 8-39] 초기 화면

 

[그림 8-40] 막대기와 충돌 후 튕기는 공(1)

 

[그림 8-41] 막대기와 충돌 후 튕기는 공(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

int g_BlockState[6] = { 3, 2, 1, 0, 5, 4 };

 

int Collision( int nX, int nY )

{       

    int i, nCount = 0; // 충돌 개수

 

    // 막대기와 충돌 체크

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

    {

        if( nY == g_sBar.nY )

        {

            if(( nX >= g_sBar.nX[0] && nX <= ( g_sBar.nX[2] + 1 ) ) ||

               (( nX + 1 ) >= g_sBar.nX[0] && ( nX + 1 ) <= ( g_sBar.nX[2] + 1 )))

            {

                 g_sBall.nDirect = g_BlockState[ g_sBall.nDirect ];

                 return 1;

            }

         }

    }

 

    // Note: 위쪽 

    if( nY < 0 )

    {

        g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

 

     return 0;

}

 

[소스 8-12] 충돌 체크 함수

 

[실습 예제 8-4]를 기본 소스로 하고 충돌 함수인 [소스 8-12]의 Collision() 함수를 추가하면 [실습 예제 8-9]를 완성하게 된다.

1행의 상태 전이 배열은 벽돌의 상태 전이 배열이지만 막대기도 같이 공유할 수 있으므로  위와 같이 선언한 것이며 구체적인 사용은 15행에서 실행된다.

 

 


STEP 04

 

[그림 8-42] 4단계 제작 로드맵

 

전체적인 흐름과 구조는 앞서 제작한 게임과 동일하다.

 

■ 스테이지 정보

 

벽돌 깨기 게임에서 필요한 스테이지 정보는 다음과 같다.

 

 

① 벽돌 개수

② 제한 시간

 

[표 8-9] 스테이지 정보

 

이를 구조체로 정의하면 다음과 같다.

 

 

typedef struct _STAGE_INFO

{

      int      nBlockCount; // 벽돌 개수

      clock_t  LimitTime;  // 제한 시간

} STAGE_INFO;

 

[소스 8-13] 스테이지 정보 정의

 

■ 게임 진행 제어와 기타

 

게임 진행 상태는 6장 두더지 잡기 게임의 [표 6-6]과 동일하며 전체 흐름도 동일하다.

만약 임의의 진행 상태가 추가된다면 이 게임 진행 상태에 일부가 추가되는 정도이므로 앞서 살펴본 내용을 이해하고 있다면 대부분의 게임에 적용할 수 있을 것이다.

 

여기까지 벽돌 깨기 게임을 살펴보았다.

벽돌 깨기 게임에서 사용된 상태전이는 다른 게임에서도 많이 사용되는 기법이므로 더욱 신경을 써서 이해하도록 하자. 그리고 이 상태 전이는 뒤에 나오는 Sogo 게임에서 적 캐릭터와 보스 캐릭터를 제어할 때 패턴(pattern)으로 사용된다.

여기까지의 내용을 기반으로 전체 게임을 제작해 보자.

 

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

[출처] https://nowcampus.tistory.com/entry/8%EC%9E%A5?category=655340

 

 

 

 

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

 

(CGP) 8장. 벽돌깨기 게임

벽돌깨기 게임에서는 개체의 속성 정의와 상태전이에 따른 공의 이동밯향의 결정이 중요한다.

특히 여기서의 상태전이는 인공지능의 한 영역에 해당이 되며 구현방법이 단순하지만 결과가 상당히

 

 

다양함을 알 수 있다.

 

슈퍼맨 유튜브 동영상 강의 주소

 

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

(2) http://youtu.be/8ALFzKqrgZE

(3) http://youtu.be/iTjp53UXJOY
(4) http://youtu.be/2s0mXDthke8

(5) http://youtu.be/4JY3lXR_u8Y

(6) http://youtu.be/5fLLgtEfR0s

(7) http://youtu.be/MbYmAiAxYtQ

 

 

  


01 기획

 

■ 스토리

 

제한 시간 안에 화면에 출력된 모든 벽돌을 맞추어라.

 

■ 게임 방식

 

게임을 시작하면 막대기와 공이 같이 움직인다. 공을 발사한 후에는 막대기로 공을 받아 튕기게 하거나 i 키를 이용하여 공을 잡아 다시 발사하는 방법으로 벽돌을 맞춘다. 이때 사용하는 키는 j(좌), l(우), k(공 발사), i(공 잡기)이다.

공을 잡았을 때는 입력키 1, 2, 3에 의해 차례대로 왼쪽, 위쪽, 오른쪽으로 발사 방향을 결정한다.

 

■ 제한 사항

 

공은 3개까지 제공되며 스테이지 난이도는 벽돌 개수와 제한 시간으로 조절한다.

 

■ 기획 화면

 

[그림 8-1] 기획 화면

 

 


02 실행 화면

 

[그림 8-2] 게임 메인 화면

 

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

 

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

 

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

 

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

 

[그림 8-7] 게임 종료 화면

 

 


03 게임 제작 로드맵

 

벽돌 깨기 게임은 슛 골인 게임과 유사한 부분이 많으므로 [그림 8-8]의 제작 로드맵을 참고하면서 제작해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

[그림 8-8] 제작 로드맵

 

 


04 단계별 프로그래밍

 

 


STEP 01

 

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

 

■ 공

 

벽돌깨기 게임의 공을 슈팅 게임과 비교한다면 공은 총알과 같은 역할을 한다.

슈팅 게임에서 총알은 적과 충돌하거나 경계영역에 닿으면 사라지지만 벽돌깨기 게임에서 공은 막대기로 받지 못하는 경우를 제외하고는 항상 게임 안에 존재한다.

또한 이 게임은 공을 벽돌 또는 경계영역과 충돌시켜 게임을 진행하므로 공의 이동 방향은 게임의 중요한 요소가 된다.

 

- 속성

 

공의 기본적인 속성은 좌표, 생명, 이동 시간의 간격, 이전 이동 시각을 들 수 있다. 공은 좌표를 따라 출력과 이동을 하게 되며 막대기가 공을 3번 놓치게 되면 게임이 종료되므로 생명이라는 속성이 있다.

또한 공의 이동 속도를 조절하기 위해 이동 시간의 간격과 이전 이동 시각은 반드시 있어야 하는 속성 중 하나이다.

 

이와 같은 기본 속성 외에 추가되는 속성은 바로 공의 이동 방향이다. 앞서 설명했지만 공이 막대기와 충돌하면 공의 방향에 따라 적절한 방향으로 튕겨 나가야 한다.

이를 구현하기 위해서 공의 이동 방향을 [그림 8-10]과 같이 정의한다.

비록 [그림 8-10]에서 공의 전체 이동 방향은 여섯 방향이지만 이것을 더욱 세분화한다면 공은 더욱 다양한 방향으로 이동하게 된다.

 

[그림 8-10] 공의 이동 방향

 

 

① 공 상태 ( 준비 상태, 이동 상태 )

② 생명

③ 좌표

④ 이동 방향

⑤ 이동 시간 간격

⑥ 이전 이동 시각

 

[표 8-1] 공 속성

 

위의 속성 중에 공 상태에 대해서 살펴보자.

공은 두 가지 상태를 가진다.

첫째, 준비 상태이다.

이 상태에서 공은 막대기와 동일한 방향으로 좌우 이동을 같이 하며 플레이어가 공을 잡은 상태가 된다. 그래서 공은 플레이어에 의해 다시 발사될 수 있는 상태가 된다.

 

둘째, 이동 상태이다.

이 상태는 현재 공의 이동 방향에 따라 공의 좌표를 증감하는 상태를 말하며 벽돌과 경계 영역과의 충돌을 체크하며 다음 이동 방향을 결정하기 위한 상태이다.

 

6방향에 대한 enum형과 공의 속성을 구조체로 정의하면 다음과 같다.

 

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

 

typedef struct _BALL

{

        int nReady; // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;   // 생명

          int  nX, nY; // 좌표

        DIRECT  nDirect; // 이동 방향

        clock_t  MoveTime; // 이동 시간 간격

        clock_t  OldTime; // 이전 이동 시각

} BALL;

 

[소스 8-1] 공 속성 정의

 

■ 막대기

 

 

- 속성

 

막대기는 블록 3개를 연결하여 구성하며, 3개의 블록은 x 좌표만 차이가 있을 뿐 y 좌표는 모두 동일하다. 그러므로 막대기는 아래와 같은 속성을 가진다.

 

 

① x 좌표 3개

② y 좌표

 

[표 8-2] 막대기 속성

 

막대기 속성 중에서 x 좌표 3개는 모두 동일한 데이터형을 가지므로 배열로 선언한다.

배열로 선언된 x 좌표값은 반복문과 인덱스를 통해 쉽게 읽을 수 있으므로 공과 충돌 체크할 때 유용하게 사용된다.

이와 같은 사항을 구조체로 정의하면 아래와 같다.

 

typedef struct _BAR

{

       int nX[3];

       int nY;         

} BAR;

 

[소스 8-2] 막대기 속성 정의

 

■ 키보드 처리

 

막대기의 이동과 공 잡기와 발사에 대한 키를 정의하면 다음과 같다.

 

 

이동키

역할

j

왼쪽으로 이동

l

오른쪽으로 이동

k

공 발사 ( 공 이동 시작 )

i

공 잡기 (막대기와 같이 이동하는 상태 )

 

[표 8-3] 키 정의

 

위의 키 입력에 따른 동작을 살펴보자.

j와 l 키는 좌우로 이동하는 키이므로 막대기의 x 좌표를 증감해주면 간단히 해결된다.

이에 대한 처리는 앞서 제작한 게임에서도 많이 다루었던 부분이다.

 

k 키는 공을 발사하는 키로 사용되는데 공이 발사된다는 것은 공의 이동을 의미한다.

j와 l키 입력이 있을 때와 k 키 입력이 있을 때 공과 막대기의 이동에는 어떤 차이점은 무엇일까? j와 l키 입력은 플레이어에 의해 막대기 이동을 시키지만 k 키 입력은 공 스스로가 일정한 속도로 이동을 해야 한다. 그래서 k 키가 입력되면 이 곳에서 공의 방향과 이전 이동 시각에 대한 초기화, 공의 상태전이를 통해 공이 스스로 이동할 수 있게 설정해야 한다.

 

i 키는 공을 잡게 하는 키 입력이다. 공을 잡는다는 것은 공과 막대기가 충돌했다는 것이며  또한 공의 상태를 준비 상태로 전이시켜 막대기의 x 좌표 증감값만큼 공의 좌표를 증감해야 한다는 것을 의미한다.

 

[실습 예제 8-1]

 

위의 [표 8-3]에 정의한 키가 입력되면 아래 그림과 같이 출력되도록 게임 프레임워크 안에서 프로그래밍해 보자.

 

[그림 8-11] 키 입력에 따른 메시지 출력(1)

 

[그림 8-12] 키 입력에 따른 메시지 출력(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

#include <stdio.h>

#include <conio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

char *g_strMessage[] = { "왼쪽 이동", "오른쪽 이동", "공 발사", "공 잡기" };

int g_nIndex = -1;  // 출력 메시지 인덱스, -1일 때 메시지는 출력에서 제외

 

void Init()

{

}

 

void Update()

{

}

 

void Render()

{       

     ScreenClear();

        

     if( g_nIndex >= 0 )

        ScreenPrint( 10, 10, g_strMessage[g_nIndex] );

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        nKey = _getch();

        switch( nKey )

        {

        case 'j' :      

                g_nIndex = 0;

                break;

        case 'l' :

                g_nIndex = 1;

                break;

        case 'k' :

                g_nIndex = 2;

                break;

        case 'i' :

                g_nIndex = 3;

                break;

        }

 

        Update();

        Render();       

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 8-3] 키 입력에 따른 출력

 

■ 이동 및 출력

 

- 공의 이동 및 출력

 

공은 아래 [그림 8-13]과 같이 총 6방향으로 이동한다.

 

 [그림 8-13] 공의 이동 방향

 

각 이동 방향으로 이동하기 위해서는 방향에 따른 x, y 좌표의 증감이 필요하다.

아래의 [표 8-4]는 방향에 따른 좌표의 증감을 나타낸다.

 

 

이동 방향

증감 부분

0

y 값 감소

1

x 값 증가, y 값 감소

2

x 값 증가, y 값 증가

3

y 값 증가

4

x 값 감소, y 값 증가

5

x 값 감소, y 값 감소

 

[표 8-4] 이동 방향과 증감

 

위의 [표 8-4]에 따른 증감과 이동 시간 간격은 공의 속도를 결정하는 요소가 된다.

 

[실습 예제 8-2]

 

숫자 0, 1, 2, 3, 4, 5 키가 입력되면 위의 [표 8-4]의 증감에 따라 공이 이동하게 프로그래밍해 보자. 그리고 경계 영역과 공이 닿으면 공은 준비 상태가 되며 f 키는 이동 상태가 된다. 

 

      

[그림 8-14] 준비 상태의 공

 

[그림 8-15] 우측 상단으로 이동하는 공 (LEFT_TOP)

 

[그림 8-16] 좌측 하단으로 이동하는 공 (RIGHT_DOWN)

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int nReady;          // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;            // 생명

          int  nX, nY;         // 좌표

        DIRECT  nDirect;     // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

BALL g_sBall;

 

void Init()

{   // Note: 공 초기화

     g_sBall.nX = 38;

     g_sBall.nY = 12;

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.nReady = 1;

     g_sBall.MoveTime = 50;

}

 

void Update()

{

     clock_t CurTime = clock();

 

      if( g_sBall.nReady == 0 )

      {

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

        {

            g_sBall.OldTime = CurTime;

        

            switch( g_sBall.nDirect )

            {

              case TOP :

                         g_sBall.nY--;

                         break;

              case LEFT_TOP :

                         g_sBall.nX++;

                         g_sBall.nY--;

                         break;

              case LEFT_DOWN :

                         g_sBall.nX++;

                         g_sBall.nY++;

                         break;

              case DOWN :

                         g_sBall.nY++;

                         break;

              case RIGHT_DOWN :

                         g_sBall.nX--;

                         g_sBall.nY++;

                         break;

              case RIGHT_TOP :

                         g_sBall.nX--;

                         g_sBall.nY--;

                         break;

              }

         }

        

         // Note: 경계 영역과 충돌

    if( g_sBall.nX < 0 || g_sBall.nX > 78 ||  g_sBall.nY > 24 || g_sBall.nY < 0 )

   {

             g_sBall.nReady = 1;

             g_sBall.nX = 38;

             g_sBall.nY = 12;

             g_sBall.nDirect = TOP; // 기본 상태

          }

     }

}

 

void Render()

{

     ScreenClear();

        

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

         if( _kbhit() )

        {

           nKey = _getch();

                        

           if( nKey == 'f' )

           {

               g_sBall.nReady = 0;  // 이동 상태

            }else{

               nDirect = nKey - '0';  // 0에서부터 5까지의 정수형으로 변환

               if( nDirect >= 0 && nDirect <= 5 )

               {

                 g_sBall.nReady = 1;

                 g_sBall.nX = 38;

                 g_sBall.nY = 12;

                 g_sBall.nDirect = nDirect;

                 g_sBall.OldTime = clock();

                }

            }

         }

          

           Update();

           Render();       

        }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-4] 키 입력에 따른 공의 이동

 

43행에서부터 64행까지는 공의 이동 방향에 따른 증감을 나타내며 [표 8-4]를 코드로 옮긴 것이다.

 

69행을 보면 경계 영역과 공의 충돌 조건식이 나와 있다. 가로 0에서부터 79컬럼이며 세로 0에서부터 24컬럼이다.

특히 오른쪽 경계 영역 충돌을 체크할 때 g_sBall.nX > 78로 한 것은 공은 특수 문자로 2컬럼의 크기를 가지기 때문이다.

 

- 막대기 속성과 이동

 

막대기 이동은 일반적인 이동과 같이 좌우 증감에 의해 이동한다.

여기서도 막대기는 특수 문자로 출력되므로 문자당 2컬럼의 크기를 가지고 좌표를 초기화할 때 크기 부분을 고려해야 한다.

또한 키 입력이 있을 때마다 즉각적으로 반응하는 것이 아니라 일정한 시간 간격에 의해 반응하여 스테이지마다 게임성을 다르게 할 수도 있다.

이와 같은 내용은 이미 앞서 제작한 슛 골인 게임에서 골대와 골대 라인을 구현할 때 다루었던 내용이다. 여기까지 설명한 막대기의 속성을 정리하고 구조체로 정의하면 아래 [표 8-5]와 같다. 

 

 

① x 좌표 3개

② y좌표

③ 이전 이동 시각

④ 이동 시간 간격

 

[표 8-5] 막대기 속성

 

 

typedef struct _BAR

{

      int nX[3];

      int nY;            

      clock_t OldTime;

      clock_t MoveTime;

} BAR;

 

[소스 8-5] 막대기의 속성 정의

 

[실습 예제 8-3]

 

아래 [그림 8-17]과 같이 막대기를 구성하는 3개의 블록이 j키와 l키에 의해 좌우로 이동하는 프로그램을 작성해 보자. 여기서 경계영역은 ( 0, 0 )에서부터 ( 79, 24 )로 한다.

 

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

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _BAR

{

      int nX[3];

      int nY;            

      clock_t OldTime;

      clock_t MoveTime;

} BAR;

 

BAR  g_sBar;

 

void Init()

{

     g_sBar.nY = 20;

     g_sBar.nX[0] = 30;

     g_sBar.nX[1] = 32;

     g_sBar.nX[2] = 34;

     g_sBar.MoveTime = 100;

     g_sBar.OldTime= clock();

}

 

void Update()

{

}

 

void Render()

{

     int i;

     ScreenClear();

        

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

     {

        ScreenPrint( g_sBar.nX[i], g_sBar.nY, "▣" );

     }

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

    clock_t CurTime;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

            nKey = _getch();

 

            switch( nKey )

            {

             case 'j' :

                   CurTime = clock();

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

                   {

                       g_sBar.OldTime = CurTime;

                       if( g_sBar.nX[0] > 0 ) // 경계 영역 충돌 체크

                       {

                         g_sBar.nX[0]--;

                         g_sBar.nX[1]--;

                         g_sBar.nX[2]--;         

                       }

                     }

                     break;

              case 'l' :

                     CurTime = clock();

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

                     {                                  

                        g_sBar.OldTime = CurTime;

                        if( g_sBar.nX[2] <= 77 )  // 경계 영역 체크

                        {

                            g_sBar.nX[0]++;

                            g_sBar.nX[1]++;

                            g_sBar.nX[2]++;              

                        }

                      }

                      break;

              }

          }

 

          Update();

          Render();      

     }

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-6] 막대기 이동

 

68행과 81행은 좌우 경계 영역과 충돌 체크를 하기 위한 조건식이다.

68행에서 블록의 g_sBar.nX[0] 좌표값은 블록의 출력 기준 좌표이고 경계 영역과 충돌이 되는 끝 좌표에 해당이 되므로 g_sBar.nX[0]로 왼쪽 경계 영역과 충돌 체크를 하면 된다.

반면 81행의 g_sBar.nX[2] 좌표값은 블록의 출력 기준 좌표일 뿐 실제 경계 영역과 충돌되는 끝 좌표는 g_sBar.nX[2] + 1의 좌표가 된다.

그래서 g_sBar.nX[2]와 경계 영역을 비교할 때에는 이런 부분을 고려한 조건식을 적용해야 한다. 81행 조건식의 경우 g_sBar.nX[2]의 값이 77일 때 85행에 의해 g_sBar.nX[2]는 78이되고 g_sBar.nX[2] + 1은 79 가 되므로 경계영역 이상의 값으로는 증가하지 않는다.

 

- 키 입력에 따른 막대기 이동과 공 잡기

 

앞에서 공 이동과 키 입력에 따른 막대기의 이동 소스까지 살펴보았다. 이 두 소스를 이용하면 키 입력에 따른 막대기의 이동과 공을 출력할 수 있다.

 

키 입력에 따라 공을 잡을 수 있는 경우는 아래 [그림 8-18]과 같이 공이 막대기의 x 좌표 범위 안에 있고 막대기의 y좌표 보다 공이 1 적은 위치에 있을 때이다.

[그림 8-18] 공 잡기

 

[실습 예제 8-4]

 

앞에서 살펴본 [실습 8-2]와 [실습 8-3]을 합치면 막대기와 공이 이동하면서 공을 발사를 할 수 있는 프로그램을 제작할 수 있다. 여기에서 j 와 l 키는 막대기의 좌우 이동이며 k 키는 공 발사, 그리고 경계 영역과 충돌하면 공은 준비 상태가 된다.

그리고 0, 1, 2, 3, 4, 5 키로 공의 방향이 결정되는 프로그램을 작성해 보자.

 

[그림 8-19] 준비 상태

 

[그림 8-20] LEFT_TOP 방향으로 이동

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int nReady;          // 1 이면 준비 상태, 0이면 이동 상태

        int  nHP;            // 생명

        int  nX, nY;         // 좌표

        DIRECT  nDirect;     // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

typedef struct _BAR

{

        int nX[3];

        int nY;          

        clock_t OldTime;

        clock_t MoveTime;

} BAR;

 

BAR  g_sBar;

BALL g_sBall;

 

void Init()

{

     g_sBar.nY = 13;

     g_sBar.nX[0] = 30;

     g_sBar.nX[1] = 32;

     g_sBar.nX[2] = 34;

     g_sBar.MoveTime = 80;

     g_sBar.OldTime= clock();

 

     g_sBall.nX = g_sBar.nX[1];

     g_sBall.nY = g_sBar.nY - 1;

     g_sBall.nReady = 1; // 준비 상태

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.MoveTime = 100;

}

 

void Update()

{

     clock_t CurTime = clock();

 

     if( g_sBall.nReady == 0 )

     {

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

        {

            g_sBall.OldTime = CurTime;

                

            switch( g_sBall.nDirect )

            {

             case TOP :

                      g_sBall.nY--;

                      break;

             case LEFT_TOP :

                      g_sBall.nX++;

                      g_sBall.nY--;

                      break;

             case LEFT_DOWN :

                      g_sBall.nX++;

                      g_sBall.nY++;

                      break;

             case DOWN :

                      g_sBall.nY++;

                      break;

             case RIGHT_DOWN :

                      g_sBall.nX--;

                      g_sBall.nY++;

                      break;

             case RIGHT_TOP :

                      g_sBall.nX--;

                      g_sBall.nY--;

                      break;

             }

         }

        

         // Note: 경계 영역과 충돌

     if( g_sBall.nX < 0 || g_sBall.nX > 78 ||  g_sBall.nY > 24 || g_sBall.nY < 0 )

         {

             g_sBall.nReady = 1;

             g_sBall.nX = g_sBar.nX[1];

             g_sBall.nY = g_sBar.nY - 1;                                   

             g_sBall.nDirect = TOP; // 기본 상태

          }

      }else{

             g_sBall.nX = g_sBar.nX[1];

           g_sBall.nY = g_sBar.nY  - 1;

      }   

}

 

void Render()

{

     int i;

     ScreenClear();

 

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

     {

        ScreenPrint( g_sBar.nX[i], g_sBar.nY, "▣" );

     }

 

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

    clock_t CurTime;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

 

           switch( nKey )

           {

           case 'k' :

                 g_sBall.nReady = 0;  // 이동 상태

                 g_sBall.OldTime = clock();

                 break;

           case 'j' :

                 CurTime = clock();

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

                 {

                     g_sBar.OldTime = CurTime;

                     if( g_sBar.nX[0] > 0 ) // 경계 영역 충돌 체크

                     {

                        g_sBar.nX[0]--;

                        g_sBar.nX[1]--;

                        g_sBar.nX[2]--;          

                      }

                   }

                   break;

            case 'l' :

                     CurTime = clock();

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

                   {                                    

                        g_sBar.OldTime = CurTime;

                        if( g_sBar.nX[2] <= 77 )  // 경계 영역 체크

                        {

                          g_sBar.nX[0]++;

                          g_sBar.nX[1]++;

                          g_sBar.nX[2]++;         

                        }

                      }

                      break;

             case '0' :

             case '1' :

             case '2' :

             case '3' :

             case '4' :

             case '5' :

                   nDirect = nKey - '0';  // 0에서부터 5까지의 정수형으로 변환

                   g_sBall.nReady = 1;

                   g_sBall.nX = g_sBar.nX[1];

                   g_sBall.nY = g_sBar.nY - 1;

                   g_sBall.nDirect = nDirect;

                   g_sBall.OldTime = clock();

                   break;

             }

        }  

 

        Update();

        Render();       

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-7] 이동하는 공과 막대기

 

 


STEP 02

 

[그림 8-21] 2단계 제작 로드맵

 

 

■ 벽돌

 

- 속성

 

이제까지 제작해온 캐릭터들을 살펴보자.

슛 골인 게임에서 공과 골대는 게임이 종료될 때까지 언제나 살아 있는 캐릭터였다.

짝 맞추기 게임에서 카드 30개도 항상 고정적으로 출현하는 캐릭터였으며 두더지 잡기 게임의 두더지도 언제나 살아 있었다.

하지만 이번 벽돌 깨기 게임에서 벽돌은 공과 충돌함과 동시에 소멸 상태를 유지해야 한다.

그래서 벽돌 속성에는 생명이라는 속성을 두어 소멸 상태와 살아 있는 상태를 구분한다.

여기서 소멸 상태란 출력에서 제외되는 상태를 말하며 살아 있는 상태는 그 반대의 상태를 말한다. 이 속성을 생명이라고 하며 이 생명에는 살아 있는 상태값과 소멸 상태값을 저장한다.

이와 같은 사항을 속성으로 정의하면 다음과 같다.

 

 

① 생명

② 좌표 ( x, y )

 

[표 8-6] 벽돌 속성

 

 

typedef struct _BLOCK

{

        int  nLife;

        int  nX, nY;

} BLOCK;

 

[소스 8-8] 벽돌 속성 정의

 

■ 벽돌 좌표 생성 및 출력

 

벽돌은 스테이지마다 중복되지 않는 임의의 좌표를 받아 설정해야 한다. 중복되지 않게 좌표를 생성하여 설정하는 방법은 앞서 제작한 게임에서도 다루었던 내용이다.

물론 rand() 함수를 이용하는 방법 외에도 다양한 방법이 있겠지만 벽돌깨기 게임에서는  이 방법을 이용한다.

 

[실습 예제 8-5]

 

30개의 벽돌이 중복되지 않게 임의의 개수만큼 출력되도록 게임 프레임워크 안에서 프로그래밍해 보자. 임의의 좌표는 rand() 함수를 이용하고 출력 좌표 범위는 ( 0, 0 )에서부터 ( 79, 10 )까지로 한다.

아래의 [그림 8-22]는 30개의 벽돌 중에서 20개만을 출력한 예이다.

 

[그림 8-22] 30개의 벽돌 중에서 20개만 출력

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _BLOCK

{

       int  nLife;

       int  nX, nY;

} BLOCK;

 

BLOCK g_sBlock[30];

 

int Search( int nEnd, int nX, int nY )

{

     int i;

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

     {

        if( g_sBlock[i].nY == nY )

        {

           if( g_sBlock[i].nX == nX || ( g_sBlock[i].nX + 1 ) == nX ||

                g_sBlock[i].nX == nX + 1 || (g_sBlock[i].nX + 1) == nX + 1 )

                return 1; // 같은 것이 있으면

        }

      }

      return 0; // 같은 것이 없으면

}

 

void SetBlock( int nBlockCount )

{

     int nX, nY, i;

 

     srand( (unsigned int )time(NULL) );

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

     {

        g_sBlock[i].nLife = 1;

        

        while( 1 )

        {              

             nX = rand() % 79; // 0 ~ 78 범위 안에서

             nY = rand() % 11; // 0 ~ 10 까지의 범위

                

               if( Search( i, nX, nY ) == 0 )

               {

                   g_sBlock[i].nX = nX;

                   g_sBlock[i].nY = nY;

                   break;

               }    

         }

     }

}

 

void Init()

{

     SetBlock( 20 );

}

 

void Update()

{

}

 

void Render()

{

     int i;

     ScreenClear();

 

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

     {

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

           ScreenPrint( g_sBlock[i].nX, g_sBlock[i].nY, "■" );

     }

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

         Update();

         Render();        

    }

 

    Release();

    ScreenRelease();     

    return 0;

}

 

[소스 8-9] 벽돌의 좌표 설정 및 출력

 

14행의 Search()함수는 중복되는 좌표가 현재의 벽돌 안에 있는지를 조사하는 함수이다.

첫 번째 매개변수인 nEnd는 좌표 조사를 하는 벽돌 배열의 끝 인덱스를 지정한다.

 

38행의 반복문은 47행의 break를 만나기까지 무한 반복하며 이 무한 반복을 종료하게 만드는 조건은 바로 43행에서 중복되지 않는 좌표를 구했을 때이다.

 

 


STEP 03

 

[그림 8-23] 3단계 제작 로드맵

 

■ 충돌

 

벽돌 깨기 게임에서 출력과 이동은 앞에서 제작한 게임과 유사하지만 공이 경계 영역 또는 벽돌과 충돌하면 공은 다른 방향으로 튕겨 나가야 한다.

이때 공이 다른 방향으로 튕겨나가도록 방향에 대한 상태 전이가 있어야 한다.

 

- 상태 전이

 

상태 전이란 현재 상태가 외부 요소에 의해 바뀌는 것을 말한다. 예를 들면, 주인공 캐릭터가 몹의 공격 영역 안에 들어오면 대부분의 몹은 공격 상태로 바뀌는데 이것을 상태 전이라고 한다. 상태 전이를 만드는 방법에는 여러 가지가 있지만 그 중 가장 일반적으로 이차원 배열을 많이 활용한다. 이에 대한 사항은 아래에 소개되는 공의 이동 방향에서 살펴보도록 하자.

 

- 공의 이동 방향과 상태 전이표 활용

 

공의 이동 방향이 바뀔 때는 경계 영역과 충돌하거나 벽돌과 충돌했을 때이다.

경계 영역 또는 벽돌과 충돌 했을 때 이동 방향의 설정은 둘 다 같으므로 여기서는 경계 영역에 대한 사항만 설명하겠다.

 

먼저 공이 충돌했을 때 이동 방향에 따라 튕기는 방향을 살펴보면 [그림 8-24]와 같다.

 

[그림 8-24] 공의 이동 방향과 튕기는 방향

 

상태 전이는 앞서 설명했듯이 현재의 상태가 외부의 상태에 의해서 변화되는 것이다.

그렇다면 위의 [그림 8-24]에서 현재 공의 상태가 0 상태일 때 외부의 상태는 과연 무엇일까? 바로 경계 영역이 된다. 경계 영역은 현재 위쪽, 아래쪽, 왼쪽, 오른쪽의 경계 영역을 가지고 있다. 공이 0 상태일 때 만날 수 있는 경계 영역은 위쪽 경계 영역외에는 없다.

즉 공이 0 상태에서 위쪽 경계 영역을 만나면 다음 상태인 3 상태로 전이를 해주면 공은 튕기게 된다. 이때 경계 영역의 인덱스를 시계방향으로 위쪽은 0, 오른쪽은 1, 아래쪽은 2, 왼쪽은 3으로 한다. 경계 영역의 인덱스를 행으로 하고 공의 상태를 열로 할 때 이차원 배열을 도표로 나타내면 아래 [그림 8-25]와 같다.

 

[그림 8-25] 상태 전이표

 

먼저 간단한 예로 경계 영역 인덱스가 1일 때 공의 이동 방향 상태 전이를 생각해 보자.

위의 [그림 8-24]를 보면 공의 이동 방향 인덱스가 1일 때 튕기는 방향은 5이다.

이 사항을 상태전이표에 적용하면 다음과 같다.

 

[그림 8-26] 상태전이 적용

 

이와 같은 적용 방법을 상태 전이표에 전부 적용하면 아래와 같다.

 

[그림 8-27] 공의 이동 방향과 튕기는 방향이 적용된 이차원 배열

 

위와 같이 도표를 만들다 보면 경우에 따라 방향이 정해질 수 없는 경우가 있다.

이 경우에는 현재 상태 전이표에 의미 없는 값을 넣어 이동에서 제외시킨다.

아래의 [그림 8-28]을 보면 -1이 설정된 것을 볼 수 있다.

 

 

[그림 8-28] -1이 적용된 상태전이표

 

이제 위의 [그림 8-28]의 상태 전이표를 사용해 보자.

현재 공이 1 상태이고 경계영역 1과 충돌 했을 때 다음 상태는 경계영역 인덱스인 행과 공의 이동 방향 인덱스인 열로 [표 8-7]에 의해 다음 상태를 구할 수 있다.

 

 

다음 상태 = array[ 경계영역 인덱스 ][ 이동 방향 인덱스 ];

 

[표 8-7] 다음 상태 전이

 

[실습 예제 8-7]

 

[실습 예제 8-2]의 소스를 이용하여 경계영역과 충돌하면 [그림 8-28] 상태 전이표에 의해 공이 계속적으로 이동하는 프로그램을 작성해 보자.

또한 f키는 공 발사키로 사용하고 0, 1, 2, 3, 4, 5 키는 공의 방향을 설정하는 키로 사용한다. 그리고 공의 초기화는 r키로 설정하며 경계영역은 ( 0, 0 )에서부터 ( 79, 24 )까지로 한다.

 

[그림 8-29] 공의 초기 상태

 

[그림 8-30] 경계영역과 충돌

 

[그림 8-31] 튕기는 공

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _DIRECT { TOP, LEFT_TOP, LEFT_DOWN, DOWN, RIGHT_DOWN,

                          RIGHT_TOP } DIRECT;

typedef struct _BALL

{

        int     nReady;     // 1 이면 준비 상태, 0이면 이동 상태

        int     nHP;        // 생명

          int    nX, nY;     // 좌표

        DIRECT   nDirect;    // 이동 방향

        clock_t  MoveTime;   // 이동 시간 간격

        clock_t  OldTime;  // 이전 이동 시각

} BALL;

 

BALL g_sBall;

int g_StateTable[4][6] = { // 벽과 충돌하게 되면 방향 상태

        {  3,  2, -1, -1, -1,  4 },

        { -1,  5,  4, -1, -1, -1 },

        { -1, -1,  1,  0,  5, -1 },

        { -1, -1, -1, -1,  2,  1 }

};

 

void Init()

{   // Note: 공 초기화

     g_sBall.nX = 38;

     g_sBall.nY = 12;

     g_sBall.nDirect = TOP;

     g_sBall.OldTime = clock();

     g_sBall.nReady = 1;

     g_sBall.MoveTime = 30;

}

 

// Note: 충돌 채크 함수   

int Collision( int nX, int nY )

{

        // Note: 위쪽 

    if( nY < 0 )

    {

       g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];           

       return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

     return 0;

}

 

void Update()

{

     clock_t CurTime = clock();

 

     if( g_sBall.nReady == 0 )

     {

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

        {

            g_sBall.OldTime = CurTime;

        

            switch( g_sBall.nDirect )

            {

             case TOP :

                      if( Collision( g_sBall.nX, g_sBall.nY - 1 ) == 0 )

                          g_sBall.nY--;                                                        break;

             case LEFT_TOP :

                      if( Collision( g_sBall.nX + 1, g_sBall.nY - 1 ) == 0 )

                      {

                          g_sBall.nX++;

                          g_sBall.nY--;

                      }

                      break;

             case LEFT_DOWN :

                      if( Collision( g_sBall.nX + 1, g_sBall.nY + 1 ) == 0 )

                      {

                         g_sBall.nX++;

                         g_sBall.nY++;

                      }

                       break;

             case DOWN : 

                      if( Collision( g_sBall.nX, g_sBall.nY + 1 ) == 0 )

                          g_sBall.nY++;

                       break;

             case RIGHT_DOWN :

                       if( Collision( g_sBall.nX - 1, g_sBall.nY + 1 ) == 0 )

                       {

                         g_sBall.nX--;

                         g_sBall.nY++;

                       }

                       break;

             case RIGHT_TOP :

                        if( Collision( g_sBall.nX - 1, g_sBall.nY - 1 ) == 0 )

                        {

                         g_sBall.nX--;

                         g_sBall.nY--;

                        }

                         break;

             }

        }

     }

}

 

void Render()

{

     ScreenClear();

        

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nDirect;

 

    ScreenInit();

    Init();

 

    while( 1 )

    {

        if( _kbhit() )

        {

            nKey = _getch();

 

            switch( nKey )

            {

             case 'f' : // 공 발사

                     g_sBall.nReady = 0;

                     break;

             case 'r' :  // 초기화

                     Init();

                     break;

             case '0' : // 공의 방향 설정

             case '1' :

             case '2' :

             case '3' :

             case '4' :

             case '5' :                  

                     g_sBall.nReady = 1;

                     g_sBall.nX = 38;

                     g_sBall.nY = 12;

                     g_sBall.nDirect = nKey - '0';

                     g_sBall.OldTime = clock();

                     break;

             }

                }               

          

          Update();

          Render();      

     }

 

     Release();

     ScreenRelease();     

     return 0;

}

 

[소스 8-10] 튕기는 공

 

20행에서부터 25행까지는 [그림 8-28]의 배열을 실제 코드로 옮긴 부분으로 이 부분이 얼마나 더 세부적으로 나눠지는가에 따라 공은 더욱 사실적으로 이동한다.

 

38행의 Collision()함수는 좌표를 입력 받아 4개의 경계 영역과 충돌을 체크하는 함수이다.

충돌 순서는 경계영역의 인덱스인 0, 1, 2, 3의 순서로 공과 충돌 체크를 한다.

43행, 50행, 57행, 63행은 충돌 체크와 함께 상태 전이를 하는 부분으로 [표 8-7]의 상태 전이가 적용되어 있다.

 

82행, 86행, 93행, 100행, 104행, 111행의 조건식은 충돌 체크를 해서 충돌이 않되었을 때 이동하는 조건식이다.

 

- 공과 블록 충돌

 

 

공은 경계영역인 벽과도 충돌하지만 벽돌과 충돌하여 점수를 얻는다. 공이 벽돌과  충돌하면 벽돌은 소멸되지만 공은 튕겨져 이동을 해야 한다. 이때 벽돌의 충돌 경계는 상하만 존재하고 좌우 경계는 존재하지 않으며 공도 [그림 8-13]과 같이 좌우 이동을 하지 않으므로 충돌할 때 상하에 대해 튕기는 방향만 설정하면 된다.

 

 

[그림 8-32] 벽돌과 충돌하는 공의 이동 방향

 

[그림 8-32]와 같이 벽돌과 충돌한 공의 튕기는 방향을 상태 전이로 만들면 아래와 같다.

 

[그림 8-33] 상태전이 배열

 

앞서 제작한 공과 경계영역은 이차원 배열이었지만 [그림 8-33]의 경우 일차원 배열로 되어 있다. 그 이유는 벽돌 하나로 충돌되었을 때 튕기는 방향인 [그림 8-32]의 모든 방향을 정의할 수 있기 때문이다. 그래서 위의 배열값은 다음 상태를 나타내며 배열 인덱스는 공의 이동 방향을 나타낸다.

예를 들어 공이 2 방향이고 벽돌과 충돌했다면 다음 상태는 아래 [표 8-8]에 의해 1이 된다.

 

 

다음 상태 = array[ 공의 이동 방향 ];

 

[표 8-8] 다음 상태전이

 

[실습 예제 8-8]

 

[실습 예제 8-5]에서 30개의 벽돌 중에서 20개를 출력하는 프로그램을 작성해 보았다.

그리고 [실습 예제 8-7]에서는 경계 영역과 공이 충돌했을 때 공이 튕기는 프로그램도 작성해 보았다. 이제 이 두 예제를 합쳐서 공이 벽돌과 충돌하면 벽돌은 소멸되고 공이 튕기도록 프로그래밍해 보자.

공 방향은 0, 1, 2, 3, 4, 5로 초기 설정할 수 있으며 f는 공 발사, r 키는 공의 초기화인 준비 상태이다.

 

[그림 8-34] 초기 화면

 

[그림 8-35] 벽돌 깨기 화면(1)

 

 

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

int g_nBlockCount;  // 벽돌의 개수

 

int Collision( int nX, int nY )

{       

   int i, nCount = 0; // 충돌 개수

  

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

   {

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

        {

            if( g_sBlock[i].nY == nY )

            {

               if( ( g_sBlock[i].nX == nX || ( g_sBlock[i].nX + 1 ) == nX ) ||

                 (g_sBlock[i].nX == (nX+1) || ( g_sBlock[i].nX + 1 ) == (nX+1) ) )

               {

                 g_sBall.nDirect = g_BlockState[ g_sBall.nDirect ];

                  g_sBlock[i].nLife = 0;

                  nCount++;                               

                }

             }                  

         }

    }

 

    // 충돌 체크

    if( nCount != 0 )

        return 1;

 

    // Note: 위쪽 

    if( nY < 0 )

    {

        g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

 

     return 0;

}

 

void Render()

{

     int i;

 

     ScreenClear();

        

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

     {

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

           ScreenPrint( g_sBlock[i].nX, g_sBlock[i].nY, "■" );

     }

     ScreenPrint( g_sBall.nX, g_sBall.nY, "●");

 

     ScreenFlipping();

}

 

[소스 8-11] 충돌 체크 함수

 

기본 소스는 [실습 예제 8-7]을 기본으로 하고 [실습 예제 8-5]에서 SetBlock()함수만을 기본 소스에 포함하면 벽돌과 공이 출력된다.

위의 소스는 벽돌과 공의 충돌 그리고 경계 영역의 충돌을 처리하는 함수로서 기존 [실습 예제 8-7]의 Collision()함수에 벽돌 충돌 부분인 7행부터 22행까지의 내용을 추가한 형태이다. 1행의 g_nBlockCount 변수는 출력하려는 벽돌 개수를 저장하는 변수이며 이 변수값 만큼 출력 함수인 Render() 함수에서 출력한다.

 

5행의 nCount 변수는 공이 충돌한 벽돌 개수를 나타낸다.

충돌한 벽돌 개수는 7행의 반복문을 이용하여 충돌한 벽돌을 찾아 nCount 값을 증가시키며 이 변수값에 따라 충돌 판정이 25행과 26행에 의해 결정된다.

이와 같이 하는 이유는 공이 아래와 같이 동시에 두 개의 벽돌과 충돌할 수 있기 때문에 처음부터 모든 벽돌과의 충돌 가능성을 찾는 것이다.

 

[그림 8-36] 공이 두 개의 벽돌과 충돌

 

- 공과 막대기 충돌

 

공과 막대기가 충돌하는 경우는 아래 [그림 3-37]과 같으며 막대기와 공의 충돌에 따른 이동 방향은 벽돌과 같으므로 벽돌과 막대기는 상태 전이 배열을 같이 공유할 수 있다.

 

[그림 8-37] 공과 막대기 충돌 예

 

[그림 8-38] 벽돌 상태와 공유되는 막대기의 상태

 

[실습 예제 8-9]

 

[실습 예제 8-4]에서 제작한 프로그램에 막대기와 공이 충돌하면 상태 배열에 따라 공이 튕기도록 프로그래밍해 보자.

이 프로그램에서 j 와 l 키는 막대기의 좌우 이동이며 k 키는 공 발사, u 키는 공 잡기가 된다. 또한 공의 방향은 0, 1, 2, 3, 4, 5 키로 공의 방향이 결정되도록 한다.

 

[그림 8-39] 초기 화면

 

[그림 8-40] 막대기와 충돌 후 튕기는 공(1)

 

[그림 8-41] 막대기와 충돌 후 튕기는 공(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

int g_BlockState[6] = { 3, 2, 1, 0, 5, 4 };

 

int Collision( int nX, int nY )

{       

    int i, nCount = 0; // 충돌 개수

 

    // 막대기와 충돌 체크

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

    {

        if( nY == g_sBar.nY )

        {

            if(( nX >= g_sBar.nX[0] && nX <= ( g_sBar.nX[2] + 1 ) ) ||

               (( nX + 1 ) >= g_sBar.nX[0] && ( nX + 1 ) <= ( g_sBar.nX[2] + 1 )))

            {

                 g_sBall.nDirect = g_BlockState[ g_sBall.nDirect ];

                 return 1;

            }

         }

    }

 

    // Note: 위쪽 

    if( nY < 0 )

    {

        g_sBall.nDirect = g_StateTable[0][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 오른쪽

     if( nX > 78 )

     {

        g_sBall.nDirect = g_StateTable[1][g_sBall.nDirect];         

        return 1 ;

     }

 

     // Note: 경우 3

     if( nY > 24 )

     {

        g_sBall.nDirect = g_StateTable[2][g_sBall.nDirect];         

        return 1 ;

     }

 

      if( nX < 0 )

      {

        g_sBall.nDirect = g_StateTable[3][g_sBall.nDirect]; 

        return 1 ;

      }

 

     return 0;

}

 

[소스 8-12] 충돌 체크 함수

 

[실습 예제 8-4]를 기본 소스로 하고 충돌 함수인 [소스 8-12]의 Collision() 함수를 추가하면 [실습 예제 8-9]를 완성하게 된다.

1행의 상태 전이 배열은 벽돌의 상태 전이 배열이지만 막대기도 같이 공유할 수 있으므로  위와 같이 선언한 것이며 구체적인 사용은 15행에서 실행된다.

 

 


STEP 04

 

[그림 8-42] 4단계 제작 로드맵

 

전체적인 흐름과 구조는 앞서 제작한 게임과 동일하다.

 

■ 스테이지 정보

 

벽돌 깨기 게임에서 필요한 스테이지 정보는 다음과 같다.

 

 

① 벽돌 개수

② 제한 시간

 

[표 8-9] 스테이지 정보

 

이를 구조체로 정의하면 다음과 같다.

 

 

typedef struct _STAGE_INFO

{

      int      nBlockCount; // 벽돌 개수

      clock_t  LimitTime;  // 제한 시간

} STAGE_INFO;

 

[소스 8-13] 스테이지 정보 정의

 

■ 게임 진행 제어와 기타

 

게임 진행 상태는 6장 두더지 잡기 게임의 [표 6-6]과 동일하며 전체 흐름도 동일하다.

만약 임의의 진행 상태가 추가된다면 이 게임 진행 상태에 일부가 추가되는 정도이므로 앞서 살펴본 내용을 이해하고 있다면 대부분의 게임에 적용할 수 있을 것이다.

 

여기까지 벽돌 깨기 게임을 살펴보았다.

벽돌 깨기 게임에서 사용된 상태전이는 다른 게임에서도 많이 사용되는 기법이므로 더욱 신경을 써서 이해하도록 하자. 그리고 이 상태 전이는 뒤에 나오는 Sogo 게임에서 적 캐릭터와 보스 캐릭터를 제어할 때 패턴(pattern)으로 사용된다.

여기까지의 내용을 기반으로 전체 게임을 제작해 보자.

 

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

[출처] https://nowcampus.tistory.com/entry/8%EC%9E%A5?category=655340

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
108 [Xamarin] Xamarin.Forms. Android 실행/ 디버깅시에 에뮬리이터 배포오류 Why am I getting this error in Xamarin.Forms using Visual Studio? file 졸리운_곰 2021.12.01 22
107 [Xamarin] Visual Studio 2019 를 설치하고 Xmarin.forms 빌드시 에러 : I am just download and start Visual Studio (Xamarin Project). But there is an ERROR NU1101. file 졸리운_곰 2021.12.01 53
106 [게임개발] How to Create Smarter NPCs in Games file 졸리운_곰 2021.08.31 23
105 (CGP)16장. 탱크 게임 file 졸리운_곰 2021.06.28 802
104 (CGP) 15장. 탱크 맵툴 만들기 file 졸리운_곰 2021.06.28 225
103 (CGP) 14장 Sogo 게임 file 졸리운_곰 2021.06.28 21
102 (CGP)13장. 패턴 뷰어 file 졸리운_곰 2021.06.28 19
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
» (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