[Hello, Habr!] Змейка в консоли. Разбираемся с с make и gcc

от автора

Всем привет! Это моя первая публикация на Хабре и я решил посвятить её тому, как я писал змейку в консоли (да коряво, но всё же).

Итак, зачем я её вообще затеял? Я просто хотел разобраться как работать с 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, а во всём интернете. Буду рад конструктивной критике. Всем до новых встреч!


ссылка на оригинал статьи https://habr.com/ru/articles/890240/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *