강의/C언어 게임 만들기

C언어로 게임 만들기 2. 시분할 (3) 시분할 구현, (4) 키 입력 함수 바꾸기

moon44432 2020. 12. 2. 16:04

C언어로 게임 만들기
2. 시분할
(3) 시분할 구현, (4) 키 입력 함수 바꾸기

반갑습니다. 이번 강의에서는 지난 강의에서 만들었던 기초적인 게임 구조 위에 시분할을 구현하고 적용해 보도록 하겠습니다.

 


​게임에 시분할을 적용하려면 어떻게 해야 할까? 

시분할이 적용된 전체 코드를 보기 전, 대략적인 구조를 먼저 예상해 봅시다. 화면을 업데이트하고, 키 입력을 받고, 여러 정보들을 출력하는 루틴이 있어야 하겠죠. 이를 간단하게 표현해 보겠습니다.

while (...)
{
    updateScreen();
    getInput();
    printPlayerInfo();
    ...
}

그러나 여기에서 문제가 발생하게 됩니다. 우리가 지금껏 써 왔던 키 입력 함수는 사용자가 키를 누르기 전까지 무한정 대기하거나, 키를 누르더라도 엔터 키를 다시 눌러 입력을 완료해야 했습니다. 작업들이 동시에 진행되는 것 같은 효과를 보기 위해서는 키가 눌리는 순간 즉시 입력을 받는 새로운 입력 방법을 찾아야 합니다. 다행히도, 이는 쉽게 구현할 수 있습니다.

다음 문제점을 찾기 위해, 예시를 하나 들어 보겠습니다. 이제 위 루프 속에 새로운 작업 두 개를 추가할 것입니다. 하나는 0.2초에 한 번씩 숫자를 출력하는 작업입니다. 또 다른 하나는 1초에 한 번씩 숫자를 출력하는 작업입니다. 이 두 작업을 동시에 수행하는 루프를 간단히 표현해 봅시다.

while (...)
{
    updateScreen();
    getInput();
    printPlayerInfo();
    printNumPer200msec(); // 0.2에 한 번씩 출력
    printNumPerSec(); // 1초에 한 번씩 출력
}

이대로 두게 되면 또 다른 문제에 직면합니다. 과연 위의 두 함수가 0.2초가 지났는지, 1초가 지났는지 어떻게 알고 작동할까요? 물론, 표준 라이브러리의 함수를 사용하여 시간을 직접 측정할 수도 있겠지만, 그런 식으로 해결하는 것은 비효율적입니다.

따라서, 앞선 강의에서 누누이 얘기했듯이, 시간을 분할한다는 개념을 활용해야 합니다.

위 그림에 주목하세요. 그림과 같이 1초를 5등분하여 0.2초에 한번씩(초당 5회) 실행되는 루프가 있다고 합시다. (이는 Sleep()이나 usleep() 등의 함수로 구현이 가능하겠죠.)

키 입력과 업데이트 등은 매번 실행되어야 합니다. printNumPer200msec() 함수도 0.2초에 한 번씩 실행되어야 하므로, 매번 호출되어야 합니다. 그럼, 1초에 한 번씩 숫자를 출력하는 printNumPerSec() 함수는 어떠한 조건을 만족할 때 호출하면 될까요?

여기서 카운터의 개념을 도입할 수 있습니다. 반복문이 매 5번 실행될 때마다 한번씩 printNumPerSec()을 호출한다면, 1초에 한번씩 호출하는 것과 거의 동일합니다. 따라서, 반복문을 한 번 실행할 때 카운터 값을 1 증가시킨다고 할 때, 카운터 값을 5로 나눈 나머지가 0이면 printNumPerSec()을 호출하면 되겠지요?

이를 응용하여 여러 작업의 실행 타이밍 또는 신호의 주기 등을 관리할 수 있습니다. 위 그림에서는 1초를 5등분했지만, 시간을 더 잘게 쪼개어 더 정교하게 관리할 수도 있습니다.

구현 코드 분석하기

설명이 길어졌습니다. 그럼 지금부터 아래의 코드를 분석해 보도록 합시다. (Windows 환경 기준으로 작성된 코드입니다. 맥이나 리눅스 환경에서는 작동하지 않으니, 이전 강의를 참고하여 헤더와 함수를 교체해 주세요.)

#pragma warning (disable:4996)

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <conio.h>
 
typedef enum { NOCURSOR, SOLIDCURSOR, NORMALCURSOR } CURSOR_TYPE;
const int refreshRate = 50;
 
void setcursortype(CURSOR_TYPE c)
{
    CONSOLE_CURSOR_INFO CurInfo;
    switch (c)
    {
    case NOCURSOR:
        CurInfo.dwSize = 1;
        CurInfo.bVisible = FALSE;
        break;
    case SOLIDCURSOR:
        CurInfo.dwSize = 100;
        CurInfo.bVisible = TRUE;
        break;
    case NORMALCURSOR:
        CurInfo.dwSize = 20;
        CurInfo.bVisible = TRUE;
        break;
    }
    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &CurInfo);
}
 
void gotoxy(int x, int y)
{
    COORD CursorPosition = { x, y };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), CursorPosition);
}
 
int main(void)
{
    setcursortype(NOCURSOR);
    int x = 0, y = 0;
    int cnt = 1;
    char ch;
    gotoxy(x, y);
    printf("@");
    while (true)
    {
        ch = '\0';
        if (kbhit()) ch = getch();
 
        gotoxy(x, y);
        printf(" ");
        switch (ch)
        {
        case 'w':
            if (y > 0) y--;
            break;
        case 's':
            if (y < 24) y++;
            break;
        case 'a':
            if (x > 0) x--;
            break;
        case 'd':
            if (x < 79) x++;
        }
        gotoxy(x, y);
        printf("@");
 
        gotoxy(0, 0);
        printf("%d", cnt);
        if (cnt % 50 == 0) printf("\n%d", cnt / refreshRate);
 
        Sleep(1000 / refreshRate);
        cnt++;
    }
}
  • #pragma warning (disable:4996)
    Visual Studio의 컴파일러는 안전상의 문제로 kbhit() 함수의 사용을 기본적으로 제한하고 있습니다. 대신에 비표준 확장인 _kbhit()을 제공하는데, 이를 사용해도 문제는 없습니다. 그러나 본 강의에서는 kbhit()을 계속 쓸 것이므로, 경고를 우회하기 위해 이 구문을 소스 코드 상단에 반드시 넣어 주세요.

  • const int refreshRate = 50;
    refreshRate 상수는 1초를 몇 등분할 것인지, 즉 1초에 몇 번 루틴을 실행할 것인지를 결정합니다. 값이 50이니, 초당 50번 반복 실행하라는 의미입니다.

  • int cnt = 1;
    위에서 언급하였던 카운터를 생성하는 부분입니다. 카운터를 이용하여 기능의 실행 주기를 결정합니다.

  • if (kbhit()) ch = getch();
    kbhit() 함수는 키보드의 키가 눌려 있으면 0이 아닌 값을, 눌려 있지 않으면 0을 반환합니다. 이를 이용하여 키보드의 키가 눌려 있을 때만 입력을 받고, 그렇지 않을 경우 무시하는 것이 가능해졌습니다.

    + 맥/리눅스 환경은 대부분 kbhit()을 지원하지 않습니다. 해결법을 찾는 대로 추가할 예정입니다. 당장 필요한 경우, 구글 등지에 검색하면 여러 가지 정의를 찾을 수 있습니다.

  • printf("%d", cnt);
    카운터 변수의 값을 출력하는 기능인데요, 앞에 별다른 주기 지정이 없으므로 매번 실행됩니다. 1초에 50번 반복되는 루프이므로 이 코드 역시 초당 50번 실행됩니다.

  • if (cnt % 50 == 0) printf("\n%d", cnt / refreshRate);
    위에서 열심히 설명하였던 부분입니다. 카운터 변수를 50으로 나눈 나머지가 0일 경우 실행되므로, 곧 1초마다 실행된다는 의미입니다. 출력하는 값은 cnt / refreshRate이니, 위 코드는 결국 프로그램을 켠 후 몇 초가 경과되었는지 출력하는 기능이라고 볼 수 있습니다.

실행 결과는 다음과 같습니다. w, s, a, d 키로 직접 플레이어를 움직여 보시고, 화면 좌 상단에 출력되는 숫자들을 관찰해 보시기 바랍니다.


이번 강의에서는 드디어 시분할 기법을 구현해 보고, 새로운 키 입력 기능도 만들어 보았습니다. 다음 강의부터는 이러한 기초적인 부분을 넘어서서 게임의 "실제 기능"들을 추가해 보도록 하겠습니다.

이상으로 강의를 마칩니다. 감사합니다.