Как реализовать алгоритм отращивания хвоста и смерти змеи, когда она сама же его пересечёт?

Пишу игру "Змейка" на СИ при помощи opengl glfw и glad. На данный момент реализована часть проекта, отвечающая за управление и движение головы змеи. Вот только не совсем понимаю, как должен реализовываться алгоритм отращивания хвоста и смерти змеи, когда она сама же его пересечёт.

#include <GLFW/glfw3.h>
#include <stdlib.h>

float squareX = 0; // Начальная позиция квадрата по X
float squareY = 0; // Начальная позиция квадрата по Y
float speedX = 0.0003f; // Скорость движения
float speedY = 0.0003f;
int flagX = 0;
int flagY = 1;
int flagVectorX = 1;
int flagVectorY = 1;
int x = 3, y = 2;
int x_bonus = 1, y_bonus = 1;
float random_coordinates[20] = { 0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1,-0.1,-0.2,-0.3,-0.4,-0.5,-0.6,-0.7,-0.8,-0.9,-1 };
int score = 0;

void updateSquarePositionX() {
    // Изменяем позицию квадрата
    if (flagX != 1 && flagVectorX == 1) {
        squareX += speedX;
    }
    else if (flagX != 1 && flagVectorX == -1) {
        squareX -= speedX;
    }
    // Если квадрат достиг границы, меняем направление
    if (squareX > 0.99f) {
        squareX = -1.00f;
    }
    else if (squareX < -1.0f) {
        squareX = 0.9f;
    }
}

void updateSquarePositionY() {
    // Изменяем позицию квадрата
    if (flagY != 1 && flagVectorY == 1) {
        squareY += speedY;
    }
    else if (flagY != 1 && flagVectorY == -1) {
        squareY -= speedY;
    }

    // Если квадрат достиг границы, меняем направление
    if (squareY > 0.99f) {
        squareY = -1.00f;
    }
    else if (squareY < -1.0f) {
        squareY = 0.9f;
    }
}


void draw_apple() {
    glBegin(GL_QUADS);
    glColor3f(1.0f, 0.0f, 0.0f); // Красный цвет
    glVertex2f(random_coordinates[x] + 0.03f, random_coordinates[y] + 0.03f);
    glVertex2f(random_coordinates[x] - 0.03f, random_coordinates[y] + 0.03f);
    glVertex2f(random_coordinates[x] - 0.03f, random_coordinates[y] - 0.03f);
    glVertex2f(random_coordinates[x] + 0.03f, random_coordinates[y] - 0.03f);
    glEnd();
}

void draw_bonus() {
    glBegin(GL_QUADS);
    glColor3f(1.0f, 0.0f, 1.0f); 
    glVertex2f(random_coordinates[x_bonus] + 0.03f, random_coordinates[y_bonus] + 0.03f);
    glVertex2f(random_coordinates[x_bonus] - 0.03f, random_coordinates[y_bonus] + 0.03f);
    glVertex2f(random_coordinates[x_bonus] - 0.03f, random_coordinates[y_bonus] - 0.03f);
    glVertex2f(random_coordinates[x_bonus] + 0.03f, random_coordinates[y_bonus] - 0.03f);
    glEnd();
}

void generateApplePosition() {
    x = rand() % 20; // Генерация нового x
    y = rand() % 20; // Генерация нового y
}


void generateBonusPosition() {
    x_bonus = rand() % 20; // Генерация нового x_bonus
    y_bonus = rand() % 20; // Генерация нового y_bonus
}

int main() {
    // Инициализация GLFW
    if (!glfwInit()) {
        return -1;
    }

    // Создание окна
    GLFWwindow* window = glfwCreateWindow(800, 800, "SNAKE", NULL, NULL); // Ширина и высота
    if (!window) {
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);

    // Главный цикл
    while (!glfwWindowShouldClose(window)) {
        // Очистка экрана

        glClear(GL_COLOR_BUFFER_BIT);
        draw_apple();
        if (score % 10 == 0 && score != 0) {
            draw_bonus();
        }

        // Обновляем позицию головы змеи
        if (flagX != 1) {
            updateSquarePositionX();
        }
        else if (flagY != 1) {
            updateSquarePositionY();
        }

        // Проверяем столкновение
        if (squareX + 0.03f >= (random_coordinates[x] - 0.03f) && squareX - 0.03f <= (random_coordinates[x] + 0.03f) && squareY + 0.03f >= (random_coordinates[y] - 0.03f) && squareY - 0.03f <= (random_coordinates[y] + 0.03f)) {
            score++;
            generateApplePosition();
        }

        if (squareX + 0.03f >= (random_coordinates[x_bonus] - 0.03f) && squareX - 0.03f <= (random_coordinates[x_bonus] + 0.03f) && squareY + 0.03f >= (random_coordinates[y_bonus] - 0.03f) && squareY - 0.03f <= (random_coordinates[y_bonus] + 0.03f)) {
            generateBonusPosition();
            score++;
        }

        // Рисование головы змеи
        glBegin(GL_QUADS);
        glColor3f(0.0f, 1.0f, 0.0f); // Зелёный цвет
        glVertex2f(squareX + 0.03f, squareY + 0.03f);
        glVertex2f(squareX - 0.03f, squareY + 0.03f);
        glVertex2f(squareX - 0.03f, squareY - 0.03f);
        glVertex2f(squareX + 0.03f, squareY - 0.03f);
        glEnd();

        // Обновление экрана
        glfwSwapBuffers(window);


        // Обработка событий
        glfwPollEvents();


        // Добавленный код для управления клавишами
        if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS) {
            flagX = 0;
            flagY = 1;
            flagVectorX = -1;
        }
        if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS) {
            flagX = 0;
            flagY = 1;
            flagVectorX = 1;
        }
        if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) {
            flagX = 1;
            flagY = 0;
            flagVectorY = 1;
        }
        if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) {
            flagX = 1;
            flagY = 0;
            flagVectorY = -1;
        }
    }

    // Завершение работы
    glfwTerminate();
    return 0;
}

---VERSION 2.0--- Функция отрисовки работает у меня хорошо(проверял, вводя координаты вручную). Но сама логика, которая заполняет массив координат квадратиков тела на основе координат головы, по прежнему не работает.

void updateSnakePosition() {
    // Обновляем позицию головы змеи
    if (flagX != 1 && flagVectorX == 1) {
        squareX[0] += speedX;
    }
    else if (flagX != 1 && flagVectorX == -1) {
        squareX[0] -= speedX;
    }
    if (flagY != 1 && flagVectorY == 1) {
        squareY[0] += speedY;
    }
    else if (flagY != 1 && flagVectorY == -1) {
        squareY[0] -= speedY;
    }

    // Если квадрат достиг границы, меняем направление
    if (squareX[0] > 0.99f) {
        squareX[0] = -1.00f;
    }
    else if (squareX[0] < -1.0f) {
        squareX[0] = 0.9f;
    }
    if (squareY[0] > 0.99f) {
        squareY[0] = -1.00f;
    }
    else if (squareY[0] < -1.0f) {
        squareY[0] = 0.9f;
    }

     Обновляем позицию тела змеи
    for (int i = 1; i <= 5; i++) {
        squareX[i] = squareX[i - 1]; // Текущий сегмент тела змеи занимает позицию предыдущего сегмента
        squareY[i] = squareY[i - 1];
    }
    for (int i = 5; i > 0; i--) {
        squareX[i] = squareX[i - 1];
        squareX[i] = squareY[i - 1];
    }
    squareX[0] = squareX[0];
    squareY[0] = squareY[0];
}

Ответы (1 шт):

Автор решения: rotabor

"чтобы сдвинуть змейку надо было пририсовать один квадратик перед головой и стереть один квадратик на кончике хвоста" - отсюда проистекает "алгоритм отращивания хвоста": не стираем квадратик на кончике хвоста. А коллизия определяется просто: если квадратик, куда должна переместится голова змеи, уже занят её телом, то это смерть.

Вот тут обсуждается реализация. Массив для змеи создавать не нужно. Достаточно в массиве поля числом закодировать, куда сместилась голова змеи, чтобы потом в хвосте найти следующий квадратик. Таким образом, нужен двумерный массив поля со змеёй, фруктами и препятствиями, и координаты головы и хвоста.

Расписываю алгоритм.

Начнём с консольного варианта 80х25. Используете только один массив int[80, 25] Playground. Можете по периметру нарисовать бортик (код 35 #). Раскидываете фрукты (42 *) и кирпичи (35 #). В центре c [38, 12] по [42, 12] - змея, код 4 - движение влево, символы на экране: @ (64) - голова; O (79) - всё остальное. Имеете head = (38, 12) и tail = (42, 12). Длина змеи snake = 5. Кстати, тут никаких проверок делать не нужно, просто сначала раскидываете кирпичи и фрукты, а потом (поверх) рисуете бортик и змею. Можно и без бортика.

Пусть игрок жмякнул вверх. Проверяем, что там пусто (0, для начала рассмотрим этот случай). Значит, в голову Playground[38, 12] пишем 5 - движение вверх, на экране печатаем O. Голова смещается вверх head = (38, 11), туда пишем тот же код Playground[38, 11] = 5, на экране печатаем @ (64). Хвост смещается по направлению кода, который в нём Playground[42, 12] = 4 - влево, значит хвост теперь tail = (41, 12), а в Playground[42, 12] пишем 0, печатаем пробел.

Дальше игрок двинул вправо, а там - фрукт! В голову Playground[38, 11] пишем 6 - движение вправо, печатаем О. Голова смещается вправо head = (39, 11), туда пишем тот же код Playground[39, 11] = 6 и рисуем на экране @. Хвост не смещается, так как змея прирастает +1. Инкрементируем длину змеи snake++ (стало 6).

Если там, куда двинул игрок, - борт, кирпич (35) или тело (4-7), то это конец :-(

Если игрок кнопки не нажимает, то змеюка ползёт в том же направлении. Для этого просто всегда помним последнюю нажатую кнопку.

Нужно ещё определить случай, когда игрок двинул назад (от головы туда, где тело). Тут можно либо сделать конец, либо просто проигнорировать. Чтобы проигнорировать, анализируем, что в голове. Если, к примеру, 6 (второй ход) - приползла слева, и игрок нажал влево, тогда игнорируем.

Дальше переходим к графике.

Тот же самый алгоритм! Просто не выводим на консоль, а отрисовываем в графике (спрайтиками, наверное).

Если использовать монотонный узор, то перерисовывать тело не придётся.

Если есть желание всё-таки изобразить, как она ползёт, то начинаем перерисовку с хвоста - там мы знаем, куда двинуться дальше.

→ Ссылка