AI에게 C++로 할 수 있는 간단한 프로젝트를 소개해 달라고 했는데 그중 재밌어 보이는 것이 있어서 포스팅하게 되었습니다. 터미널 환경(CLI)에서 키보드만 사용해서 할 수 있는 고전 게임 “스네이크"입니다.

snake

구성

1
2
3
4
5
6
7
8
.
├── .git
├── src/
│   └── main.cpp
├── .gitattributes
├── .gitignore
├── CMakeLists.txt
└── README.md

핵심 내용

  1. 터미널 특정 좌표에 출력할 수 있어야 함
  2. 뱀이 움직일 수 있는 프레임이 있고 그 밖을 벗어나면 종료
  3. 먹이를 먹으면 꼬리가 길어지고 뱀 몸통을 가로지를 수 없음

터미널 제어 라이브러리 ncurses

  • 터미널에서 글자와 기호로 화면을 구성하는 프로그램을 만들 때 쓰는 라이브러리
  • 커서 이동, 색상 표시, 키보드 입력 처리, 화면 갱신 같은 기능을 제공
  • 여러 창·메뉴·입력 폼을 터미널 안에 만들 수 있음
  • 텍스트 편집기나 설정 화면처럼 그래픽 없이 작동하는 TUI(텍스트 사용자 인터페이스)​를 구현할 때 유용함
  • C 프로그램에서 자주 사용하며, ncurses.h 헤더를 포함해 기능을 불러옴

구현

💡 전체 프로젝트는 이곳에서 볼 수 있습니다.

터미널 관련 내용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 터미널 설정
initscr();              // ncurses 초기화
cbreak();               // Enter 키를 기다리지 않음(문자 단위 입력 모드)
noecho();               // 입력한 키가 터미널 화면에 표시되지 않게 함
keypad(stdscr, TRUE);   // 방향키 같은 특수 키 입력 사용
curs_set(0);            // 커서 숨김
nodelay(stdscr, true);  // 입력을 기다리지 않음, 입력 되지 않으면 ERR 반환
WINDOW* board = newwin(kHeight, kWidth, 0, 0);  // 새로운 창을 만듦

// 프레임 갱신...

// 터미널 정리
delwin(board);          // 만들었던 창을 소멸 시킴
endwin();               // ncurses 종료

게임 내용

 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
// 프레임 갱신
while(!gameOver) {
    ch = getch();
    head = snake.front();
    
    // 키보드 입력 받고
    switch(ch) {
        case KEY_UP:    if(dir!=KEY_DOWN) dir = KEY_UP; break;
        case KEY_DOWN:  if(dir!=KEY_UP) dir = KEY_DOWN; break;
        case KEY_LEFT:  if(dir!=KEY_RIGHT) dir = KEY_LEFT; break;
        case KEY_RIGHT: if(dir!=KEY_LEFT) dir = KEY_RIGHT; break;
    }
    
    // 머리 위치 조정하기
    switch(dir) {
        case KEY_UP:    head.y--; break;
        case KEY_DOWN:  head.y++; break;
        case KEY_LEFT:  head.x--; break;
        case KEY_RIGHT: head.x++; break;
    }
    
    // 갱신된 머리 위치 넣고
    snake.push_front(head);
    if(head == food) {
        // 먹이를 먹었으면 새로운 위치 갱신
        do {
            food.x = xDist(gen);
            food.y = yDist(gen);
        } while([&food, &snake]() {
            bool duplicated = false;
            for(const Point& p : snake) {
                if(p.x == food.x && p.y == food.y) {
                    duplicated = true;
                }
            }

            return duplicated;
        }());
        
    } else {
        // 먹이를 먹지 못한다면 꼬리는 하나 지우기
        snake.pop_back();
    }
    
    // GameOver
    if(head.x == 0 || head.x >= kWidth - 1 || head.y == 0 || head.y >= kHeight - 1) {
        gameOver = true;
        break;
    }

    // 전체를 지우고 새로운 화면으로 다시 그리기
    // 테두리 그리기
    werase(board);
    box(board, 0, 0);
    
    // 먹이 그리기
    mvwaddch(board, food.y, food.x, '@');
    
    //뱀 그리기
    for(i=0; i<snake.size(); i++) {
        if(i==0) {
            mvwaddch(board, snake[i].y, snake[i].x, 'o');
        } else {
            mvwaddch(board, snake[i].y, snake[i].x, '*');
        }
    }

    wrefresh(board);

    std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
  • :8-11 키 입력을 받고 진행 방향의 반대 방향으로 가면 무시
  • :23, :42 뱀의 이동 좌표를 만들어 넣고 뺌
  • :27-38 먹이를 먹으면 새로운 먹이를 드롭
  • :46 머리가 테두리를 벗어나면 게임 오버
  • :53-68 갱신된 내용을 터미널에 그리기
  • :70 5FPS로 맞추기 위한 delay
play

마무리

한 프레임에 표현해야 할 것들을 생각하고 구현한 대로 터미널에 찍히니 재미있었습니다. 그리고 게임을 진행해 보니 뱀 머리가 몸통을 가로지르지 못하도록 해야 하는데 이 로직도 추가해야 할 것 같습니다.

지금은 ncurses를 이용해서 리눅스/유닉스 계열에서만 사용 가능한데 시간이 된다면 크로스 플랫폼으로 윈도우에서도 실행할 수 있게 해보겠습니다.

추가로 터미널 UI 라이브러리 중 크로스 플랫폼을 지원하는 FTXUI가 있다고 합니다.