Всем привет! Это моя первая публикация на Хабре и я решил посвятить её тому, как я писал змейку в консоли (да коряво, но всё же).
Итак, зачем я её вообще затеял? Я просто хотел разобраться как работать с 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!rn"
"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!rn");
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!rn Your score: %d", Score);
GetConsoleCursorInfo(hConsole, &cInfo);
cInfo.bVisible = FALSE;
SetConsoleCursorInfo(hConsole, &cInfo);
getch();
Прошу сильно не бить! Это моя первая статья не то, что на Habr, а во всём интернете. Буду рад конструктивной критике. Всем до новых встреч!
Автор: Procher