소개
이 프로젝트에서는 ncurses 라이브러리를 사용하여 C 로 간단한 뱀 게임을 만드는 방법을 배웁니다. 이 고전적인 게임은 뱀을 제어하여 음식을 먹고 길어지면서 벽과 자기 자신과의 충돌을 피하는 것을 포함합니다. 게임의 기능은 초기화, 게임 루프, 뱀 이동, 충돌 감지 등 여러 주요 구성 요소로 나뉩니다. 이 프로젝트가 끝나면 터미널에서 실행할 수 있는 기본적인 뱀 게임을 갖게 됩니다.
👀 미리보기

🎯 과제
이 프로젝트에서 다음을 배우게 됩니다.
- 뱀의 위치를 업데이트하고 사용자 입력을 처리하기 위한 게임 루프를 구현하는 방법.
- 게임을 초기화하고, 게임 창을 그리고, 게임 오버 메시지를 표시하는 함수를 만드는 방법.
- 벽, 뱀의 몸, 음식과의 충돌을 확인하기 위한 충돌 감지를 구현하는 방법.
- 음식을 먹을 때 뱀의 길이를 늘리는 기능과 같은 기능을 개발하는 방법.
🏆 성과
이 프로젝트를 완료하면 다음을 수행할 수 있습니다.
- C 에서 ncurses 라이브러리를 사용하여 터미널 기반 게임을 만들 수 있습니다.
- 게임 상태를 업데이트하고 사용자 입력을 처리하는 등 게임 로직을 구현할 수 있습니다.
- 뱀과 음식과 같은 게임 객체를 나타내기 위해 데이터 구조를 생성하고 조작할 수 있습니다.
- 게임 규칙을 제공하고 게임이 종료되어야 하는 시기를 결정하기 위해 충돌 감지를 구현할 수 있습니다.
기본 지식
텔레타이프 머신의 광범위한 사용 시대에 텔레타이프 머신은 케이블을 통해 중앙 컴퓨터에 연결된 출력 터미널 역할을 했습니다. 사용자는 터미널 화면에 출력을 제어하기 위해 일련의 특정 제어 명령을 터미널 프로그램에 보내야 했습니다. 예를 들어, 화면에서 커서 위치 변경, 화면의 특정 영역 내용 지우기, 화면 스크롤, 표시 모드 전환, 텍스트 밑줄 긋기, 문자 모양, 색상, 밝기 등을 변경하는 것입니다. 이러한 제어는 이스케이프 시퀀스 (escape sequence) 라는 문자열을 통해 구현됩니다. 이스케이프 시퀀스는 이러한 연속된 바이트가 이스케이프 문자 (ESC 키를 눌러 입력하는 문자) 인 0x1B 문자로 시작하기 때문에 그렇게 불립니다. 지금도 터미널 에뮬레이션 프로그램에 이스케이프 시퀀스를 입력하여 그 시대의 텔레타이프 터미널의 출력 효과를 시뮬레이션할 수 있습니다. 터미널 (또는 터미널 에뮬레이션 프로그램) 에 색상이 있는 배경으로 텍스트를 표시하려면 다음 이스케이프 시퀀스를 명령 프롬프트에 입력할 수 있습니다.
echo "^[[0;31;40mIn Color"
여기서 ^와 [는 소위 이스케이프 문자입니다. (참고: 이 경우 ^[는 하나의 문자입니다. ^와 [ 문자를 순차적으로 입력하여 입력하는 것이 아닙니다. 이 문자를 인쇄하려면 먼저 Ctrl+V를 누른 다음 ESC 키를 눌러야 합니다.) 위의 명령을 실행하면 In Color의 배경이 빨간색으로 변경된 것을 볼 수 있습니다. 그 후, 표시되는 모든 텍스트는 이 효과로 출력됩니다. 이 효과를 종료하고 원래 형식으로 돌아가려면 다음 명령을 사용할 수 있습니다.
echo "^[[0;37;40m"
이제 이러한 문자 (이스케이프 시퀀스) 의 목적을 아십니까? (세미콜론 사이의 매개변수를 변경하고 어떤 결과를 얻는지 시도해 보세요.) 아마도 상상했던 것과 다를 수 있습니다. 터미널 환경이 다르기 때문일 수 있으며, 이는 다른 터미널 또는 운영 체제에 따라 다릅니다. (단색 터미널에서 색상 문자를 표시할 수 없죠?) 이러한 호환성 문제를 피하고 서로 다른 터미널에서 일관된 출력을 얻기 위해 UNIX 의 설계자는 termcap이라는 메커니즘을 발명했습니다. termcap은 실제로 이스케이프 시퀀스와 함께 릴리스되는 파일입니다. 이 파일은 현재 터미널이 올바르게 실행할 수 있는 모든 이스케이프 시퀀스를 나열하여 입력 이스케이프 시퀀스의 실행 결과가 이 파일의 사양을 준수하도록 보장합니다. 그러나 이 메커니즘이 발명된 후 몇 년 동안 terminfo라는 또 다른 메커니즘이 점차 termcap을 대체했습니다. 그 이후로 사용자는 프로그래밍할 때 더 이상 복잡한 이스케이프 시퀀스 사양을 참조할 필요가 없으며, terminfo 데이터베이스에 액세스하여 화면 출력을 제어하기만 하면 됩니다.
모든 애플리케이션이 terminfo를 사용하여 출력을 제어한다고 가정하면 (예: 제어 문자 전송 등) 곧 이러한 코드 호출로 인해 전체 프로그램을 제어하고 관리하기 어려워질 것입니다. 이러한 문제의 출현으로 CURSES가 탄생했습니다. CURSES라는 이름은 cursor optimization이라는 말장난에서 유래되었습니다.
CURSES 라이브러리는 터미널의 기본 제어 코드 (이스케이프 시퀀스) 를 캡슐화하여 사용자에게 유연하고 효율적인 API (application programming interface) 를 제공하여 사용자가 커서를 제어하고, 창을 만들고, 전경 및 배경 색상을 변경하고, 마우스 작업을 처리할 수 있도록 합니다. 이를 통해 사용자는 문자 터미널에서 애플리케이션을 작성할 때 성가신 하위 수준 메커니즘을 우회할 수 있습니다.
NCURSES는 System V Release 4.0 (SVr4)의 CURSES의 복제본입니다. 이전 버전의 CURSES와 완벽하게 호환되는 자유롭게 구성 가능한 라이브러리입니다. 간단히 말해서, 애플리케이션이 터미널 화면의 표시를 직접 제어할 수 있도록 하는 라이브러리입니다. 나중에 CURSES 라이브러리가 언급될 때, 이는 또한 NCURSES 라이브러리를 의미합니다.
NCURSES는 기본 터미널 기능을 캡슐화할 뿐만 아니라 아름다운 인터페이스를 생성하기 위한 상당히 안정적인 작업 프레임워크를 제공합니다. 창을 만드는 기능이 포함되어 있습니다. 그리고 자매 라이브러리인 Menu, Panel, Form은 CURSES 기본 라이브러리의 확장입니다. 이러한 라이브러리는 일반적으로 CURSES와 함께 배포됩니다. 여러 창, 메뉴, 패널 및 양식을 포함하는 애플리케이션을 구축할 수 있습니다. 창은 스크롤하거나 숨기는 등 독립적으로 관리할 수 있습니다. 메뉴를 통해 사용자는 명령 실행을 쉽게 하기 위해 명령 옵션을 만들 수 있습니다. 양식을 통해 사용자는 간단한 데이터 입력 및 표시를 위한 창을 만들 수 있습니다. 패널은 NCURSES 창 관리 기능의 확장이며 창을 겹치거나 쌓을 수 있습니다.
상수 정의
먼저 터미널을 열고 다음 명령을 실행하여 ncurses 라이브러리를 설치합니다.
sudo apt-get update
sudo apt-get install libncurses5-dev
~/project 디렉토리로 이동하여 프로젝트 파일 snake.c를 생성합니다.
cd ~/project
touch snake.c
다음으로 C 코드를 작성해야 합니다. 첫 번째 단계는 헤더 파일을 포함하는 것입니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>
main() 함수를 작성하기 전에 몇 가지 기본 작업을 완료해 보겠습니다. 터미널 문자 인터페이스를 사용하고 있으므로 ASCII 문자가 필수적입니다. 따라서 몇 가지 상수를 정의해야 합니다.
#define TBool int
#define True 1
#define False 0
#define SHAPE_FOOD '@' // 음식 (Food)
#define SHAPE_SNAKE '#' // 뱀 몸통 (Snake body)
#define GAMEWIN_YLEN 15
#define GAMEWIN_XLEN 60
#define LOGWIN_YLEN 7
#define LOGWIN_XLEN (GAMEWIN_XLEN)
#define LOGBUF_NUM (LOGWIN_YLEN-2)
#define LOGBUF_LEN (GAMEWIN_XLEN-2)
#define MAXLEVEL 12
#define GetSnakeTail(s) ((s)->head->front)
WINDOW *logwin; // 로그 창 선언 (Declare a log window)
#define INITRUNLOG() logwin = newlogw() // 사용자 정의 함수 newlogw() 를 호출하여 로그 창 생성 (Create a log window by calling the custom function newlogw())
#define RUNLOG(str) runlog(logwin, str) // 게임 프롬프트를 표시하기 위해 로그 창 실행 (Run the log window to display game prompts)
#define DESTROYRUNLOG() delwin(logwin)
int g_level; // 플레이어 레벨, 전역 변수 (Player level, a global variable)
또한 몇 가지 구조체 (struct) 및 열거형 (enum) 정의를 추가했습니다.
enum TDirection {
DIR_UP,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT
};
struct TFood {
int y;
int x;
};
struct TSnakeNode {
int y;
int x;
struct TSnakeNode *front;
};
struct TSnake {
int length;
struct TSnakeNode *head;
enum TDirection dir;
};
이제 생성할 함수를 선언합니다.
int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);
메인 함수
int main()
{
initscr(); /* 초기화, ncurses 모드 진입 (Initialize, enter ncurses mode) */
raw(); /* 라인 버퍼링 비활성화, 즉시 결과 확인 (Disable line buffering, see results immediately) */
noecho(); /* 터미널에 Ctrl+C 와 같은 제어 문자를 표시하지 않음 (Do not display control characters on the terminal, such as Ctrl+C) */
keypad(stdscr, TRUE); /* 터미널에서 사용자가 키보드를 사용할 수 있도록 허용 (Allow user to use keyboard in terminal) */
curs_set(0); /* 커서 가시성 설정, 0 은 보이지 않음, 1 은 보임, 2 는 완전히 보임 (Set cursor visibility, 0 is invisible, 1 is visible, 2 is completely visible) */
refresh(); /* 가상 화면의 내용을 디스플레이에 쓰고 새로 고침 (Write the contents of the virtual screen to the display and refresh) */
g_level = 1;
INITRUNLOG();
RUNLOG(" 'q' 또는 'Q'를 눌러 종료합니다.");
RUNLOG(" 'w/s/a/d' 또는 'W/S/A/D'를 눌러 뱀을 움직입니다.");
RUNLOG("정보:");
WINDOW *gwin = newgamew(); /* newgamew 라는 사용자 정의 함수로 구현된 게임 창 생성 (Create game window, implemented by a custom function called newgamew) */
struct TSnake *psnake = initsnake();
drawsnakew(gwin, psnake);
while (refreshgamew(gwin, psnake) >= 0)
;
/* getch() 는 getchar() 와 다릅니다. (getch() is different from getchar()) */
getch();
destroysnake(psnake);
delwin(gwin); /* 게임 창을 지우고 창 데이터 구조의 메모리와 정보를 해제 (Clear the game window and free the memory and information of the window data structure) */
DESTROYRUNLOG(); /* 정보 표시 창 지우기 (Clear the information display window) */
endwin(); /* ncurses 모드 종료 (Exit ncurses mode) */
return 0;
}
keypad(stdscr, TRUE)에서 stdscr은 모든 작업이 먼저 기록되는 가상 창을 나타내며, refresh 함수를 사용하여 stdscr의 내용이 화면에 표시됩니다.
printw를 사용할 때 데이터는 실제로 stdscr이라는 가상 창에 인쇄되며, 이는 화면에 직접 출력되지 않습니다. printw() 함수의 목적은 일부 표시 마커 및 관련 데이터 구조를 가상 디스플레이에 지속적으로 쓰고 이러한 데이터를 stdscr의 버퍼에 쓰는 것입니다. 따라서 이 버퍼의 데이터를 표시하려면 refresh() 함수를 사용하여 curses 시스템에 버퍼의 내용을 화면에 출력하도록 지시해야 합니다. 이 메커니즘을 통해 프로그래머는 가상 화면에 데이터를 지속적으로 쓰고 refresh()가 호출될 때 한 번에 모두 완료된 것처럼 보이게 할 수 있습니다. refresh() 함수는 변경된 창 및 데이터 부분만 확인하기 때문에 이 유연한 디자인은 효율적인 피드백 메커니즘을 제공합니다.
윈도우 메커니즘
창 메커니즘은 CURSES 의 핵심 개념입니다. 이전 예제에서 보았듯이 모든 함수는 기본적으로 "창" (stdscr) 에서 작동합니다. 가장 간단한 그래픽 사용자 인터페이스 (GUI) 를 설계하더라도 여전히 창을 사용해야 합니다. 창을 사용하는 주요 이유 중 하나는 화면을 여러 부분으로 나누어 동시에 작업할 수 있다는 것입니다. 이는 효율성을 향상시킬 수 있습니다. 또 다른 이유는 프로그램에서 항상 더 좋고 관리하기 쉬운 설계를 위해 노력해야 하기 때문입니다. 크고 복잡한 사용자 인터페이스를 설계하는 경우 이러한 부분을 미리 설계하면 효율성이 향상됩니다.
WINDOW* newlogw()
{
/* 매개변수는 다음과 같습니다: 높이, 너비, 창의 시작 위치 (y,x) (Parameters are: height, width, starting position (y,x) of the window) */
WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);
/* 매개변수는 다음과 같습니다: 알려진 창 포인터, 0 과 0 은 문자의 기본 행 및 열 시작 위치입니다. (Parameters are: known window pointer, 0 and 0 are the default row and column starting positions of the characters) */
box(win, 0, 0);
mvwprintw(win, 0, 2, " LOG ");
wrefresh(win); // 지정된 창 새로 고침 (Refresh the specified window)
return win;
}
WINDOW* newlogw() 함수에서 창 생성은 newwin()으로 시작하는 것을 볼 수 있습니다. 창을 만들었지만 아직 보이지 않습니다. 이는 HTML 의 <div> 요소와 유사합니다. <div> 요소에 스타일을 추가하지 않으면 웹 페이지에서 아무것도 볼 수 없습니다. 따라서 box 함수를 사용하여 알려진 창에 테두리를 추가해야 합니다.
mvwprintw는 지정된 창 내의 지정된 좌표 (y,x) 에 지정된 내용을 인쇄합니다.
게임 정보 표시
RUNLOG는 사용자 정의 함수 runlog를 호출하는 매크로입니다.
void runlog(WINDOW *win, char *str)
{
static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
static int index = 0;
strcpy(logbuf[index], str);
int i = 0;
/* #define LOGBUF_NUM (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
for (; i < LOGBUF_NUM; ++i) {
/* 사용자 정의 함수, cleanline (Custom function, cleanline) */
cleanline(win, i+1, 1);
/* 각 줄에 문자열 인쇄 (Print string on each line) */
mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
wrefresh(win);
}
index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}
runlog는 알려진 창과 표시할 정보를 매개변수로 사용합니다. str은 strcpy 함수를 사용하여 2 차원 배열 (logbuf) 에 저장됩니다. 그런 다음, 다음에 인쇄될 줄을 지우기 위해 사용자 정의 함수 cleanline이 호출됩니다. mvwprintw 함수는 정보를 인쇄하는 데 사용됩니다.
/* 창 win 의 좌표 (x,y) 를 지웁니다. (Clear the coordinates (x,y) of the window win) */
void cleanline(WINDOW *win, int y, int x)
{
char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57
/* 배열의 위치 0-56 을 빈 문자로 설정 (Set positions 0-56 of the array to empty characters) */
memset(EMPTYLINE, ' ', LOGBUF_LEN-1);
/* 창 win 에서 커서를 위치 (y,x) 로 이동하고 문자열 EMPTYLINE 을 인쇄합니다. (Move the cursor to position (y,x) in the window win and print the string EMPTYLINE) */
mvwprintw(win, y, x, EMPTYLINE);
/* 지정된 창에 내용 표시 (Display content on the specified window) */
wrefresh(win);
}
스네이크 큐 초기화
이제 뱀의 데이터 구조를 정의하고, 이를 초기화하고 게임 창에 그리는 함수를 만들 것입니다.
struct TSnake* initsnake()
{
struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));
psnake->dir = DIR_LEFT;
psnake->length = 4; // 뱀의 길이를 4 로 초기화 (Initialize the length of the snake to 4)
newsnakenode (
&newsnakenode (
&newsnakenode (
&newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
)->front, 4, 52
)->front, 4, 51
)->front = psnake->head;
return psnake;
}
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
*ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
(*ppsnode)->y = y;
(*ppsnode)->x = x;
(*ppsnode)->front = NULL;
return *ppsnode;
}
이 두 함수의 목적은 뱀 게임에서 뱀의 데이터 구조를 초기화하는 것입니다. 여기에는 뱀의 초기 방향과 길이를 설정하는 것뿐만 아니라 초기 몸통 노드를 가진 뱀을 생성하는 것도 포함됩니다. newsnakenode 함수는 개별 노드를 생성하는 데 사용되며, 이러한 노드는 중첩된 함수 호출을 통해 연결되어 초기 뱀을 형성합니다.
게임 창에 스네이크 표시
void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
static int taily = 0;
static int tailx = 0;
if (taily != 0 && tailx != 0) {
mvwaddch(win, taily, tailx, ' ');
}
/* #define GetSnakeTail(s) ((s)->head->front) */
struct TSnakeNode *psnode = GetSnakeTail(psnake);
int i = 0;
for (; i < psnake->length; ++i) {
mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
psnode = psnode->front;
}
taily = GetSnakeTail(psnake)->y;
tailx = GetSnakeTail(psnake)->x;
wrefresh(win);
}
각 노드의 (y, x) 좌표를 표시하기 위해 매크로 정의 GetSnakeTail을 사용하여 좌표를 가져올 수 있습니다. 나머지 단계는 mvwaddch를 사용하여 루프를 통해 표시하는 것입니다. mvwaddch는 지정된 창 (win) 에서 커서를 지정된 위치 (taily, tailx)로 이동한 다음 문자를 출력하는 데 사용됩니다.
실제로 화면에 표시하려면 wrefresh(win)을 사용해야 합니다.
게임 핵심 로직 구현
while (refreshgamew(gwin, psnake) >= 0); 뒤의 세미콜론을 살펴보십시오. 이것은 뱀의 움직임과 길이 변경, 경계 감지 및 기타 문제를 처리하는 루프를 설정합니다.
while 루프는 refreshgamew 함수를 호출합니다.
int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
static TBool ffood = False;
struct TFood pfood;
/* When starting the game or when food is eaten, ffood=False, and drawfoodw is executed to redraw the food */
if (!ffood) {
drawfoodw(win, &pfood, psnake);
ffood = True;
}
int key = -1;
fd_set set;
FD_ZERO(&set);
FD_SET(0, &set);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;
// Next, we will use the select kernel API, which will listen for a series of keyboard and mouse inputs. We use `key=getch()` and `switch` to determine the input type. The second `switch` is used to display the level and the current movement speed of the snake.
if (select(1, &set, NULL, NULL, &timeout) < 0)
return -1;
if (FD_ISSET(0, &set)) {
while ((key = getch()) == -1);
switch (key) {
case 'w':
case 'W':
(psnake->dir == DIR_DOWN) ? : (psnake->dir = DIR_UP);
break;
case 's':
case 'S':
(psnake->dir == DIR_UP) ? : (psnake->dir = DIR_DOWN);
break;
case 'a':
case 'A':
(psnake->dir == DIR_RIGHT) ? : (psnake->dir = DIR_LEFT);
break;
case 'd':
case 'D':
(psnake->dir == DIR_LEFT) ? : (psnake->dir = DIR_RIGHT);
break;
case 'q':
case 'Q':
RUNLOG("Quit Game!");
gameover(win, "Quit Game!");
return -1;
default:
break;
}
}
movesnake(psnake);
drawsnakew(win, psnake);
switch (checksnake(&pfood, psnake)) {
case 0:
break;
// Food has been eaten, set ffood to 0 to redraw the food.
case 1:
ffood = False;
if (++g_level > MAXLEVEL) {
RUNLOG("Win!!!");
gameover(win, "Win!!!");
return -1;
}
mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
wrefresh(win);
RUNLOG("Level UP!");
snakegrowup(&pfood, psnake);
break;
default:
RUNLOG("Game over!");
gameover(win, "Game over!");
return -1;
}
return 1;
}
탐욕스러운 뱀의 움직임 구현 (Greedy Snake)
탐욕스러운 뱀의 움직임은 movesnake 함수를 사용하여 구현됩니다. 나머지 부분은 유사하므로 case DIR_UP 세그먼트를 분석해 보겠습니다.
/* The structure TSnake is an inverted linked list with the head and tail connected.
* Example: [a]<-[b]<-[c]<-[d] a is the head
* | ^ When the snake moves, only the head points to d,
* `--------------' and the (y,x) of d is modified to the position where the head moves to. */
void movesnake(struct TSnake *psnake)
{
int hy = psnake->head->y;
int hx = psnake->head->x;
psnake->head = GetSnakeTail(psnake);
switch (psnake->dir) {
case DIR_UP:
psnake->head->y = hy - 1;
psnake->head->x = hx;
break;
case DIR_DOWN:
psnake->head->y = hy + 1;
psnake->head->x = hx;
break;
case DIR_LEFT:
psnake->head->y = hy;
psnake->head->x = hx - 1;
break;
case DIR_RIGHT:
psnake->head->y = hy;
psnake->head->x = hx + 1;
break;
default:
break;
}
}
좌표 원점이 왼쪽 상단 모서리에 위치하고 (y, x) 값이 아래쪽과 오른쪽으로 증가하므로 좌표계는 x 축을 기준으로 180 도 뒤집힙니다. 따라서 뱀을 위로 이동시키려면 1 을 뺍니다. 따라서 플레이어가 w/W 키를 누르면 뱀이 위로 이동합니다.
뱀 몸통 늘리기 구현 (Greedy Snake)
뱀 몸체의 길이를 늘리려면 비교적 간단한 snakegrowup 함수를 구현하여 이를 달성할 수 있습니다. 단순히 큐에 새 노드를 추가하면 됩니다.
void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
// Allocate memory for a new snake node (struct TSnakeNode) and assign the pointer pnode to the new node. This new node will become the new head of the snake.
struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
switch (psnake->dir) {
case DIR_UP:
pnode->y = psnake->head->y - 1;
pnode->x = psnake->head->x;
break;
case DIR_DOWN:
pnode->y = psnake->head->y + 1;
pnode->x = psnake->head->x;
break;
case DIR_LEFT:
pnode->y = psnake->head->y;
pnode->x = psnake->head->x - 1;
break;
case DIR_RIGHT:
pnode->y = psnake->head->y;
pnode->x = psnake->head->x + 1;
break;
default:
break;
}
// Set the front pointer of the new node (pnode) to the current snake tail in order to establish a connection between the new head and tail.
pnode->front = GetSnakeTail(psnake);
// Set the front pointer of the current snake head to the new node to ensure that the new node becomes the new head.
psnake->head->front = pnode;
psnake->head = pnode;
// Increase the length of the snake to indicate that the snake body has extended by one unit.
++psnake->length;
}
switch 문은 뱀의 현재 이동 방향 (psnake->dir)을 기반으로 새 머리 노드의 좌표를 결정합니다. 다른 방향에 따라 새 노드의 좌표를 업데이트하여 현재 뱀 머리 (psnake->head)에서 한 셀 위, 아래, 왼쪽 또는 오른쪽으로 이동합니다.
이 코드의 목적은 뱀 게임에서 뱀 몸체의 길이를 늘리는 것입니다. 뱀의 현재 이동 방향을 기반으로 새 머리 노드를 생성하고 이 새 머리 노드를 뱀의 앞에 삽입하여 뱀의 몸체를 더 길게 만듭니다. 이것은 뱀 게임에서 뱀이 음식을 섭취한 후 뱀 몸체의 길이를 늘리는 핵심 로직입니다.
음식 생성 위치 구현 (Greedy Snake)
drawfoodw 함수는 게임 창 (win) 에 음식을 그리는 데 사용됩니다.
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
do {
pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
} while (False == checkfood(pfood, psnake));
checkfood(pfood, psnake);
mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
wrefresh(win);
}
random()함수는 임의의 좌표 값을 생성하는 데 사용되며, 이를 통해 음식은 게임 창 내의 임의의 위치에 나타납니다.do-while루프를 사용하여 생성된 음식 위치가 뱀의 몸체 노드와 겹치는지 확인합니다. 겹치는 부분이 있으면 뱀의 몸체와 겹치지 않는 위치를 찾을 때까지 새 음식 위치를 생성합니다.mvwaddch함수는 게임 창의 지정된 위치에 음식의 모양을 그리는 데 사용됩니다.
checkfood 함수는 음식이 뱀 위에 나타나지 않도록 확인하는 데 사용됩니다.
TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
struct TSnakeNode *pnode = GetSnakeTail(psnake);
int i = 0;
for (; i < psnake->length; ++i, pnode = pnode->front)
if (pfood->y == pnode->y && pfood->x == pnode->x)
return False;
return True;
}
- 먼저 뱀의 꼬리 노드를 가져옵니다. 그런 다음 루프를 사용하여 뱀의 모든 몸체 노드를 반복하고 음식의 좌표가 노드의 좌표와 일치하는지 확인합니다.
- 음식의 좌표가 뱀 노드의 좌표와 일치하면 음식의 위치가 뱀의 몸체 노드와 겹치는 것을 의미하며, 함수는
False를 반환합니다. - 겹치는 부분이 없으면 함수는
True를 반환하여 음식 위치가 유효함을 나타냅니다.
경계 감지 구현 (Greedy Snake)
checksnake는 뱀이 게임 경계와 충돌했는지 확인하는 데 사용됩니다. 여기에는 게임의 상단, 하단, 왼쪽 및 오른쪽 측면의 경계에 대한 감지가 포함됩니다. 뱀 자신의 몸체와의 충돌도 확인하며, 이 두 상황 모두 게임 종료로 이어집니다.
int checksnake(struct TFood *pfood, struct TSnake *psnake)
{
/* Check if the coordinates of the snake's head have collided with the game borders on the top, bottom, left, or right */
if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
|| psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
{
return -1;
}
struct TSnakeNode *pnode = GetSnakeTail(psnake);
int i = 0;
/* Check that the snake's head does not collide with any part of its body */
for (; i < psnake->length - 1; ++i, pnode = pnode->front)
if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
return -1;
/* Of course, colliding with food is allowed */
if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
return 1;
return 0; // No collision occurred
}
게임 오버 구현 (Greedy Snake)
gameover 함수는 뱀 게임이 종료될 때 지정된 창 (win) 에 게임 오버 메시지를 표시하는 데 사용됩니다.
void gameover(WINDOW *win, char *str)
{
mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Press any key to quit...");
wrefresh(win);
}
mvwprintw함수를 사용하여 게임 오버 메시지 텍스트str을 창의 중앙에 인쇄합니다. 이렇게 하면 게임 오버 메시지가 창의 중앙에 표시됩니다.- 다음으로, "Press any key to quit..."을 창의 다음 줄 중앙에 인쇄하여 플레이어에게 아무 키나 눌러 게임을 종료하도록 안내합니다.
- 마지막으로,
wrefresh함수를 사용하여 창을 새로 고치고 게임 오버 메시지와 종료 프롬프트가 화면에 올바르게 그려지도록 합니다.
메모리 누수를 방지하기 위해 destroysnake 함수를 사용하여 뱀 게임에 의해 점유된 메모리를 해제합니다. 여기에는 뱀 노드와 뱀 구조체 자체가 포함됩니다. 이는 게임이 종료되거나 다시 시작될 때 일반적인 정리 단계입니다.
void destroysnake(struct TSnake *psnake)
{
struct TSnakeNode *psnode = GetSnakeTail(psnake);
struct TSnakeNode *ptmp = NULL;
int i = 0;
for (; i < psnake->length; ++i) {
ptmp = psnode;
psnode = psnode->front;
free(ptmp);
}
free(psnake);
psnake = NULL;
}
함수 내부에서는 먼저 뱀 노드 연결 리스트를 반복하기 위해 psnode와 ptmp라는 두 개의 포인터를 선언합니다.
뱀의 길이 (노드 수) 와 같은 횟수만큼 반복하는 루프에 들어갑니다. 각 반복에서 다음 작업을 수행합니다.
- 현재 노드
psnode를 가리키도록ptmp를 설정하여 나중에 현재 노드의 메모리를 해제합니다. psnode를 다음 노드 (front가 가리키는 노드) 로 이동합니다.
루프가 끝나면 모든 뱀 노드가 해제됩니다.
마지막으로, 함수는 뱀 구조체 자체의 메모리를 해제하고 (free(psnake)) 뱀 구조체를 가리키는 포인터 psnake를 NULL로 설정하여 해제된 메모리가 더 이상 사용되지 않도록 합니다.
컴파일 및 실행 (Ncurses 뱀 게임)
컴파일 명령은 일반적인 것과 약간 다릅니다. ncurses 라이브러리를 포함하기 위해 gcc 에 -l 옵션을 추가해야 합니다.
cd ~/project
gcc -o snake snake.c -l ncurses
./snake

요약
ncurses 라이브러리를 사용하여 C 언어로 간단한 뱀 게임을 성공적으로 만들었습니다. 이 게임에는 게임 창, 뱀 이동, 음식 생성 및 충돌 감지가 포함되어 있습니다. 위의 단계를 따르면 터미널에서 게임을 실행하고 즐길 수 있습니다. 즐겁게 플레이하세요!



