Как написать игру на c змейка

Давным-давно, когда мониторы были зелёными, а 64Кб оперативы на борту считалось нормой, существовала игрушка под названием Snake. Она также была известна под названиями Змейка, Удав, Питон и даже Червяк. По прошествии времени появилось множество клонов этой игры под различные платформы: от Flash до мобильных телефонов и смартфонов. Но вот та реализация, работающая в текстовом режиме, видимо умерла вместе с теми компьютерами, для которых она была написана.

И вот, за пару свободных вечеров был написан очередной клон легендарного Snake, который я и представляю вашему вниманию: Oldschool Snake.

Как играть

Управление змейкой клавишами управления курсором. Esc — завершение игры. Для выхода из игры надо нажать Esc или клавишу N на вопрос «Once more?». Змейка не должна натыкаться на стенки и на собственный хвост. Это — смерть. Змейка не умеет ползать хвостом вперёд. Попытаться заставить её это сделать — верная смерть. Кормить змейку надо, естественно, долларами. Когда змейка ест, она растёт.

Top 10 определяется по рейтингу. Общий рейтинг складывается из суммы рейтинговых очков, полученных за каждую съеденную еду. Рейтинговые очки прямо пропорциональны длине змейки и обратно пропорциональны времени, затраченному на достижение очередной порции еды.

Окончание игры

Лицензия

GNU GPL. То есть можно свободно распространять, изучать исходный текст, вносить изменения в исходный текст, использовать в своих некоммерческих проектах.

Замечания по реализации

Игрушка очень простая. Основа игрушки была написана за пару вечеров. Правда потом, наверное, неделя ушла на отладку и, главным образом, на тестирование и допиливание. Вполне возможно, что где-то затаились недобитые баги. Отстрел разрешён.

В этот вариант программы уже заложены некоторые возможности по усовершенствованию игры. Но при этом теряется аутентичность.

Программа написана для Windows 2000 Professional (и выше). Для переноса под другие операционки необходимо переписать реализацию класса CScreen и иметь порт библиотеки conio.h .

Компилировал TDM-GCC 4.8.1 С другими компиляторами не проверял.

Вопросы, замечание, предложения, ошибки — пожалуйста в комментарии. Но, скажу сразу, не судите строго за стиль — написано было быстро и, что называется, «для себя».

Сразу же перейдем к геймдеву и будем писать маленькую игрульку нашего детства.

Для начала подключаем библиотеку OpenGL к своей среде разработки, я лично программирую в Microsoft Visual Studio 2013.

200?’200px’:»+(this.scrollHeight+5)+’px’);">
#include "stdafx.h"
#include
#include
#include
#include // подключаем все необходимые инклюды.

int N = 30, M = 20; // т.к. змейка будем ездить по квадратикам, создадим их, для нашего окна в идеале будет 30×20 квадратов
int scale = 25; // размер квадрата. Когда OpenGL будет расчерчивать поле для игры, расстояние между гранями квадрата будет 25 пикселей

int w = scale*N; // ширина поля
int h = scale*M; // его высота

int dir, num = 4; // 4 направления и начальный размер змеи.
struct < int x; int y; >s[100]; // структура змеи, X и Y координаты, массив с длинной.

class fruct // класс фруктов, тех самых, которые будет есть наша змея
<
public:
int x, y; //координаты фруктов, что и где будет находится

void New() // паблик с новыми фруктами. Он будет вызываться в начале игры и в тот момент, когда змея съест один из фруктов
<
x = rand() % N; // вычисление X координаты через рандом
y = rand() % M; // вычисление Y координаты через рандом
>

void DrawFruct() // паблик, отрисовывающий фрукты
<
glColor3f(0.0, 1.0, 1.0); // цвет фруктов. в openGL он задается от 0 до 1, а не от 0 до 256, как многие привыкли
glRectf(x*scale, y*scale, (x + 1)*scale, (y + 1)*scale); // "Закрашиваем" квадрат выбранным цветом, таким образом в нем "появляется" фрукт
>
> m[5]; // масив с фруктами, таким образом, у нас появится одновременно 5 фруктов в разных местах, а не один, как мы привыкли

void Draw() // функция, которая отрисовывает линии
<
glColor3f(1.0, 0.0, 0.0); // цвет наших линий, в данном слуае — красный
glBegin(GL_LINES); // начинаем рисовать и указываем, что это линии
for (int i = 0; i 0; —i) // движение змеи. Система остроумна и проста : блок перемешается вперед, а остальные X блоков, на X+1( 2 блок встанет на место 1, 3 на место 2 и т.д. )
<
s[i].x = s[i — 1].x; // задаем Х координату i блока координатой i — 1
s[i].y = s[i — 1].y; // то же самое делаем и с Y координатой
>
// далее у нас система направлений.
if (dir == 0) s[0].y += 1; // если направление равно 0, то первый фрагмент массива перемещается на один по Y
if (dir == 1) s[0].x -= 1; // если направление равно 1, то первый фрагмент массива перемещается на минус один по X
if (dir == 2) s[0].x += 1; // аналогиная система
if (dir == 3) s[0].y -= 1; // аналогичная система

for (int i = 0; i N) dir = 1; // Ей обратное направление. Например, если она выйдет за экран по высоте, то задаем ей направление, при котором она ползет
if (s[0].y > M) dir = 3; // вниз
if (s[0].x <
Display(); // Вызов функций
tick();
glutTimerFunc(100, timer, 0); // новый вызов таймера( 100 — промежуток времени(в милисекундах), через который он будет вызыватся, timer — вызываемый паблик)
>

Читайте также:  Как отключить порт ide в панели управления

Тем, кому не интересно читать пост, а интересно сразу увидеть код и/или историю коммитов, предлагаю пойти поглядеть на репозиторий.

Небольшой дисклеймер

Да, я в курсе, что можно добавить множество разных фич (в том числе мультиплеер и уровни с процедурно генерируемыми стенками) и много чего сделать лучше как в самой игре, так и в исходниках. Возможно, я когда-то это и сделаю. На данный момент ничего особо примечательного в коде нету, потому и stupid-snake
Но была поставлена цель сделать не максимально хорошо и качественно, а быстро и чтобы работало. Ну и ещё по возможности наглядно — идея написать пост всплыла с самого начала. Эта цель в полной мере достигнута.

Немного пояснений

C — довольно низкоуровневый язык, и уж никак не "кроссплатформенный по-умолчанию", как, например, Python. Тот факт, что я не использую ncurses, ещё больше усугубляет ситуацию.
Очень многое в игре реализовано путём функций, имеющихся лишь в POSIX-системах, а то и вообще лишь в Linux и glibc. Windows POSIX-системой не является, да и терминал там вряд ли настолько же функционален, потому собрать stupid-snake под Windows у вас, скорее всего, никак не получится. На OS X — не знаю, пусть кто-то попробует и доложит об успехах.
Может также и не на каждом дистрибутиве Linux собраться, в таком случае напишите мне — помогу и добавлю нужные флаги в мейкфайл.

Если своего линукса у вас нигде не завалялось, а испытать игру очень хочется, рекомендую использовать VirtualBox или иной софт для работы с виртуальными машинами, какой вы предпочитаете.
Если же вам интересен сам процесс и разъяснения к коду — вы совершенно ничего не теряете, читая этот пост хоть с Windows, хоть с Linux, хоть с OS/2.

Почему терминал? Почему даже без ncurses? Зачем так усложнять себе жизнь?
Во-первых потому, что мне так захотелось. Во-вторых потому, что ncurses — это очень большая, толстая и сложная либа, так что не факт, что было бы проще. Пояснять, скорее всего, стало бы лишь сложнее.

Я пойму пост, если не знаю C?
Понятия не имею. Если вы действительно не знаете ни C, ни преступно подобных ему языков (типа C++) — напишите, пожалуйста, поняли вы или нет. Мне интересно.
Пост планировался как понятный полным "чайникам", но получилось ли у меня — совсем другой вопрос.

Если я прочитаю пост, я выучу C?
Нет.

Если я прочитаю пост, я научусь писать игры?
Нет. Скорее всего.

Зачем тогда мне читать пост?
Откуда я знаю, зачем вам читать пост? Может, вам интересно смотреть, как другие люди мыслят и решают задачи. Может, вам интересно узнать, какой это он — C в консолечке юниксовой. Может, просто скучно.
Впрочем, я думаю, в моём посте можно будет отыскать некоторые интересные факты и подходы, до сих пор вам неизвестные.

Какъ собрать и запустить ваше игрище бѣсовское, сударь?
Для бояр, которые этого до сих пор никогда не делали, а гуглить лень, кратенькое пояснение:
$ git clone ‘https://gitlab.com/saxahoid/stupid-snake.git’
$ cd stupid-snake
$ make
$ ./snake

Управление?
Стрелки, выход — Ctrl+C или врубиться в стенку/себя.

А вот я вижу место, где как-то очень хитроподвывернуто сделано, нельзя ли проще?
Скорее всего, нет. Как я дальше поясню в посте, в процессе разработки вылезло несколько очень интересных особенностей работы с терминалом, и эти хитроподвывернутые костыли — единственный рабочий вариант.

Поехали

Давайте-ка сделаем игру

Что нужно, дабы сделать игру для терминала? Знать какой-нибудь язык программирования более-менее уверенно и уметь гуглить, чтобы разобраться с некоторыми особенностями этого самого терминала.
Знать кучу библиотек (ну разве только ncurses, если хочется более серьёзную игру, чем у меня, с минимумом геморроя), особенности работы вашей любимой оконной системы, да ещё заодно щедрую дозу компьютерной графики и уж тем более быть дизайнером-художником абсолютно не обязательно и даже вредно. Потому что так не интересно.

Текстовый ввод/вывод — один из самых простых и основных для любого современного ЯП. Если вы уверенно (да даже если криво сикось накось) знаете какой-нибудь, то вы знаете и как работать с текстом. Поэтому для создания игры, использующей текст для общения с пользователем, не нужно знать ничего специфичного. Поэтому же такие игры, даже если примитивные по сегодняшним меркам, становятся неплохими упражнениями для мозга.

Всякая разработка (не только игр) проходит в несколько этапов. Для простоты я исключу более творческие и сложно характеризуемые типа идеи (она уже есть) и сосредоточусь на технических (да к тому же применимых в данном случае; архитектуры, скажем, тут нету).

Дизайн

Прошу заметить, что имеется в виду не тот дизайн, которым занимается Артемий Лебедев, а технический дизайн. Оно же проектирование.

Читайте также:  Как посмотреть название модели ноутбука

На этапе дизайна структура программы обдумывается "на бумажке", без написания рабочего исполняющегося кода (шаблоны делать можно). Такой подход кажется непродуктивным, но, если хорошо им овладеть, можно избавить себя от множества "Эврика!"-моментов, когда половину всех исходников приходится переписывать из-за неверно выбранного в самом начале решения. А потом ещё разок. Затраты на такое растут с размерами проекта очень нехило, так что частенько даже несколько месяцев чистого дизайна окупаются сполна.
В идеале садиться за код разработчик должен лишь тогда, когда уже полностью представляет себе программу.

Комплексная задача (игра "Змейка") разбивается на более мелкие:
• Обновление экрана;
Очевидно, для того, чтобы игра была динамической и отвечала на пользовательский ввод, нужно периодически обновлять экран. Так как "змейка" — не походовая забава, делать это необходимо по таймеру.
На нашем ламерском уровне более чем достаточно воспользоваться командой вроде sleep (приказать программе ничего не делать 1/fps часть секунды, где fps — желаемая кадровая частота), но это неинтересно и вообще я болею перфекционизмом, потому буду использовать часы реального времени.

• Визуализация змейки;
Терминал современного Linux поддерживает юникод — вот уж где простор для фантазии! Но по моему мнению реализация юникода в самом C не то чтобы очевидная, потому (для начала, во всяком случае) решаю использовать самый стандартный из всех стандартов — ASCII. Прямое тело змеи изображать буду символами "-|", повороты — "/", направленную в разные стороны голову (распахнутую пасть) — "V ".
На том бы и остановиться, вот только неинтересно (и я болею перфекционизмом, помните?). Нужна ещё анимация. Как насчёт заставлять змейку закрывать рот каждый ход? Досточно будет заменить символ "головы" на соответствующий направлению символ "тела". Переход " — строки со 121 по 219
Эти четыре функции очень схожи между собой. Они устанавливают новое направление головы змеи и рисуют новый сегмент тела на месте головы, если определённый поворот возможен из нынешней позиции. Возвращают int, работающий в качестве булевой переменной (можно было бы обойтись char, но мне почему-то захотелось int).

Рассмотрим на примере turn_right [196]. Определяется нынешнее направление движения [201], и в зависимости от него определяются дальнейшие действия. Если змея двигалась вертикально (вверх [202] или вниз [205]), поворот вправо возможен — turn_right вернёт 1 и установит соответствующий повороту сегмент тела на поле. Если змея и так двигалась вправо или вообще получила неправильный аргумент [210], turn_right просто вернёт 1 и ничего больше не сделает (это нужно для эффекта ускорения при нажатой клавише). Если змея двигалась влево [208], поворот направо невозможен — turn_right вернёт 0.

Если держать нажатой стрелку в какую-то сторону, змейка должна двигаться в эту сторону резвее. Я реализую это поведение очень простым способом: экран обновляется (а соответственно, змейка движется раньше, чем ей положено по таймеру) при каждом успешном нажатии клавиши.

У этого подхода есть только один минус: ускорение змейки зависит от пользовательской настройки, определяющей частоту повторения символа при зажатой клавише. Игра поменять эту настройку не может (да и не должна). Но мы работаем с терминалом, лучшего способа просто нет.

process_key — строка 223
Эта функция считывает с терминала три байта [227] (именно тремя байтами кодируется нажатие клавиши-стрелки). Если эти три байта соответствуют одной из заранее определённых констант [55], вызывается функция соответствующего поворота.
Очень важно, что в данном случае переменная c инициализируется в 0: так четвёртый байт, который не считывается, но всё равно остаётся "болтаться", будет нулевым и не станет мешать. Вместо переменной int можно было использовать массив char, но сравнивать с трёхбайтными константами его намного сложнее, чем простым switch.

Возвращает функция 1 или 0 в зависимости от того, смогла ли сдвинуться змея в ответ на нажатие клавиши.

init_playfield — строка 251
Простенькая функция инициализации игрового поля. Просто забивает его всё пробелами.

init_snake — строка 259
Функция посложнее, инициализация змеи. Голова змеи устанавливается в условный центр поля и изначально смотрит вправо. Ровно в ту же позицию помещается хвост, длина змеи устанавливается в 1 и буфер длины — в 4. Таким образом, начальная длина змейки — 5.

redraw_all — строка 278
Функция отрисовки игрового поля.

Вначале курсор помещается в стартовую позицию 0, 0 (верхний левый угол) с помощью ещё одного управляющего спец-символа [279]. Далее за несколько циклов выводятся сначала верхняя стенка [281], затем боковые и само поле [286], затем нижняя стенка [295] и немного информации: длина змеи, уровень [300]. Ничего интересного здесь нет; такое писал каждый, кто когда-либо выводил матрицы на экран.

redraw_animation — строка 305
Немного более интересная функция, которая отвечает за анимацию.

С использованием, опять же, спец-символа (но в этот раз динамически сгенерированного) курсор устанавливается на координаты головы змеи [307] (поправки +2 нужны из-за того, что в змее хранятся координаты относительно игрового поля-матрицы, а нужны координаты относительно экрана). Далее вместо головы рисуется "закрытый рот" (просто сегмент тела, соответствующий направлению) [309].

Читайте также:  Как объединить два диска на windows

Затем курсор путём всё той же манипуляции со спец-символом устанавливается на позицию змеиного хвоста [317] и с хвостом происходит анимация выпрямления (при условии, что он ещё не прямой), в общем-то идентичная таковой для головы.

move_snake — строка 338
Самая сложная, интересная и важная функция. Перемещает змею в игровом поле и вообще обновляет её состояние.

Если направление змеи не менялось (поля dir и new_dir одинаковы), значит, была нажата клавиша того же направления, либо не была нажата никакая. В обеих этих случаях функции turn_ ничего не рисуют, потому необходимо нарисовать новый сегмент тела [345-350]. Не рисуют turn_* именно потому, что возможных случая тут два (нажата та же, либо не нажата никакая), и во втором случае turn_* вызваны не будут. Получается, для этого второго случая отрисовку внутри move_snake предусматривать всё равно придётся, а раз она тут уже нужна, зачем повторно в turn_*?
Если же направление поменялось, new_dir сохраняется как dir [352].

Далее определяется символ для отображения головы и обновляются её координаты ("голова сдвинулась на новое место"). Это зависит исключительно от направления головы и обрабатывается простым switch [357].

Затем новая позиция головы анализируется. Если там на данный момент стена [379], еда [392] или что угодно ещё кроме пробела (это может быть только тело змеи во всех его разнообразных вариациях) [397], выполняются соответствующие действия (game over или съедение еды). Встреча со стеной определяется как выход за рамки позволенных координат (это позволяет, например, очень легко не рисовать стены или заменить символ стены на какой-то ещё, да к тому же позволяет не занимать память зазря символами стен, никогда не меняющимися).

Наконец, после того как все возможные проблемы с новой позицией улажены (и game over не наступил), туда помещается голова змеи [407].

И теперь обрабатывается хвост [411]. Если у змеи не пустой буфер длины, она должна вырасти; в этом случае с хвостом ничего не происходит и он остаётся на старом месте [412]. Если же буфер пустой, всё намного интереснее.
Первым делом хвост заменяется пробелом [415]. Затем координаты хвоста обновляются, чтобы он занял новую позицию: следующий после старого хвоста сегмент тела [418]. Этот switch очень похож на таковой для головы [357], но ничего не рисуется.
Дальнейшая задача — определить новое направление хвоста. Сделать это можно, проанализировав сегмент на новой позиции. Если он прямой, направление хвоста остаётся прежним [437-439]. Если он "/", новое направление определяется в зависимости от старого [441]. Если он "", направление определяется аналогично, но с другими значениями [359].

Например, если хвост двигался вверх, но выглядит как "/", то дальше ему нужно двигаться вправо.

gen_food — строка 483
Функция генерации пищи. Она управляется константой FOOD_RARITY, определяющей частоту еды на поле как "на FOOD_RARITY доступного пространства должен приходиться 1 юнит пищи". Соответственно, необходимое кол-во еды определяется простым делением [487]. +1 там нужно для округления вверх (то есть чтобы, например, при установленной FOOD_RARITY 512 и размере поля 600, на нём был не 1, а 2 куска еды).

Если еды на поле недостаточно (следить за её количеством можно через переменную food_cnt), генерируется новый кусок [490]. Координаты еды определяются генератором случайных чисел и подгоняются в заданные координатные рамки путём операции суммы по модулю (%), или же остатка. Полученная позиция проверяется, и если она пуста — туда помещается пища, а если занята — генерация выполняется вновь.

Из-за того, что генерироваться еда будет "до упора", в случае, близком к победе (всё поле занято змеёй), игра может начать тормозить, а когда места для необходимого кол-ва еды просто не останется, банально уйдёт в бесконечный цикл. Если вы когда-либо добьётесь такой ситуации — вы победили! И удачи с выключением игры, Ctrl+C не сработает — он перехвачен.

level_up — строка 502
Довольно простая функция, повышающая игровой уровень (а значит — скорость) по достижению змеи очередной LVLUP_LENGTH. Например, в моём случае LVLUP_LENGTH установлена в 50 — так левелап будет происходить каждые 50 очков длины.

Исползуется static (сохраняющая своё значение меж вызовами функции) переменная upped_already, чтобы избежать проблем при нескольких вызовах level_up за те хода, пока длина змеи сохраняется кратной LVLUP_LENGTH. Она устанавливается в 1 после повышения уровня, и в 0, когда длина змеи не соответствует нужному для левел-апа значению. Так как длина может лишь расти, это адекватный подход.

Послесловие

Надеюсь, кому-то было интересно прочитать про то, как можно упорото и хардкорно написать змейку.

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

Adblock
detector