Всем привет! Это моя первая публикация на Хабре и я решил посвятить её тому, как я писал змейку в консоли (да коряво, но всё же).
Итак, зачем я её вообще затеял? Я просто хотел разобраться как работать с make и gcc и для примера решил написать змейку в консоли ¯\_(ツ)_/¯
Я написал самый обыкновенный makefile, в подробности его устройства вникать не будем. Просто покажу код:
Makefile
C = gcc DEBUGER = gdb ld = gcc ld_FLAGS = -lgcc CFLAGS = -Wall -lmsvcrt SRC_D = src OBJ_D = obj BIN_D = bin SRC_C = $(wildcard $(SRC_D)/*.c) OBJ_C = $(patsubst $(SRC_D)/%.c, $(OBJ_D)/%.o, $(SRC_C)) TARGET1 = FirstGCC all : $(TARGET1) $(TARGET1): $(OBJ_C) $(ld) $(ld_FLAGS) -o $@ $< $(OBJ_C): $(SRC_C) $(C) $(CFLAGS) -c $< -o $@ DEBUG: $(TARGET1) $(DEBUGER) $(TARGET1) cls mkdirs: mkdir $(OBJ_D) mkdir $(BIN_D)
Небольшая подготовка
С чего начинается C? С функции main. В ней я сначала получил дескриптор выводного потока консоли (ой как понадобиться в дальнейшем), а так же сделал курсор в консоли невидимым:
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); if (hConsole == INVALID_HANDLE_VALUE) { printf("ERROR! Console handle is invalid!"); return 1; } CONSOLE_CURSOR_INFO cInfo; GetConsoleCursorInfo(hConsole, &cInfo); cInfo.bVisible = FALSE; SetConsoleCursorInfo(hConsole, &cInfo);
Надобно поздороваться с пользователем и дать ему время морально настроиться:
printf("Hello world!\r\n" "Press any key to continue..."); getch();
Основной цикл
Итак, игрок нажал любую кнопку и игра началась... Запускается бесконечный цикл игры, в котором: рисуется карта и, собственно, наша змейка; запрашивается действие пользователя
P.S. змейка управляется кнопками W S A D
srand(time(0)); // просто так да while (1) { DrawNCart(hConsole); snakeV = getch(); if (snakeV == 'q' || snakeV == 'Q') isGameEnd = TRUE; if (isGameEnd) break; }
DrawNCart - рисует карту и занимается логикой игрыsnakeV - сохраняется действие пользователя, которое обрабатывается в следующем кадреisGameEnd - продолжается игра или нет (false/true)
Рассмотрим саму логику игры:
DrawNCart
#define W 50 // ширина поля игры #define H 20 // высота поля игры void DrawNCart(HANDLE hConsole) { system("cls"); // очищаем экран COORD cPosition = {0, 0}; // позиция курсора (X - столбец; Y - строка) for (short i = 0; i < W; i++) // рисуем верхнюю границу { cPosition.X = i; SetConsoleCursorPosition(hConsole, cPosition); WriteConsoleA(hConsole, "#", 1, 0, 0); } for (short i = 0; i < H; i++) // рисуем боковые границы { cPosition.X = 0; cPosition.Y = i; SetConsoleCursorPosition(hConsole, cPosition); WriteConsoleA(hConsole, "#", 1, 0, 0); cPosition.X = W; cPosition.Y = i; SetConsoleCursorPosition(hConsole, cPosition); WriteConsoleA(hConsole, "#", 1, 0, 0); } cPosition.Y = H; for (short i = 0; i < W; i++) // рисуем нижнюю гр��ницу { cPosition.X = i; SetConsoleCursorPosition(hConsole, cPosition); WriteConsoleA(hConsole, "#", 1, 0, 0); } HandleSnake(hConsole); // рисуем змею SetFruct(hConsole); // раскидываем фрукты SetConsoleCursorPosition(hConsole, (COORD){1, H+1}); // выведем счёт printf("Score %d", Score); }
В этой функции мы в первую очередь очищаем экран от предыдущих кадров. Позицией вывода символов управляем с помощью SetConsoleCursorPosition, символы выводим с помощью WriteConsoleA и printf. Наша карта шириной 50 символов (за вычетом границ) и высотой 20 символов (опять же, за вычетом границ).
Нарисуем змейку? Для этого напишем функцию HandleSnake:
HandleSnake
if (Snake == 0) { Snake = (COORD *)malloc(sizeof(COORD) * snakeSize); if (!Snake) { printf("Not have free memory for game!\r\n"); exit(-1); } Snake[0] = (COORD){25, 10}; } TransformSnake(); for (int i = 0; i < snakeSize; i++) { SetConsoleCursorPosition(hConsole, Snake[i]); if (i == 0) WriteConsoleA(hConsole, "9", 1, 0, 0); else WriteConsoleA(hConsole, "8", 1, 0, 0); }
Змейка представляет собой массив позиций каждой её части, при чём первый элемент массива - голова змеи. Так, если массив пустой (змеи не существует) создаётся массив с одним элементом (только голова змеи). При запуске игры snakeSize равен 1. Голова змеи располагается ровно в центре карты. Голову змеи обозначаем циферкой 9, а тело и хвост циферками 8. TransformSnake - перемещает змейку в направлении выбранном пользователем и, в случае, если она съела яблоко, увеличивает её длину на 1.
Устройство этой функции предельно просто, хотя кода много:
пользователь нажал W - змея двигается вперёд (вверх), пользователь нажал S - змейка двигается назад (вниз) и т.д.
switch (snakeV) { case 'W': case 'w': { if (snakeLV == 'S' || snakeLV == 's') // защита от дурака { snakeV = snakeLV; // змея продолжает двигаться в прежнем направлении TransformSnake(); break; } for (int i = snakeSize - 1; i >= 0; i--) // двигает каждый элемент змейки в нужном направлении { if (i == 0) { Snake[i].Y -= 1; } else { Snake[i].Y = Snake[i - 1].Y; Snake[i].X = Snake[i - 1].X; } } snakeLV = snakeV; }break; // ... }
Змея укусила себя или забор? Игра окончена
if (Snake[0].X == W || Snake[0].X == 0) { isGameEnd = TRUE; // укусила боковые границы } if (Snake[0].Y == H || Snake[0].Y == 0) { isGameEnd = TRUE; // укусила нижнюю или верхнюю границы } for (int i = 1; i < snakeSize; i++) { if (Snake[0].X == Snake[i].X && Snake[0].Y == Snake[i].Y) isGameEnd = TRUE; // укусила себя }
Раскидаем яблоки - накормим змею!
void SetFruct(HANDLE hConsole) { if (fructPos.X == 0 && fructPos.Y == 0) // Если фрукт не существует или проглочен { while (1) { fructPos.X = RandomRange(1, W - 1); fructPos.Y = RandomRange(1, H - 1); BOOL isGoodCoord = TRUE; for (int i = 0; i < snakeSize; i++) { if (Snake[i].X == fructPos.X && Snake[i].X == fructPos.Y) isGoodCoord = FALSE; } if (isGoodCoord) break; } } SetConsoleCursorPosition(hConsole, fructPos); WriteConsoleA(hConsole, "0", 1, 0, 0); }
int RandomRange(int minN, int maxN) { return (rand() % (maxN - minN)) + minN; // "золотой стандарт" }
fructPos - переменная типа COORD. При запуске игры или при съедении обозначается как {0,0}. На этих координатах фрукт существовать не может (верхний левый угол границы поля игры)
В цикле фрукту подбираются такая позиция, чтобы он не оказался внутри змеи, а затем до тех пор, пока не будет проглочен фрукт отображается на этой позиции. Фрукт обозначен циферкой 0
Ну съела змея фрукт, что дальше? А дальше нам нужно удлинить её на один за каждый фрукт:
Узнаём съела ли змея фрукт (TransformSnake )
if (Snake[0].X == fructPos.X && Snake[0].Y == fructPos.Y) // голова змеи = позиция фрукта { fructPos = (COORD){0, 0}; // фрукт - съеден Score += 1; // счёт = счёт + 1 AddSnakeLength(); // сейчас раскрою, как её удлиню }
Ну в целом удлинить змею дело не затейливое. Достаточна гирька Понадобиться всего лишь написать функцию на 48 строк:
AddSnakeLength
void AddSnakeLength() { snakeSize++; COORD *nSnake = (COORD *)realloc(Snake, snakeSize * sizeof(COORD)); if (nSnake == NULL) { isGameEnd = TRUE; return; } Snake = nSnake; if (snakeSize == 2) { switch (snakeV) { case 'W': case 'w': { Snake[1].X = Snake[0].X; Snake[1].Y = Snake[0].Y + 1; } break; case 'S': case 's': { Snake[1].X = Snake[0].X; Snake[1].Y = Snake[0].Y - 1; } break; case 'A': case 'a': { Snake[1].X = Snake[0].X + 1; Snake[1].Y = Snake[0].Y; } break; case 'D': case 'd': { Snake[1].X = Snake[0].X - 1; Snake[1].Y = Snake[0].Y; } break; } } else if (snakeSize > 2) { unsigned int XVector = Snake[snakeSize - 3].X - Snake[snakeSize - 2].X; unsigned int YVector = Snake[snakeSize - 3].Y - Snake[snakeSize - 2].Y; Snake[snakeSize - 1] = (COORD){Snake[snakeSize - 2].X + XVector, Snake[snakeSize - 2].Y + YVector}; } }
Первым делом увеличим счётчик длины змеи snakeSize, затем переопределим массив элементов нашей змеи Snake увеличив его на 1.
В одиннадцатой строчке проверяем змею на "новорождённую". Если да, то удлиняем в необходимом направлении, если нет, то вычисляем направление хвоста змеи и удлиняем в этом направлении:
unsigned int XVector = Snake[snakeSize - 3].X - Snake[snakeSize - 2].X; // направление хвоста по X unsigned int YVector = Snake[snakeSize - 3].Y - Snake[snakeSize - 2].Y; // направление хвоста по Y Snake[snakeSize - 1] = (COORD){Snake[snakeSize - 2].X + XVector, Snake[snakeSize - 2].Y + YVector};
Ах да, чуть не забыл освободить ресурсы при выходе из игры:
// в конце игры... int main... free(Snake); system("cls"); printf("Game is over!\r\n Your score: %d", Score); GetConsoleCursorInfo(hConsole, &cInfo); cInfo.bVisible = FALSE; SetConsoleCursorInfo(hConsole, &cInfo); getch();
Прошу сильно не бить! Это моя первая статья не то, что на Habr, а во всём интернете. Буду рад конструктивной критике. Всем до новых встреч!
