Создание игры "Змейка" на языке C

CCIntermediate
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом проекте вы научитесь создавать простую игру "Змейка" на языке C с использованием библиотеки ncurses. В этой классической игре вам нужно управлять змейкой, заставляя ее есть еду, увеличивать длину и избегать столкновений со стенами и самой собой. Функциональность игры разделена на несколько ключевых компонентов: инициализация, игровой цикл, движение змейки, обнаружение столкновений и так далее. По завершении этого проекта у вас будет простая игра "Змейка", которую можно запустить в терминале.

👀 Предпросмотр

Игра "Змейка"

🎯 Задачи

В этом проекте вы научитесь:

  • Реализовывать игровой цикл для обновления положения змейки и обработки пользовательского ввода.
  • Создавать функции для инициализации игры, рисования игрового окна и вывода сообщений о конце игры.
  • Реализовывать обнаружение столкновений для проверки столкновений со стенами, телом змейки и едой.
  • Разрабатывать такие функции, как увеличение длины змейки при поедании еды.

🏆 Достижения

После завершения этого проекта вы сможете:

  • Использовать библиотеку ncurses на языке C для создания игры, работающей в терминале.
  • Реализовывать игровую логику, включая обновление состояния игры и обработку пользовательского ввода.
  • Создавать и манипулировать структурами данных для представления игровых объектов, таких как змейка и еда.
  • Реализовывать обнаружение столкновений для обеспечения правил игры и определения момента завершения игры.

Основные знания

В эпоху широкого распространения телетайпных машин они выступали в качестве выходных терминалов, подключенных к центральным компьютерам по кабелям. Пользователям приходилось отправлять ряд специальных управляющих команд в программу терминала, чтобы управлять выводом на экране терминала. Например, изменять положение курсора на экране, очищать содержимое определенной области на экране, прокручивать экран, переключать режимы отображения, подчеркивать текст, изменять внешний вид, цвет, яркость и т. д. символов. Эти управляющие действия реализуются с помощью строки, называемой последовательностью escape-символов. Они так называются, потому что эти последовательные байты начинаются с символа 0x1B, который является escape-символом (символ, вводимый при нажатии клавиши ESC). Даже сейчас можно имитировать эффект вывода телетайпных терминалов той эпохи, вводя последовательности escape-символов в программы эмуляции терминала. Если вы хотите отобразить текст на терминале (или программе эмуляции терминала) с цветным фоном, вы можете ввести следующую последовательность escape-символов в командную строку:

echo "^[[0;31;40mIn Color"

Здесь ^ и [ - это так называемые escape-символы. (Примечание: в данном случае ^[ - это один символ. Он не вводится последовательным нажатием символов ^ и [. Чтобы напечатать этот символ, сначала нужно нажать Ctrl+V, а затем клавишу ESC.) После выполнения вышеуказанной команды вы должны увидеть, что фон текста In Color изменился на красный. С этого момента весь отображаемый текст будет выводиться с этим эффектом. Если вы хотите прекратить этот эффект и вернуться к исходному формату, можно использовать следующую команду:

echo "^[[0;37;40m"

Теперь вы знаете, для чего нужны эти символы (последовательности escape-символов)? (Попробуйте изменить параметры между точками с запятой и посмотрите, какой результат получится.) Возможно, он будет отличаться от того, что вы представляли? Это может быть связано с различиями в терминальных средах, которые зависят от различных терминалов или операционных систем. (Вы не можете заставить монохромный терминал отображать цветные символы, верно?) Чтобы избежать таких проблем совместимости и обеспечить единообразный вывод на разных терминалах, разработчики UNIX придумали механизм, называемый termcap. termcap - это на самом деле файл, который распространяется вместе с последовательностями escape-символов. В этом файле перечислены все последовательности escape-символов, которые текущий терминал может корректно выполнить, обеспечивая, чтобы результат выполнения введенных последовательностей escape-символов соответствовал спецификациям этого файла. Однако в последующие годы после изобретения этого механизма другой механизм, называемый terminfo, постепенно начал заменять termcap. С тех пор пользователям больше не нужно было изучать сложные спецификации последовательностей escape-символов в termcap при программировании, а нужно было только обращаться к базе данных terminfo для управления экранным выводом.

Предположим, что все приложения обращаются к базе данных terminfo для управления выводом (например, отправки управляющих символов и т. д.) в ситуации, когда используется terminfo. Вскоре такие вызовы кода сделают весь программу сложной для контроля и управления. Появление этих проблем привело к появлению CURSES. Название CURSES происходит от игры слов cursor optimization (оптимизация курсора).

Библиотека CURSES предоставляет пользователям гибкий и эффективный API (прикладной программный интерфейс), инкапсулируя низкоуровневые управляющие коды (последовательности escape-символов) терминала. Это позволяет пользователям управлять курсором, создавать окна, изменять цвета переднего и заднего плана и обрабатывать операции с мышью. Таким образом, пользователи могут обойти эти утомительные низкоуровневые механизмы при написании приложений для текстовых терминалов.

NCURSES - это клон CURSES из System V Release 4.0 (SVr4). Это свободно настраиваемая библиотека, полностью совместимая со старыми версиями 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       '@'  // Еда
#define SHAPE_SNAKE      '#'  // Тело змейки
#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; // Объявление окна лога
#define INITRUNLOG()     logwin = newlogw() // Создание окна лога с помощью вызова пользовательской функции newlogw()
#define RUNLOG(str)      runlog(logwin, str) // Запуск окна лога для отображения игровых подсказок
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // Уровень игрока, глобальная переменная

Мы также добавили некоторые определения структур и перечислений:

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);
✨ Проверить решение и практиковаться

Функция main

int main()
{
    initscr();  /* Инициализация, вход в режим ncurses */
    raw();      /* Отключение буферизации по строкам, мгновенное отображение результатов */
    noecho();   /* Не отображать управляющие символы на терминале, например, Ctrl+C */
    keypad(stdscr, TRUE);   /* Разрешить пользователю использовать клавиатуру в терминале */
    curs_set(0);    /* Установка видимости курсора, 0 - невидимый, 1 - видимый, 2 - полностью видимый */
    refresh();      /* Запись содержимого виртуального экрана на дисплей и обновление */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Press 'q' or 'Q' to quit.");
    RUNLOG("  Press 'w/s/a/d' or 'W/S/A/D' to move the snake.");
    RUNLOG("Info:");

    WINDOW *gwin = newgamew(); /* Создание игрового окна, реализовано пользовательской функцией newgamew */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

    while (refreshgamew(gwin, psnake) >= 0)
        ;

    /* getch() отличается от getchar() */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* Очистка игрового окна и освобождение памяти и информации структуры данных окна */
    DESTROYRUNLOG(); /* Очистка окна отображения информации */
    endwin();        /* Выход из режима ncurses */

    return 0;
}

В keypad(stdscr, TRUE) stdscr относится к виртуальному окну, куда сначала записываются все наши операции, а затем содержимое stdscr отображается на экране с помощью функции refresh.

Когда мы используем printw, данные фактически печатаются на виртуальном окне, называемом stdscr, которое не выводится напрямую на экран. Цель функции printw() - непрерывно записывать некоторые маркеры отображения и связанные структуры данных на виртуальном дисплее и записывать эти данные в буфер stdscr. Поэтому, чтобы отобразить данные в этом буфере, мы должны использовать функцию refresh(), чтобы сообщить системе curses о необходимости вывести содержимое буфера на экран. Этот механизм позволяет программисту непрерывно записывать данные на виртуальный экран и сделать это выглядеть, как будто все происходит сразу при вызове refresh(). Поскольку функция refresh() проверяет только те части окна и данных, которые изменились, этот гибкий дизайн обеспечивает эффективный механизм обратной связи.

✨ Проверить решение и практиковаться

Механизм окон

Механизм окон является центральным концептом CURSES. Как вы видели из предыдущих примеров, все функции по умолчанию работают с "окном" (stdscr). Даже если вы разрабатываете простейший графический пользовательский интерфейс (GUI), вам все равно придется использовать окна. Одна из главных причин использования окон заключается в том, что вы можете разделить экран на разные части и одновременно выполнять операции в них. Это может повысить эффективность. Другой причиной является стремление к более хорошему и управляемому дизайну программы. Если вы разрабатываете большой и сложный пользовательский интерфейс, предварительный дизайн этих частей повысит вашу эффективность.

WINDOW* newlogw()
{
    /* Параметры: высота, ширина, начальная позиция (y,x) окна */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* Параметры: известный указатель на окно, 0 и 0 - это начальные позиции строки и столбца символов по умолчанию */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // Обновить указанное окно

    return win;
}

В функции WINDOW* newlogw() вы можете видеть, что создание окна начинается с newwin(). Хотя мы создали окно, оно еще не видно, что аналогично элементу <div> в HTML. Если вы не добавите стили к элементу <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 */
        cleanline(win, i+1, 1);

        /* Вывод строки на каждой строке */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

    index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}

runlog принимает в качестве параметров известное окно и информацию, которую нужно отобразить. Строка str будет сохранена в двумерном массиве (logbuf) с помощью функции strcpy. Затем вызывается пользовательская функция cleanline для очистки строки, которая будет напечатана следующей. Функция mvwprintw используется для вывода информации.

/* Очистка координат (x,y) окна win */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* Установка элементов массива с 0 по 56 в пустые символы */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* Перемещение курсора в позицию (y,x) в окне win и вывод строки EMPTYLINE */
    mvwprintw(win, y, x, EMPTYLINE);

    /* Отображение содержимого на указанном окне */
    wrefresh(win);
}
✨ Проверить решение и практиковаться

Инициализация очереди змейки

Теперь вы определите структуру данных змейки и создадите функции для ее инициализации и отображения на игровом окне.

struct TSnake* initsnake()
{
    struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // Инициализация длины змейки равной 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 используется для перемещения курсора в указанную позицию (taily,tailx) в указанном окне (win) и последующего вывода символа.

Для фактического отображения на экране нам нужно использовать wrefresh(win).

✨ Проверить решение и практиковаться

Реализация ядра игры

Обратите внимание на точку с запятой после while (refreshgamew(gwin, psnake) >= 0);. Это устанавливает цикл для обработки движения змейки, изменения ее длины, а также обнаружения границ и других проблем.

Цикл while вызывает функцию refreshgamew:

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* При запуске игры или когда еда съедена, ffood=False, и выполняется drawfoodw для перерисовки еды */
    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;

    // Далее мы будем использовать системный вызов select, который будет слушать серию вводов с клавиатуры и мыши. Мы используем `key=getch()` и `switch` для определения типа ввода. Второй `switch` используется для отображения уровня и текущей скорости движения змейки.
    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;

    // Еда съедена, установить ffood в 0 для перерисовки еды.
    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;
}
✨ Проверить решение и практиковаться

Движение змейки

Движение змейки реализовано с помощью функции movesnake. Мы проанализируем сегмент case DIR_UP, так как остальные аналогичны:

/* Структура TSnake представляет собой обратный связный список с соединенными головой и хвостом.
 * Пример: [a]<-[b]<-[c]<-[d]    a - голова
 *          |              ^     Когда змейка двигается, только голова указывает на d,
 *          `--------------'     и координаты (y,x) d изменяются на позицию, куда движется голова. */
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) увеличиваются вниз и вправо, система координат перевернута на 180 градусов относительно оси x. Поэтому, чтобы сделать движение змейки вверх, мы вычитаем 1. Таким образом, когда игрок нажимает клавишу w/W, змейка будет двигаться вверх.

✨ Проверить решение и практиковаться

Увеличение длины тела змейки

Для увеличения длины тела змейки можно реализовать функцию snakegrowup, которая является относительно простой. Достаточно добавить новый узел в очередь.

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // Выделить память для нового узла змейки (struct TSnakeNode) и присвоить указатель pnode новому узлу. Этот новый узел станет новой головой змейки.
    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;
    }

    // Установить указатель front нового узла (pnode) на текущий хвост змейки, чтобы установить связь между новой головой и хвостом.
    pnode->front = GetSnakeTail(psnake);

    // Установить указатель front текущей головы змейки на новый узел, чтобы убедиться, что новый узел станет новой головой.
    psnake->head->front = pnode;
    psnake->head = pnode;

    // Увеличить длину змейки, чтобы показать, что тело змейки увеличилось на одну единицу.
    ++psnake->length;
}

Оператор switch определяет координаты нового головного узла на основе текущего направления движения змейки (psnake->dir). В зависимости от разных направлений он обновляет координаты нового узла, чтобы переместить его на одну ячейку вверх, вниз, влево или вправо от текущей головы змейки (psnake->head).

Цель этого кода - увеличить длину тела змейки в игре "змейка". Он создает новый головной узел на основе текущего направления движения змейки и вставляет этот новый головной узел в начало змейки, делая ее тело длиннее. Это является основой логики увеличения длины тела змейки после того, как она съедает еду в игре "змейка".

✨ Проверить решение и практиковаться

Расположение появления еды

Функция 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, что означает, что позиция еды допустима.
✨ Проверить решение и практиковаться

Проверка на столкновение с границами

Функция checksnake используется для проверки, столкнулась ли змейка с границами игрового поля. Это включает в себя проверку столкновений с верхней, нижней, левой и правой границами игрового поля. Также проверяется столкновение змейки с самой собой, так как в обоих случаях игра заканчивается.

int checksnake(struct TFood *pfood, struct TSnake *psnake)
{

	/* Проверка, столкнулись ли координаты головы змейки с верхней, нижней, левой или правой границами игрового поля */
    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;

	/* Проверка, чтобы голова змейки не столкнулась с какой-либо частью ее тела */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

  	/* Конечно, столкновение с едой разрешено */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // Столкновения не произошло
}
✨ Проверить решение и практиковаться

Конец игры

Функция 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, для обхода связного списка узлов змейки.

Входится в цикл, который выполняется столько раз, сколько элементов (узлов) в змейке. На каждой итерации выполняются следующие действия:

  • Устанавливается ptmp на текущий узел psnode, чтобы позже освободить память текущего узла.
  • Перемещается psnode на следующий узел (на который указывает front).

После завершения цикла все узлы змейки были освобождены.

Наконец, функция освобождает память самой структуры змейки, free(psnake), и устанавливает указатель psnake, который указывал на структуру змейки, в NULL, чтобы гарантировать, что освобожденная память больше не используется.

✨ Проверить решение и практиковаться

Компиляция и запуск

Команда компиляции немного отличается от обычной. Необходимо добавить опцию -l в gcc для включения библиотеки ncurses:

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Игра "Змейка"
✨ Проверить решение и практиковаться

Итог

Вы успешно создали простую игру "змейка" на языке C с использованием библиотеки ncurses. Игра включает в себя игровое окно, движение змейки, генерацию еды и обнаружение столкновений. Следуя приведенным выше шагам, вы можете запустить и насладиться игрой в терминале. Приятной игры!