C 언어로 플래피 버드 만들기

CBeginner
지금 연습하기

소개

Flappy Bird 는 단순하면서도 도전적인 게임 플레이로 엄청난 인기를 얻은 인기 있고 중독성 있는 모바일 게임입니다. 이 프로젝트에서는 C 프로그래밍 언어를 사용하여 Flappy Bird 의 자체 버전을 구현하는 방법을 배웁니다.

이 프로젝트를 따르면 다음을 수행할 수 있습니다.

  • 텍스트 기반 화면 그리기를 위해 ncurses 라이브러리를 사용하는 방법을 배웁니다.
  • Linux 에서 데이터 구조 및 시스템 호출에 대한 지식을 습득합니다.
  • C 프로그램에서 키보드 이벤트 및 실시간 업데이트를 처리하는 경험을 얻습니다.

👀 미리보기

Flappy Bird Preview

🎯 과제

이 프로젝트에서는 다음을 배우게 됩니다.

  • C 를 사용하여 문자 기반 버전의 Flappy Bird 를 구현하는 방법.
  • 새의 움직임을 제어하기 위해 키보드 이벤트를 처리하는 방법.
  • 장애물을 오른쪽에서 왼쪽으로 이동시켜 전진하는 착시 현상을 만드는 방법.
  • ncurses 라이브러리를 사용하여 문자 인터페이스를 그리는 방법.

🏆 성과

이 프로젝트를 완료하면 다음을 수행할 수 있습니다.

  • C 프로그래밍 언어에 대한 숙련도를 보여줍니다.
  • 키보드 이벤트를 처리하는 기술을 개발합니다.
  • C 프로그램에서 실시간 업데이트를 구현합니다.
  • 텍스트 기반 화면 그리기를 위해 ncurses 라이브러리를 활용합니다.
  • Linux 에서 데이터 구조 및 시스템 호출을 이해합니다.
이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 중급 레벨의 실험이며 완료율은 63%입니다.학습자들로부터 88%의 긍정적인 리뷰율을 받았습니다.

기본 지식

저희 프로젝트는 데이터 구조에 대한 약간의 지식을 필요로 하며 Linux 에서 몇 가지 시스템 호출을 포함합니다.

또한, ncurses라는 텍스트 기반 화면 그리기 라이브러리도 사용합니다. 따라서 컴파일 시 -lcurses 옵션을 추가해야 합니다.

디자인 아이디어

문자 기반 버전의 Flappy Bird 를 구현하기 위해 다음 세 가지 핵심 사항의 구현부터 시작합니다.

  1. 프로그램은 키보드 이벤트에 응답할 수 있어야 합니다.
  2. 문자 인터페이스는 실시간으로 업데이트될 수 있어야 합니다.
  3. 새는 앞으로 날아가는 시각적 착시를 주어야 합니다.

위의 세 가지 문제에 대한 해결책은 다음과 같습니다.

  1. Linux 에서 제공하는 시스템 인터페이스를 사용하여 키보드 이벤트를 캡처합니다.
  2. ncurses 라이브러리 함수를 사용하여 문자 인터페이스를 그립니다.
  3. 새가 앞으로 날아가는 착시를 만들기 위해:

가장 간단한 방법은 새가 수평 방향으로 왼쪽에서 오른쪽으로 움직이도록 하는 것이지만, 이렇게 하면 새가 어느 시점에서 오른쪽 경계를 초과하게 됩니다.

대신, 반대로 생각해 봅시다. 사람이 앞으로 이동하는 동안 차 밖의 풍경을 보면 뒤로 움직이는 것처럼 보입니다 (운동은 상대적입니다). 따라서 장애물이 오른쪽에서 왼쪽으로 움직이도록 하여 동일한 시각적 효과를 얻고 새가 경계를 초과하는 문제를 피합니다.

✨ 솔루션 확인 및 연습

상수 정의

먼저 Xfce 터미널을 열고 다음 명령을 실행하여 ncurses 라이브러리를 설치합니다.

sudo apt update
sudo apt-get install libncurses5-dev

~/project 디렉토리로 이동하여 프로젝트 파일 flappy_bird.c를 생성합니다.

cd ~/project
touch flappy_bird.c

다음으로, C 코드를 작성해야 합니다. 첫 번째 단계는 헤더 파일을 포함하는 것입니다.

## include <curses.h>
## include <stdlib.h>
## include <signal.h>
## include <sys/time.h>

main() 함수를 작성하기 전에 몇 가지 기본 작업을 완료해 보겠습니다. 터미널 문자 인터페이스를 사용하므로 ASCII 문자가 필수적입니다. 따라서 몇 가지 상수를 정의해야 합니다.

배경의 기둥을 나타내기 위해 *를 사용하고, 새를 나타내기 위해 O를 사용합니다. 코드는 다음과 같습니다.

## define CHAR_BIRD 'O'  // 새 문자 정의
## define CHAR_STONE '*'  // 기둥을 구성하는 돌 정의
## define CHAR_BLANK ' '  // 빈 문자 정의

배경의 기둥은 단일 연결 리스트를 사용하여 저장됩니다. 구조체는 다음과 같이 정의됩니다.

typedef struct node {
    int x, y;
    struct node *next;
}node, *Node;

몇 가지 전역 변수도 정의해 보겠습니다.

Node head, tail;
int bird_x, bird_y;
int ticker;

이제 생성할 함수를 선언합니다.

void init();  // 게임의 초기화 작업을 관리하는 초기화 함수
void init_bird();  // 새의 위치 좌표 초기화
void init_draw();  // 배경 초기화
void init_head();  // 기둥을 저장하는 연결 리스트 헤드 초기화
void init_wall();  // 기둥을 저장하는 연결 리스트 초기화
void drop(int sig);  // 시스템 신호를 수신하고 기둥을 오른쪽에서 왼쪽으로 이동하는 신호 수신 함수
int set_ticker(int n_msec);  // 커널의 타이머 틱 간격 설정
✨ 솔루션 확인 및 연습

타이밍 문제

이제 배경을 정기적인 간격으로 움직이게 하는 문제를 해결해 보겠습니다. Linux 시스템에서 제공하는 기능, 즉 신호를 사용합니다.

신호가 무엇인지 잘 모르시겠다고요? 걱정하지 마세요. Linux 커널에서 일정 시간마다 우리 프로그램에 신호를 보내는 타이머라고 생각할 수 있습니다. 신호 처리 함수 drop(int sig)는 신호를 수신하면 자동으로 실행됩니다. drop(int sig) 함수에서 기둥을 이동하기만 하면 됩니다. 또한, 신호는 Linux 커널에서 전송되므로 신호를 수신하는 동안 키보드 신호 수신이 차단되지 않습니다.

이제 코드를 구현하고 set_ticker(int n_msec) 함수를 사용하여 커널의 타이머 주기를 설정해 보겠습니다.

int set_ticker(int n_msec)
{
    struct itimerval timeset;
    long n_sec, n_usec;

    n_sec = n_msec / 1000;
    n_usec = (n_msec % 1000) * 1000L;

    timeset.it_interval.tv_sec = n_sec;
    timeset.it_interval.tv_usec = n_usec;

    timeset.it_value.tv_sec = n_sec;
    timeset.it_value.tv_usec = n_usec;

    return setitimer(ITIMER_REAL, &timeset, NULL);
}

신호 처리 함수 drop(int sig):

void drop(int sig)
{
    int j;
    Node tmp, p;

    // 원래 새 위치의 기호 지우기
    move(bird_y, bird_x);
    addch(CHAR_BLANK);
    refresh();

    // 새의 위치 업데이트 및 화면 새로 고침
    bird_y++;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();

    // 새가 기둥과 충돌하면 게임 종료
    if((char)inch() == CHAR_STONE)
    {
        set_ticker(0);
        sleep(1);
        endwin();
        exit(0);
    }

    // 첫 번째 벽이 경계를 벗어나는지 확인
    p = head->next;
    if(p->x < 0)
    {
        head->next = p->next;
        free(p);
        tmp = (node *)malloc(sizeof(node));
        tmp->x = 99;
        tmp->y = rand() % 11 + 5;
        tail->next = tmp;
        tmp->next = NULL;
        tail = tmp;
        ticker -= 10;  // 가속
        set_ticker(ticker);
    }
    // 새 기둥 그리기
    for(p = head->next; p->next != NULL; p->x--, p = p->next)
    {
        // CHAR_STONE 을 CHAR_BLANK 로 대체
        for(j = 0; j < p->y; j++)
        {
            move(j, p->x);
            addch(CHAR_BLANK);
            refresh();
        }
        for(j = p->y+5; j <= 23; j++)
        {
            move(j, p->x);
            addch(CHAR_BLANK);
            refresh();
        }

        if(p->x-10 >= 0 && p->x < 80)
        {
            for(j = 0; j < p->y; j++)
            {
                move(j, p->x-10);
                addch(CHAR_STONE);
                refresh();
            }
            for(j = p->y + 5; j <= 23; j++)
            {
                move(j, p->x-10);
                addch(CHAR_STONE);
                refresh();
            }
        }
    }
    tail->x--;
}

신호 처리 함수에서 배경을 한 열 앞으로 이동시키고 동시에 새를 한 행 아래로 떨어뜨립니다. 또한 새가 기둥과 충돌하는지 확인합니다. 충돌하면 게임이 종료됩니다.

✨ 솔루션 확인 및 연습

main() 함수

main() 함수에서 먼저 초기화 함수 init()을 호출한 다음 while() 루프에 들어갑니다. 루프는 주로 세 부분으로 구성됩니다.

  1. 사용자의 입력을 확인합니다. "w" 키 또는 스페이스바를 누르면 새가 두 행 위로 이동합니다. "q" 키를 누르면 게임이 종료됩니다. "z" 키를 누르면 게임이 일시 중지됩니다.
  2. 새를 이동시키고 다시 그립니다.
  3. 새가 파이프에 부딪히는지 확인합니다.

코드를 살펴보겠습니다.

int main()
{
    char ch;

    init();
    while(1)
    {
        ch = getch();  // 키보드 입력 받기
        if(ch == ' ' || ch == 'w' || ch == 'W')  // 스페이스바 또는 "w" 키를 누르면
        {
            // 새를 이동시키고 다시 그립니다.
            move(bird_y, bird_x);
            addch(CHAR_BLANK);
            refresh();
            bird_y--;
            move(bird_y, bird_x);
            addch(CHAR_BIRD);
            refresh();

            // 새가 파이프에 부딪히면 게임 종료
            if((char)inch() == CHAR_STONE)
            {
                set_ticker(0);
                sleep(1);
                endwin();
                exit(0);
            }
        }
        else if(ch == 'z' || ch == 'Z')  // 일시 중지
        {
            set_ticker(0);
            do
            {
                ch = getch();
            } while(ch != 'z' && ch != 'Z');
            set_ticker(ticker);
        }
        else if(ch == 'q' || ch == 'Q')  // 종료
        {
            sleep(1);
            endwin();
            exit(0);
        }
    }
    return 0;
}

main() 함수에서는 먼저 화면을 초기화한 다음 루프에서 키보드 입력을 받습니다. "w" 키 또는 스페이스바를 누르면 새가 두 행 위로 이동합니다. "q" 키를 누르면 게임이 종료됩니다. "z" 키를 누르면 게임이 일시 중지됩니다.

이제 init() 함수를 살펴보겠습니다.

void init()
{
    initscr();
    cbreak();
    noecho();
    curs_set(0);
    srand(time(0));
    signal(SIGALRM, drop);

    init_bird();
    init_head();
    init_wall();
    init_draw();
    sleep(1);
    ticker = 500;
    set_ticker(ticker);
}

init() 함수는 먼저 ncurses에서 제공하는 함수를 사용하여 화면을 초기화합니다. 그런 다음 여러 하위 함수를 호출하여 특정 초기화를 수행합니다. 신호 처리 함수 drop()을 설치하고 타이머 간격을 설정합니다.

각 초기화 하위 함수를 살펴보겠습니다.

init_bird() 함수는 새의 위치를 초기화합니다.

void init_bird()
{
    bird_x = 5;
    bird_y = 15;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();
    sleep(1);
}

init_head()init_wall() 함수는 파이프를 저장하기 위해 연결 리스트를 초기화합니다.

void init_head()
{
    Node tmp;

    tmp = (node *)malloc(sizeof(node));
    tmp->next = NULL;
    head = tmp;
    tail = head;
}
void init_wall()
{
    int i;
    Node tmp, p;

    p = head;
    for(i = 0; i < 5; i++)
    {
        tmp = (node *)malloc(sizeof(node));
        tmp->x = (i + 1) * 19;
        tmp->y = rand() % 11 + 5;
        p->next = tmp;
        tmp->next = NULL;
        p = tmp;
    }
    tail = p;
}

init_draw() 함수는 화면을 초기화합니다.

void init_draw()
{
    Node p;
    int i, j;

    // 연결 리스트 순회
    for(p = head->next; p->next != NULL; p = p->next)
    {
        // 파이프 그리기
        for(i = p->x; i > p->x-10; i--)
        {
            for(j = 0; j < p->y; j++)
            {
                move(j, i);
                addch(CHAR_STONE);
            }
            for(j = p->y+5; j <= 23; j++)
            {
                move(j, i);
                addch(CHAR_STONE);
            }
        }
        refresh();
        sleep(1);
    }
}

이것으로 플래피 버드 게임이 완성되었습니다.

✨ 솔루션 확인 및 연습

컴파일 및 실행

gcc 명령을 실행하여 컴파일합니다.

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird
Flappy Bird 코드 컴파일
✨ 솔루션 확인 및 연습

요약

이 프로젝트에서는 C 프로그래밍 언어를 사용하여 텍스트 기반 플래피 버드 게임을 구현했습니다. 학생들은 이 과정을 바탕으로 파이프에 색상을 추가하거나 파이프 너비를 무작위로 변경하는 등 게임을 더욱 향상시킬 수 있습니다.