(CGP)7장. 짝 맞추기 게임

짝 맞추기 게임에서는 각 개체의 속성과 상태 전이를 이해하고 게임 흐름을 코드로 옮기는 것이 가장 중요 합니다. 

또한 경과시간의 개념은 게임의 모든 개체를 동기화 할 수 있으므로 이 사항도 그냥 지나갈 수 없는 내용이라 할 수 있습니다. 

 

신나2유튜브 동영상 강의 주소

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

(2) http://youtu.be/-YvOEnCKFGQ

(3) http://youtu.be/78rCKJ6GgOk

(4) http://youtu.be/MV1bEt6_6Q4

 


01 기획

 

■ 스토리

 

제한 시간 안에 같은 문자의 카드를 맞추어라.

 

■ 게임 방식

 

게임이 시작되면 30개의 카드를 약 2초 동안 전부 보여주고, 플레이어는 대략적인 카드를 확인한 후에 짝을 맞춘다. 플레이어는 j(좌), l(우), i(위), k(아래), s(카드 뒤집기) 키를 이용하여 카드 2개를 선택하고 동일한 카드인지를 확인하며 진행한다. 30개의 카드 짝을 제한 시간 안에 다 맞추면 게임은 종료한다.

 

■ 제한 사항

 

카드는 30개로 고정되어 있고 스테이지마다 제한 시간을 달리하여 게임의 난이도를 설정한다.

 

■ 기획 화면

 

[그림 7-1] 기획 화면

 

 

 

 


02 실행 화면

 

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

 

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

 

 

[그림 7-4] 게임 진행하기 전에 모든 카드를 보여주는 화면 (CARD_OPEN 상태)

 

[그림 7-5] 게임 진행 화면

 

[그림 7-6] 미션 성공 화면

 

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

 

[그림 7-8] 결과 화면

 

 


03 게임 제작 로드맵

 

짝 맞추기 게임은 6장 두더지 게임의 일부가 적용되는데, 카드를 일정시간 보여주고 덮어버리는 부분은 마치 두더지 게임에서 망치가 타격상태에서 일정 시간을 대기하는 것과 같은 원리를 사용하고 있다.

그 외에는 이제까지 우리가 다루었던 부분이므로 쉽게 프로그래밍할 수 있을 것이다.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[그림 7-9] 제작 로드맵

 

 


04 단계별 프로그래밍

 

STEP 01

 

[그림 7-10] 1단계 제작 로드맵

 

■ 카드

 

- 속성

 

카드는 앞면과 뒷면으로 되어 있으며 앞면은 알파벳 문자가 보이는 부분이고 뒷면은 감춰지는 부분이다. 이 게임에서는 카드에 26개의 알파벳 문자를 지정하되 중복되지 않게 2개씩 같은 문자를 설정한다.

 

카드에는 세 가지 상태가 있다. 우선 카드의 앞면을 나타내는 OPEN 상태와 카드의 뒷면을 나타내는 CLOSE 상태 그리고 카드가 선택됨을 나타내는 MATCH 상태가 있다.

각 상태에 따라 카드의 모양을 다르게 표현할 수 있는데 아래 [그림 7-11]과 같다.

 

[CLOSE 상태]

[MATCH 상태]

[OPEN 상태]

 

[그림 7-11] 상태별 카드 모양

 

위의 카드 상태를 코드로 정의하면 다음과 같다.

 

 

typedef enum _CARD_STATE { OPEN, CLOSE, MATCH } CARD_STATE;

 

[소스 7-1] 카드 상태 enum형

 

이제 위에서 설명한 카드의 속성을 생각해 보자. 먼저 카드를 출력하기 위해서는 위치 정보가 필요하다. 일반적인 게임은 캐릭터가 이동을 하지만 이 게임에서는 카드가 고정 위치이므로 게임 스테이지를 실행할 때 한번만 위치 설정을 해주면 된다.

 

카드에 할당하는 문자는 중복되지 않아야 하며 2개씩 설정하기 위해 먼저 알파벳 26개의 배열에서 임의의 문자를 선택하고 30개의 카드 중에서 알파벳 문자가 할당되지 않은 임의의 카드 2개를 찾아 알파벳을 설정한다.

이때 26개의 알파벳 문자 배열에서 임의의 문자가 선택되었다는 정보를 설정하여 다음 문자를 선택할 때 중복되지 않게 한다.

 

효율적으로 배열을 검색하는 방법은 매우 많다. 그러나 여기에서는 간단히 rand() 함수를 이용하여 임의의 배열 인덱스를 구한다.

 

 

[그림 7-12] 카드 배열에 알파벳 문자 할당

 

알파벳 문자 배열과 카드 배열의 속성을 정리하면 [표 7-1]과 같다.

 

 

카드 문자 배열 속성

카드 배열 속성

 문자의 사용 유무

② 알파벳 문자

 

 

① 카드 상태

② 카드의 사용 유무

③ 알파벳 문자

④ 출력 위치

 

[표 7-1] 속성

 

위의 속성을 코드로 정의하면 [소스 7-2]와 같다.

 

 

카드 문자 배열 속성 구조체

카드 배열 속성 구조체

typedef struct _CARD_DATA

{

       int  nUse;

       char cMunja;

} CARD_DATA;

 

 

typedef struct _CARD_INFO

{

       CARD_STATE nCardState;

       int  nUse;

       char cMunja;

       int  nX, nY;

} CARD_INFO;

 

[소스 7-2] 속성 정의

 

[실습 예제 7-1]

 

카드 배열에 알파벳 문자를 중복되지 않게 한 쌍씩 설정하고 출력하는 프로그램을 작성해 보자. 아래 [그림 7-13] 과 [그림 7-14]를 보면 모든 문자가 한 쌍씩 중복되지 않게 설정되어 있는 것을 알 수 있다.

 

[그림 7-13] 첫 번째 알파벳 문자를 할당한 결과

 

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

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#include <time.h>

 

typedef enum _CARD_STATE { OPEN, CLOSE, MATCH } CARD_STATE;

 

// Note: 카드 배열 속성 정보

typedef struct _CARD_INFO

{

       CARD_STATE nCardState;

       int  nUse;

       char cMunja;

       int  nX, nY;

} CARD_INFO;

 

// Note: 알파벳 문자 배열 속성

typedef struct _ALPHA_DATA

{

        int   nUse;

        char cMunja;

} ALPHA_DATA;

 

ALPHA_DATA g_sAplhaData[26]; // Note: 알파벳은 26자

CARD_INFO  g_sGameCard[30]; // Note: 카드 30장

 

int main( void )

{       

     int i, j = 0;

     int nCardIndex, nAlphaIndex;

 

     // Note: 알파벳 문자 배열을 초기화

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

     {

        g_sAplhaData[i].nUse = 0;

        g_sAplhaData[i].cMunja = 'A' + i;

     }

 

         // Note: 카드 배열을 초기화

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

          g_sGameCard[i].nUse = 0;  

 

 

     srand( (unsigned)time( NULL ) );    

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

     {          

        while( 1 )

        {

             // Note: 임의의 알파벳 문자 인덱스

             nAlphaIndex = rand() % 26;

                                

             if( g_sAplhaData[nAlphaIndex].nUse == 0 )

             {

                g_sAplhaData[nAlphaIndex].nUse = 1;

                break;

             }

          }

                

          // Note: 카드 배열

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

          {

               while( 1 )

               {

                  // Note: 임의의 카드 인덱스

                  nCardIndex = rand() % 30;

                  if( g_sGameCard[nCardIndex].nUse == 0 )

                  {

                      g_sGameCard[nCardIndex].nUse = 1;            

                        g_sGameCard[nCardIndex].cMunja =                          

                                              g_sAplhaData[nAlphaIndex].cMunja;

                      break;

                   }

                }

            }                           

     }

 

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

     {

        printf( "%c ", g_sGameCard[i].cMunja );            

     }

 

     return 0;

}

 

[소스 7-3] 알파벳 할당 소스

 

33행부터 41행까지는 카드와 알파벳 문자를 초기화하는 부분이다.

특히 알파벳 문자는 대문자 A에서부터 Z까지를 26개의 배열에 설정하는데 알파벳의 아스키코드 값을 보면 A부터 Z까지 1 만큼 차이가 나므로 36행과 같이 설정할 수 있다.

참고적으로 아스키코드 값은 아래와 같이 간단한 소스로 확인할 수 있다.

 

 

1

2

3

4

5

6

7

8

9

10

#include <stdio.h>

 

int main(void)

{       

    int i;

    for( i = 'A' ; i <= 'Z' ; i++ )

         printf( "%c = %d \n", i, i );

 

    return 0;

}

 

[소스 7-4] A부터 Z까지 아스키코드 출력

 

[그림 7-15] 아스키코드 출력

 

44행의 srand()는 2장에서 살펴본 것과 같이 rand()함수의 시작 값을 설정하는 역할을 한다. srand()를 사용하는 이유는 rand()로부터 얻어지는 난수가 반복되는 것을 줄이기 위함이다. 물론 rand() 함수만을 사용해도 되지만 처음 rand()를 사용하기 전에 한번 호출해 주면 위의 문제를 해결하는데 도움이 된다.

 

카드는 30장이지만 같은 알파벳 문자가 한 쌍씩 설정되므로 15번 한 쌍씩 하나의 알파벳을 설정하면 된다. 그래서 45행과 같이 전체 알파벳 설정은 15번 반복된다.

 

55행과 71행의 break는 반복문을 빠져나가기 위해서 존재한다. 현재 반복문은 무한 루프이므로 break가 반복문의 종료 조건이 된다.

 

69행과 70행은 47행에서부터 57행까지의 반복을 통해 구해진 알파벳을 카드 배열에 설정하는 부분이다.

 

- 커서 이동 

 

이 게임에서 커서는 [그림 7-16]과 같이 카드를 가리키는 역할을 하며 커서 위치는 카드 위치를 기준으로 결정된다.

 

[그림 7-5] 커서 이동 화면

 

모두 30개의 카드를 가로 6개, 세로 5개씩 일정한 간격으로 화면에 출력하기 위해서 다음과 같은 계산이 필요하다.

 

 

1

2

3

4

5

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

{

       g_sGameCard[i].nX = ( i % 6 ) * 8 + 3;  // --------> ①

       g_sGameCard[i].nY = ( i / 6 ) * 4 + 1;   // --------> ②

}

 

[소스 7-5] 카드 출력 좌표

 

% 연산자는 나눈 나머지를 의미하고 / 연산자는 나눈 몫을 의미한다.

[소스 7-5]의 3행에서 가로 x 좌표는 ( i % 6 )*8을 하여 0, 8, 16, 24, 32, 40까지 값을 구하게 된다. 이 좌표는 너무 왼쪽에 가까우므로 3을 더해 3, 11, 19, 27, 35, 43의 고정 x 좌표를 만든 것이 위의 ①식이다.

 

4행은 세로 y 좌표를 1행씩 간격을 주기 위한 식으로 ( i / 6 )은 30개의 인덱스를 6개 단위로 나누는 역할을 한다. 예를 들어 15 번째 카드는 ( 15 / 6 )에 의해 2행에 위치하는 값을 구하게 된다. 그리고 각 카드의 세로 길이가 4 컬럼이므로 ( i / 6 ) *4를 하여 실제 출력하기 위한 좌표를 구하게 되지만 각 행 단위로 1 컬럼의 여유를 주어 커서가 이동하게  ( i / 6 )*4 + 1과 같이 한다.

 

j, i, k, l 키에 의한 행과 열의 증감값을 카드 배열의 인덱스로 변환하여 카드의 좌표를 읽어 올수 있다. 이 좌표는 커서를 일정 영역에 출력하기 위한 기준 좌표가 된다.

그러면 행과 열의 2차원 배열의 인덱스를 일차원 배열의 인덱스로 변환하는 과정을 살펴보자.

 

 

[그림 7-17] 이차원 배열 개념

[그림 7-18] 실제 메모리에 나열되는 이차원 배열]

 

 

[그림 7-17]의 이차원 배열은 한 행이 3개의 열로 이루어져 있는 것을 알 수 있다.

하지만 실제 이차원 배열은 일차원 배열과 같이 연속적인 메모리로 나열되어 있으며 다만 행과 열을 이용하여 일차원 배열의 값을 읽어오는 형태가 바로 이차원 배열이다.

현재 한 행이 3개의 열 단위로 이루어져 있으므로 (1, 1)의 행과 열을 일차원 배열의 인덱스로 바꾼다면 다음의 식으로 변환할 수 있다.

 

 

인덱스 = ( 행 * 한 행의 열 개수 ) + 열

 

[식 7-1] 인덱스 변환

  

[식 7-1]을 자세히 살펴보면 한 행과 다음 행간의 거리는 한 행의 전체 열 개수만큼 떨어져 있다. 예를 들어 위의 [그림 7-17]과 같이 전체 2행 3열의 배열에서 1행의 첫 번째 인덱스는 1행 0열이며 1행은 3열 단위로 이루어져 있으므로 위의 [식 7-1]에 의해 이 되며 [그림 7-18]에서 확인할 수 있다.

즉 일차원 배열의 4번째 위치의 값이 1행 0열의 값이 된다.

 

이제는 일차원 배열을 이차원 배열의 행과 열로 바꾸는 방법을 살펴보자.

 

 

➀ 행 =  일차원 배열 인덱스 / 이차원 한 행의 열 개수 

➁ 열  = 일차원 배열 인덱스 % 이차원 한 행의 열 개수

 

[식 7-2] 일차원 배열의 인덱스를 이차원 배열의 행과 열로 변환

 

행은 일정한 수의 열 단위로 되어 있으므로 위의 식 ➀과 같이 하면 현재 일차원 배열 인덱스가 이차원 배열의 어느 행에 속해 있는지를 알 수 있다.

또한 열은 한 행 안에서의 위치이므로 식 ➁와 같이하면 열 인덱스를 계산할 수 있다.

 

[실습 예제 7-2]

 

위에서 설명한 내용을 기반으로 키 입력(j, i, k, l)에 따라 커서가 이동되도록 게임 프레임워크안에서 프로그래밍해 보자.

 

[그림 7-19] 커서

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _CARD_STATE { OPEN, CLOSE, MATCH } CARD_STATE;

 

// Note: 카드 정보

typedef struct _CARD_INFO

{

       CARD_STATE nCardState;

       int  nUse;

       char cMunja;

       int  nX, nY;

} CARD_INFO;

 

CARD_INFO g_sGameCard[30];

int g_nRow, g_nCol;

 

void Card( int x, int y)

{

     ScreenPrint( x, y,     "┌─┐");

     ScreenPrint( x, y + 1, "│  │");

     ScreenPrint( x, y + 2, "└─┘");     

}

 

void Init()

{

     int i;

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

     {

        g_sGameCard[i].nX = ( i % 6 ) * 8 + 3;

        g_sGameCard[i].nY = ( i / 6 ) * 4 + 1;

     }

}

 

void Update()

{

}

 

void Render()

{       

     int i, nIndex;

        

     ScreenClear();   

 

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

        Card( g_sGameCard[i].nX, g_sGameCard[i].nY );               

         

     nIndex = g_nRow * 6 + g_nCol;

     ScreenPrint( g_sGameCard[nIndex].nX + 2, g_sGameCard[nIndex].nY + 3, "↑" );

 

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{               

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

   

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

 

           switch( nKey )

           {

            case 'j' :  // 왼쪽

                   if( g_nCol - 1 < 0 )

                       g_nCol = 0;

                   else

                       g_nCol--;

                   break;

             case 'l' :  // 오른쪽

                    if( g_nCol + 1 > 5 )

                       g_nCol = 5;

                    else

                       g_nCol++;

                    break;

              case 'i' :  // 위쪽

                     if( g_nRow - 1 < 0 )

                        g_nRow = 0;

                     else

                        g_nRow--;

                     break;

              case 'k' : // 아래쪽

                     if( g_nRow + 1 > 4 )

                         g_nRow = 4;

                     else

                        g_nRow++;

                      break;

             }

          }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력        

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 7-6] 카드 출력과 커서 이동

 

51행은 앞에서 설명한 [식 7-1]로써 이차원 배열의 행과 열의 개념을 일차원 배열의 인덱스로 변환한 것이다.

 

 


STEP 02

 

[그림 7-20] 2단계 제작 로드맵

 

■ 카드 짝 확인

 

선택한 카드의 짝이 맞는지를 확인하기 위한 과정은 [그림 7-21]과 같이 2단계로 나눈다. 1단계는 플레이어가 카드를 선택하면 현재 선택된 카드 상태가 CLOSE 상태인지를 확인하고 현재의 상태를 MATCH 상태로 바꾸어 선택된 카드임을 나타내도록 한다.

그리고 현재 선택된 카드의 인덱스를 저장하여 다른 한 카드와 비교할 수 있도록 한다.

 

2단계는 1단계에서 선택된 카드 인덱스와 현재 선택된 카드 인덱스를 이용하여 카드의 알파벳 문자가 서로 같은지를 확인하는 단계이다.

이때 카드 배열의 알파벳이 모두 같으면 OPEN 상태로 설정하고 같지 않으면 CLOSE 상태로 설정한다.

 

[그림 7-21] 카드 확인 과정

 

- 카드 확인 정보(속성)

 

플레이어가 선택할 수 있는 카드는 2개이며, 선택된 카드가 같으면 모두 OPEN 상태가 되고 틀리면 잠시 카드를 보여준 후에 CLOSE 상태로 설정하여 플레이어가 카드 문자를 확인 할 수 있는 절차를 가질 수 있게 한다.

이와 같은 점을 고려하여 카드를 확인하기 위한 정보는 [표 7-2]와 같다.

 

 

① 선택된 카드 개수

② 선택된 카드 인덱스

③ 이전 시간

 

[표 7-2] 카드 확인 정보

 

첫째, 선택된 카드 개수는 현재까지 선택된 카드의 개수를 말하며 카드 개수가 2가 되면 카드를 확인하는 과정을 거쳐서 CLOSE를 할 것인지, OPEN을 할 것인지를 결정한다.

 

둘째, 선택된 카드 인덱스는 [그림 7-21]에도 나와 있지만 1단계에서 카드가 선택되면 카드의 인덱스를 차례대로 저장하고 선택된 카드 개수가 2가 되면 저장된 인덱스로 카드 배열의 알파벳 문자를 확인한다.

 

셋째, 이전 시간은 카드 2개가 선택되면 일정 시간 동안 MATCH 상태를 유지하여 플레이어가 두 카드를 기억할 수 있도록 해주기 위한 대기 시간을 주기 위한 부분이다.

이 시간은 카드 2개가 선택될 때 시각이 저장되며 이 시각부터 매번 현재 시간을 읽어서 경과 시간을 체크한다.

  

위의 [표 7-2]를 구조체로 정의하면 다음과 같다.

 

 

typedef struct _MATCH_CARD_DATA

{      

        int nCardCount;

        int nCardIndex[2]; 

        clock_t OldTime;

} MATCH_CARD_DATA;

 

[소스 7-7] 카드 확인 정보를 정의

 

[실습 예제 7-3]

 

[실습 예제 7-1]에서는 카드와 문자를 초기화하고 중복되지 않게 설정하는 방법을 프로그래밍을 했었다. 그리고 [실습 예제 7-2]에서는 카드를 행과 열 단위로 출력하고 커서를 이동하는 것까지 프로그래밍을 했었다.

이제 이 두 예제와 위에서 살펴본 카드를 확인하고 카드 상태를 바꾸는 부분을 합쳐서 아래 [그림 7-22]에서부터 [그림 7-24]와 같이 동작되도록 프로그래밍해 보자.

커서 이동은 i(위쪽), j(왼쪽), k(아래쪽), l(오른쪽)이며 카드 선택은 s키를 이용한다.

참고적으로 [실습 예제 7-3]은 전체 게임의 80%를 제작한 것과 같다.

 

[그림 7-22] 초기 화면

 

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

[그림 7-23] 카드 한 개 선택 (MATCH 상태)

 

[그림 7-24] 카드 확인후에 OPEN한 상태

 

 

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

#include <stdio.h>

#include <windows.h>

#include <conio.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _CARD_STATE { OPEN, CLOSE, MATCH } CARD_STATE;

 

// Note: 카드 정보

typedef struct _CARD_INFO

{

       CARD_STATE nCardState;

       int  nUse;

       char cMunja;

       int  nX, nY;

} CARD_INFO;

 

// Note: 알파벳 문자 배열 속성

typedef struct _ALPHA_DATA

{

        int   nUse;

        char cMunja;

} ALPHA_DATA;

 

// Note: 카드 짝 확인 정보

typedef struct _MATCH_CARD_DATA

{      

       int nCardCount;

       int nCardIndex[2]; 

       clock_t OldTime;

} MATCH_CARD_DATA;

 

ALPHA_DATA g_sAplhaData[26]; // Note: 알파벳은 26자

CARD_INFO g_sGameCard[30];

MATCH_CARD_DATA g_sMatchCardInfo;

int g_nRow, g_nCol;

 

void OpenCard( int x, int y, char cMunja )

{

     char string[100];

 

     ScreenPrint( x, y,      "┌─┐");    

     ScreenPrint( x, y + 1, "│  │");

     ScreenPrint( x, y + 2, "└─┘");

     sprintf( string, "%c", cMunja );

     ScreenPrint( x + 2, y + 1, string );

}

 

void MatchCard( int x, int y, char cMunja )

{

     char string[100];

 

     ScreenPrint( x, y,      "┏━┓");    

     ScreenPrint( x, y + 1, "┃  ┃");

     ScreenPrint( x, y + 2, "┗━┛");

     sprintf( string, "%c", cMunja );

     ScreenPrint( x + 2, y + 1, string );

}

 

void CloseCard(int x, int y)

{

     ScreenPrint( x, y,      "┌─┐");    

     ScreenPrint( x, y + 1, "│■│");

     ScreenPrint( x, y + 2, "└─┘");

}

 

 

void Init()

{               

     int i, j;

     int nCardIndex, nAlphaIndex;

 

     // Note: 알파벳 문자 배열을 초기화

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

     {

        g_sAplhaData[i].nUse = 0;

        g_sAplhaData[i].cMunja = 'A' + i;

     }

 

     // Note: 카드 배열을 초기화

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

          g_sGameCard[i].nUse = 0;  

 

     srand( (unsigned)time( NULL ) );    

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

     {          

        while( 1 )

        {

             // Note: 임의의 알파벳 문자 인덱스

             nAlphaIndex = rand() % 26;

                                        

             if( g_sAplhaData[nAlphaIndex].nUse == 0 )

             {

                g_sAplhaData[nAlphaIndex].nUse = 1;

                break;

              }

         }

                

         // Note: 카드 배열

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

         {

              while( 1 )

              {

                 // Note: 임의의 카드 인덱스

                 nCardIndex = rand() % 30;

                 if( g_sGameCard[nCardIndex].nUse == 0 )

                 {

                     g_sGameCard[nCardIndex].nUse = 1;             

                     g_sGameCard[nCardIndex].cMunja =

                                      g_sAplhaData[nAlphaIndex].cMunja;

                     break;

                  }

               }

          }                             

     }

 

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

     {

        g_sGameCard[i].nX = ( i % 6 ) * 8 + 3;

        g_sGameCard[i].nY = ( i / 6 ) * 4 + 1;

        g_sGameCard[i].nCardState = CLOSE;

     }

}

 

void Update()

{

     clock_t CurTime = clock();

 

     // Note: 데이터 갱신

     if( g_sMatchCardInfo.nCardCount == 2 )

     {   // 0.8초 동안 카드는 MATCH 상태를 유지

        if( CurTime - g_sMatchCardInfo.OldTime > 800 )

        {       

              g_sMatchCardInfo.nCardCount = 0;                          

              if( g_sGameCard[ g_sMatchCardInfo.nCardIndex[0] ].cMunja ==

                           g_sGameCard[ g_sMatchCardInfo.nCardIndex[1] ].cMunja )

              {

                g_sGameCard[ g_sMatchCardInfo.nCardIndex[0] ].nCardState = OPEN;

                g_sGameCard[ g_sMatchCardInfo.nCardIndex[1] ].nCardState = OPEN;

                // Note: 점수 가산 코드 삽입 부분

              }else{

                g_sGameCard[ g_sMatchCardInfo.nCardIndex[0] ].nCardState = CLOSE;

                g_sGameCard[ g_sMatchCardInfo.nCardIndex[1] ].nCardState = CLOSE; 

             }

         }

     }

}

 

void Render()

{       

     int i, nIndex;

        

     ScreenClear();   

 

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

     {

        switch( g_sGameCard[i].nCardState )

        {

         case OPEN :

          OpenCard( g_sGameCard[i].nX, g_sGameCard[i].nY, g_sGameCard[i].cMunja );

          break;

         case CLOSE :

          CloseCard( g_sGameCard[i].nX, g_sGameCard[i].nY );

          break;

         case MATCH :

          MatchCard(g_sGameCard[i].nX, g_sGameCard[i].nY, g_sGameCard[i].cMunja );

          break;

         }

     }

         

     nIndex = g_nRow * 6 + g_nCol;

     ScreenPrint( g_sGameCard[nIndex].nX + 2, g_sGameCard[nIndex].nY + 3,"↑" );

   

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{               

    int nKey, nIndex;

 

    ScreenInit();

    Init();        // 초기화

   

    while( 1 )

    {

        if( _kbhit() )

        {

            nKey = _getch();

 

            switch( nKey )

            {

             case 'j' :  // 왼쪽

                     if( g_nCol - 1 < 0 )

                        g_nCol = 0;

                     else

                        g_nCol--;

                     break;

             case 'l' :  // 오른쪽

                     if( g_nCol + 1 > 5 )

                        g_nCol = 5;

                     else

                        g_nCol++;

                     break;

             case 'i' :  // 위쪽

                     if( g_nRow - 1 < 0 )

                        g_nRow = 0;

                     else

                        g_nRow--;

                     break;

             case 'k' : // 아래쪽

                     if( g_nRow + 1 > 4 )

                        g_nRow = 4;

                     else

                        g_nRow++;

                     break;

             case 's' : // 카드 선택

                     nIndex = g_nRow * 6 + g_nCol;

                     switch( g_sMatchCardInfo.nCardCount )

                     {

                      case 0 :

                            // 첫 번재 선택된 카드

                            g_sMatchCardInfo.nCardIndex[0] = nIndex;

                            g_sGameCard[nIndex].nCardState = MATCH; 

                            g_sMatchCardInfo.nCardCount++;                                                 
                            break;

                      case 1 :

                        // 두 번째 선택된 카드

                            if( g_sMatchCardInfo.nCardIndex[0] != nIndex )

                            {

                                g_sMatchCardInfo.OldTime = clock();

                                g_sMatchCardInfo.nCardIndex[1] = nIndex;

                                g_sGameCard[nIndex].nCardState = MATCH;

                                g_sMatchCardInfo.nCardCount++;

                             }

                             break;

                     }

                     break;

            }

        }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력 

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 7-8] 카드 확인 소스

 

68행에서부터 123행까지의 Init()함수 내용은 [실습 예제 7-1]의 소스임을 알 수 있다.

 

130행의 if( g_sMatchCardInfo.nCardCount == 2 ) 조건식은 현재 카드가 2개 선택되었고 선택된 카드의 인덱스가 g_sMatchCardInfo.nCardIndex[  ] 배열 안에 모두 저장된 것을 의미한다. 그래서 현재 MATCH 상태의 두 카드의 문자를 비교하고 OPEN 상태로 할 것인지 CLOSE 상태로 할 것인지를 결정하게 된다.

이때 앞에서도 언급했듯이 두 카드의 MATCH 상태가 일정 시간동안 경과되어야 하므로  132행과 같은 코드가 필요한 것이다.

 

카드의 선택은 s 키 입력에 의해 이루어진다. 이때 커서의 행과 열을 일차원 배열의 인덱스로 변환해야 한다. 221행의 코드가 변화 코드이며 이 인덱스는 현재 선택된 카드 개수가 0이면 g_sMatchCardInfo.nCardIndex[0]에 저장하고 1이면 g_sMatchCardInfo.nCardIndex[1]에 저장한다. 이때 선택된 카드 개수는 237행에 의해 2가 되며 이 값은 Update()함수의 130행에서 카드 확인을 하는 조건이 된다.

 

■ 제한시간 설정

 

게임의 난이도는 스테이지마다 제한 시간을 다르게 하여 설정하며 게임이 경과된 시간은 [식 7-3]에 의해 구하면 된다.

 

 

게임이 경과된 시간(초 단위) = (현재 시각 - 게임 시작 시각 ) / 1000

 

[식 7-3] 게임이 경과된 시간

 

위 식에서 (현재 시각 - 게임 시작 시각) 은 게임이 경과된 시간이고 1000으로 나누는 것은 밀리세컨드 단위를 초 단위로 변환하기 위한 것이다.

 

[실습 예제 7-4]

 

위의 [식 7-3]을 이용하여 제한 시간을 10초로 하였을 때 경과된 시간을 출력하는 프로그램을 게임 프레임워크 안에서 작성하여 보자.

 

[그림 7-25] 경과된 시간 출력

 

 

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

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

clock_t g_StartTime, g_Time, g_LimitTime;

int g_nExit;

 

void Init()

{

     g_StartTime = clock();

     g_LimitTime = 10;

}

 

void Update()

{

     g_Time = ( clock() - g_StartTime ) / 1000;

     if( g_Time > g_LimitTime )

        g_nExit = 1;

}

 

void Render()

{

     char string[100];

        

     if( g_nExit )

        return ;

 

      ScreenClear();

      sprintf( string, "제한 시간: %d초 경과된 시간: %d초",  g_LimitTime, g_Time );

      ScreenPrint( 10, 10, string );

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{       

    ScreenInit();

    Init();        // 초기화

   

    while( 1 )

    {

        if( g_nExit )

           break;

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력         

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 7-9] 제한 시간 출력 소스

 

경과 시간과 제한 시간이 같아지면 프로그램은 종료해야 된다.

그래서 7행에서 g_nExit 변수를 선언하고 이 변수의 값으로 26행에서는 출력을 제한했으며 46행에서는 44행의 while문을 탈출하게 하여 프로그램이 종료되게 한다.

 

 


STEP 03

 

[그림 7-26] 3단계 제작 로드맵

 

■ 게임 스테이지 정보

 

짝 맞추기 게임은 앞에서 제작한 게임과 약간 차이가 있다. 일반적인 슈팅 게임은 적 캐릭터의 수 또는 속도 등이 스테이지의 난이도가 되겠지만 짝 맞추기 게임에서는 단순히 제한 시간을 다르게 하여 스테이지를 구분할 뿐 그 외에 다른 것은 없다.

 

 

① 제한 시간

 

[표 7-3] 스테이지 정보

 

아래의 [소스 7-10]은 스테이지를 3개로 구성하고 제한 시간을 초기화하는 코드이다.

 

 

                               // 분*초*밀리세컨트

clock_t g_StageLimitTime[3] = { 4*60*1000, 3*60*1000, 2*60*1000 };

 

[소스 7-10] 스테이지별 제한 시간

 

제한 시간은 모두 밀리세컨드로 환산해야 하므로, 만약 4분의 제한 시간을 준다면 먼저 분을 초로 환산하여 =240초를 구한 후에 이것을 밀리세컨드로 환산하여 =240000 을 구한다.

참고로 4분 30초라는 값을 밀리세컨드로 환산한다면 이 된다.

 

■ 게임 진행 제어

 

기본적인 게임 진행은 앞서 제작한 게임과 동일하지만 한 가지 추가되는 사항은 준비 상태(READY) 다음에 현재 초기화된 카드를 일정 시간 동안 보여주기 위한 CARD_OPEN 상태가 있다. 이와 같은 내용을 전체 상태와 흐름도로 나타내면 다음과 같다.

 

 

상태

설명

초기 상태(INIT)

게임 변수의 초기화 및 사운드 초기화, 스테이지별 데이터 설정

준비상태(READY)

스테이지 정보 출력

카드 보기 상태(CARD_OPEN)

게임이 시작되기 전에 전체 카드의 내용을 잠시 보여준다.

게임 진행 상태(RUNNING)

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

중지 상태(STOP)

미션 성공과 실패를 판단

미션 성공 상태(SUCCESS)

미션 성공 화면을 출력 및 다음 스테이지로 진행

미션 실패 상태(FAILED)

미션 실패 화면을 출력한 후에 종료와 재시작 여부를 묻고 종료 또는 재시작함

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

게임 결과 출력 및 종료

 

[표 7-4] 게임 진행 상태

 

 

typedef enum _GAME_STATE { INIT, READY, CRAD_OPEN, RUNNING, STOP, SUCCEESS, FAILED,  RESULT } GAME_STATE;

 

[소스 7-11] enum형

 

[그림 7-27] 전체 게임 흐름도

 

■ 참고 사항

 

게임 진행 제어와 전체 흐름에 관한 프로그래밍은 앞서 살펴본 게임과 동일하다.

 

여기까지 짝 맞추기 게임에 대한 대부분의 사항을 살펴보았다.

앞서 살펴 본 [실습 예제 7-3]은 전체 게임의 80% 부분에 해당하며 [실습 예제 7-4]가 나머지 10%를 차지한다. 이 두 부분을 전체 게임 흐름에 넣어 구성하는 부분이 최종 나머지 10%에 해당이 된다. 이미 전체 흐름을 구성하는 프로그램은 슛 골인 게임과 두더지 잡기 게임에서 반복했으므로 나머지 10%를 스스로 제작하여 전체 게임을 구성해 보자.

 

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

[출처] https://nowcampus.tistory.com/entry/7%EC%9E%A5-1?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
97 (CGP) 8장. 벽돌깨기 게임 file 졸리운_곰 2021.06.28 48
» (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