(CGP) 5장. 슛골인 게임

 

 이 장의 학습 목표는 다음과 같습니다.

 

각 개체의 속성을 정의하고 프로그래밍할 수 있다.

중심 좌표와 클리핑 개념 이해하고 캐릭터에 적용하여 프로그래밍할 수 있다.

전체 제작 로드맵을 보면서 단계별로 게임의 모듈을 완성할 수 있다.

단계별로 제작한 소스를 이용하여 전체 프로그램을 완성할 수 있다.

 

오케이2 유튜브 동영상 강의 주소

 

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

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

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

(4) http://youtu.be/6UvEGfo4u8g

(5) http://youtu.be/-X-4voBH9CE

(6) http://youtu.be/3aLGSSUhfM0

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

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

(9) http://youtu.be/8usvcH2AZiA

(10) http://youtu.be/WN4lwCliCM0

(11) http://youtu.be/4c2OakFrX_c

(12) http://youtu.be/7T4trwmYdik

 

 

 

 

 

 


01 기획

 

- 스토리

 

제한 시간 안에 목표한 공을 넣어라.

 

- 게임 방식

 

왼쪽 키는 j, 오른쪽 키는 l, 슛 키는 k이다. 스테이지마다 정해진 목표의 골인 수가 있으며 제한 시간 안에 골인을 완료해야 한다.

 

- 제한 사항

 

공은 한 번에 한 개씩 슛할 수 있고 슛한 공이 소멸될 때까지 새로운 공이 생성되지 않는다. 게임 난이도는 스테이지마다 골대의 길이와 공의 속도로 조절한다.

 

 

[그림 5-1] 슛골인 게임 기획 화면

 

 

 


02 실행 화면

  

[그림 5-2] 게임 시작 화면

 

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

 

[그림 5-4] 게임 실행 화면

 

[그림 5-5] 골인 축하 화면

 

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

 

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

 

 

03 게임 제작 로드맵

 

제작 로드맵은 게임을 개발하기 위한 전체 설계도와 같다.

제작 로드맵의 각 단계는 전체 게임을 개발하는 과정을 부분적으로 나눈 것으로 각 단계에서 만들어지는 각종 프로그램들은 전체 프로그램을 구성하게 된다.

 

프로그램 개발에 있어서 가장 중요한 부분은 설계이다.

설계는 전체 프로그램을 무엇을 어떻게 만들어야 하는지에 대한 전체 과정을 볼 수 있는 역할을 하며 아래의 제작 로드맵은 그와 같은 역할을 한다.

그러면 이제 단계별로 프로그래밍하면 전체 프로그램을 완성해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

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

 

 

 


04 단계별 프로그래밍

 

STEP 01

 

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

 

■ 주인공

 

- 속성 정의

 

아래 [그림 5-10]의 게임 진행 화면 속에 있는 주인공을 표현하기 위해서는 어떠한 데이터가 필요할까?

기본적으로 주인공은 키보드 입력에 따라 좌우로 이동해야 하므로 좌표가 필요하다.

 

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

 

그리고 이 게임에서 주인공은 적 캐릭터로부터 공격을 받지 않으므로 생명과 같은 속성은  필요하지 않다. 결국 주인공의 속성은 좌표만 있으면 되며 이 좌표를 이용하여 출력하게 된다.

 

주인공 캐릭터에게 좌표는 두 가지 의미를 가진다. 첫째는 이동 좌표로써의 의미로 ‘주인공 캐릭터를 어디에 출력할 것인가?’ 이다. 둘째는 중심 좌표로써의 의미로 ‘캐릭터를 출력할 때 어디를 기준으로 출력할 것인가?’ 이다.

이와 같이 좌표를 이동 좌표와 중심 좌표로 나눠서 생각하는 이유는 주인공 캐릭터가 길이를 가지기 때문이다.

현재 주인공 캐릭터를 나타내는 특수문자는 한 문자당 가로로 두 컬럼의 길이를 가지므로 아래 [그림 5-11]의 경우는 총 6컬럼의 가로 길이를 가진다.

 

[그림 5-11] 특수 문자로 구성한 주인공 캐릭터

 

만약 이동 좌표만을 생각하고 [그림 5-12]와 같이 길이가 다른 두 개의 캐릭터를 같은 좌표에 출력하면 어떻게 될까?

 

[그림 5-12] 길이가 다른 주인공 캐릭터

 

실행된 결과는 다음 [그림 5-13] 과 같다.

 

[그림 5-13 ] 길이가 다른 캐릭터를 좌측으로 이동

 

왜 이런 결과가 나왔을까? 그것은 출력하는 좌표의 기준이 좌측 상단이기 때문이다.

이 캐릭터의 경우 중심이 머리이므로 길이가 다른 캐릭터라도 같은 좌표에 출력하면 머리가 겹쳐진 형태로 출력되어 같은 좌표에서 공을 슛할 수 있다.

중심 좌표를 기준으로 하지 않고 출력하는 경우에 위의 두 캐릭터를 최대한 좌측으로 움직여 공을 슛한다고 가정해 보자. 이때 팔이 긴 캐릭터를 아무리 좌측으로 이동시킨다고 하더라도 팔이 짧은 캐릭터와 같은 좌표에서 절대 슛 동작을 할 수 없다.

 

[그림 5-14] 중심 좌표를 고려하지 않고 출력한 경우

 

이 부분은 가로 세로 길이를 가지는 캐릭터를 출력할 때 더욱 큰 문제가 되는데 아래의 두 그림을 비교해 보자.

 

[그림 5-15] 출력 기준 왼쪽 상단인 캐릭터를 같은 좌표에 출력한 결과

 

[그림 5-15]는 캐릭터를 출력할 때 왼쪽 위를 기준으로 출력하므로 크기가 다른 캐릭터에 같은 좌표를 지정하면 서로 다른 위치에 캐릭터가 출력된 것과 같이 보인다.

이렇게 되면 크기에 따라 이동 좌표가 전부 달라지므로 캐릭터마다 서로 다른 좌표계를 사용하는 결과가 된다.

이제 중심 좌표의 개념이 적용된 예를 살펴보자.

 

   

[그림 5-16] 중심 좌표인 중앙의 하단을 기준으로 같은 좌표에 출력한 결과

 

[그림 5-16]과 같이 크기가 다른 캐릭터라도 중심 좌표를 이동 좌표에 맞추고 출력 기준 좌표를 계산하면 크기가 다르다고 해도 같은 좌표에 캐릭터를 출력할 수 있다.

게임에서는 이와 같은 중심 좌표의 개념이 상당히 중요한 역할을 한다.

 

[그림 5-17] 좌표 개념

 

일반적으로 서로 다른 크기를 가지는 캐릭터를 출력하기 위해 중심 좌표를 적용할 때는 아래와 같은 툴을 만들어서 설정한다.

 

[그림 5-18] 스프라이트 툴

 

이와 같은 사항을 적용하여 주인공 캐릭터의 속성을 정의하면 아래와 같다.

 

① 중심 좌표

② 이동 좌표

③ 출력 기준 좌표

 

[표 5-1] 주인공 속성

 

 

typedef struct _PLAYER

{

          int nCenterX, nCenterY;

        int  nMoveX, nMoveY;

          int  nX, nY;

} PLAYER;

PLAYER g_sPlayer;

 

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

 

실제 출력 기준 좌표(nX, nY)는 아래와 같은 식에 의해 구할 수 있다.

 

 

( 이동 좌표 x - 중심 좌표 x,  이동 좌표 y - 중심 좌표 y )

 

[식 5-1] 출력 기준 좌표 식

 

현재 슛 골인 게임에서는 주인공 캐릭터가 모두 같지만 만약 각 동작마다 서로 다른 크기의 캐릭터 모양을 갖는다면 아래 [소스 5-2]와 같이 여러 개의 중심 좌표를 저장할 수 있도록  배열 또는 포인터의 형태로 정의할 수 있다.

 

 

typedef struct _POSITION

{

          int  nX, nY;      

} POSITION;

 

typedef struct _PLAYER

{

        POSITION nCenter[10]; // 캐릭터 10개에 대한 중심 좌표

        int nMoveX, nMoveY;   // 이동 좌표

        int nX, nY;             // 출력 기준 좌표

        int nIndex;             // 중심 좌표 인덱스       

} PLAYER;

 

PLAYER g_sPlayer;

 

[소스 5-2] 10개의 중심좌표를 저장하는 주인공 속성 구조체

 

- 이동 및 키보드 처리

 

현재 이 게임은 주인공의 y 좌표가 고정된 상태에서 x 좌표에만 변화를 주어 좌우로 이동하는 게임이다. 좌우 이동 키는 j, l 키이며 x 좌표를 1씩 증감시켜 이동한다.

키보드에 대한 처리는 3장의 [소스 3-6]을 통해 앞서 살펴본 내용을 그대로 적용한다.

 

[실습 예제 5-1]

 

주인공 캐릭터를 j키와 l키의 입력에 따라 좌우로 이동하는 프로그램을 작성해 보자.

이때 주인공 캐릭터에 중심 좌표를 적용하여 출력하고 상단에는 아래 [표 5-2]와 같이 주인공 캐릭터의 이동 좌표를 계속적으로 출력하도록 한다.

 

 

주인공 이동 좌표 : x, y

 

[표 5-2] 출력 정보

 

기본적인 게임 구조는 3장에서 작성한 프레임워크를 적용하며 이동 거리는 1컬럼으로 하지만 특수 문자는 2컬럼 단위로 이동하므로 [그림 5-19]에서 [그림 5-20과 [그림 5-21]이 될 때에는 2 컬럼씩 이동하도록 한다.

이와 같이 경계 영역에서 캐릭터의 일부를 출력하지 않는 기법을 클리핑(clipping)이라고 하며 3장의 게임 프로그래밍 용어에서 설명했었다.

이와 같은 부분을 오른쪽 경계 영역에도 동일하게 적용한다.

 

[그림 5-19] 주인공 캐릭터 출력

 

[그림 5-20] 클리핑 적용(1)

 

[그림 5-21] 클리핑 적용(2)

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <stdio.h>

#include "Screen.h"

#include <windows.h>

 

typedef struct _PLAYER

{

         int nCenterX, nCenterY;

         int nMoveX, nMoveY;

         int nX, nY;

} PLAYER;

 

PLAYER g_sPlayer;                    

char g_strPlayer[] = "┗━●━┛";      // 주인공 캐릭터

int g_nLength;                          // 주인공 캐릭터 전체 길이

// Note: 초기화

void Init()

{

     g_sPlayer.nCenterX = 4;     // 주인공 캐릭터의 중심 좌표

     g_sPlayer.nCenterY = 0;     // 주인공 캐릭터의 중심 좌표

     g_sPlayer.nMoveX = 20;     // 주인공 캐릭터의 이동 좌표 초기화

     g_sPlaye.nY = g_sPlayer.nMoveY = 22;     // 주인공 캐릭터의 이동 좌표 초기화

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX; // 주인공 캐릭터의 출력 기준 좌표

     g_nLength = strlen( g_strPlayer );  // 주인공 캐릭터의 전체 길이

}

// Note: 데이터 갱신

void Update()

{

}

// Note: 출력

void Render()

{

     char string[100] = { 0, };       

     ScreenClear();

 

     // Note: 렌더링 시작 

     // Note: 2 컬럼씩 클리핑

     if( g_sPlayer.nX < 0 )  //  왼쪽 클리핑 처리

        ScreenPrint( 0, g_sPlayer.nMoveY, &g_strPlayer[g_sPlayer.nX*-1]);   

     else if( g_sPlayer.nMoveX + (g_nLength - g_sPlayer.nCenterX + 1) > 79 )

     {

        strncat( string, g_strPlayer, g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX

                                                      + 1) - 79 ) );

        ScreenPrint( g_sPlayer.nX, g_sPlaye.nY, string );          

     }else{ // 1 컬럼씩 이동

        ScreenPrint( g_sPlayer.nX, g_sPlaye.nY, g_strPlayer );

     }

         

     sprintf( string, "주인공 이동 좌표 : %d, %d", g_sPlayer.nMoveX, g_sPlaye.nY );

     ScreenPrint( 0, 0, string );

 

     // Note: 렌더링 끝

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

               break;

           switch( nKey )

           {

            case 'j' :

                 g_sPlayer.nMoveX--;

                     // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                 nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                 // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                 if( g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ||

                                                      g_sPlayer.nMoveX + nRemain > 79 )  

                     g_sPlayer.nMoveX--;

                  g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                  break;

            case 'l' :

                   g_sPlayer.nMoveX++;

                       // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                   nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                   // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                  if( g_sPlayer.nMoveX + nRemain > 79 ||

                                            g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ) )

                     g_sPlayer.nMoveX++;

                  g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                  break;

            }

         }

 

           Update();    // 데이터 갱신

           Render();    // 화면 출력   

     }

     

     Release();   // 해제

     ScreenRelease();

     return 0;

}

 

[소스 5-3] 전체 소스

 

24행은 이동 좌표 y와 출력 좌표 y를 초기화 하는 부분이다.

현재 주인공 캐릭터의 세로 길이가 1 컬럼이고 중심 좌표 y가 0이므로 이동 좌표 y와 출력 기준 좌표 y는 같다. 그래서 24행과 같이 초기화를 하고 있다.

25행은 캐릭터의 중심 좌표와 이동 좌표를 통해 출력 기준 좌표를 계산하고 있다.

 

40행에서 49행은 주인공 캐릭터의 출력 기준 좌표가 경계 영역을 넘었는지 조사하여 클리핑 적용 여부를 판단하는 부분이다.

클리핑은 주인공 캐릭터의 특수 문자를 저장하고 있는 g_strPlayer[] 배열의 인덱스를 이용하면 쉽게 할 수 있다.

예를 들어 왼쪽 클리핑의 경우는 g_strPlayer[] 배열의 출력할 시작 인덱스를 조정하면 된다. 41행에 &g_strPlayer[g_sPlayer.nX*-1]과 같이 되어 있는 것은 왼쪽 클리핑의 시작은 g_sPlayer.nX가 음수가 될 때 시작되고 이 좌표 값에 -1을 곱하면 좌표가 2, 4 등이 된다.

이 값은 g_strPlayer[]의 시작 인덱스로 사용하면 일정 부분에서부터 출력되므로 클리핑과 같은 효과를 가져온다.

  

왼쪽 클리핑의 경우 특수 문자 ●는 [그림 5-22]와 같이 왼쪽이 중심이므로 이동 좌표 x가 0이면 [그림 5-23]과 같이 왼쪽 경계 영역에 닿게 되지만 이동 좌표 x가 78일 때는 특수 문자 ●는 오른쪽 경계 영역에 닿게 된다.

왜냐하면 특수 문자는 2컬럼이므로 아래 [그림 5-22]의 ‘중심 좌표 + 1’ 되는 컬럼이 오른쪽 경계 영역에 먼저 닿기 때문이다.

[그림 5-22] 특수 문자 ● 

 

[그림 5-23] 경계 영역과 클리핑(clipping)

 

44행의 strncat() 함수는 오른쪽 경계 영역에 주인공 캐릭터가 닿았을 때 출력하려는 문자를 일부 복사하기 위한 함수로 사용한다.

이와 같이 해주는 이유는 문자열의 끝은 널문자로 구분되기 때문이다.

이때 복사 영역을 결정해야하는 데 복사 영역은 g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX + 1) - 79 )에 의해 구할 수 있다.

이 계산에서 g_sPlayer.nMoveX + g_sPlayer.nCenterX + 1 은 최우측 좌표를 구하게 된다. 여기에 79을 빼주면 경계 영역을 벗어난 컬럼을 구할 수 있다.

이 컬럼만큼 주인공 캐릭터의 전체 길이에서 빼주면 출력하기 위한 주인공 캐릭터의 최대 인덱스를 구하게 되며 이 인덱스를 이용하여 주인공 캐릭터의 일부를 복사하게 된다.

 

81행과 91행의 nRemain 변수는 중심 좌표의 이후의 길이를 구한 변수이다.

이 값은 84행과 93행에서 주인공 캐릭터의 최우측 좌표가 경계 영역에 닿았는지를 조사하기 위해 이동 좌표에 이 값을 더한다.

 

83행과 84행, 93행과 94행을 보면 같은 조건식임을 알 수 있다.

하지만 조건식의 우선순위가 서로 반대로 되어 있는 것은 왼쪽 이동일 때는 최우선적으로 조사해야 하는 부분은 왼쪽 경계 영역에 닿을 가능성이므로 이 부분을 조사하는 식이 먼저 오는 것이며 93행과 94행은 그 반대이므로 같은 조건식임에도 불구하고 조건의 우선순위를 다르게 배치한 것이다.

 

■ 공

 - 속성 정의

 

전체 흐름과 개체 제어를 동기화하는 방법으로 시간을 사용한다.

이 방법을 간단히 요약하면 시스템으로부터 시간을 밀리세컨드(millisecond) 단위로 읽어 현재 시각과 이전 이동 시각의 차이로 이동과 진행을 결정하는 방법이다.

속성을 정의하기 전에 이와 같은 설명을 하는 것은 공은 키보드 입력에 따라 이동하는 것이 아니라 스스로 이동하기 때문이다.

 

그러면 이제 공의 속성을 정의하여 보자.

 

첫째는 공 상태이다.

공 상태는 슛 상태와 준비 상태가 있다.

슛 상태는 공이 아래에서 위를 향해 이동하는 상태를 말하며 준비 상태는 주인공 캐릭터가  공을 잡고 있는 상태를 말한다. 준비 상태에서 공을 주인공 캐릭터가 잡고 있는 것처럼 출력하기 위해서는 주인공 캐릭터와 공의 이동 좌표가 같으면 된다.

 

둘째는 이동 좌표이다.

이동 좌표는 실제 공이 출력되는 기준 좌표가 된다.

왜냐하면 공에는 중심 좌표의 개념이 없기 때문이다.

 

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

이 속성은 이동을 언제 할 것인지를 결정하는 부분으로 이동 속력과도 연관된다.

속력은 단위 시간당 이동 거리이므로 이 속성은 단위 시간에 해당이 된다.

각 스테이지마다 골대의 길이와 공의 이동 시간 간격에 변화를 주면 각 스테이지를 다양하게 구성할 수 있다.

 

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

현재 이동을 했다면 이 순간부터 다음 이동까지의 시간 차이를 측정해야 하며 이 시간을 경과 시간이라고 한다. 모든 개체의 이동 여부는 이 경과 시간과 이동 시간 간격을 비교하여  결정한다. 이와 같은 과정을 적용하기 위해서는 현재의 이동 시간을 이전 이동 시간으로 저장하고 현재 시각을 계속 읽어 이전 이동 시간과 비교하여 이동 여부를 결정하면 된다.

현재 이 속성은 시간차를 이용하는 모든 개체에 공통적으로 들어가는 속성중 하나이다.

 

지금까지 언급한 속성을 정리하면 다음과 같다.

 

 

① 공 상태

② 이동 좌표

③ 이동 시간 간격 

④ 이전 이동 시각

 

[표 5-3] 공 속성

 

 

typedef struct  _BALL

{

        int      nIsReady;     // 준비상태(1), 슛상태(0)

        int      nMoveX, nMoveY;    // 이동 좌표

        clock_t  MoveTime; // 이동 시간 간격

        clock_t  OldTime;   // 이전 이동 시각

} BALL;

 

[소스 5-4] 속성 구조체

 

- 이동 및 키보드 처리

 

키보드 처리 부분을 먼저 살펴보면 k 키가 입력되면 공은 자신의 상태 속성을 준비 상태에서 슛 상태로 바꿔 이동하게 된다. 앞의 게임 기획서의 제안사항을 보면 공은 연속적으로 슛할 수 있는 것이 아니라 한 번에 한 개씩 슛할 수 있다고 되어 있다. 이것은 공이 슛된 상태에서는 또 다른 공을 슛할 수 없다는 것이다. 그래서 키 입력 처리를 할 때는 슛 상태와 준비 상태를 먼저 파악하여 슛 상태에서 또 슛 동작이 되지 않게 해야 한다.

 

이동에 있어서 공이 준비 상태라면 이것은 주인공 캐릭터가 잡고 있는 상태이므로 주인공 캐릭터의 이동 좌표를 공의 이동 좌표에 대입하여 항상 같이 이동하도록 하면 된다.

만약 슛 상태라면 이동 시간 간격에 따라 y 좌표를 일정한 거리만큼 빼주면 공은 아래에서 위로 이동하게 된다.

 

[실습 예제 5-2]

 

[실습 예제 5-1]에서 작성한 소스에 추가적으로 k키가 입력되면 주인공 캐릭터가 공을 슛하는 프로그램을 작성해 보자.

단, 공이 슛 상태에서 공의 이동 좌표가 경계 영역 y 좌표 0과 같으면 충돌로 판단하고 공의 상태를 준비 상태로 바꾼다. 그리고 공의 초기 상태는 항상 준비 상태이다.

 

[그림 5-24] 준비 상태 출력

 

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

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _PLAYER

{

        int nCenterX, nCenterY;

        int nMoveX, nMoveY;

        int nX, nY;

} PLAYER;

 

typedef struct _BALL

{

        int nIsReady;        // 준비 상태(1), 슛상태(0)

        int nMoveX, nMoveY;  // 이동 좌표

        clock_t MoveTime;    // 이동 시간 간격

        clock_t OldTime;     // 이전 이동 시각

} BALL;

 

BALL g_sBall;

PLAYER g_sPlayer;

char g_strPlayer[] = "┗━●━┛";

int g_nLength;

 

void Init()

{

     g_sPlayer.nCenterX = 4;

     g_sPlayer.nCenterY = 0;

     g_sPlayer.nMoveX = 20;

     g_sPlayer.nMoveY = 22;

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

     g_nLength = strlen( g_strPlayer );

 

     // Note: 공의 초기화 --> 추가된 부분

     g_sBall.nIsReady = 1;

     g_sBall.nMoveX = g_sPlayer.nMoveX;

     g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

     g_sBall.MoveTime = 100;

}

 

void Update()

{

     // Note: 공의 이동 처리 --> 추가된 부분

     clock_t CurTime = clock();

     if( g_sBall.nIsReady == 0 ) // 이동 중일 때

     {   // 이동 시간 간격에 의한 이동

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

        {

            if( g_sBall.nMoveY - 1 > 0 )

            {

                g_sBall.nMoveY--;

                   // 다음 이동 시각과 비교하기 위해 현재 시간을 이전 시간 변수에 저장

                g_sBall.OldTime = CurTime;

             }else{

                g_sBall.nIsReady = 1;

                g_sBall.nMoveX = g_sPlayer.nMoveX;

                g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

             }

          }

        }else{

                 g_sBall.nMoveX = g_sPlayer.nMoveX;

          }

     }

}

 

void Render()

{

     char string[100] = { 0, };       

     ScreenClear();

 

     // Note: 렌더링 시작 

 

     // Note: 2 컬럼씩 클리핑

     if( g_sPlayer.nX < 0 )  //  왼쪽 클리핑 처리

         ScreenPrint( 0, g_sPlayer.nMoveY, &g_strPlayer[g_sPlayer.nX*-1]);    

     else if( g_sPlayer.nMoveX + (g_nLength - (g_sPlayer.nCenterX + 1)) > 79 )

     {

         strncat( string, g_strPlayer, g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX  + 1) - 79 ) );

         ScreenPrint( g_sPlayer.nX, g_sPlayer.nMoveY, string );            

     }else{ // 1 컬럼씩 이동

        ScreenPrint( g_sPlayer.nX, g_sPlayer.nMoveY, g_strPlayer );

     }

 

     ScreenPrint( g_sBall.nMoveX, g_sBall.nMoveY, "⊙" );

     sprintf( string, "주인공 이동 좌표 : %d, %d", g_sPlayer.nMoveX, g_sPlayer.nMoveY );

     ScreenPrint( 0, 0, string );

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;

 

           switch( nKey )

           {

            case 'j' :

                 // [실습 예제 5-1]과 동일

                 break;

            case 'l' :

                   // [실습 예제 5-1]과 동일

                   break;

            case 'k' :

                   if( g_sBall.nIsReady )

                   {

                       g_sBall.nMoveX = g_sPlayer.nMoveX;

                       g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

                       g_sBall.OldTime = clock();

                       g_sBall.nIsReady = 0;

                    }

                    break;

             }

        }

 

        Update();    // 데이터 갱신

        Render();    // 화면 출력              

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-5] 공과 주인공을 이동하기 위한 전체 소스

 

47행부터 66행까지는 이동에 따른 좌표의 변화에 대한 내용들이다.

각 개체의 이동 동기화는 시간 차이를 이용하므로 현재 시각과 이전 시각의 시간 간격에 따라 이동이 결정된다. 50행은 이와 같은 내용을 코드로 옮긴 것이다.

참고로 50행의 조건을 아래와 같이 작성해도 같은 의미가 되는데 C언어에서는 연산자 우선순위가 있어서 아래와 같은 순서에 의해 계산되지만 괄호를 분명하게 하는 것이 좋다.

 

 

계산 순서 : ➀ → ➁

 

 

 

우선순위

연산자

1

(), [], ->, .

2

sizeof, & ++, ==, ~, !, *(포인터연산자), +(부호), -(부호)

3

*, /, %

4

+, -

5

<<, >>

6

<, <=, >=, >

7

==, !=

8

&

9

^

10

|

11

&&

12

||

13

?:

14

=, *=, +=, /=, %=, &=, ^=, |=, <<=, =>>

15

, (콤마)

 

[표 5-4] 연산자 우선순위

 

124행부터 130행은 공이 준비 상태일 때 k 키가 눌려지면 공을 초기화하는 부분이다.

공이 슛 상태일 때는 g_sBall.nIsReady가 0이므로 k키가 여러 번 눌려진다고 해도 이 조건식에 의해 공은 초기화 되지 않는다.

 

STEP 02

 

 

[그림 5-26] 2단계 제작 로드맵

 

■ 골대

 

- 속성 정의 및 출력

 

골대를 구현하기 위한 속성을 정의해 보자.

 

첫째, 골대는 좌우로 이동한다.

좌우로 이동한다는 것은 골대를 출력하기 위한 이동 좌표가 필요하다는 것이다.

이 이동 좌표는 곧 출력 기준 좌표로써 [그림 5-27]과 같이 왼쪽 골대의 두 컬럼 중에서 첫째 컬럼의 좌표에 해당된다.

 

[그림 5-27] 이동 좌표

 

둘째, 골대는 일정한 시간 간격으로 이동한다.

이 속성은 첫째 속성을 좀 더 구체화한 속성이 된다.

일정한 시간 간격으로 이동하기 위해서는 앞서 살펴본 공의 속성과 같이 이전 이동 시간과 이동 거리 속성이 있어야 한다. 그래서 셋째 속성은 이전 이동 시간이며 넷째 속성은 이동 거리가 된다.

 

셋째, 경과 시간을 계산하기 위한 이전 이동 시간이다.

 

넷째, 일정한 시간 간격으로 이동하기 위한 이동 거리이다.

 

다섯째, 각 스테이지마다 다양한 길이의 골대가 있다.

골대의 길이가 가변적이라는 것은 크기를 가진다는 의미이므로 골대를 출력할 때 중심 좌표를 기준으로 출력해야 하지만 골대가 단순히 경계영역 안에서만 이동하는 것이므로 중심 좌표를 적용할 필요가 없다. 그래서 첫째 속성인 이동 좌표는 출력 기준 좌표가 된다.

 

여섯째, 골대에는 골인의 기준이 되는 골인 라인이 있다.

골인 라인은 골대의 길이에 따라 가변적으로 출력된다.

골대의 특성상 [그림 5-28]과 같이 반드시 한 개의 골인 라인은 있어야 한다.

 

 

[그림 5-28] 기본 공대

[그림 5-29] 골대 길이가 2

 

 

예를 들어, 골대의 길이가 2만큼 늘어난다는 것은 [그림 5-29]와 같이 1개의 기본 골인 라인 외에 양 옆으로 2개씩 확장되는 것을 의미하므로 총 5개의 골인 라인이 있어야 한다.

이 게임은 골대의 최대 라인의 길이를 3으로 제한하고 있으므로 골인 라인의 최대 개수는 7이 된다. 

 

골인 라인은 골인 영역을 출력하기 위한 단순한 역할만을 하므로 골인 라인을 출력하기 위한 x 좌표만 지정하고 y좌표는 골대의 y좌표를 사용한다.

 

여기까지 골대의 6가지 속성을 살펴보았다.

골대가 일반적인 캐릭터 속성과 다른 점은 생명에 관한 속성이 없다는 것이다.

일반적으로 캐릭터가 충돌하면 생명 속성에 의해 출력에서 제외되지만 골대는 게임이 종료될 때까지 좌우로만 계속 이동하면 되므로 생명이라는 속성이 필요 없다.

 

이와 같은 내용을 정리하면 아래 [표 5-5]와 같으며 [소스 5-6]과 같이 정의할 수 있다.

 

 

① 이동 좌표

② 골대 길이

③ 골인 라인의  x 좌표 ( 7개 )

④ 이동 시간 간격

⑤ 이전 이동 시각

⑥ 이동 거리

 

[표 5-5] 골대 속성

 

 

typedef struct _GOAL_DAE

{

int           nMoveX, nMoveY;  // 이동 좌표

int        nLength;     // 골대 길이   

int           nLineX[7]; // 골인 라인 x 좌표 (7개)

int           nDist;      // 이동 거리

clock_t     MoveTime;  // 이동 시간 간격

clock_t     OldTime;    // 이전 이동 시간

} GOAL_DAE;

 

[소스 5-4] 골대 속성 정의

 

[실습 예제 5-3]

 

골대 길이 설정값에 따라 골인 라인이 최대 7개까지 출력되도록 [그림 5-30]과 같이 프로그래밍해 보자. 기본적인 구조는 3장의 프레임워크를 적용한다.

 

[그림 5-30] 골대와 골인 라인 출력

 

 

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _GOAL_DAE

{

        int nMoveX, nMoveY;     // 이동 좌표

        int nLength;              // 골대 길이  

        int nLineX[7];            // 골인 라인 x 좌표 (7개)

          int nDist;                 // 이동 거리

        clock_t MoveTime;        // 이동 시간 간격

        clock_t OldTime;         // 이전 이동 시간

        

} GOAL_DAE;

 

GOAL_DAE g_sGoalDae;

 

void Init()

{

     int nLength, i;

     g_sGoalDae.nMoveX = 20;

     g_sGoalDae.nMoveY = 2;

     g_sGoalDae.nLength = 1;

     g_sGoalDae.MoveTime = 100;

     g_sGoalDae.OldTime = clock();

     g_sGoalDae.nDist = 1;

     nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

                

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

     {

        g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);    

     }   

}

 

void Update()

{       

}

 

void Render()

{       

     int nLength, i;

     ScreenClear();

 

     // Note: 렌더링 시작    

     ScreenPrint( g_sGoalDae.nMoveX, g_sGoalDae.nMoveY, "□" );

     nLength = g_sGoalDae.nLength*2 + 1;

 

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

        ScreenPrint( g_sGoalDae.nLineX[i], g_sGoalDae.nMoveY, "━");

        

     ScreenPrint( g_sGoalDae.nLineX[nLength-1] + 2, g_sGoalDae.nMoveY, "□");

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;              

        }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력    

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-7] 골대 길이가 적용된 골대 전체 소스

 

30행은 골대 길이에 따라 골인 라인의 개수를 계산하는 부분이다.

여기서 구한 골인 라인의 개수는 7개의 배열에서 사용할 배열 개수를 의미한다.

그래서 32행과 49행을 보면 이 배열 개수를 골대 라인의 x 좌표를 설정하거나 출력할 때 반복문의 최대 반복수로 사용하고 있다.

 

 

길이

배열의 크기

0

1

1

3

2

5

3

7

 

[표 5-6] 골대 길이에 따른 골인 라인의 배열 개수

 

34행을 보면 이동 좌표를 기준으로 2 컬럼씩 차이가 나게 골대 라인의 x 좌표를 설정하고 있다.

 

48행과 54행을 보면 골대를 출력할 때 골인 라인의 x 좌표를 이용하여 간단히 출력하는 것을 알 수 있다.

프로그래밍에서 실시간으로 계산하여 값을 적용할 수도 있겠지만 48행과 54행과 같이 미리 계산된 값을 이용하는 것도 때로는 프로그래밍을 간편하게 한다.

 

- 이동

 

골대가 경계 영역에 닿게 되면 이동 방향을 바꾸게 되는데 이것은 이동 거리에 변화를 주는 것이 아니라 이동 거리의 부호에 영향을 주면 된다.

이 방법은 3D에서도 상당히 많이 사용되는 개념으로 곱셈의 항등수인 ‘-1’을 이용한다.

 

골대가 경계 영역에 닿으면 이동 거리에 -1을 곱하면 크기는 변하지 않지만 부호가 바뀌게 되어 골대의 이동 방향이 바뀌게 된다. 이러한 성질을 이용하면 충돌할 때 방향을 바꾸기는 상당히 쉽다.

 

 

 

[표 5-7] 항등수의 성질

 

[실습 예제 5-4]

 

[실습 예제 5-3]에서 구현한 골대가 좌우로 이동되게 Update() 함수를 프로그래밍해 보자. 단, 골대가 경계 영역에 닿게 되면 방향을 바꾸도록 하고 x 좌표의 경계 영역은 0과 79이다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

void Update()

{

     clock_t CurTime = clock();

     int nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

     int i;

 

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

     {

        g_sGoalDae.OldTime = CurTime;

        if( g_sGoalDae.nMoveX + g_sGoalDae.nDist >= 0 &&

                      ((g_sGoalDae.nLineX[nLength-1] + 3 ) + g_sGoalDae.nDist) <= 79)

         {

                g_sGoalDae.nMoveX += g_sGoalDae.nDist;

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

                {

                     g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);        

                }       

         }else{

                  g_sGoalDae.nDist = g_sGoalDae.nDist * -1; // -1 이 방향을 바꾸어 줌.

         }

     }   

}

 

[소스 5-8] Update() 함수

 

7행은 시간 간격을 조사하여 이동여부를 결정하는 부분이다.

이 조건은 10행에서 골대가 이동 영역 안에 있는지 조사하여 이동과 이동 방향 전환을 선택하게 된다. 골대 속성 중에서 이동 좌표는 [그림 5-31]과 같으며 우측 경계 영역에 닿는 부분은 11행과 같이 g_sGoalDae.nLine[nLength-1] + 3 되는 위치가 된다. 이 위치는 아래 [그림 5-32]이다.

 

[그림 5-31] 골대의 이동 좌표

[그림 5-32] 경계 영역과 닿는 우측 x 좌표

 

STEP 03

 

 

[그림 5-33] 3단계 제작 로드맵

 

■ 충돌

 

- 주인공 캐릭터와 경계 영역의 충돌

 

주인공 캐릭터가 경계 영역에 충돌하는 경우는 [그림 5-34]와 같이 두 가지 경우가 있다.

 

  [그림 5-34] 경계 영역에 충돌한 주인공 캐릭터

 

주인공 캐릭터와 경계 영역과의 충돌 테스트는 언제하면 될까?

주인공 캐릭터는 플레이어의 키 입력에 따라 이동하므로 이동키가 입력되었을 때 충돌 테스트를 하면 된다. [그림 5-35]를 보면 주인공 캐릭터의 좌우 경계 좌표가 중심 좌표-2, 중심 좌표+3이 되는데 이것은 주인공의 팔과 머리에 해당하는 문자가 특수문자로서 2컬럼의 가로 길이를 갖기 때문이다.

 

[그림 5-35] 주인공 캐릭터의 충돌 경계 좌표

 

[실습 예제 5-5]

 

주인공 캐릭터와 경계 영역과의 충돌 테스트가 되도록 [실습 예제 5-2]의 [소스 5-5]에 코드를 추가하여 보자.

 

 

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

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

               break;

           switch( nKey )

           {

            case 'j' :

                     if( g_sPlayer.nMoveX > 0 )   // 왼쪽 충돌 경계 좌표 체크

                     {

                    g_sPlayer.nMoveX--;

                        // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                    nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                    // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                    if( g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ||

                                                       g_sPlayer.nMoveX + nRemain > 79 )  

                       g_sPlayer.nMoveX--;

                        g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                      }

                  break;

            case 'l' :

                       if( g_sPlayer.nMoveX + 1 < 79 )  // 오른쪽 충돌 경계 좌표 체크

                       {

                      g_sPlayer.nMoveX++;

                          // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                      nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                      // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                     if( g_sPlayer.nMoveX + nRemain > 79 ||

                                            g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0  )

                        g_sPlayer.nMoveX++;

                     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                      }

                    break;

              case 'k' :

                        // [소스 5-3] 과 동일함

                      break;

           }

         }

         Update();

         Render();       

     }

     Release();

     ScreenRelease();

     return 0;

}      

 

[소스 5-9] [실습 예제 5-4]의 부분 소스

 

- 공과 골대의 충돌

 

공이 골대와 충돌되는 경우는 두 가지가 있다.

첫째, [그림 5-36]과 같이 공이 골인 라인에 닿는 경우이며 득점과 연관된다.

 

[그림 5-36] 공과 골인 라인의 충돌

 

둘째, [그림 5-37]과 같이 공이 골대에 닿는 경우이며 이때 공은 주인공 캐릭터가 다시 슛할 수 있도록 준비상태가 된다.

 

 

[그림 5-37] 공과 골대와의 충돌

 

[실습 예제 5-6]

 

좌우로 이동하는 골대를 구현한 [실습 예제 5-3]과 주인공 캐릭터의 충돌 경계 영역이 적용된 [실습 예제 5-5]를 기본으로 하여 공과 골대의 충돌을 감지할 수 있는 프로그램을 작성하여 보자. 그리고 충돌했을 때 공은 주인공 캐릭터가 다시 슛 동작을 할 수 있도록 준비 상태가 된다.

 

[그림 5-35] 공과 골대 충돌

 

 

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

6

void Update()

{

     clock_t CurTime = clock();

     int nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

     int i;

 

     // Note: 골대

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

     {

        g_sGoalDae.OldTime = CurTime;

        if( g_sGoalDae.nMoveX + g_sGoalDae.nDist >= 0 &&

                  ((g_sGoalDae.nLineX[nLength-1] + 3 ) + g_sGoalDae.nDist) <= 79)

        {

           g_sGoalDae.nMoveX += g_sGoalDae.nDist;

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

           {

                 g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);     

           }    

         }else{

            g_sGoalDae.nDist = g_sGoalDae.nDist * -1; // -1 이 방향을 바꾸어 줌.

         }

     }   

 

     if( g_sBall.nIsReady == 0 ) // 이동 중일 때

     {   // 이동 시간 간격에 의한 이동

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

        {

            if( g_sBall.nMoveY - 1 > 0 )

            {

               g_sBall.nMoveY--;

                 // 다음 이동 시각과 비교하기 위해 현재 시간을 이전 시간 변수에 저장

               g_sBall.OldTime = CurTime;

        

               // 골대 라인 충돌

                if( g_sBall.nMoveX >= g_sGoalDae.nLineX[0] &&

                                   g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[nLength-1] )

                {

                   if( g_sBall.nMoveY <= g_sGoalDae.nMoveY )

                   {   // 공 초기화

                      g_sBall.nIsReady = 1;

                      g_sBall.nMoveX = g_sPlayer.nMoveX;

                      g_sBall.nMoveY = g_sPlayer.nMoveY - 1;          

                      g_nBallCount++;  //득점

                   } 

                  // 골대 충돌

                }else if( ( g_sBall.nMoveX >= g_sGoalDae.nLineX[0] - 2 &&

                                 g_sBall.nMoveX <= g_sGoalDae.nLineX[0] - 1 ) ||

                          ( g_sBall.nMoveX + 1 >= g_sGoalDae.nLineX[0] - 2 &&

                                  g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[0] - 1 ) ||

                          ( g_sBall.nMoveX >= g_sGoalDae.nLineX[nLength-1] + 2 &&

                                  g_sBall.nMoveX <= g_sGoalDae.nLineX[nLength-1] + 3) ||                       ( g_sBall.nMoveX + 1 >= g_sGoalDae.nLineX[nLength-1] + 2 &&

                              g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[nLength-1] + 3 ))

                {

                    if( g_sBall.nMoveY <= g_sGoalDae.nMoveY )

                    {   // 공 초기화

                       g_sBall.nIsReady = 1;

                       g_sBall.nMoveX = g_sPlayer.nMoveX;

                       g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

                     }

                 }

               }else{ // 공 초기화

                g_sBall.nIsReady = 1;

                g_sBall.nMoveX = g_sPlayer.nMoveX;

                g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

               }

           }

        }else{

                 g_sBall.nMoveX = g_sPlayer.nMoveX;

        }       

}

 

[소스 5-10] 전체 소스 중에서 Update()함수

 

위의 실습 예제는 [실습 예제 5-3]과 [실습 예제 5-5]를 합치고 충돌 체크를 하는 부분만 추가하면 된다.

두 예제가 중복되는 부분은 제외하고 충돌 체크를 하는 부분만 살펴보면 위의 [소스 5-10]과 같다.

 

공과 골대의 충돌은 35행부터 61행까지가 된다.

먼저 공과 골대 라인과의 충돌을 체크하기 위해서 35행과 36행에서는 공이 골대 라인의 x 좌표 안에 있는가를 먼저 조사하고 38행와 같이 y좌표를 조사하여 충돌을 체크한다.

 

그리고 득점과는 상관없지만 공과 골대가 충돌하면 다시 공을 슛할 수 있는 준비 상태로 설정되어야 하므로 46행부터 61행은 이와 같은 처리를 하고 있다.

특히 골대는 2컬럼의 특수문자로 되어 있어서 공이 골대와 충돌했는지를 판단하기 위해서는 공이 이 2컬럼 안에 있는지를 개별적으로 조사해야 한다.

이와 같은 부분은 46행부터 53행까지가 해당된다.

 

그 외의 소스 코드는 이미 각 예제에서 완성된 코드이므로 각 코드를 붙여넣기 하여 완성한다.

 

STEP 04

 

[그림 5-39] 4단계 제작 로드맵

 

게임 출력 화면은 크게 게임 소개 화면, 스테이지 화면, 진행 화면, 미션 실패 및 성공 화면, 결과 화면으로 나눌 수 있다.

 

■ 화면 출력 및 키 입력 확인

 

[그림 5-40] 키 입력에 따른 진행

 

 

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

void FailureScreen()

{       

     ScreenPrint( 0, 0, "┏━━━━━━━━━━━━━━━━━━━━━┓");

     ScreenPrint( 0, 1, "┃                                          ┃");

     ScreenPrint( 0, 2, "┃                                          ┃");

     ScreenPrint( 0, 3, "┃                                          ┃");

     ScreenPrint( 0, 4, "┃                                          ┃");

     ScreenPrint( 0, 5, "┃                                          ┃");

     ScreenPrint( 0, 6, "┃                                          ┃");

     ScreenPrint( 0, 7, "┃                                          ┃");

     ScreenPrint( 0, 8, "┃                                          ┃");

     ScreenPrint( 0, 9, "┃                                          ┃");

     ScreenPrint( 0,10, "┃                                          ┃");

     ScreenPrint( 0,11, "┃                    미션 실패 !!!!         ┃");

     ScreenPrint( 0,12, "┃                                          ┃");

     ScreenPrint( 0,13, "┃                                          ┃");

     ScreenPrint( 0,14, "┃                 ●┳━┓                 ┃");

     ScreenPrint( 0,15, "┃                   ┛  ┗                 ┃");

     ScreenPrint( 0,16, "┃                  ■■■■                 ┃");

     ScreenPrint( 0,17, "┃                                          ┃");

     ScreenPrint( 0,18, "┃        다시 하시겠습니까? (y/n)          ┃");

     ScreenPrint( 0,19, "┃                                          ┃");

     ScreenPrint( 0,20, "┃                                          ┃");

     ScreenPrint( 0,21, "┃                                          ┃");

     ScreenPrint( 0,22, "┗━━━━━━━━━━━━━━━━━━━━━┛");             

}

 

[소스 5-11] [그림 5-40]의 소스

 

[그림 5-40]과 같은 결과 화면을 출력하고 입력 받는 값은 y, Y, n, N 중 하나이다. 이때 입력 받는 값이 문자열이 아니라 단일 문자이므로 한 문자를 화면으로 입력 받기 위해서는 main() 부분에서 아래와 같이 처리하면 된다.

 

 

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

int main(void)

{

   int nKey;

 

   Init();

   while( 1 )

   {

         if( _kbhit() )

         {

            nKey = _getch();

            switch( nKey )

            {

             case 'y' :

             case 'Y' : // 입력 처리 부분

                       break;

             case 'n' :

             case 'N' : // 입력 처리 부분

                       break;

          }

          

          Update();

          Render();         

    }

    Release();

    ScreenRelease();

}

 

[소스 5-12] 입력 받는 소스

 

위의 소스 13행과 14행 이후를 보면 y와 Y는 같은 처리하므로 13행 case에 break를 생략하면 두 키 입력에 대한 같은 처리할 수 있다.

이와 같이 case에 반드시 break가 하나씩 있어야 하는 것이 아니라 때로는 생략함으로써 효율을 높일 수 있다.

 

■ 골 세리머니 화면 출력 (효과 화면)

 

골 세리머니 화면은 게임 진행과는 상관없이 일정 시간동안 출력되고 사라지는 효과 화면을 말한다. 이 처럼 잠시 출력되는 효과 화면은 2D와 3D 게임에서도 많이 사용되는데 원리적인 부분은 이와 유사하다. 이 효과 화면도 시간 간격 차이를 이용하여 일정한 시간 간격 동안 출력되는 것이므로 이것은 골대와 공이 이동하는 방법과 같은 원리이다.

 

 

효과가 출력된 경과 시간 = 현재 시간 - 효과 시작 시각

 

[식 5-2] 효과가 출력된 경과 시간

 

효과를 출력하기 이전에 효과의 속성을 정의해 보자.

효과는 일정한 시간동안 출력되다가 사라져야 한다. 따라서 효과를 지속하기 위한 시간 속성이 있어야 하며 효과 출력 시작 시간부터 매번 현재 시각을 읽어 경과된 시간을 체크한다. 이 경과 시간과 효과 지속 시간을 비교하여 효과 출력을 결정하게 된다.

효과를 출력하기 위한 속성을 정의하면 [표 5-8]과 [소스 5-13]과 같다.

 

 

① 효과 지속 시간

② 효과 시작 시각

 

[표 5-8] 효과 속성

 

 

typedef struct _EFFECT

{

        clock_t StratTime; // 효과 발생 시각

        clock_t StayTime;  // 효과 지속 시간

} EFFECT;

 

[소스 5-13] 효과 속성 정의

 

[실습 예제 5-7]

 

k 키를 누르면 [그림 5-41]과 같이 3초 동안 효과 화면이 출력되도록 프로그램을 작성해 보자.

 

[그림 5-41] 골 세레머니 화면

 

 

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

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

typedef struct _EFFECT

{

        clock_t StratTime; // 효과 발생 시각

        clock_t StayTime;  // 효과 지속 시간     

} EFFECT;

 

EFFECT g_sEffect;

int g_nIsGoal;

 

void GoalMessage( int nX, int nY )

{

     ScreenPrint( nX, nY,     "☆ )) 골인 (( ★" );

     ScreenPrint( nX, nY + 1, "\(^^')/ \(\"*')/" );

     ScreenPrint( nX, nY + 2, "   ■       ■");

     ScreenPrint( nX, nY + 3, "  ┘┐    ┌└" );

}

 

void Init()

{       

     g_sEffect.StayTime = 3000; // 3초 설정

}

 

void Update()

{

     clock_t CurTime = clock();

     if( g_nIsGoal == 1 )

     {   // 효과 지속 시간 체크

         if( (CurTime - g_sEffect.StratTime) > g_sEffect.StayTime )

             g_nIsGoal = 0;

     }   

}

 

void Render()

{               

     ScreenClear();

 

     // Note: 렌더링 시작    

     if( g_nIsGoal == 1 )

     {

        GoalMessage( 10, 5 );

     }   

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

          if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;      

 

           if( nKey == 'k' )

           {

               if( g_nIsGoal == 0 )

               {

                g_nIsGoal = 1;

                g_sEffect.StratTime = clock();

               }

            }

         }

 

           Update();    // 데이터 갱신

           Render();    // 화면 출력   

    }   

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-14] 효과 출력 소스

 

14행에 선언된 g_nIsGoal 변수는 현재 효과가 화면에 출력되고 있는지를 알 수 있게 해주는 변수이다. 이 변수값에 따라 효과가 발생된다.

 

■ 게임 진행 제어

 

이 게임은 진행 상태를 다음과 같이 일곱 가지 형태로 나눌 수 있다.

 

 

상태

설명

초기 상태(INIT)

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

준비상태(READY)

스테이지 정보 출력

게임 진행 상태(RUNNING)

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

중지 상태(STOP)

미션 성공과 실패를 판단

미션 성공 상태(SUCCESS)

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

미션 실패 상태(FAILED)

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

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

게임 결과 출력 및 종료

 

[표 5-9] 게임 상태 값

 

이 일곱 가지 형태를 프로그래밍 안에서 구별하기 위해 enum형으로 아래와 같이 정의한다.

 

 

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

 

[소스 5-15] enum형

 

게임의 흐름은 위의 열거형 데이터로 게임의 진행 과정을 결정하며 스테이지를 나타내는 변수는 스테이지를 초기화할 때 사용된다.

이와 같이 전체 게임의 흐름은 게임 상태를 나타내는 열거형 변수와 스테이지 변수 이 두 개로 게임 전체를 제어한다.

전체적인 게임의 흐름은 아래 [그림 5-42]와 같다.

 

[그림 5-42] 게임 전체 흐름도

 

[실습 예제 5-8]

 

위의 [그림 5-42]와 같이 게임이 진행되도록 프로그래밍해 보자. 스테이지가 종결되는 조건은 스테이지별 제한 시간(예: 10초)이며 종결된 후에는 미션의 성공과 실패에 따라 게임 진행 여부를 결정하게 된다. 이때 미션 성공은 s 키로 설정하며 미션 실패는 f 키로 설정하여 다음 단계로 진행되게 하자. s 키와 f 키의 설정은 제한 시간이 되기 전에 설정하여 STOP 상태에서 미션 상태를 판단하게 한다. 전체적인 진행 과정은 아래의 [그림 5-43]부터 [그림 5-46]과 같이 출력되게 하자.

 

[그림 5-43] 게임 전체 초기화

 

[그림 5-44] 스테이지 초기화

 

[그림 5-45] 스테이지 준비

 

[그림 5-46] 게임 진행

 

 

[그림 5-47] 게임 미션 실패

 

 

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _GAME_STATE { INIT, READY, RUNNING, STOP, SUCCESS, FAILED,

                                RESULT } GAME_STATE;

                                

GAME_STATE g_GameState = INIT;

int g_nGoal, g_nStage, g_GameStartTime;

 

char g_strMessage[100]; // 게임 상태를 저장하는 문자열

clock_t g_OldTime; // 게임 상태 전이를 위한 이전 시각 저장

 

void Init()

{      

     g_OldTime = clock();

}

 

void Update()

{

     clock_t CurTime = clock();      

 

     switch( g_GameState )

     {

     case INIT :  // 초기화

               if( g_nStage == 0 )

               {

                sprintf( g_strMessage, "%s", "게임 및 사운드 초기화" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_nStage = 1;

                }

                }else{

                sprintf( g_strMessage, "[INIT] 게임 %d 스테이지 초기화", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = READY;

                }

                }

                break;

      case READY : // 스테이지 준비   

                sprintf( g_strMessage, "[READY] %d 스테이지", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = RUNNING;

                    g_GameStartTime = CurTime;

                }

                break;

     case RUNNING : // 게임 진행                             

                if( CurTime - g_GameStartTime > 10000 ) // Note: 제한 시간

                {

                   g_GameState = STOP;                                    

                }else{                   

                   sprintf( g_strMessage, "[RUNNING] 제한 시간 : 10초  현재시간 : %d",

                                 ( CurTime - g_GameStartTime ) / 1000 );                     }               

                break;

     case STOP : // 게임 미션 판정                            

                if( g_nGoal == 1 )

                   g_GameState = SUCCESS;

                else

                    g_GameState = FAILED; 

                break;

     case SUCCESS : // 미션 성공

                sprintf( g_strMessage, "%s", "미션 성공" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = INIT;

                    ++g_nStage;                                  

                }

                break;

     case FAILED : // 미션 실패

                sprintf( g_strMessage, "%s", "미션 실패! 계속 하시겠습니까? <y/n> " );

                break;

 

     case RESULT: // 게임 결과

                sprintf( g_strMessage, "%s", "게임 결과 화면" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;                                  

                }                               

                break;

        }

}

 

void Render()

{       

     clock_t CurTime = clock();

   

     ScreenClear();

 

     // Note: 렌더링 시작    

     ScreenPrint( 20, 10, g_strMessage );

                         

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

         if( _kbhit() )

        {

           nKey = _getch();

             if( g_GameState == RESULT ) // RESULT상태에서 아무키나 입력되면 종결

               break;

 

           switch( nKey )

             {

              case 's' :

                        g_nGoal = 1;

                        break;

              case 'f' :

                      g_nGoal = 0;

                        break;

             case 'y' :

             case 'Y' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = INIT;

                        g_OldTime = clock();

                     }

                     break;

             case 'n' :

             case 'N' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = RESULT;

                        g_OldTime = clock();

                     }

                     break;

              }

        }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력    

    }    

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-16] 게임 흐름 출력 소스

 

위의 소스는 Update()에서 게임 진행 상태에 대한 전이를 전부 맡아서 처리하고 Render() 함수는 진행된 결과만을 출력하는 구조로 되어 있다.

게임 결과를 출력할 때 일정시간 화면에 출력하여 현재 게임 상태를 파악할 수 있도록 하기 위해 아래와 같은 코드가 Update() 함수에 있는 것을 알 수 있다.

 

if( CurTime - g_OldTime > 3000 )

{

    g_OldTime = CurTime;

    ...             

}

 

[소스 5-17] 메시지를 출력하는 경과 시간을 체크

 

미션 실패일 때는 화면으로 y, Y, n, N의 입력을 받아야 하므로 134행과 135행, 142행과 143행은 대소문자 구분 없이 입력에 따라 처리를 하기 위해 switch문의 break를 일부로 생략하고 있다.

 

■ 스테이지 구성

 

 

게임 미션이 완료되면 또 다른 새로운 미션이 시작이 된다. 이와 같은 새로운 미션을 제공하기 위해서 프로그램 전체 구조를 바꾸는 것이 아니라 일부 데이터를 바꾸어 게임의 난이도를 조절하게 된다. 물론 경우에 따라서 새로운 게임 프로그램이 실행되게 할 수도 있겠지만 여기서는 난이도를 조정하는 것으로 스테이지에 변화를 주도록 한다.

 

슛 골인 게임에서 난이도를 조절할 수 있는 요소로 무엇이 있을까 생각해 보면 다음과 같다.

 

첫째, 목표 골인 개수이다.

각 스테이지마다 목표 골인 개수를 주어 게임의 긴장감을 줄 수 있으며 목표 골인 개수는 플레이어가 다음 미션으로 넘어가게 하는 요소가 된다.

 

둘째, 제한 시간이다.

목표 골인 개수가 없이 무한대의 시간에서 게임을 진행한다면 아마도 게임의 긴장감은 전혀  없을 것이다. 그래서 시간을 제한하고 그 시간 안에 목표한 골인을 할 수 있도록 한다.

 

셋째, 골대 길이이다.

골대 길이는 플레이어가 골인을 할 수 있는 확률을 높여준다. 골대 길이가 길면 길수록 더욱 골인을 넣기가 쉬워진다는 것이다.

 

넷째, 골대의 최초 이동 좌표이다.

골대가 항상 멀리 있는 것이 아니라 주인공 캐릭터의 바로 앞에서도 빠르게 이동할 수 있는 것이다. 특히 골대의 x 좌표는 이동에 의해 좌표값이 변하지만 y 좌표는 한번 고정되면 게임이 종료되기까지 유지되므로 좌표의 설정도 스테이지에 변화를 줄 수 있는 요소에 해당이 된다.

 

다섯째, 골대의 이동 시간 간격이다.

골대의 이동 시간 간격은 골대의 이동 속도와 연관된다.

이동 시간 간격이 적다는 것은 그 만큼 짧은 시간마다 x 좌표에 변화를 준다는 것이며 이것은 골대는 빠르게 이동하게 한다.

 

여섯째, 골대 이동 거리이다.

현재는 1 컬럼씩 좌우로 움직이지만 같은 이동 시간 간격을 가지며 골대 이동 거리를 크게 한다면 더욱 빠르게 움직이게 된다.

그러므로 이 요소는 골대의 속도와 연관이 되는 부분이며 골대의 이동 시간 간격과 이동 거리는 항상 같이 다루어진다.

위의 여섯 가지 요소를 정리하면 아래 [표 5-10]과 같다.

 

 

① 목표 골인 개수

② 제한 시간

③ 골대 길이

④ 골대의 최초 이동 좌표

⑤ 골대의 이동 시간 간격

⑥ 골대의 이동 거리

 

[표 5-10] 스테이지 정보

 

여기까지 스테이지를 구성하기 위한 여섯 가지 기본 요소에 대해 살펴보았지만 더 많은 요소를 스테이지 요소로 넣는다면 더욱 다양한 스테이지를 구성할 수 있을 것이다.

각 요소의 설정값은 다양한 형태로 읽혀져서 설정되는데 현재 슛 골인 게임의 경우 2 스테지만 설정하고 있으므로 파일이 아닌 배열로 선언하여 설정하고 있다.

일반적으로는 파일로부터 데이터를 읽어 설정하는 방법을 사용하며 이 부분은 11장 하트 게임을 만들 때 적용해 보겠다.

 

 

typedef struct _STAGE_INFO

{

        int nGoalBall;        // 골인해야 할 볼의 개수

        clock_t LimitTime;     // 제한 시간

        int nGoalDaeLength;    // 골대 길이

        int nGoalDaeX;       // 골대 이동 X 좌표

        int nGoalDaeY;       // 골대 이동 Y 좌표

        clock_t MoveTime;    // 골대 이동 시간 간격

        int nDist;            // 골대 이동 거리 

} STAGE_INFO;

 

[소스 5-18] 스테이지 정보 정의

 

[소스 5-18]에서 정의한 구조체는 스테이지가 초기화될 때마다 Init() 함수 안에서 현재의 스테이지를 나타내는 변수와 함께 다음과 같이 설정된다.

 

 

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

typedef struct _STAGE_INFO

{

        int nGoalBall;        // 골인해야 할 볼의 개수

        clock_t LimitTime;     // 제한 시간

        int nGoalDaeLength;    // 골대 길이

        int nGoalDaeX;       // 골대 이동 X 좌표

        int nGoalDaeY;       // 골대 이동 Y 좌표

        clock_t MoveTime;    // 골대 이동 시간 간격

        int nDist;            // 골대 이동 거리 

} STAGE_INFO;

 

STAGE_INFO g_sStageInfo[] = { { 1, 1000*20, 1, 20, 3, 300 },

                                  { 10, 1000*30, 2, 20, 5, 300 } };

 

void Init()

{

     int nLength, i;

 

     if( g_nStage == -1 )

     {

        SoundInit(); // 사운드 초기화

        g_nStage = 0;

     }

 

     g_LimitTime = g_sStageInfo[g_nStage].LimitTime;  // 제한 시간 설정

     g_nGoalBallCount = g_sStageInfo[g_nStage].nGoalBall; // 목표 골인 개수

 

     g_sPlayer.nCenterX = 4;

     g_sPlayer.nCenterY = 0;

     g_sPlayer.nMoveX = 20;

     g_sPlayer.nMoveY = 22;

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

     g_nLength = strlen( g_strPlayer );

 

     // 공의 초기화

     g_sBall.nIsReady = 1;

     g_sBall.nMoveX = g_sPlayer.nMoveX;

     g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

     g_sBall.MoveTime = 100;

 

     // 골대 초기화

     g_sGoalDae.nMoveX = g_sStageInfo[g_nStage].nGoalDaeX;

     g_sGoalDae.nMoveY = g_sStageInfo[g_nStage].nGoalDaeY;

     g_sGoalDae.nLength = g_sStageInfo[g_nStage].nGoalDaeLength;

     g_sGoalDae.MoveTime = g_sStageInfo[g_nStage].MoveTime;

     g_sGoalDae.OldTime = clock();

     g_sGoalDae.nDist = g_sStageInfo[g_nStage].nDist;

     nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

                

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

      {

         g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);   

      }    

}

 

[소스 5-19] 스테이지 설정

 

위의 소스에서 g_nStage 변수는 현재의 스테이지를 나타내는 변수로써 이 변수와 게임 진행 상태를 나타내는 [소스 5-15]의 enum형 변수가 게임 전체 흐름을 제어하게 된다.

이 게임에서는 19행에서와 같이 g_nStage가 -1이면 게임이 최초 시작되는 순간이므로 이때에는 전체 게임에 적용되는 초기화가 실행된다.

이후에 g_nStage가 0 이상의 값을 가지게 되면 각 스테이지에 대한 개별적인 설정을 하게 된다. 그러면 g_nStage 값은 언제 증가될까? 이것은 미션이 성공되었을 때이다.

 

■ 게임 구조 안에 사운드 추가

 

4장에서 FMOD 사운드 시스템에 대해 살펴보았다.

우리가 앞으로 제작할 대부분의 게임은 4장의 내용만으로도 게임 안에서 사운드를 출력할 수 있다. 게임의 기본적인 흐름은 [실습 예제 5-8]과 같으므로 전체 게임 코드를 작성하기  이전에 간단한 게임 제작 구조 안에 사운드를 프로그래밍해 본다면 사운드 제어를 보다 분명히 할 수 있다. 사운드 프로그래밍은 앞으로 제작하는 모든 게임에 공통적으로 적용되는 부분이므로 5장에서만 설명하고 나머지 게임에는 이와 같은 코드를 동일하게 적용한다.

 

실제 슛 골인 게임에서 사용되는 사운드를 정리하면 아래 [표 5-11]과 같다.

 

 

구분

파일명

게임 상태

출력 시점

배경음

init.wav

INIT

게임이 초기 화면 출력할 때

run.wav

RUNNING

게임이 진행될 때

fail.wav

FAILED

미션 실패가 될 때

효과음

ready.wav

READY

스테이지 초기화를 할 때

success.wav

SUCCESS

미션 성공을 했을 때

shoot.wav

RUNNING

k 키가 눌렸을 때

wow.wav

RUNNING

골대 라인에 공이 충돌되었을 때

 

[표 5-11] 사운드 파일과 출력 시점

 

- FMOD 초기화

 

FMOD 사운드 시스템을 사용하기 위해서는 FMOD 사운드 시스템과 사운드 파일을 관리하는 FMOD 사운드를 사운드 개수만큼 생성해야 한다.

여러 개의 FMOD 사운드는 같은 데이터형이므로 배열로 선언할 수 있으며 아래 [소스 5-20]의 16행에서부터 23행까지 사운드 파일과 FMOD 사운드를 일대일 대응시킨다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

FMOD_SYSTEM *g_System;  // Note: FMOD system 변수선언

FMOD_SOUND  *g_Sound[7];

                            // 배경 음악                     효과 음악

char *g_strFileName[7] = { "init.wav", "run.wav", "fail.wav", "ready.wav", "success.wav",

                             "shoot.wav", "wow.wav" };

FMOD_CHANNEL *g_Channel[7];

 

void Init()

{  

     int i;

 

     g_OldTime = clock();

     FMOD_System_Create(&g_System);

     FMOD_System_Init( g_System, 32, FMOD_INIT_NORMAL,  NULL); 

 

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

        FMOD_System_CreateSound( g_System, g_strFileName[i], FMOD_LOOP_NORMAL, 0,

                                      &g_Sound[i]);

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

        FMOD_System_CreateSound( g_System, g_strFileName[i], FMOD_DEFAULT, 0,

                                      &g_Sound[i]);

      FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE, g_Sound[0], 0,

                                                                            &g_Channel[0]);

}

 

[소스 5-20] 사운드 파일과 FMOD 사운드 생성

 

4행의 char *g_strFileName[7] 선언은 7개의 사운드 파일명을 저장하기 위한 부분이다.

사운드를 배경 사운드로 사용할 것인지 아니면 효과 사운드로 사용할 것인지에 따라 FMOD 사운드를 생성하는 옵션이 다르다. 그래서 g_strFileName[0]부터 g_strFileName[2]까지는 배경 사운드 파일명의 메모리 주소를 저장하게 했으며 g_strFileName[3]부터 g_strFileName[6]까지는 효과 사운드 파일명의 메모리 주소를 저장하도록 선언과 동시에 초기화하고 있다. 이런 저장 방식은 배열의 인덱스로 배경 사운드와 효과 사운드를 구분할 수 있으므로 반복문을 이용하여 16행부터 18행까지 배경 FMOD 사운드 생성하고 19행부터 21행까지 효과 FMOD 사운드를 생성할 수 있다.

이와 같은 과정은 모든 사운드 파일이 FMOD 사운드와 연결된 상태를 유지하게 한다.

 

- 사운드 출력과 정지

 

사운드 출력은 [표 5-11]의 출력 시점에 따라 사운드 출력과 정지를 반복하게 된다.

주로 Update() 함수 안에서 게임상태 전이가 이루어지므로 사운드도 여기서 출력과 정지가 결정된다.

아래의 [소스 15-21]는 [표 5-11]의 시점에 따라 사운드를 출력하거나 정지하고 있다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

void Update()

{

     clock_t CurTime = clock();      

 

     switch( g_GameState )

     {

     case INIT :

              if( g_nStage == 0 )

              {

                sprintf( g_strMessage, "%s", "게임 및 사운드 초기화" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_nStage = 1;                                         

                }

               }else{

                sprintf( g_strMessage, "[INIT] 게임 %d 스테이지 초기화", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = READY;

                        FMOD_Channel_Stop( g_Channel[0] ); // 배경음 중지

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                             g_Sound[3], 0, &g_Channel[3]); // ready 사운드

                 }

               }

               break;

     case READY : 

                sprintf( g_strMessage, "[READY] %d 스테이지", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = RUNNING;

                    g_GameStartTime = CurTime;

                        

                    FMOD_Channel_Stop( g_Channel[3] );  // ready 사운드 중지

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                            g_Sound[1], 0, &g_Channel[1]); // running 배경음

                }

                break;

     case RUNNING :                               

                if( CurTime - g_GameStartTime > 10000 ) // Note: 제한 시간

                {

                    g_GameState = STOP;   

                    FMOD_Channel_Stop( g_Channel[1] );  // running 배경음 중지

                }else{                   

                    sprintf( g_strMessage, "[RUNNING] 제한 시간 : 10초  현재시간 : %d",

                                         ( CurTime - g_GameStartTime ) / 1000 );                }               

                break;

      case STOP :                         

                FMOD_Channel_Stop( g_Channel[1] );  // running 배경음 중지

                if( g_nGoal == 1 )

                {

                    g_GameState = SUCCESS;

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[4], 0, &g_Channel[4]); // 미션 성공 사운드

                }else{

                    g_GameState = FAILED; 

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[2], 0, &g_Channel[2]); // 미션 실패 사운드

                }

                break;

      case SUCCESS :

                sprintf( g_strMessage, "%s", "미션 성공" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = INIT;

                    ++g_nStage;                                  

                    FMOD_Channel_Stop( g_Channel[4] );  // 미션 성공 사운드 출력 중지

                }

                break;

        case FAILED :

                sprintf( g_strMessage, "%s", "미션 실패! 계속 하시겠습니까? <y/n> " );                 break;

        case RESULT:

                sprintf( g_strMessage, "%s", "게임 결과 화면" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;                                  

                }                               

                break;

     }

}

 

[소스 5-21] 사운드 출력과 정지

 

새로운 사운드의 출력과 정지가 되는 시점은 대부분 게임 상태가 바뀌는 부분이다.

위의 소스를 보면 대부분의 게임 상태를 출력하기 위하여 경과 시간을 측정하고 있다.

예를 들어 17행부터 24행까지는 스테이지에 대한 초기화가 실행되고 스테이지 준비 상태로 들어가기 위한 부분이다. 이때 게임이 처음 실행했을 때의 배경음은 정지해야 하므로 22행과 같은 코드를 작성할 수 있고 다음 스테이지에 대한 사운드를 출력해야 하므로 22행부터 23행까지 출력 코드를 작성하면 된다.

여기까지가 일반적인 게임 안에서의 사운드 출력과 정지이다.

 

만약 미션을 실패했을 때는 y, n 키 입력에 따라 다시 현재의 스테이지를 시작하거나 게임 종료를 위한 결과를 출력해야 한다.

그래서 미션 실패의 경우 사운드 정지는 Update() 함수에서 하지 않고 main() 함수의 키 입력 처리 부분에서 하게 된다.

 

 

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

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

            if( g_GameState == RESULT )

              break;

 

           nKey = _getch();

           switch( nKey )

             {

             case 's' :

                        g_nGoal = 1;

                       break;

             case 'f' :

                     g_nGoal = 0;

                       break;

           case 'y' :

           case 'Y' :

                   if( g_GameState == FAILED )

                   {

                      g_GameState = INIT;

                      g_OldTime = clock();  

                      FMOD_Channel_Stop( g_Channel[2] );  // 미션 실패 사운드 출력 중지  

                     }

                    break;

            case 'n' :                                     

            case 'N' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = RESULT;

                        g_OldTime = clock();

                        FMOD_Channel_Stop( g_Channel[2] );//미션 실패 사운드출력 중지

                     }

                    break;

             case 'k' :

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[5], 0, &g_Channel[5]); // 슛동작 소리 출력

                    break;

              }

        }

 

        Update();    // 데이터 갱신

        Render();    // 화면 출력

 

        FMOD_System_Update( g_System );      

    }    

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-22] main() 함수 안에서 사운드 처리

 

40행은 k 키가 입력 됐을 때 슛 동작 소리를 출력해 주는 부분이다.

 

- FMOD 해제 

 

게임이 종료될 때 Release() 함수를 호출함으로 이 안에 FMOD 해제 코드를 작성하면 된다. 해제를 할 때는 항상 FMOD 사운드를 먼저 해제한 후에 FMOD 사운드 시스템을 해제한다.

 

 

1

2

3

4

5

6

7

8

9

void Release()

{

     int i;

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

        FMOD_Sound_Release( g_Sound[i] );

 

    FMOD_System_Close( g_System );

    FMOD_System_Release( g_System );

}

 

[소스 5-23] FMOD 해제

 

여기까지 슛 골인 게임을 제작하기 위한 기본 골격에서부터 사운드까지 살펴보았다.

각 예제는 실제로 슛 골인 게임을 만들기 위한 부분 소스이므로 단계별로 차근차근 완성해 나가면 최종적으로 전체 게임을 제작하게 된다.    

이제까지 제작한 모든 예제의 소스를 참고하여 전체 소스를 제작해 보자.

 

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

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

 

 

(CGP) 5장. 슛골인 게임

 

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

 이 장의 학습 목표는 다음과 같습니다.

 

각 개체의 속성을 정의하고 프로그래밍할 수 있다.

중심 좌표와 클리핑 개념 이해하고 캐릭터에 적용하여 프로그래밍할 수 있다.

전체 제작 로드맵을 보면서 단계별로 게임의 모듈을 완성할 수 있다.

단계별로 제작한 소스를 이용하여 전체 프로그램을 완성할 수 있다.

 

오케이2 유튜브 동영상 강의 주소

 

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

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

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

(4) http://youtu.be/6UvEGfo4u8g

(5) http://youtu.be/-X-4voBH9CE

(6) http://youtu.be/3aLGSSUhfM0

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

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

(9) http://youtu.be/8usvcH2AZiA

(10) http://youtu.be/WN4lwCliCM0

(11) http://youtu.be/4c2OakFrX_c

(12) http://youtu.be/7T4trwmYdik

 

 

 

 

 

 


01 기획

 

- 스토리

 

제한 시간 안에 목표한 공을 넣어라.

 

- 게임 방식

 

왼쪽 키는 j, 오른쪽 키는 l, 슛 키는 k이다. 스테이지마다 정해진 목표의 골인 수가 있으며 제한 시간 안에 골인을 완료해야 한다.

 

- 제한 사항

 

공은 한 번에 한 개씩 슛할 수 있고 슛한 공이 소멸될 때까지 새로운 공이 생성되지 않는다. 게임 난이도는 스테이지마다 골대의 길이와 공의 속도로 조절한다.

 

 

[그림 5-1] 슛골인 게임 기획 화면

 

 

 


02 실행 화면

  

[그림 5-2] 게임 시작 화면

 

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

 

[그림 5-4] 게임 실행 화면

 

[그림 5-5] 골인 축하 화면

 

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

 

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

 

 

03 게임 제작 로드맵

 

제작 로드맵은 게임을 개발하기 위한 전체 설계도와 같다.

제작 로드맵의 각 단계는 전체 게임을 개발하는 과정을 부분적으로 나눈 것으로 각 단계에서 만들어지는 각종 프로그램들은 전체 프로그램을 구성하게 된다.

 

프로그램 개발에 있어서 가장 중요한 부분은 설계이다.

설계는 전체 프로그램을 무엇을 어떻게 만들어야 하는지에 대한 전체 과정을 볼 수 있는 역할을 하며 아래의 제작 로드맵은 그와 같은 역할을 한다.

그러면 이제 단계별로 프로그래밍하면 전체 프로그램을 완성해 보자.

 

[STEP 01]

 

[STEP 02]

 

[STEP 03]

 

[STEP 04]

 

[STEP 05]

 

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

 

 

 


04 단계별 프로그래밍

 

STEP 01

 

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

 

■ 주인공

 

- 속성 정의

 

아래 [그림 5-10]의 게임 진행 화면 속에 있는 주인공을 표현하기 위해서는 어떠한 데이터가 필요할까?

기본적으로 주인공은 키보드 입력에 따라 좌우로 이동해야 하므로 좌표가 필요하다.

 

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

 

그리고 이 게임에서 주인공은 적 캐릭터로부터 공격을 받지 않으므로 생명과 같은 속성은  필요하지 않다. 결국 주인공의 속성은 좌표만 있으면 되며 이 좌표를 이용하여 출력하게 된다.

 

주인공 캐릭터에게 좌표는 두 가지 의미를 가진다. 첫째는 이동 좌표로써의 의미로 ‘주인공 캐릭터를 어디에 출력할 것인가?’ 이다. 둘째는 중심 좌표로써의 의미로 ‘캐릭터를 출력할 때 어디를 기준으로 출력할 것인가?’ 이다.

이와 같이 좌표를 이동 좌표와 중심 좌표로 나눠서 생각하는 이유는 주인공 캐릭터가 길이를 가지기 때문이다.

현재 주인공 캐릭터를 나타내는 특수문자는 한 문자당 가로로 두 컬럼의 길이를 가지므로 아래 [그림 5-11]의 경우는 총 6컬럼의 가로 길이를 가진다.

 

[그림 5-11] 특수 문자로 구성한 주인공 캐릭터

 

만약 이동 좌표만을 생각하고 [그림 5-12]와 같이 길이가 다른 두 개의 캐릭터를 같은 좌표에 출력하면 어떻게 될까?

 

[그림 5-12] 길이가 다른 주인공 캐릭터

 

실행된 결과는 다음 [그림 5-13] 과 같다.

 

[그림 5-13 ] 길이가 다른 캐릭터를 좌측으로 이동

 

왜 이런 결과가 나왔을까? 그것은 출력하는 좌표의 기준이 좌측 상단이기 때문이다.

이 캐릭터의 경우 중심이 머리이므로 길이가 다른 캐릭터라도 같은 좌표에 출력하면 머리가 겹쳐진 형태로 출력되어 같은 좌표에서 공을 슛할 수 있다.

중심 좌표를 기준으로 하지 않고 출력하는 경우에 위의 두 캐릭터를 최대한 좌측으로 움직여 공을 슛한다고 가정해 보자. 이때 팔이 긴 캐릭터를 아무리 좌측으로 이동시킨다고 하더라도 팔이 짧은 캐릭터와 같은 좌표에서 절대 슛 동작을 할 수 없다.

 

[그림 5-14] 중심 좌표를 고려하지 않고 출력한 경우

 

이 부분은 가로 세로 길이를 가지는 캐릭터를 출력할 때 더욱 큰 문제가 되는데 아래의 두 그림을 비교해 보자.

 

[그림 5-15] 출력 기준 왼쪽 상단인 캐릭터를 같은 좌표에 출력한 결과

 

[그림 5-15]는 캐릭터를 출력할 때 왼쪽 위를 기준으로 출력하므로 크기가 다른 캐릭터에 같은 좌표를 지정하면 서로 다른 위치에 캐릭터가 출력된 것과 같이 보인다.

이렇게 되면 크기에 따라 이동 좌표가 전부 달라지므로 캐릭터마다 서로 다른 좌표계를 사용하는 결과가 된다.

이제 중심 좌표의 개념이 적용된 예를 살펴보자.

 

   

[그림 5-16] 중심 좌표인 중앙의 하단을 기준으로 같은 좌표에 출력한 결과

 

[그림 5-16]과 같이 크기가 다른 캐릭터라도 중심 좌표를 이동 좌표에 맞추고 출력 기준 좌표를 계산하면 크기가 다르다고 해도 같은 좌표에 캐릭터를 출력할 수 있다.

게임에서는 이와 같은 중심 좌표의 개념이 상당히 중요한 역할을 한다.

 

[그림 5-17] 좌표 개념

 

일반적으로 서로 다른 크기를 가지는 캐릭터를 출력하기 위해 중심 좌표를 적용할 때는 아래와 같은 툴을 만들어서 설정한다.

 

[그림 5-18] 스프라이트 툴

 

이와 같은 사항을 적용하여 주인공 캐릭터의 속성을 정의하면 아래와 같다.

 

① 중심 좌표

② 이동 좌표

③ 출력 기준 좌표

 

[표 5-1] 주인공 속성

 

 

typedef struct _PLAYER

{

          int nCenterX, nCenterY;

        int  nMoveX, nMoveY;

          int  nX, nY;

} PLAYER;

PLAYER g_sPlayer;

 

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

 

실제 출력 기준 좌표(nX, nY)는 아래와 같은 식에 의해 구할 수 있다.

 

 

( 이동 좌표 x - 중심 좌표 x,  이동 좌표 y - 중심 좌표 y )

 

[식 5-1] 출력 기준 좌표 식

 

현재 슛 골인 게임에서는 주인공 캐릭터가 모두 같지만 만약 각 동작마다 서로 다른 크기의 캐릭터 모양을 갖는다면 아래 [소스 5-2]와 같이 여러 개의 중심 좌표를 저장할 수 있도록  배열 또는 포인터의 형태로 정의할 수 있다.

 

 

typedef struct _POSITION

{

          int  nX, nY;      

} POSITION;

 

typedef struct _PLAYER

{

        POSITION nCenter[10]; // 캐릭터 10개에 대한 중심 좌표

        int nMoveX, nMoveY;   // 이동 좌표

        int nX, nY;             // 출력 기준 좌표

        int nIndex;             // 중심 좌표 인덱스       

} PLAYER;

 

PLAYER g_sPlayer;

 

[소스 5-2] 10개의 중심좌표를 저장하는 주인공 속성 구조체

 

- 이동 및 키보드 처리

 

현재 이 게임은 주인공의 y 좌표가 고정된 상태에서 x 좌표에만 변화를 주어 좌우로 이동하는 게임이다. 좌우 이동 키는 j, l 키이며 x 좌표를 1씩 증감시켜 이동한다.

키보드에 대한 처리는 3장의 [소스 3-6]을 통해 앞서 살펴본 내용을 그대로 적용한다.

 

[실습 예제 5-1]

 

주인공 캐릭터를 j키와 l키의 입력에 따라 좌우로 이동하는 프로그램을 작성해 보자.

이때 주인공 캐릭터에 중심 좌표를 적용하여 출력하고 상단에는 아래 [표 5-2]와 같이 주인공 캐릭터의 이동 좌표를 계속적으로 출력하도록 한다.

 

 

주인공 이동 좌표 : x, y

 

[표 5-2] 출력 정보

 

기본적인 게임 구조는 3장에서 작성한 프레임워크를 적용하며 이동 거리는 1컬럼으로 하지만 특수 문자는 2컬럼 단위로 이동하므로 [그림 5-19]에서 [그림 5-20과 [그림 5-21]이 될 때에는 2 컬럼씩 이동하도록 한다.

이와 같이 경계 영역에서 캐릭터의 일부를 출력하지 않는 기법을 클리핑(clipping)이라고 하며 3장의 게임 프로그래밍 용어에서 설명했었다.

이와 같은 부분을 오른쪽 경계 영역에도 동일하게 적용한다.

 

[그림 5-19] 주인공 캐릭터 출력

 

[그림 5-20] 클리핑 적용(1)

 

[그림 5-21] 클리핑 적용(2)

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <stdio.h>

#include "Screen.h"

#include <windows.h>

 

typedef struct _PLAYER

{

         int nCenterX, nCenterY;

         int nMoveX, nMoveY;

         int nX, nY;

} PLAYER;

 

PLAYER g_sPlayer;                    

char g_strPlayer[] = "┗━●━┛";      // 주인공 캐릭터

int g_nLength;                          // 주인공 캐릭터 전체 길이

// Note: 초기화

void Init()

{

     g_sPlayer.nCenterX = 4;     // 주인공 캐릭터의 중심 좌표

     g_sPlayer.nCenterY = 0;     // 주인공 캐릭터의 중심 좌표

     g_sPlayer.nMoveX = 20;     // 주인공 캐릭터의 이동 좌표 초기화

     g_sPlaye.nY = g_sPlayer.nMoveY = 22;     // 주인공 캐릭터의 이동 좌표 초기화

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX; // 주인공 캐릭터의 출력 기준 좌표

     g_nLength = strlen( g_strPlayer );  // 주인공 캐릭터의 전체 길이

}

// Note: 데이터 갱신

void Update()

{

}

// Note: 출력

void Render()

{

     char string[100] = { 0, };       

     ScreenClear();

 

     // Note: 렌더링 시작 

     // Note: 2 컬럼씩 클리핑

     if( g_sPlayer.nX < 0 )  //  왼쪽 클리핑 처리

        ScreenPrint( 0, g_sPlayer.nMoveY, &g_strPlayer[g_sPlayer.nX*-1]);   

     else if( g_sPlayer.nMoveX + (g_nLength - g_sPlayer.nCenterX + 1) > 79 )

     {

        strncat( string, g_strPlayer, g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX

                                                      + 1) - 79 ) );

        ScreenPrint( g_sPlayer.nX, g_sPlaye.nY, string );          

     }else{ // 1 컬럼씩 이동

        ScreenPrint( g_sPlayer.nX, g_sPlaye.nY, g_strPlayer );

     }

         

     sprintf( string, "주인공 이동 좌표 : %d, %d", g_sPlayer.nMoveX, g_sPlaye.nY );

     ScreenPrint( 0, 0, string );

 

     // Note: 렌더링 끝

      ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

               break;

           switch( nKey )

           {

            case 'j' :

                 g_sPlayer.nMoveX--;

                     // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                 nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                 // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                 if( g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ||

                                                      g_sPlayer.nMoveX + nRemain > 79 )  

                     g_sPlayer.nMoveX--;

                  g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                  break;

            case 'l' :

                   g_sPlayer.nMoveX++;

                       // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                   nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                   // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                  if( g_sPlayer.nMoveX + nRemain > 79 ||

                                            g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ) )

                     g_sPlayer.nMoveX++;

                  g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                  break;

            }

         }

 

           Update();    // 데이터 갱신

           Render();    // 화면 출력   

     }

     

     Release();   // 해제

     ScreenRelease();

     return 0;

}

 

[소스 5-3] 전체 소스

 

24행은 이동 좌표 y와 출력 좌표 y를 초기화 하는 부분이다.

현재 주인공 캐릭터의 세로 길이가 1 컬럼이고 중심 좌표 y가 0이므로 이동 좌표 y와 출력 기준 좌표 y는 같다. 그래서 24행과 같이 초기화를 하고 있다.

25행은 캐릭터의 중심 좌표와 이동 좌표를 통해 출력 기준 좌표를 계산하고 있다.

 

40행에서 49행은 주인공 캐릭터의 출력 기준 좌표가 경계 영역을 넘었는지 조사하여 클리핑 적용 여부를 판단하는 부분이다.

클리핑은 주인공 캐릭터의 특수 문자를 저장하고 있는 g_strPlayer[] 배열의 인덱스를 이용하면 쉽게 할 수 있다.

예를 들어 왼쪽 클리핑의 경우는 g_strPlayer[] 배열의 출력할 시작 인덱스를 조정하면 된다. 41행에 &g_strPlayer[g_sPlayer.nX*-1]과 같이 되어 있는 것은 왼쪽 클리핑의 시작은 g_sPlayer.nX가 음수가 될 때 시작되고 이 좌표 값에 -1을 곱하면 좌표가 2, 4 등이 된다.

이 값은 g_strPlayer[]의 시작 인덱스로 사용하면 일정 부분에서부터 출력되므로 클리핑과 같은 효과를 가져온다.

  

왼쪽 클리핑의 경우 특수 문자 ●는 [그림 5-22]와 같이 왼쪽이 중심이므로 이동 좌표 x가 0이면 [그림 5-23]과 같이 왼쪽 경계 영역에 닿게 되지만 이동 좌표 x가 78일 때는 특수 문자 ●는 오른쪽 경계 영역에 닿게 된다.

왜냐하면 특수 문자는 2컬럼이므로 아래 [그림 5-22]의 ‘중심 좌표 + 1’ 되는 컬럼이 오른쪽 경계 영역에 먼저 닿기 때문이다.

[그림 5-22] 특수 문자 ● 

 

[그림 5-23] 경계 영역과 클리핑(clipping)

 

44행의 strncat() 함수는 오른쪽 경계 영역에 주인공 캐릭터가 닿았을 때 출력하려는 문자를 일부 복사하기 위한 함수로 사용한다.

이와 같이 해주는 이유는 문자열의 끝은 널문자로 구분되기 때문이다.

이때 복사 영역을 결정해야하는 데 복사 영역은 g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX + 1) - 79 )에 의해 구할 수 있다.

이 계산에서 g_sPlayer.nMoveX + g_sPlayer.nCenterX + 1 은 최우측 좌표를 구하게 된다. 여기에 79을 빼주면 경계 영역을 벗어난 컬럼을 구할 수 있다.

이 컬럼만큼 주인공 캐릭터의 전체 길이에서 빼주면 출력하기 위한 주인공 캐릭터의 최대 인덱스를 구하게 되며 이 인덱스를 이용하여 주인공 캐릭터의 일부를 복사하게 된다.

 

81행과 91행의 nRemain 변수는 중심 좌표의 이후의 길이를 구한 변수이다.

이 값은 84행과 93행에서 주인공 캐릭터의 최우측 좌표가 경계 영역에 닿았는지를 조사하기 위해 이동 좌표에 이 값을 더한다.

 

83행과 84행, 93행과 94행을 보면 같은 조건식임을 알 수 있다.

하지만 조건식의 우선순위가 서로 반대로 되어 있는 것은 왼쪽 이동일 때는 최우선적으로 조사해야 하는 부분은 왼쪽 경계 영역에 닿을 가능성이므로 이 부분을 조사하는 식이 먼저 오는 것이며 93행과 94행은 그 반대이므로 같은 조건식임에도 불구하고 조건의 우선순위를 다르게 배치한 것이다.

 

■ 공

 - 속성 정의

 

전체 흐름과 개체 제어를 동기화하는 방법으로 시간을 사용한다.

이 방법을 간단히 요약하면 시스템으로부터 시간을 밀리세컨드(millisecond) 단위로 읽어 현재 시각과 이전 이동 시각의 차이로 이동과 진행을 결정하는 방법이다.

속성을 정의하기 전에 이와 같은 설명을 하는 것은 공은 키보드 입력에 따라 이동하는 것이 아니라 스스로 이동하기 때문이다.

 

그러면 이제 공의 속성을 정의하여 보자.

 

첫째는 공 상태이다.

공 상태는 슛 상태와 준비 상태가 있다.

슛 상태는 공이 아래에서 위를 향해 이동하는 상태를 말하며 준비 상태는 주인공 캐릭터가  공을 잡고 있는 상태를 말한다. 준비 상태에서 공을 주인공 캐릭터가 잡고 있는 것처럼 출력하기 위해서는 주인공 캐릭터와 공의 이동 좌표가 같으면 된다.

 

둘째는 이동 좌표이다.

이동 좌표는 실제 공이 출력되는 기준 좌표가 된다.

왜냐하면 공에는 중심 좌표의 개념이 없기 때문이다.

 

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

이 속성은 이동을 언제 할 것인지를 결정하는 부분으로 이동 속력과도 연관된다.

속력은 단위 시간당 이동 거리이므로 이 속성은 단위 시간에 해당이 된다.

각 스테이지마다 골대의 길이와 공의 이동 시간 간격에 변화를 주면 각 스테이지를 다양하게 구성할 수 있다.

 

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

현재 이동을 했다면 이 순간부터 다음 이동까지의 시간 차이를 측정해야 하며 이 시간을 경과 시간이라고 한다. 모든 개체의 이동 여부는 이 경과 시간과 이동 시간 간격을 비교하여  결정한다. 이와 같은 과정을 적용하기 위해서는 현재의 이동 시간을 이전 이동 시간으로 저장하고 현재 시각을 계속 읽어 이전 이동 시간과 비교하여 이동 여부를 결정하면 된다.

현재 이 속성은 시간차를 이용하는 모든 개체에 공통적으로 들어가는 속성중 하나이다.

 

지금까지 언급한 속성을 정리하면 다음과 같다.

 

 

① 공 상태

② 이동 좌표

③ 이동 시간 간격 

④ 이전 이동 시각

 

[표 5-3] 공 속성

 

 

typedef struct  _BALL

{

        int      nIsReady;     // 준비상태(1), 슛상태(0)

        int      nMoveX, nMoveY;    // 이동 좌표

        clock_t  MoveTime; // 이동 시간 간격

        clock_t  OldTime;   // 이전 이동 시각

} BALL;

 

[소스 5-4] 속성 구조체

 

- 이동 및 키보드 처리

 

키보드 처리 부분을 먼저 살펴보면 k 키가 입력되면 공은 자신의 상태 속성을 준비 상태에서 슛 상태로 바꿔 이동하게 된다. 앞의 게임 기획서의 제안사항을 보면 공은 연속적으로 슛할 수 있는 것이 아니라 한 번에 한 개씩 슛할 수 있다고 되어 있다. 이것은 공이 슛된 상태에서는 또 다른 공을 슛할 수 없다는 것이다. 그래서 키 입력 처리를 할 때는 슛 상태와 준비 상태를 먼저 파악하여 슛 상태에서 또 슛 동작이 되지 않게 해야 한다.

 

이동에 있어서 공이 준비 상태라면 이것은 주인공 캐릭터가 잡고 있는 상태이므로 주인공 캐릭터의 이동 좌표를 공의 이동 좌표에 대입하여 항상 같이 이동하도록 하면 된다.

만약 슛 상태라면 이동 시간 간격에 따라 y 좌표를 일정한 거리만큼 빼주면 공은 아래에서 위로 이동하게 된다.

 

[실습 예제 5-2]

 

[실습 예제 5-1]에서 작성한 소스에 추가적으로 k키가 입력되면 주인공 캐릭터가 공을 슛하는 프로그램을 작성해 보자.

단, 공이 슛 상태에서 공의 이동 좌표가 경계 영역 y 좌표 0과 같으면 충돌로 판단하고 공의 상태를 준비 상태로 바꾼다. 그리고 공의 초기 상태는 항상 준비 상태이다.

 

[그림 5-24] 준비 상태 출력

 

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

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _PLAYER

{

        int nCenterX, nCenterY;

        int nMoveX, nMoveY;

        int nX, nY;

} PLAYER;

 

typedef struct _BALL

{

        int nIsReady;        // 준비 상태(1), 슛상태(0)

        int nMoveX, nMoveY;  // 이동 좌표

        clock_t MoveTime;    // 이동 시간 간격

        clock_t OldTime;     // 이전 이동 시각

} BALL;

 

BALL g_sBall;

PLAYER g_sPlayer;

char g_strPlayer[] = "┗━●━┛";

int g_nLength;

 

void Init()

{

     g_sPlayer.nCenterX = 4;

     g_sPlayer.nCenterY = 0;

     g_sPlayer.nMoveX = 20;

     g_sPlayer.nMoveY = 22;

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

     g_nLength = strlen( g_strPlayer );

 

     // Note: 공의 초기화 --> 추가된 부분

     g_sBall.nIsReady = 1;

     g_sBall.nMoveX = g_sPlayer.nMoveX;

     g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

     g_sBall.MoveTime = 100;

}

 

void Update()

{

     // Note: 공의 이동 처리 --> 추가된 부분

     clock_t CurTime = clock();

     if( g_sBall.nIsReady == 0 ) // 이동 중일 때

     {   // 이동 시간 간격에 의한 이동

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

        {

            if( g_sBall.nMoveY - 1 > 0 )

            {

                g_sBall.nMoveY--;

                   // 다음 이동 시각과 비교하기 위해 현재 시간을 이전 시간 변수에 저장

                g_sBall.OldTime = CurTime;

             }else{

                g_sBall.nIsReady = 1;

                g_sBall.nMoveX = g_sPlayer.nMoveX;

                g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

             }

          }

        }else{

                 g_sBall.nMoveX = g_sPlayer.nMoveX;

          }

     }

}

 

void Render()

{

     char string[100] = { 0, };       

     ScreenClear();

 

     // Note: 렌더링 시작 

 

     // Note: 2 컬럼씩 클리핑

     if( g_sPlayer.nX < 0 )  //  왼쪽 클리핑 처리

         ScreenPrint( 0, g_sPlayer.nMoveY, &g_strPlayer[g_sPlayer.nX*-1]);    

     else if( g_sPlayer.nMoveX + (g_nLength - (g_sPlayer.nCenterX + 1)) > 79 )

     {

         strncat( string, g_strPlayer, g_nLength - (( g_sPlayer.nMoveX + g_sPlayer.nCenterX  + 1) - 79 ) );

         ScreenPrint( g_sPlayer.nX, g_sPlayer.nMoveY, string );            

     }else{ // 1 컬럼씩 이동

        ScreenPrint( g_sPlayer.nX, g_sPlayer.nMoveY, g_strPlayer );

     }

 

     ScreenPrint( g_sBall.nMoveX, g_sBall.nMoveY, "⊙" );

     sprintf( string, "주인공 이동 좌표 : %d, %d", g_sPlayer.nMoveX, g_sPlayer.nMoveY );

     ScreenPrint( 0, 0, string );

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;

 

           switch( nKey )

           {

            case 'j' :

                 // [실습 예제 5-1]과 동일

                 break;

            case 'l' :

                   // [실습 예제 5-1]과 동일

                   break;

            case 'k' :

                   if( g_sBall.nIsReady )

                   {

                       g_sBall.nMoveX = g_sPlayer.nMoveX;

                       g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

                       g_sBall.OldTime = clock();

                       g_sBall.nIsReady = 0;

                    }

                    break;

             }

        }

 

        Update();    // 데이터 갱신

        Render();    // 화면 출력              

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-5] 공과 주인공을 이동하기 위한 전체 소스

 

47행부터 66행까지는 이동에 따른 좌표의 변화에 대한 내용들이다.

각 개체의 이동 동기화는 시간 차이를 이용하므로 현재 시각과 이전 시각의 시간 간격에 따라 이동이 결정된다. 50행은 이와 같은 내용을 코드로 옮긴 것이다.

참고로 50행의 조건을 아래와 같이 작성해도 같은 의미가 되는데 C언어에서는 연산자 우선순위가 있어서 아래와 같은 순서에 의해 계산되지만 괄호를 분명하게 하는 것이 좋다.

 

 

계산 순서 : ➀ → ➁

 

 

 

우선순위

연산자

1

(), [], ->, .

2

sizeof, & ++, ==, ~, !, *(포인터연산자), +(부호), -(부호)

3

*, /, %

4

+, -

5

<<, >>

6

<, <=, >=, >

7

==, !=

8

&

9

^

10

|

11

&&

12

||

13

?:

14

=, *=, +=, /=, %=, &=, ^=, |=, <<=, =>>

15

, (콤마)

 

[표 5-4] 연산자 우선순위

 

124행부터 130행은 공이 준비 상태일 때 k 키가 눌려지면 공을 초기화하는 부분이다.

공이 슛 상태일 때는 g_sBall.nIsReady가 0이므로 k키가 여러 번 눌려진다고 해도 이 조건식에 의해 공은 초기화 되지 않는다.

 

STEP 02

 

 

[그림 5-26] 2단계 제작 로드맵

 

■ 골대

 

- 속성 정의 및 출력

 

골대를 구현하기 위한 속성을 정의해 보자.

 

첫째, 골대는 좌우로 이동한다.

좌우로 이동한다는 것은 골대를 출력하기 위한 이동 좌표가 필요하다는 것이다.

이 이동 좌표는 곧 출력 기준 좌표로써 [그림 5-27]과 같이 왼쪽 골대의 두 컬럼 중에서 첫째 컬럼의 좌표에 해당된다.

 

[그림 5-27] 이동 좌표

 

둘째, 골대는 일정한 시간 간격으로 이동한다.

이 속성은 첫째 속성을 좀 더 구체화한 속성이 된다.

일정한 시간 간격으로 이동하기 위해서는 앞서 살펴본 공의 속성과 같이 이전 이동 시간과 이동 거리 속성이 있어야 한다. 그래서 셋째 속성은 이전 이동 시간이며 넷째 속성은 이동 거리가 된다.

 

셋째, 경과 시간을 계산하기 위한 이전 이동 시간이다.

 

넷째, 일정한 시간 간격으로 이동하기 위한 이동 거리이다.

 

다섯째, 각 스테이지마다 다양한 길이의 골대가 있다.

골대의 길이가 가변적이라는 것은 크기를 가진다는 의미이므로 골대를 출력할 때 중심 좌표를 기준으로 출력해야 하지만 골대가 단순히 경계영역 안에서만 이동하는 것이므로 중심 좌표를 적용할 필요가 없다. 그래서 첫째 속성인 이동 좌표는 출력 기준 좌표가 된다.

 

여섯째, 골대에는 골인의 기준이 되는 골인 라인이 있다.

골인 라인은 골대의 길이에 따라 가변적으로 출력된다.

골대의 특성상 [그림 5-28]과 같이 반드시 한 개의 골인 라인은 있어야 한다.

 

 

[그림 5-28] 기본 공대

[그림 5-29] 골대 길이가 2

 

 

예를 들어, 골대의 길이가 2만큼 늘어난다는 것은 [그림 5-29]와 같이 1개의 기본 골인 라인 외에 양 옆으로 2개씩 확장되는 것을 의미하므로 총 5개의 골인 라인이 있어야 한다.

이 게임은 골대의 최대 라인의 길이를 3으로 제한하고 있으므로 골인 라인의 최대 개수는 7이 된다. 

 

골인 라인은 골인 영역을 출력하기 위한 단순한 역할만을 하므로 골인 라인을 출력하기 위한 x 좌표만 지정하고 y좌표는 골대의 y좌표를 사용한다.

 

여기까지 골대의 6가지 속성을 살펴보았다.

골대가 일반적인 캐릭터 속성과 다른 점은 생명에 관한 속성이 없다는 것이다.

일반적으로 캐릭터가 충돌하면 생명 속성에 의해 출력에서 제외되지만 골대는 게임이 종료될 때까지 좌우로만 계속 이동하면 되므로 생명이라는 속성이 필요 없다.

 

이와 같은 내용을 정리하면 아래 [표 5-5]와 같으며 [소스 5-6]과 같이 정의할 수 있다.

 

 

① 이동 좌표

② 골대 길이

③ 골인 라인의  x 좌표 ( 7개 )

④ 이동 시간 간격

⑤ 이전 이동 시각

⑥ 이동 거리

 

[표 5-5] 골대 속성

 

 

typedef struct _GOAL_DAE

{

int           nMoveX, nMoveY;  // 이동 좌표

int        nLength;     // 골대 길이   

int           nLineX[7]; // 골인 라인 x 좌표 (7개)

int           nDist;      // 이동 거리

clock_t     MoveTime;  // 이동 시간 간격

clock_t     OldTime;    // 이전 이동 시간

} GOAL_DAE;

 

[소스 5-4] 골대 속성 정의

 

[실습 예제 5-3]

 

골대 길이 설정값에 따라 골인 라인이 최대 7개까지 출력되도록 [그림 5-30]과 같이 프로그래밍해 보자. 기본적인 구조는 3장의 프레임워크를 적용한다.

 

[그림 5-30] 골대와 골인 라인 출력

 

 

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef struct _GOAL_DAE

{

        int nMoveX, nMoveY;     // 이동 좌표

        int nLength;              // 골대 길이  

        int nLineX[7];            // 골인 라인 x 좌표 (7개)

          int nDist;                 // 이동 거리

        clock_t MoveTime;        // 이동 시간 간격

        clock_t OldTime;         // 이전 이동 시간

        

} GOAL_DAE;

 

GOAL_DAE g_sGoalDae;

 

void Init()

{

     int nLength, i;

     g_sGoalDae.nMoveX = 20;

     g_sGoalDae.nMoveY = 2;

     g_sGoalDae.nLength = 1;

     g_sGoalDae.MoveTime = 100;

     g_sGoalDae.OldTime = clock();

     g_sGoalDae.nDist = 1;

     nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

                

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

     {

        g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);    

     }   

}

 

void Update()

{       

}

 

void Render()

{       

     int nLength, i;

     ScreenClear();

 

     // Note: 렌더링 시작    

     ScreenPrint( g_sGoalDae.nMoveX, g_sGoalDae.nMoveY, "□" );

     nLength = g_sGoalDae.nLength*2 + 1;

 

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

        ScreenPrint( g_sGoalDae.nLineX[i], g_sGoalDae.nMoveY, "━");

        

     ScreenPrint( g_sGoalDae.nLineX[nLength-1] + 2, g_sGoalDae.nMoveY, "□");

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;              

        }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력    

    }

     

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-7] 골대 길이가 적용된 골대 전체 소스

 

30행은 골대 길이에 따라 골인 라인의 개수를 계산하는 부분이다.

여기서 구한 골인 라인의 개수는 7개의 배열에서 사용할 배열 개수를 의미한다.

그래서 32행과 49행을 보면 이 배열 개수를 골대 라인의 x 좌표를 설정하거나 출력할 때 반복문의 최대 반복수로 사용하고 있다.

 

 

길이

배열의 크기

0

1

1

3

2

5

3

7

 

[표 5-6] 골대 길이에 따른 골인 라인의 배열 개수

 

34행을 보면 이동 좌표를 기준으로 2 컬럼씩 차이가 나게 골대 라인의 x 좌표를 설정하고 있다.

 

48행과 54행을 보면 골대를 출력할 때 골인 라인의 x 좌표를 이용하여 간단히 출력하는 것을 알 수 있다.

프로그래밍에서 실시간으로 계산하여 값을 적용할 수도 있겠지만 48행과 54행과 같이 미리 계산된 값을 이용하는 것도 때로는 프로그래밍을 간편하게 한다.

 

- 이동

 

골대가 경계 영역에 닿게 되면 이동 방향을 바꾸게 되는데 이것은 이동 거리에 변화를 주는 것이 아니라 이동 거리의 부호에 영향을 주면 된다.

이 방법은 3D에서도 상당히 많이 사용되는 개념으로 곱셈의 항등수인 ‘-1’을 이용한다.

 

골대가 경계 영역에 닿으면 이동 거리에 -1을 곱하면 크기는 변하지 않지만 부호가 바뀌게 되어 골대의 이동 방향이 바뀌게 된다. 이러한 성질을 이용하면 충돌할 때 방향을 바꾸기는 상당히 쉽다.

 

 

 

[표 5-7] 항등수의 성질

 

[실습 예제 5-4]

 

[실습 예제 5-3]에서 구현한 골대가 좌우로 이동되게 Update() 함수를 프로그래밍해 보자. 단, 골대가 경계 영역에 닿게 되면 방향을 바꾸도록 하고 x 좌표의 경계 영역은 0과 79이다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

void Update()

{

     clock_t CurTime = clock();

     int nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

     int i;

 

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

     {

        g_sGoalDae.OldTime = CurTime;

        if( g_sGoalDae.nMoveX + g_sGoalDae.nDist >= 0 &&

                      ((g_sGoalDae.nLineX[nLength-1] + 3 ) + g_sGoalDae.nDist) <= 79)

         {

                g_sGoalDae.nMoveX += g_sGoalDae.nDist;

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

                {

                     g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);        

                }       

         }else{

                  g_sGoalDae.nDist = g_sGoalDae.nDist * -1; // -1 이 방향을 바꾸어 줌.

         }

     }   

}

 

[소스 5-8] Update() 함수

 

7행은 시간 간격을 조사하여 이동여부를 결정하는 부분이다.

이 조건은 10행에서 골대가 이동 영역 안에 있는지 조사하여 이동과 이동 방향 전환을 선택하게 된다. 골대 속성 중에서 이동 좌표는 [그림 5-31]과 같으며 우측 경계 영역에 닿는 부분은 11행과 같이 g_sGoalDae.nLine[nLength-1] + 3 되는 위치가 된다. 이 위치는 아래 [그림 5-32]이다.

 

[그림 5-31] 골대의 이동 좌표

[그림 5-32] 경계 영역과 닿는 우측 x 좌표

 

STEP 03

 

 

[그림 5-33] 3단계 제작 로드맵

 

■ 충돌

 

- 주인공 캐릭터와 경계 영역의 충돌

 

주인공 캐릭터가 경계 영역에 충돌하는 경우는 [그림 5-34]와 같이 두 가지 경우가 있다.

 

  [그림 5-34] 경계 영역에 충돌한 주인공 캐릭터

 

주인공 캐릭터와 경계 영역과의 충돌 테스트는 언제하면 될까?

주인공 캐릭터는 플레이어의 키 입력에 따라 이동하므로 이동키가 입력되었을 때 충돌 테스트를 하면 된다. [그림 5-35]를 보면 주인공 캐릭터의 좌우 경계 좌표가 중심 좌표-2, 중심 좌표+3이 되는데 이것은 주인공의 팔과 머리에 해당하는 문자가 특수문자로서 2컬럼의 가로 길이를 갖기 때문이다.

 

[그림 5-35] 주인공 캐릭터의 충돌 경계 좌표

 

[실습 예제 5-5]

 

주인공 캐릭터와 경계 영역과의 충돌 테스트가 되도록 [실습 예제 5-2]의 [소스 5-5]에 코드를 추가하여 보자.

 

 

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

int main(void)

{

    int nKey, nRemain;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

               break;

           switch( nKey )

           {

            case 'j' :

                     if( g_sPlayer.nMoveX > 0 )   // 왼쪽 충돌 경계 좌표 체크

                     {

                    g_sPlayer.nMoveX--;

                        // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                    nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                    // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                    if( g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0 ||

                                                       g_sPlayer.nMoveX + nRemain > 79 )  

                       g_sPlayer.nMoveX--;

                        g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                      }

                  break;

            case 'l' :

                       if( g_sPlayer.nMoveX + 1 < 79 )  // 오른쪽 충돌 경계 좌표 체크

                       {

                      g_sPlayer.nMoveX++;

                          // 전체 길이 - ( 중심 좌표 + 1 )은 남은 길이

                      nRemain = g_nLength - (g_sPlayer.nCenterX + 1);

                      // 2컬럼씩 이동하기 위한 부분 ( 팔이 걸친 경우 )

                     if( g_sPlayer.nMoveX + nRemain > 79 ||

                                            g_sPlayer.nMoveX - g_sPlayer.nCenterX < 0  )

                        g_sPlayer.nMoveX++;

                     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

                      }

                    break;

              case 'k' :

                        // [소스 5-3] 과 동일함

                      break;

           }

         }

         Update();

         Render();       

     }

     Release();

     ScreenRelease();

     return 0;

}      

 

[소스 5-9] [실습 예제 5-4]의 부분 소스

 

- 공과 골대의 충돌

 

공이 골대와 충돌되는 경우는 두 가지가 있다.

첫째, [그림 5-36]과 같이 공이 골인 라인에 닿는 경우이며 득점과 연관된다.

 

[그림 5-36] 공과 골인 라인의 충돌

 

둘째, [그림 5-37]과 같이 공이 골대에 닿는 경우이며 이때 공은 주인공 캐릭터가 다시 슛할 수 있도록 준비상태가 된다.

 

 

[그림 5-37] 공과 골대와의 충돌

 

[실습 예제 5-6]

 

좌우로 이동하는 골대를 구현한 [실습 예제 5-3]과 주인공 캐릭터의 충돌 경계 영역이 적용된 [실습 예제 5-5]를 기본으로 하여 공과 골대의 충돌을 감지할 수 있는 프로그램을 작성하여 보자. 그리고 충돌했을 때 공은 주인공 캐릭터가 다시 슛 동작을 할 수 있도록 준비 상태가 된다.

 

[그림 5-35] 공과 골대 충돌

 

 

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

6

void Update()

{

     clock_t CurTime = clock();

     int nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

     int i;

 

     // Note: 골대

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

     {

        g_sGoalDae.OldTime = CurTime;

        if( g_sGoalDae.nMoveX + g_sGoalDae.nDist >= 0 &&

                  ((g_sGoalDae.nLineX[nLength-1] + 3 ) + g_sGoalDae.nDist) <= 79)

        {

           g_sGoalDae.nMoveX += g_sGoalDae.nDist;

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

           {

                 g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);     

           }    

         }else{

            g_sGoalDae.nDist = g_sGoalDae.nDist * -1; // -1 이 방향을 바꾸어 줌.

         }

     }   

 

     if( g_sBall.nIsReady == 0 ) // 이동 중일 때

     {   // 이동 시간 간격에 의한 이동

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

        {

            if( g_sBall.nMoveY - 1 > 0 )

            {

               g_sBall.nMoveY--;

                 // 다음 이동 시각과 비교하기 위해 현재 시간을 이전 시간 변수에 저장

               g_sBall.OldTime = CurTime;

        

               // 골대 라인 충돌

                if( g_sBall.nMoveX >= g_sGoalDae.nLineX[0] &&

                                   g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[nLength-1] )

                {

                   if( g_sBall.nMoveY <= g_sGoalDae.nMoveY )

                   {   // 공 초기화

                      g_sBall.nIsReady = 1;

                      g_sBall.nMoveX = g_sPlayer.nMoveX;

                      g_sBall.nMoveY = g_sPlayer.nMoveY - 1;          

                      g_nBallCount++;  //득점

                   } 

                  // 골대 충돌

                }else if( ( g_sBall.nMoveX >= g_sGoalDae.nLineX[0] - 2 &&

                                 g_sBall.nMoveX <= g_sGoalDae.nLineX[0] - 1 ) ||

                          ( g_sBall.nMoveX + 1 >= g_sGoalDae.nLineX[0] - 2 &&

                                  g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[0] - 1 ) ||

                          ( g_sBall.nMoveX >= g_sGoalDae.nLineX[nLength-1] + 2 &&

                                  g_sBall.nMoveX <= g_sGoalDae.nLineX[nLength-1] + 3) ||                       ( g_sBall.nMoveX + 1 >= g_sGoalDae.nLineX[nLength-1] + 2 &&

                              g_sBall.nMoveX + 1 <= g_sGoalDae.nLineX[nLength-1] + 3 ))

                {

                    if( g_sBall.nMoveY <= g_sGoalDae.nMoveY )

                    {   // 공 초기화

                       g_sBall.nIsReady = 1;

                       g_sBall.nMoveX = g_sPlayer.nMoveX;

                       g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

                     }

                 }

               }else{ // 공 초기화

                g_sBall.nIsReady = 1;

                g_sBall.nMoveX = g_sPlayer.nMoveX;

                g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

               }

           }

        }else{

                 g_sBall.nMoveX = g_sPlayer.nMoveX;

        }       

}

 

[소스 5-10] 전체 소스 중에서 Update()함수

 

위의 실습 예제는 [실습 예제 5-3]과 [실습 예제 5-5]를 합치고 충돌 체크를 하는 부분만 추가하면 된다.

두 예제가 중복되는 부분은 제외하고 충돌 체크를 하는 부분만 살펴보면 위의 [소스 5-10]과 같다.

 

공과 골대의 충돌은 35행부터 61행까지가 된다.

먼저 공과 골대 라인과의 충돌을 체크하기 위해서 35행과 36행에서는 공이 골대 라인의 x 좌표 안에 있는가를 먼저 조사하고 38행와 같이 y좌표를 조사하여 충돌을 체크한다.

 

그리고 득점과는 상관없지만 공과 골대가 충돌하면 다시 공을 슛할 수 있는 준비 상태로 설정되어야 하므로 46행부터 61행은 이와 같은 처리를 하고 있다.

특히 골대는 2컬럼의 특수문자로 되어 있어서 공이 골대와 충돌했는지를 판단하기 위해서는 공이 이 2컬럼 안에 있는지를 개별적으로 조사해야 한다.

이와 같은 부분은 46행부터 53행까지가 해당된다.

 

그 외의 소스 코드는 이미 각 예제에서 완성된 코드이므로 각 코드를 붙여넣기 하여 완성한다.

 

STEP 04

 

[그림 5-39] 4단계 제작 로드맵

 

게임 출력 화면은 크게 게임 소개 화면, 스테이지 화면, 진행 화면, 미션 실패 및 성공 화면, 결과 화면으로 나눌 수 있다.

 

■ 화면 출력 및 키 입력 확인

 

[그림 5-40] 키 입력에 따른 진행

 

 

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

void FailureScreen()

{       

     ScreenPrint( 0, 0, "┏━━━━━━━━━━━━━━━━━━━━━┓");

     ScreenPrint( 0, 1, "┃                                          ┃");

     ScreenPrint( 0, 2, "┃                                          ┃");

     ScreenPrint( 0, 3, "┃                                          ┃");

     ScreenPrint( 0, 4, "┃                                          ┃");

     ScreenPrint( 0, 5, "┃                                          ┃");

     ScreenPrint( 0, 6, "┃                                          ┃");

     ScreenPrint( 0, 7, "┃                                          ┃");

     ScreenPrint( 0, 8, "┃                                          ┃");

     ScreenPrint( 0, 9, "┃                                          ┃");

     ScreenPrint( 0,10, "┃                                          ┃");

     ScreenPrint( 0,11, "┃                    미션 실패 !!!!         ┃");

     ScreenPrint( 0,12, "┃                                          ┃");

     ScreenPrint( 0,13, "┃                                          ┃");

     ScreenPrint( 0,14, "┃                 ●┳━┓                 ┃");

     ScreenPrint( 0,15, "┃                   ┛  ┗                 ┃");

     ScreenPrint( 0,16, "┃                  ■■■■                 ┃");

     ScreenPrint( 0,17, "┃                                          ┃");

     ScreenPrint( 0,18, "┃        다시 하시겠습니까? (y/n)          ┃");

     ScreenPrint( 0,19, "┃                                          ┃");

     ScreenPrint( 0,20, "┃                                          ┃");

     ScreenPrint( 0,21, "┃                                          ┃");

     ScreenPrint( 0,22, "┗━━━━━━━━━━━━━━━━━━━━━┛");             

}

 

[소스 5-11] [그림 5-40]의 소스

 

[그림 5-40]과 같은 결과 화면을 출력하고 입력 받는 값은 y, Y, n, N 중 하나이다. 이때 입력 받는 값이 문자열이 아니라 단일 문자이므로 한 문자를 화면으로 입력 받기 위해서는 main() 부분에서 아래와 같이 처리하면 된다.

 

 

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

int main(void)

{

   int nKey;

 

   Init();

   while( 1 )

   {

         if( _kbhit() )

         {

            nKey = _getch();

            switch( nKey )

            {

             case 'y' :

             case 'Y' : // 입력 처리 부분

                       break;

             case 'n' :

             case 'N' : // 입력 처리 부분

                       break;

          }

          

          Update();

          Render();         

    }

    Release();

    ScreenRelease();

}

 

[소스 5-12] 입력 받는 소스

 

위의 소스 13행과 14행 이후를 보면 y와 Y는 같은 처리하므로 13행 case에 break를 생략하면 두 키 입력에 대한 같은 처리할 수 있다.

이와 같이 case에 반드시 break가 하나씩 있어야 하는 것이 아니라 때로는 생략함으로써 효율을 높일 수 있다.

 

■ 골 세리머니 화면 출력 (효과 화면)

 

골 세리머니 화면은 게임 진행과는 상관없이 일정 시간동안 출력되고 사라지는 효과 화면을 말한다. 이 처럼 잠시 출력되는 효과 화면은 2D와 3D 게임에서도 많이 사용되는데 원리적인 부분은 이와 유사하다. 이 효과 화면도 시간 간격 차이를 이용하여 일정한 시간 간격 동안 출력되는 것이므로 이것은 골대와 공이 이동하는 방법과 같은 원리이다.

 

 

효과가 출력된 경과 시간 = 현재 시간 - 효과 시작 시각

 

[식 5-2] 효과가 출력된 경과 시간

 

효과를 출력하기 이전에 효과의 속성을 정의해 보자.

효과는 일정한 시간동안 출력되다가 사라져야 한다. 따라서 효과를 지속하기 위한 시간 속성이 있어야 하며 효과 출력 시작 시간부터 매번 현재 시각을 읽어 경과된 시간을 체크한다. 이 경과 시간과 효과 지속 시간을 비교하여 효과 출력을 결정하게 된다.

효과를 출력하기 위한 속성을 정의하면 [표 5-8]과 [소스 5-13]과 같다.

 

 

① 효과 지속 시간

② 효과 시작 시각

 

[표 5-8] 효과 속성

 

 

typedef struct _EFFECT

{

        clock_t StratTime; // 효과 발생 시각

        clock_t StayTime;  // 효과 지속 시간

} EFFECT;

 

[소스 5-13] 효과 속성 정의

 

[실습 예제 5-7]

 

k 키를 누르면 [그림 5-41]과 같이 3초 동안 효과 화면이 출력되도록 프로그램을 작성해 보자.

 

[그림 5-41] 골 세레머니 화면

 

 

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

#include <stdio.h>

#include <conio.h>

#include <time.h>

#include <windows.h>

#include "Screen.h"

 

typedef struct _EFFECT

{

        clock_t StratTime; // 효과 발생 시각

        clock_t StayTime;  // 효과 지속 시간     

} EFFECT;

 

EFFECT g_sEffect;

int g_nIsGoal;

 

void GoalMessage( int nX, int nY )

{

     ScreenPrint( nX, nY,     "☆ )) 골인 (( ★" );

     ScreenPrint( nX, nY + 1, "\(^^')/ \(\"*')/" );

     ScreenPrint( nX, nY + 2, "   ■       ■");

     ScreenPrint( nX, nY + 3, "  ┘┐    ┌└" );

}

 

void Init()

{       

     g_sEffect.StayTime = 3000; // 3초 설정

}

 

void Update()

{

     clock_t CurTime = clock();

     if( g_nIsGoal == 1 )

     {   // 효과 지속 시간 체크

         if( (CurTime - g_sEffect.StratTime) > g_sEffect.StayTime )

             g_nIsGoal = 0;

     }   

}

 

void Render()

{               

     ScreenClear();

 

     // Note: 렌더링 시작    

     if( g_nIsGoal == 1 )

     {

        GoalMessage( 10, 5 );

     }   

 

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

          if( _kbhit() )

        {

           nKey = _getch();

           if( nKey == 'q' )

              break;      

 

           if( nKey == 'k' )

           {

               if( g_nIsGoal == 0 )

               {

                g_nIsGoal = 1;

                g_sEffect.StratTime = clock();

               }

            }

         }

 

           Update();    // 데이터 갱신

           Render();    // 화면 출력   

    }   

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-14] 효과 출력 소스

 

14행에 선언된 g_nIsGoal 변수는 현재 효과가 화면에 출력되고 있는지를 알 수 있게 해주는 변수이다. 이 변수값에 따라 효과가 발생된다.

 

■ 게임 진행 제어

 

이 게임은 진행 상태를 다음과 같이 일곱 가지 형태로 나눌 수 있다.

 

 

상태

설명

초기 상태(INIT)

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

준비상태(READY)

스테이지 정보 출력

게임 진행 상태(RUNNING)

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

중지 상태(STOP)

미션 성공과 실패를 판단

미션 성공 상태(SUCCESS)

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

미션 실패 상태(FAILED)

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

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

게임 결과 출력 및 종료

 

[표 5-9] 게임 상태 값

 

이 일곱 가지 형태를 프로그래밍 안에서 구별하기 위해 enum형으로 아래와 같이 정의한다.

 

 

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

 

[소스 5-15] enum형

 

게임의 흐름은 위의 열거형 데이터로 게임의 진행 과정을 결정하며 스테이지를 나타내는 변수는 스테이지를 초기화할 때 사용된다.

이와 같이 전체 게임의 흐름은 게임 상태를 나타내는 열거형 변수와 스테이지 변수 이 두 개로 게임 전체를 제어한다.

전체적인 게임의 흐름은 아래 [그림 5-42]와 같다.

 

[그림 5-42] 게임 전체 흐름도

 

[실습 예제 5-8]

 

위의 [그림 5-42]와 같이 게임이 진행되도록 프로그래밍해 보자. 스테이지가 종결되는 조건은 스테이지별 제한 시간(예: 10초)이며 종결된 후에는 미션의 성공과 실패에 따라 게임 진행 여부를 결정하게 된다. 이때 미션 성공은 s 키로 설정하며 미션 실패는 f 키로 설정하여 다음 단계로 진행되게 하자. s 키와 f 키의 설정은 제한 시간이 되기 전에 설정하여 STOP 상태에서 미션 상태를 판단하게 한다. 전체적인 진행 과정은 아래의 [그림 5-43]부터 [그림 5-46]과 같이 출력되게 하자.

 

[그림 5-43] 게임 전체 초기화

 

[그림 5-44] 스테이지 초기화

 

[그림 5-45] 스테이지 준비

 

[그림 5-46] 게임 진행

 

 

[그림 5-47] 게임 미션 실패

 

 

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

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <stdio.h>

#include <windows.h>

#include <time.h>

#include "Screen.h"

 

typedef enum _GAME_STATE { INIT, READY, RUNNING, STOP, SUCCESS, FAILED,

                                RESULT } GAME_STATE;

                                

GAME_STATE g_GameState = INIT;

int g_nGoal, g_nStage, g_GameStartTime;

 

char g_strMessage[100]; // 게임 상태를 저장하는 문자열

clock_t g_OldTime; // 게임 상태 전이를 위한 이전 시각 저장

 

void Init()

{      

     g_OldTime = clock();

}

 

void Update()

{

     clock_t CurTime = clock();      

 

     switch( g_GameState )

     {

     case INIT :  // 초기화

               if( g_nStage == 0 )

               {

                sprintf( g_strMessage, "%s", "게임 및 사운드 초기화" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_nStage = 1;

                }

                }else{

                sprintf( g_strMessage, "[INIT] 게임 %d 스테이지 초기화", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = READY;

                }

                }

                break;

      case READY : // 스테이지 준비   

                sprintf( g_strMessage, "[READY] %d 스테이지", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = RUNNING;

                    g_GameStartTime = CurTime;

                }

                break;

     case RUNNING : // 게임 진행                             

                if( CurTime - g_GameStartTime > 10000 ) // Note: 제한 시간

                {

                   g_GameState = STOP;                                    

                }else{                   

                   sprintf( g_strMessage, "[RUNNING] 제한 시간 : 10초  현재시간 : %d",

                                 ( CurTime - g_GameStartTime ) / 1000 );                     }               

                break;

     case STOP : // 게임 미션 판정                            

                if( g_nGoal == 1 )

                   g_GameState = SUCCESS;

                else

                    g_GameState = FAILED; 

                break;

     case SUCCESS : // 미션 성공

                sprintf( g_strMessage, "%s", "미션 성공" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = INIT;

                    ++g_nStage;                                  

                }

                break;

     case FAILED : // 미션 실패

                sprintf( g_strMessage, "%s", "미션 실패! 계속 하시겠습니까? <y/n> " );

                break;

 

     case RESULT: // 게임 결과

                sprintf( g_strMessage, "%s", "게임 결과 화면" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;                                  

                }                               

                break;

        }

}

 

void Render()

{       

     clock_t CurTime = clock();

   

     ScreenClear();

 

     // Note: 렌더링 시작    

     ScreenPrint( 20, 10, g_strMessage );

                         

     // Note: 렌더링 끝

     ScreenFlipping();

}

 

void Release()

{

}

 

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

         if( _kbhit() )

        {

           nKey = _getch();

             if( g_GameState == RESULT ) // RESULT상태에서 아무키나 입력되면 종결

               break;

 

           switch( nKey )

             {

              case 's' :

                        g_nGoal = 1;

                        break;

              case 'f' :

                      g_nGoal = 0;

                        break;

             case 'y' :

             case 'Y' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = INIT;

                        g_OldTime = clock();

                     }

                     break;

             case 'n' :

             case 'N' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = RESULT;

                        g_OldTime = clock();

                     }

                     break;

              }

        }

 

          Update();    // 데이터 갱신

          Render();    // 화면 출력    

    }    

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-16] 게임 흐름 출력 소스

 

위의 소스는 Update()에서 게임 진행 상태에 대한 전이를 전부 맡아서 처리하고 Render() 함수는 진행된 결과만을 출력하는 구조로 되어 있다.

게임 결과를 출력할 때 일정시간 화면에 출력하여 현재 게임 상태를 파악할 수 있도록 하기 위해 아래와 같은 코드가 Update() 함수에 있는 것을 알 수 있다.

 

if( CurTime - g_OldTime > 3000 )

{

    g_OldTime = CurTime;

    ...             

}

 

[소스 5-17] 메시지를 출력하는 경과 시간을 체크

 

미션 실패일 때는 화면으로 y, Y, n, N의 입력을 받아야 하므로 134행과 135행, 142행과 143행은 대소문자 구분 없이 입력에 따라 처리를 하기 위해 switch문의 break를 일부로 생략하고 있다.

 

■ 스테이지 구성

 

 

게임 미션이 완료되면 또 다른 새로운 미션이 시작이 된다. 이와 같은 새로운 미션을 제공하기 위해서 프로그램 전체 구조를 바꾸는 것이 아니라 일부 데이터를 바꾸어 게임의 난이도를 조절하게 된다. 물론 경우에 따라서 새로운 게임 프로그램이 실행되게 할 수도 있겠지만 여기서는 난이도를 조정하는 것으로 스테이지에 변화를 주도록 한다.

 

슛 골인 게임에서 난이도를 조절할 수 있는 요소로 무엇이 있을까 생각해 보면 다음과 같다.

 

첫째, 목표 골인 개수이다.

각 스테이지마다 목표 골인 개수를 주어 게임의 긴장감을 줄 수 있으며 목표 골인 개수는 플레이어가 다음 미션으로 넘어가게 하는 요소가 된다.

 

둘째, 제한 시간이다.

목표 골인 개수가 없이 무한대의 시간에서 게임을 진행한다면 아마도 게임의 긴장감은 전혀  없을 것이다. 그래서 시간을 제한하고 그 시간 안에 목표한 골인을 할 수 있도록 한다.

 

셋째, 골대 길이이다.

골대 길이는 플레이어가 골인을 할 수 있는 확률을 높여준다. 골대 길이가 길면 길수록 더욱 골인을 넣기가 쉬워진다는 것이다.

 

넷째, 골대의 최초 이동 좌표이다.

골대가 항상 멀리 있는 것이 아니라 주인공 캐릭터의 바로 앞에서도 빠르게 이동할 수 있는 것이다. 특히 골대의 x 좌표는 이동에 의해 좌표값이 변하지만 y 좌표는 한번 고정되면 게임이 종료되기까지 유지되므로 좌표의 설정도 스테이지에 변화를 줄 수 있는 요소에 해당이 된다.

 

다섯째, 골대의 이동 시간 간격이다.

골대의 이동 시간 간격은 골대의 이동 속도와 연관된다.

이동 시간 간격이 적다는 것은 그 만큼 짧은 시간마다 x 좌표에 변화를 준다는 것이며 이것은 골대는 빠르게 이동하게 한다.

 

여섯째, 골대 이동 거리이다.

현재는 1 컬럼씩 좌우로 움직이지만 같은 이동 시간 간격을 가지며 골대 이동 거리를 크게 한다면 더욱 빠르게 움직이게 된다.

그러므로 이 요소는 골대의 속도와 연관이 되는 부분이며 골대의 이동 시간 간격과 이동 거리는 항상 같이 다루어진다.

위의 여섯 가지 요소를 정리하면 아래 [표 5-10]과 같다.

 

 

① 목표 골인 개수

② 제한 시간

③ 골대 길이

④ 골대의 최초 이동 좌표

⑤ 골대의 이동 시간 간격

⑥ 골대의 이동 거리

 

[표 5-10] 스테이지 정보

 

여기까지 스테이지를 구성하기 위한 여섯 가지 기본 요소에 대해 살펴보았지만 더 많은 요소를 스테이지 요소로 넣는다면 더욱 다양한 스테이지를 구성할 수 있을 것이다.

각 요소의 설정값은 다양한 형태로 읽혀져서 설정되는데 현재 슛 골인 게임의 경우 2 스테지만 설정하고 있으므로 파일이 아닌 배열로 선언하여 설정하고 있다.

일반적으로는 파일로부터 데이터를 읽어 설정하는 방법을 사용하며 이 부분은 11장 하트 게임을 만들 때 적용해 보겠다.

 

 

typedef struct _STAGE_INFO

{

        int nGoalBall;        // 골인해야 할 볼의 개수

        clock_t LimitTime;     // 제한 시간

        int nGoalDaeLength;    // 골대 길이

        int nGoalDaeX;       // 골대 이동 X 좌표

        int nGoalDaeY;       // 골대 이동 Y 좌표

        clock_t MoveTime;    // 골대 이동 시간 간격

        int nDist;            // 골대 이동 거리 

} STAGE_INFO;

 

[소스 5-18] 스테이지 정보 정의

 

[소스 5-18]에서 정의한 구조체는 스테이지가 초기화될 때마다 Init() 함수 안에서 현재의 스테이지를 나타내는 변수와 함께 다음과 같이 설정된다.

 

 

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

typedef struct _STAGE_INFO

{

        int nGoalBall;        // 골인해야 할 볼의 개수

        clock_t LimitTime;     // 제한 시간

        int nGoalDaeLength;    // 골대 길이

        int nGoalDaeX;       // 골대 이동 X 좌표

        int nGoalDaeY;       // 골대 이동 Y 좌표

        clock_t MoveTime;    // 골대 이동 시간 간격

        int nDist;            // 골대 이동 거리 

} STAGE_INFO;

 

STAGE_INFO g_sStageInfo[] = { { 1, 1000*20, 1, 20, 3, 300 },

                                  { 10, 1000*30, 2, 20, 5, 300 } };

 

void Init()

{

     int nLength, i;

 

     if( g_nStage == -1 )

     {

        SoundInit(); // 사운드 초기화

        g_nStage = 0;

     }

 

     g_LimitTime = g_sStageInfo[g_nStage].LimitTime;  // 제한 시간 설정

     g_nGoalBallCount = g_sStageInfo[g_nStage].nGoalBall; // 목표 골인 개수

 

     g_sPlayer.nCenterX = 4;

     g_sPlayer.nCenterY = 0;

     g_sPlayer.nMoveX = 20;

     g_sPlayer.nMoveY = 22;

     g_sPlayer.nX = g_sPlayer.nMoveX - g_sPlayer.nCenterX;

     g_nLength = strlen( g_strPlayer );

 

     // 공의 초기화

     g_sBall.nIsReady = 1;

     g_sBall.nMoveX = g_sPlayer.nMoveX;

     g_sBall.nMoveY = g_sPlayer.nMoveY - 1;

     g_sBall.MoveTime = 100;

 

     // 골대 초기화

     g_sGoalDae.nMoveX = g_sStageInfo[g_nStage].nGoalDaeX;

     g_sGoalDae.nMoveY = g_sStageInfo[g_nStage].nGoalDaeY;

     g_sGoalDae.nLength = g_sStageInfo[g_nStage].nGoalDaeLength;

     g_sGoalDae.MoveTime = g_sStageInfo[g_nStage].MoveTime;

     g_sGoalDae.OldTime = clock();

     g_sGoalDae.nDist = g_sStageInfo[g_nStage].nDist;

     nLength = g_sGoalDae.nLength*2 + 1; // Note: 배열의 최대 길이

                

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

      {

         g_sGoalDae.nLineX[i] = g_sGoalDae.nMoveX + 2*(i+1);   

      }    

}

 

[소스 5-19] 스테이지 설정

 

위의 소스에서 g_nStage 변수는 현재의 스테이지를 나타내는 변수로써 이 변수와 게임 진행 상태를 나타내는 [소스 5-15]의 enum형 변수가 게임 전체 흐름을 제어하게 된다.

이 게임에서는 19행에서와 같이 g_nStage가 -1이면 게임이 최초 시작되는 순간이므로 이때에는 전체 게임에 적용되는 초기화가 실행된다.

이후에 g_nStage가 0 이상의 값을 가지게 되면 각 스테이지에 대한 개별적인 설정을 하게 된다. 그러면 g_nStage 값은 언제 증가될까? 이것은 미션이 성공되었을 때이다.

 

■ 게임 구조 안에 사운드 추가

 

4장에서 FMOD 사운드 시스템에 대해 살펴보았다.

우리가 앞으로 제작할 대부분의 게임은 4장의 내용만으로도 게임 안에서 사운드를 출력할 수 있다. 게임의 기본적인 흐름은 [실습 예제 5-8]과 같으므로 전체 게임 코드를 작성하기  이전에 간단한 게임 제작 구조 안에 사운드를 프로그래밍해 본다면 사운드 제어를 보다 분명히 할 수 있다. 사운드 프로그래밍은 앞으로 제작하는 모든 게임에 공통적으로 적용되는 부분이므로 5장에서만 설명하고 나머지 게임에는 이와 같은 코드를 동일하게 적용한다.

 

실제 슛 골인 게임에서 사용되는 사운드를 정리하면 아래 [표 5-11]과 같다.

 

 

구분

파일명

게임 상태

출력 시점

배경음

init.wav

INIT

게임이 초기 화면 출력할 때

run.wav

RUNNING

게임이 진행될 때

fail.wav

FAILED

미션 실패가 될 때

효과음

ready.wav

READY

스테이지 초기화를 할 때

success.wav

SUCCESS

미션 성공을 했을 때

shoot.wav

RUNNING

k 키가 눌렸을 때

wow.wav

RUNNING

골대 라인에 공이 충돌되었을 때

 

[표 5-11] 사운드 파일과 출력 시점

 

- FMOD 초기화

 

FMOD 사운드 시스템을 사용하기 위해서는 FMOD 사운드 시스템과 사운드 파일을 관리하는 FMOD 사운드를 사운드 개수만큼 생성해야 한다.

여러 개의 FMOD 사운드는 같은 데이터형이므로 배열로 선언할 수 있으며 아래 [소스 5-20]의 16행에서부터 23행까지 사운드 파일과 FMOD 사운드를 일대일 대응시킨다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

FMOD_SYSTEM *g_System;  // Note: FMOD system 변수선언

FMOD_SOUND  *g_Sound[7];

                            // 배경 음악                     효과 음악

char *g_strFileName[7] = { "init.wav", "run.wav", "fail.wav", "ready.wav", "success.wav",

                             "shoot.wav", "wow.wav" };

FMOD_CHANNEL *g_Channel[7];

 

void Init()

{  

     int i;

 

     g_OldTime = clock();

     FMOD_System_Create(&g_System);

     FMOD_System_Init( g_System, 32, FMOD_INIT_NORMAL,  NULL); 

 

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

        FMOD_System_CreateSound( g_System, g_strFileName[i], FMOD_LOOP_NORMAL, 0,

                                      &g_Sound[i]);

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

        FMOD_System_CreateSound( g_System, g_strFileName[i], FMOD_DEFAULT, 0,

                                      &g_Sound[i]);

      FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE, g_Sound[0], 0,

                                                                            &g_Channel[0]);

}

 

[소스 5-20] 사운드 파일과 FMOD 사운드 생성

 

4행의 char *g_strFileName[7] 선언은 7개의 사운드 파일명을 저장하기 위한 부분이다.

사운드를 배경 사운드로 사용할 것인지 아니면 효과 사운드로 사용할 것인지에 따라 FMOD 사운드를 생성하는 옵션이 다르다. 그래서 g_strFileName[0]부터 g_strFileName[2]까지는 배경 사운드 파일명의 메모리 주소를 저장하게 했으며 g_strFileName[3]부터 g_strFileName[6]까지는 효과 사운드 파일명의 메모리 주소를 저장하도록 선언과 동시에 초기화하고 있다. 이런 저장 방식은 배열의 인덱스로 배경 사운드와 효과 사운드를 구분할 수 있으므로 반복문을 이용하여 16행부터 18행까지 배경 FMOD 사운드 생성하고 19행부터 21행까지 효과 FMOD 사운드를 생성할 수 있다.

이와 같은 과정은 모든 사운드 파일이 FMOD 사운드와 연결된 상태를 유지하게 한다.

 

- 사운드 출력과 정지

 

사운드 출력은 [표 5-11]의 출력 시점에 따라 사운드 출력과 정지를 반복하게 된다.

주로 Update() 함수 안에서 게임상태 전이가 이루어지므로 사운드도 여기서 출력과 정지가 결정된다.

아래의 [소스 15-21]는 [표 5-11]의 시점에 따라 사운드를 출력하거나 정지하고 있다.

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

void Update()

{

     clock_t CurTime = clock();      

 

     switch( g_GameState )

     {

     case INIT :

              if( g_nStage == 0 )

              {

                sprintf( g_strMessage, "%s", "게임 및 사운드 초기화" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_nStage = 1;                                         

                }

               }else{

                sprintf( g_strMessage, "[INIT] 게임 %d 스테이지 초기화", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = READY;

                        FMOD_Channel_Stop( g_Channel[0] ); // 배경음 중지

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                             g_Sound[3], 0, &g_Channel[3]); // ready 사운드

                 }

               }

               break;

     case READY : 

                sprintf( g_strMessage, "[READY] %d 스테이지", g_nStage );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = RUNNING;

                    g_GameStartTime = CurTime;

                        

                    FMOD_Channel_Stop( g_Channel[3] );  // ready 사운드 중지

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                            g_Sound[1], 0, &g_Channel[1]); // running 배경음

                }

                break;

     case RUNNING :                               

                if( CurTime - g_GameStartTime > 10000 ) // Note: 제한 시간

                {

                    g_GameState = STOP;   

                    FMOD_Channel_Stop( g_Channel[1] );  // running 배경음 중지

                }else{                   

                    sprintf( g_strMessage, "[RUNNING] 제한 시간 : 10초  현재시간 : %d",

                                         ( CurTime - g_GameStartTime ) / 1000 );                }               

                break;

      case STOP :                         

                FMOD_Channel_Stop( g_Channel[1] );  // running 배경음 중지

                if( g_nGoal == 1 )

                {

                    g_GameState = SUCCESS;

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[4], 0, &g_Channel[4]); // 미션 성공 사운드

                }else{

                    g_GameState = FAILED; 

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[2], 0, &g_Channel[2]); // 미션 실패 사운드

                }

                break;

      case SUCCESS :

                sprintf( g_strMessage, "%s", "미션 성공" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;

                    g_GameState = INIT;

                    ++g_nStage;                                  

                    FMOD_Channel_Stop( g_Channel[4] );  // 미션 성공 사운드 출력 중지

                }

                break;

        case FAILED :

                sprintf( g_strMessage, "%s", "미션 실패! 계속 하시겠습니까? <y/n> " );                 break;

        case RESULT:

                sprintf( g_strMessage, "%s", "게임 결과 화면" );

                if( CurTime - g_OldTime > 3000 )

                {

                    g_OldTime = CurTime;                                  

                }                               

                break;

     }

}

 

[소스 5-21] 사운드 출력과 정지

 

새로운 사운드의 출력과 정지가 되는 시점은 대부분 게임 상태가 바뀌는 부분이다.

위의 소스를 보면 대부분의 게임 상태를 출력하기 위하여 경과 시간을 측정하고 있다.

예를 들어 17행부터 24행까지는 스테이지에 대한 초기화가 실행되고 스테이지 준비 상태로 들어가기 위한 부분이다. 이때 게임이 처음 실행했을 때의 배경음은 정지해야 하므로 22행과 같은 코드를 작성할 수 있고 다음 스테이지에 대한 사운드를 출력해야 하므로 22행부터 23행까지 출력 코드를 작성하면 된다.

여기까지가 일반적인 게임 안에서의 사운드 출력과 정지이다.

 

만약 미션을 실패했을 때는 y, n 키 입력에 따라 다시 현재의 스테이지를 시작하거나 게임 종료를 위한 결과를 출력해야 한다.

그래서 미션 실패의 경우 사운드 정지는 Update() 함수에서 하지 않고 main() 함수의 키 입력 처리 부분에서 하게 된다.

 

 

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

int main(void)

{

    int nKey;

 

    ScreenInit();

    Init();        // 초기화

         

    while( 1 )

    {

        if( _kbhit() )

        {

            if( g_GameState == RESULT )

              break;

 

           nKey = _getch();

           switch( nKey )

             {

             case 's' :

                        g_nGoal = 1;

                       break;

             case 'f' :

                     g_nGoal = 0;

                       break;

           case 'y' :

           case 'Y' :

                   if( g_GameState == FAILED )

                   {

                      g_GameState = INIT;

                      g_OldTime = clock();  

                      FMOD_Channel_Stop( g_Channel[2] );  // 미션 실패 사운드 출력 중지  

                     }

                    break;

            case 'n' :                                     

            case 'N' :

                     if( g_GameState == FAILED )

                     {

                        g_GameState = RESULT;

                        g_OldTime = clock();

                        FMOD_Channel_Stop( g_Channel[2] );//미션 실패 사운드출력 중지

                     }

                    break;

             case 'k' :

                    FMOD_System_PlaySound( g_System, FMOD_CHANNEL_FREE,

                                          g_Sound[5], 0, &g_Channel[5]); // 슛동작 소리 출력

                    break;

              }

        }

 

        Update();    // 데이터 갱신

        Render();    // 화면 출력

 

        FMOD_System_Update( g_System );      

    }    

    Release();   // 해제

    ScreenRelease();

    return 0;

}

 

[소스 5-22] main() 함수 안에서 사운드 처리

 

40행은 k 키가 입력 됐을 때 슛 동작 소리를 출력해 주는 부분이다.

 

- FMOD 해제 

 

게임이 종료될 때 Release() 함수를 호출함으로 이 안에 FMOD 해제 코드를 작성하면 된다. 해제를 할 때는 항상 FMOD 사운드를 먼저 해제한 후에 FMOD 사운드 시스템을 해제한다.

 

 

1

2

3

4

5

6

7

8

9

void Release()

{

     int i;

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

        FMOD_Sound_Release( g_Sound[i] );

 

    FMOD_System_Close( g_System );

    FMOD_System_Release( g_System );

}

 

[소스 5-23] FMOD 해제

 

여기까지 슛 골인 게임을 제작하기 위한 기본 골격에서부터 사운드까지 살펴보았다.

각 예제는 실제로 슛 골인 게임을 만들기 위한 부분 소스이므로 단계별로 차근차근 완성해 나가면 최종적으로 전체 게임을 제작하게 된다.    

이제까지 제작한 모든 예제의 소스를 참고하여 전체 소스를 제작해 보자.

 

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

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

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED