Змейка на C
Недавно уже давно (а нечего посты задерживать!) мне захотелось написать небольшую и очень простенькую игру — змейку.
Для юниксового терминала. На чистом C — даже без ncurses и прочих подобных финтифлюшек.
А сегодня я расскажу и покажу, что получилось.
Небольшой дисклеймер
Да, я в курсе, что можно добавить множество разных фич (в том числе мультиплеер и уровни с процедурно генерируемыми стенками) и много чего сделать лучше как в самой игре, так и в исходниках. Возможно, я когда-то это и сделаю. На данный момент ничего особо примечательного в коде нету, потому и stupid-snakeНо была поставлена цель сделать не максимально хорошо и качественно, а быстро и чтобы работало. Ну и ещё по возможности наглядно — идея написать пост всплыла с самого начала. Эта цель в полной мере достигнута.
Немного пояснений
C — довольно низкоуровневый язык, и уж никак не «кроссплатформенный по-умолчанию», как, например, Python. Тот факт, что я не использую ncurses, ещё больше усугубляет ситуацию.Очень многое в игре реализовано путём функций, имеющихся лишь в POSIX-системах, а то и вообще лишь в Linux и glibc. Windows POSIX-системой не является, да и терминал там вряд ли настолько же функционален, потому собрать stupid-snake под Windows у вас, скорее всего, никак не получится. На OS X — не знаю, пусть кто-то попробует и доложит об успехах.
Если своего линукса у вас нигде не завалялось, а испытать игру очень хочется, рекомендую использовать 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. >».
На том бы и остановиться, вот только неинтересно (и я болею перфекционизмом, помните?). Нужна ещё анимация. Как насчёт заставлять змейку закрывать рот каждый ход? Досточно будет заменить символ «головы» на соответствующий направлению символ «тела». Переход «<» ➛ «-» сносно создаёт иллюзию закрывшегося рта.
• Связь между элементами игры;
Змейка должна погибнуть, наткнувшись на стену, и вырасти, съев пищу. Для того, чтобы увязать меж собой положение стен, еды, головы, тела и хвоста змейки, необходимо использовать общую для них всех систему координат. И самый очевидный вариант — применить стандартные координаты терминала (0, 0 — верхний левый угол, и разрешение, разумеется, посимвольное — про пиксели терминал ничего знать не знает).
Дальше остаётся в общем два пути: 1) Создать все требующие учёта элементы как отдельные сущности, свойствами которых являются координаты; 2) Создать одну сущность — координатное поле — служащую отображением нынешнего состояния терминала, и разместить элементы-символы в ней.
Первый путь — очень громоздкий. Например, для учёта положения еды необходимо будет создать массив (а то и список) сущностей типа «еда», у каждой из которых будет свой набор координат. Чтобы проверить, не съела ли змейка еду, нужно будет каждый раз проходиться по всему этому массиву/списку. Аналогично при выводе на экран будет необходимо обойти каждую сущность-элемент, чтобы узнать нужные координаты для вывода.
Второй путь — намного более легковесный и удобный. Чтобы проверить, не наткнулась ли змея на что-то, достаточно перед её перемещением проверить «новую» позицию — нет ли там уже чего? С выводом на экран тоже легко: просто print каждый элемент из координатной сетки. Но есть и проблема у второго пути — как найти саму змею? Обходить всё координатное поле поиском?
И да, если вы подумали, что первый вариант — без координатного поля — бесполезен, то вы сильно ошибаетесь! У него есть свои применения. Например, если полное отображение координатного поля потребует десятка гибибайт памяти, тогда как сущностей создать нужно было бы всего десяток-другой. Но подобные расклады настолько же далеки от «змейки», насколько автор этого поста — от мирового господства.
• Передвижение змейки;
Чтобы передвигать змейку, необходимо знать нынешнюю позицию головы, следующую позицию головы, нынешнюю позицию хвоста и следующую позицию хвоста. При выполнении «хода» нарисовать голову на новой позиции, сегмент тела — на старой; старую позицию хвоста очистить. Новую позицию хвоста надо знать лишь для того, чтобы запомнить её на будущее.
Но тут возникает проблема: если с переменой направления головы всё ясно (игрок нажал влево — повернуть голову влево), то как быть с хвостом? Не желая хранить дополнительную информацию в больших объёмах исключительно для правильной обработки хвоста, я прибегаю к простому трюку: зная направление хвоста и следующий в этом направлении сегмент тела, новое направление определить очень легко.
• Генерация еды;
Генерируем, разумеется, случайным образом. Количество генерируемой еды зависит от размеров поля; скажем, 1 единица на каждые 512 пустых позиций.
• Обработка стенок;
Можно держать стенки внутри координатного поля (и проверять столкновение по принципу «Является ли следующая позиция головы занята символом стены?». Можно держать их снаружи (и проверять столкновение по принципу «Не вышла ли голова змеи за разрешённые пределы координат?»). Мне больше понравился второй способ. Он также кажется более гибким (позволяет легко заменить логику столкновения со стеной на логику «телепортации» на другой край поля — тоже интересный подход).
Если мне не изменяет память, именно перечисленные выше пункты были обдуманы мною на этапе дизайна. Выглядит сложно, но заняло это у меня всего-то пару часов не самого усердного размышления с блокнотом (и всего три страницы последнего).
Код
Откройте исходник, я буду ссылаться на номера строк (в квадратных скобках), функции и так далее.Немного слов о файловой структуре кода. Она очень простая — один исходник (snake.c), один makefile. LICENSE и README.md не используются при сборке совершенно никак и не важны для понимания программы. По-хорошему функции надо было бы вынести в отдельные файлы и сгруппировать таким образом по их предназначению, но мне показалось более правильным использовать структуру попроще.
main() — строка 515
main — основная функция программы; сама программа, если пожелаете. Исполнение начинается отсюда. Так как мне нет нужды на данный момент принимать какие-либо аргументы из командной строки, main я объявляю без параметров (void).
В начале вызываются несколько инициализирующих функций (переключение терминала в нужный режим, установка обработчика сигнала INT (Ctrl+C) и инициализация генератора псевдослучайных чисел), затем объявляются нужные переменные, затем производится ещё несколько инициализаций — уже касающихся самой игры — и первый вывод картинки в терминал.
Наконец, получив значение начального момента времени, код входит в цикл обновления изображения.
Этот цикл имеет несколько интересных особенностей. Начнём с условия — while(run). run является [64] переменной особого типа sig_atomic_t, придуманного специально для использования в обработчиках сигналов. Свой кастомный обработчик [82] я устанавливаю на строке [517], и всё, что он делает — сбрасывает при вызове этот самый run в 0. Обрабатывать SIGINT нужно затем, чтобы корректно выйти из игры и вернуть терминал в адекватное состояние — стандартный предоставляемый системой обработчик просто завершает программу «здесь и сейчас».
Обработчик вызывается в тот момент, когда сигнал приходит, вне зависимости от исполняемого участка программы. После завершения обработчика, исполнение возвращается в старое место и всё продолжается, как ни в чём не бывало. И когда я говорю «вне зависимости от исполняемого участка», я это и имею в виду — обработчик может быть вызван даже сам из себя.
Ввиду такой специфики применения в обработчиках нельзя использовать большую часть функций (на самом деле, существует даже официальный список тех стандартных функций, которые таки можно с уверенностью применять в обработчиках. Он небольшой). Хотя «нельзя» в C и означает всегда «Можно, но мы вообще ничего не гарантируем и ты сам за всё отвечаешь», я настоятельно не советую это простое правило нарушать.
Следующий интересный момент — обновление экрана. Не мудрствуя лукаво, я использую здесь самый банальный подход: прямую зависимость скорости змеи от частоты кадров. Два кадра — один ход и одна анимация. При 10 fps получаем 5 ходов за секунду; при 200 fps — 100.
Работает это так: при каждой итерации цикла сохраняется момент времени [541], после чего рассчитывается разность [542] меж ним и моментом, когда в прошлый раз был отрисован кадр. Затем полученная разность сравнивается с кадровой задержкой (задержка = 1 секунда / fps), и если она превышает задержку — кадр обновляется, сохранив новый момент обновления.
FPS не является чётко заданным конкретным числом, а находится как «(уровень + 1) * 5». Иными словами, на первом уровне имеем частоту 10, на втором — 15, на третьем — 20, и так далее.
Кадров бывает два вида: полноценный и анимационный. Определяю я, какой из них надо рисовать, простым флагом animation_frame. В случае анимационного кадра отрисовывается исключительно анимация; в случае полноценного — перерисовывается всё игровое поле, а также генерируется пища, производится по необходимости левел-ап и всё такое.
set_terminal_mode — строка 90
reset_input_mode — строка 74
Здесь происходит терминальная магия. В первой функции устанавливается нужный режим, во второй возвращается старый. В чём дело?
Для работы с параметрами терминала используется набор функций termios. Происходит это так: вынуть нынешние параметры в структуру termios с помощью функции tcgetattr [105], отредактировать её, запихнуть обратно в терминал с помощью tcsetattr [109].
По-умолчанию юниксовый терминал ждёт ввода полной строки (нажатия Return) и позволяет перед вводом эту строку всячески редактировать. Это называется каноническим режимом. Чтобы нажатия передавались нашей игре сразу, без необходимости клацать Enter, необходимо переключить терминал в режим неканонический.
Также по-умолчанию юниксовый терминал сразу выводит любую нажатую клавишу на экран. Это называется эхо-режимом. Чтобы этого не происходило, надо выключить эхо.
Канонический режим и эхо-режим выключаю одновременно [106]. Это битовые флаги, и если вы не понимаете вообще, что там происходит, то советую почитать где-нибудь о побитовых операциях — тема объёмная и полезная.
Устанавливаю VMIN [107] и VTIME [108] в массиве параметров c_cc в нулевые значения.
VMIN отвечает за минимальное количество символов, которого будет дожидаться терминал в неканоническом режиме. Если там будет что-то кроме нуля, любая попытка читать с терминала заблокирует программу до получения VMIN символов. Мне это совершенно не нужно, блокироваться — плохая идея, надо кадры обновлять.
VTIME отвечает за то, сколько (при ненулевом VMIN) терминал будет ожидать символы.
Вообще говоря, достаточно установить в 0 один из этих параметров, но и оба сразу — не повредит.
И в set_terminal_mode, и в reset_terminal_mode имеются очень странные printf. Это — вывод в терминал особых последовательностей байтов, которые терминал, следующий стандарту ANSI, обязан понимать и интерпретировать. Как символы они записаны исключительно для удобства. Полное описание есть на википедии.
Не вдаваясь в подробности, «\x1b[?25l» означает «выключить отображение курсора», а «\x1b[?25h» — «включить».
Интересна также функция atexit [113]. Она регистрирует любую другую функцию в системе так, чтобы та была запущена при корректном выходе из программы. Ключевой момент тут — корректный выход; именно чтобы обеспечить его я перехватываю SIGINT.
Набор функций turn_<direction> — строки со 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.
Буфер длины нужен по той простой причине, что за один ход змея способна вырасти ровно на один сегмент, и никак не больше. То есть (теоретический) бонус, сразу дающий 5 очков длины, возможно реализовать только путём буфера, где эта «уже съеденная, но ещё не обработанная» еда будет храниться и «отращиваться» в хвост по одной каждый ход. Пронаблюдать, как именно это работает, можно в функции move_snake.
redraw_all — строка 278
Функция отрисовки игрового поля.
Вначале курсор помещается в стартовую позицию 0, 0 (верхний левый угол) с помощью ещё одного управляющего спец-символа [279]. Далее за несколько циклов выводятся сначала верхняя стенка [281], затем боковые и само поле [286], затем нижняя стенка [295] и немного информации: длина змеи, уровень [300]. Ничего интересного здесь нет; такое писал каждый, кто когда-либо выводил матрицы на экран.
redraw_animation — строка 305
Немного более интересная функция, которая отвечает за анимацию.
С использованием, опять же, спец-символа (но в этот раз динамически сгенерированного) курсор устанавливается на координаты головы змеи [307] (поправки +2 нужны из-за того, что в змее хранятся координаты относительно игрового поля-матрицы, а нужны координаты относительно экрана). Далее вместо головы рисуется «закрытый рот» (просто сегмент тела, соответствующий направлению) [309].
Затем курсор путём всё той же манипуляции со спец-символом устанавливается на позицию змеиного хвоста [317] и с хвостом происходит анимация выпрямления (при условии, что он ещё не прямой), в общем-то идентичная таковой для головы.
move_snake — строка 338
Самая сложная, интересная и важная функция. Перемещает змею в игровом поле и вообще обновляет её состояние.
Если направление змеи не менялось (поля dir и new_dir одинаковы), значит, была нажата клавиша того же направления, либо не была нажата никакая. В обеих этих случаях функции turn_<direction> ничего не рисуют, потому необходимо нарисовать новый сегмент тела [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, когда длина змеи не соответствует нужному для левел-апа значению. Так как длина может лишь расти, это адекватный подход.
Послесловие
Надеюсь, кому-то было интересно прочитать про то, как можно упорото и хардкорно написать змейку.Я для себя выношу одну истину: писать посты про свой код очень сложно. Мне было физически тяжело возвращаться к этому посту и пытаться выразить в словах то, что кажется очевидным из структуры программы. Возможно, в другой раз я попробую какой-нибудь иной формат.
Об игре Snake Battle — Бои программируемых змеек
Об игре
Эта онлайн игра создана по мотивам игры Snake Battle 1992 года компании Gamos.
Каждый игрок сам создает своего «бойца», который после этого будет жить собственной жизнью. Этот «боец» — змейка. Процесс создания поведения змеи прост, но создание сильного и несокрушимого война требует больше смекалки и логики. Змее нужно уметь есть чужие хвосты и не терять свой.
Бой основан на умении хозяина обучить свою змею.
Бой проходит на ринге размером 25х25 квадратов. В квадрате может находиться голова, тело или хвост змеи. Строение змеи — обычное: начинается она с головы и заканчивается хвостом. Между головой и хвостом — тело, которого может и не быть. Во время сражения змея может двигаться до последнего выполняя свою программу, пока ее голова не будет съедена соперником. На ринг выползает от 2 до 4 змей (согласно вашим настройкам поединка или распределению в турнире). Полная длина змеи в начале боя — 10 квадратов.
Игра рассчитана на 500 ходов каждой змеи. Свои ходы змеи делают по очереди. В свой ход, согласно программе, змея изучает поле вокруг себя. При совпадении схемы поведения и поля вокруг головы происходит перемещение на одну клетку. При этом может пострадать вражеский хвост. Когда змее улыбается удача и вражеский хвост пропадает в ее чреве, длина тела охотника увеличивается на один квадрат, соответственно длина тела жертвы уменьшается на этот квадрат. Если ни одна схема поведения не подошла, то змея ползет на случайную пустую клетку, если такая есть.
Игра заканчивается в следующих случаях:
- когда все змеи запутались в своих кольцах или кольцах соперников и не могут двигаться
- когда на игровом поле только одна змея
- когда все змеи сделали по 500 ходов
Рейтинг
Каждая змея после создания имеет свой личный рейтинг. Изначально 1000. После проведения каждого боя, полученные очки плюсуются к этому рейтингу.
«Строевые упражнения.Игра Змейка» — физкультура, уроки
государственное бюджетное профессиональное образовательное учреждение
Ростовской области
«Зерноградский педагогический колледж»
ТЕХНОЛОГИЧЕСКАЯ КАРТА
урока физической культуры,
проведённого 21.11.2018 г. в 1 «А» классе
МБОУ лицея г. Зернограда
студенткой ГБПОУ РО «ЗернПК»
Соколовой Александрой Андреевной
Учитель: Жукова Т. А.
Методист: Шевченко И. Ф.
_____________________
(подпись)
Зерноград
2018г.
ТЕХНОЛОГИЧЕСКАЯ КАРТА УРОКА
Класс: 1 «А»
Тема урока: Строевые упражнения. Игра «Змейка».
Цель урока: развитие физических качеств, быстроты, ловкости, скорости.
Учебные задачи:
Направленные на достижение личностных результатов:
— проявление культуры взаимодействия, терпимости и толерантности в совместной деятельности;
— формирование способностей активно включаться в совместные действия;
— развитие навыков сотрудничества со взрослыми и сверстниками.
Направленные на достижение метапредметных результатов:
— овладение способностью принимать и сохранять цели и задачи учебной деятельности;
— формирование умения планировать, контролировать и оценивать учебные действия в соответствии с поставленной задачей и условиями её реализации.
Направленные на достижение предметных результатов:
— Развивать координационные способности, скоростно–силовые качества, быстроту и ловкость.
— овладеть умениями организовывать здоровье сберегающую жизнедеятельность (подвижные игры).
Инвентарь для учителя: свисток, технологическая карта
Инвентарь для учащихся: волейбольные мячи, конусы, листы нормативов
ХОД УРОКА
Этапы урока | Деятельность учителя | Деятельность учащихся | УУД |
1.Организационный этап. | Построение детей. Приветствие. -Класс- становись, равняйсь, смирно! -Здравствуйте ребята, вольно! -Меня зовут Александра Андреевна и сегодня урок физической культуры проведу у вас я. Не шнурок я и не шланг, -Кто это? -Как вы думаете, с чем будет связана тема нашего урока? -Да, действительно, тема нашего урока это «Строевые упражнения. Игра «Змейка». -Совершенно верно! | Приветствую учителя. -Здравствуйте Воспринимают на слух информацию учителя. -Змея -Со змеёй | Р.: овладение способностью принимать и сохранять цели и задачи учебной деятельности |
2.Этап актуализации знаний | Выполнение строевых команд на месте -Класс, Равняйсь! Смирно! Дает команду: «Напра-во!» «В обход налево, шагом марш! Дает команду «Руки на пояс ставь, на носках марш! Держим спину прямо.» Дает команду «Руки за голову, ходьба на пятках марш! Спина ровная» Дает команду: «Обычным шагом марш!» «Медленным бегом- марш!» Дает команду: «Руки на пояс ставь правым боком, приставными шагами!», «Другим боком!» Даёт команду: «Бег с захлёстыванием голени назад» Даёт команду: «Бег с высоким подниманием бедра.» Бег без задания. Шагом марш! Восстановление дыхания после бега. По линии на место построения. Левой, левой 1-2-3, «Направляющий на месте. Левой, левой 1-2-3. Стой! Раз-два! Налево!» Дает команду: Класс, Равняйсь! Смирно! «На 1-й,2-й рассчитайсь!». Расчёт. -Ребята, давайте с вами вспомним как правильно перестраиваться в 2 шеренги. -Что во время перестроения в 2 шеренги делают 1 номера? -Что в этот момент делают 2 номера? Класс, в 2 шеренги стройся, 1-2-3. В одну шеренгу стройся, 1-2-3. В две шеренги стройся, 1-2-3. На вытянутые руки разомкнись. ОРУ без предмета. Приложение 1 | Повторяют строевые команды. Внимательно слушают учителя. Выполняют ходьбу с заданиями. Выполняют бег с заданием. Рассчитываются Размыкаются Выполняют общеразвивающие упражнения | Л.: развитие навыков сотрудничества со взрослыми и сверстниками Р.: формирование умения планировать, контролировать и оценивать учебные действия в соответствии с поставленной задачей и условиями её реализации |
3. Изучение нового материала. | -Ребята, сейчас мы с вами повторим строевые упражнения. Класс! Равняйсь! Смирно! Напра-во (1,2)! Нале-во (1,2)! Кру-гом (1,2)! Кру-гом (1,2)! Нале-во (1,2) На месте, Шагом-марш (левой, левой, раз, два, три)! Стой (1,2)! Нале – во!(1,2) -А теперь давайте поиграем в игру «Змейка». Правила игры очень простые: выбирается один ведущий, который будет «головой» змейки. Он встает в начале, остальные участники игры берут предыдущего игрока за талию и становятся сзади него. Таким образом, получается змейка. Остальные – следуют за ним. Цель: как можно быстрее сократить «туловище» змейки, т.е. водящий, бежит по какой-то причудливой траектории, делает резкие смены направления движения. При этом своими резкими поворотами он как бы «закручивает» змейку. Задача – заставить участников игры как можно быстрее расцепить руки (тем самым разорвав змейку). «Оторванная» часть туловища змейки выбывает из игры. Остальные продолжают все так же играть. Выиграет тот, кто сможет дольше всех продержаться. -А теперь давайте поиграем в игру на внимание «Небо, земля, вода». Содержание: «Небо-руки вверх, земля-присесть, руки на пол, вода-руки вперед». Учитель называет команды в разной последовательности, учащиеся выполняет задание делают шаг вперед. Победителями становятся, те учащиеся, которые ни разу не ошиблись и остались стоять на месте. Подведение итогов урока. Упражнение на релаксацию и восстановление. | Играют в игру «Змейку» Играют в игру «Небо, земля, вода» Подводят итогов урока. Выполняют упражнение на релаксацию и восстановление | Л.: проявление культуры взаимодействия, терпимости и толерантности в достижение в совместной деятельности; — формирование способностей активно включаться в совместные действия; — развитие навыков сотрудничества со взрослыми и сверстниками |
4. Итог урока. Рефлексия | Дает команду: «Класс в одну шеренгу становись! Равняйсь! Смирно! -Ребята, давайте вспомним, чем мы сегодня занимались на уроке. -Какие действия мы выполняли на уроке? -Что нового узнали? Урок окончен. До свидания. За направляющим в класс шагом марш. | Учащиеся вступают в диалог с учителем и поводят итог урока. — ходьба змейкой, ходьба по гимнастической скамейке | Р.: формирование умения контролировать и оценивать учебные действия в соответствии с поставленной задачей и условиями её реализации |
Приложение 1
1 упражнение: «Наклоны головы»
Исходное положение: Ноги на ширине плеч. Руки на пояс. На счёт «Раз» — наклон головы вперёд, на счёт «Два» – наклон головы назад, на счёт «Три» — наклон головы вправо, на счёт «Четыре» – наклон головы влево.
Исходное положения принять, упражнение вперёд, начинай. Выполнить 5 – 6 раз.
Раз-два-три-стой раз-два.
2 упражнение: «Круговые движения в плечевых суставах»
Исходное положение: Ноги на ширине плеч, руки на пояс, на счёт «Раз, два, три, четыре» — выполняем 4 круговых движения вперёд, по команде «В обратную сторону» — выполняем 4 круговых движения назад.
Исходное положения принять, упражнение вперёд, начинай. Выполнить 5 – 6 раз.
Раз-два-три-стой раз-два.
3 упражнение: «Рывки руками с поворотом»
Исходное положение: Ноги на ширине плеч. Руки перед грудью. На счёт «Раз, два» — 2 рывка руками перед собой, на счёт «Три, четыре» — 2 рывка руками с поворотом туловища в правую и левую сторону.
Исходное положения принять, упражнение вправо начинай. Выполнить 5 – 6 раз.
Раз-два-три-стой раз-два.
5 упражнение: «Наклоны туловища»
Исходное положение: Ноги на ширине плеч. Руки на пояс. На счет “раз -два” – 2 наклона туловища вперёд, “три-четыре” -2 наклона назад, “раз — два” -2 наклона вправо, “три — четыре” – 2 наклона влево.
Исходное положение принять, упражнение вперёд начинай. Выполнить 5 – 6 раз.
Раз-два-три-стой раз-два.
6 упражнение: «Круговые движения туловищем»
Исходное положение: Ноги на ширине плеч. Руки на пояс. На счет «Раз, два, три, четыре» — выполняем 4 круговых движения в правую сторону, по команде «В обратную сторону» — выполняем 4 круговых движения в левую сторону.
Исходное положения принять, упражнение в правую сторону, начинай. Выполнить 5 – 6 раз.
Раз-два-три-стой раз-два.
7 упражнение: «Отжимания». Мальчики – 10 раз, девочки- 8 раз. Исходное положения принять. Упражнение начинай.
8 упражнение: «Приседания». Мальчики — 20 раз, девочки — 15 раз. Упражнение начинай. По окончании упражнения подаётся команда «На месте шагом марш!»
9 упражнение: «Прыжки с хлопками».
Исходное положение основная стойка. На счёт раз – прыжок ноги врозь, хлопок над головой; на счёт два – прыжок ноги вместе руки вниз. 3 – 4 то же самое. Исходное положения принять, упражнение начинай. Выполнить 8 – 10 прыжков.
По окончании прыжков подаётся команда «На месте шагом марш!». «Стой 1 – 2»
Восстановление дыхания.
Приложение 2
Игра «Змейка»
Правили игры: выбирается один ведущий, который будет «головой» змейки. Он встает в начале, остальные участники игры берут предыдущего игрока за талию и становятся сзади него. Таким образом, получается змейка. Первый (ведущий) игрок начинает движение. Остальные – следуют за ним.
Цель – как можно быстрее сократить «туловище» змейки, т.е. водящий, бежит по какой-то причудливой траектории, делает резкие смены направления движения. При этом своими резкими поворотами он как бы «закручивает» змейку. Задача — заставить участников игры как можно быстрее расцепить руки (тем самым разорвав змейку). «Оторванная» часть туловища змейки выбывает из игры. Остальные продолжают все так же играть.
Выиграет тот, кто сможет дольше всех продержаться.
2.1 Правила игры. Разработка программы «Игра «Змейка»
Похожие главы из других работ:
Аспекты задачи сокращения перебора при анализе сложных позиционных игр
1.5 Правила игры в нарды
· Игроки ходят по очереди. · Направление перемещения шашек отличается в разных вариантах игры. Но в любом случае шашки движутся по кругу и для каждого игрока направление их движения фиксировано…
Вся правда о компьютерных играх
1. Компьютерные игры
…
Деловые игры
Цель игры:
• Дать представление о заключении договора по оказанию сложносоставной услуги. • Создать модель взаимодействия заказчика и исполнителя услуги…
Игра «Жизнь»
5.2 Запуск игры
Запустите приложение запускающее игру или кликните на файл LifeGame.exe перед вами появится окно, в котором нужно указать размеры поля и клетки, а также форму самой клетки и нажать кнопку «Создать»…
Игра «Ловушка»
2. Описание игры
…
Игра крестики–нолики
3.1 Правила игры
Правила игры состоят в том, что необходимо выстроить линию из пяти крестиков (ноликов) в любом направлении и каким угодно способом (по горизонтали, вертикали или диагонали)…
Компьютеризация общества
2.4 Компьютерные Игры
За компьютером можно не только эффективно работать, но и хорошо отдыхать. Этот факт со всей убедительностью доказывают многочисленные фирмы, выпускающие развлекательные программы. Не секрет…
Логическая игра «Морской бой»
2. Правила игры
«Морской бой» — игра для двух участников, в которой игроки по очереди называют координаты на неизвестной им карте соперника. Если у соперника по этим координатам имеется корабль (координаты заняты), то корабль или его часть «топится»…
Математическая модель цифрового устройства игры «шашки»
Правила и стратегия игры в шашки
Пространственно — композиционная игра шашки напоминает игру типа «крестиков-ноликов». Квадратура развивает пространственное мышление и «видение поля» — умение взглядом охватить и оценить ситуацию на поле, заполненном шашками игроков…
Программирование на языке высокого уровня
1. Сценарий игры
Надо ловить яйца появляющиеся случайным образом. Игроку начисляется по 10 очков за каждое пойманное яйцо. При набирании 100 очков игрок выигрывает…
Проектирование игр для мобильных устройств на базе операционной системы Android
3.1.2 Алгоритм игры
При запуске приложения инициируется проверка на наличие доступа в интернет. При отсутствии доступа, появляется соответствующее сообщение и приложение завершает работу. При наличии интернет соединения…
Разработка базы данных «Компьютерные игры»
2.1.1 Таблица «Игры»
В данной таблице хранится информация об играх, которые и будут составлять основное содержание моей БД. В этой таблице я задал 3 ключевых поля. Таблица состоит из 11 полей, которые различаются по типам данных, которые в них можно внести…
Разработка компьютерной игры «Танчики»
2.1 Правила игры
Правила игры просты и заключаются в следующем: 1) Ваша главная задача: уничтожить всех противников. 2) Если все противники уничтожены, вы выиграли. 3) Если в вас попали, вы проиграли. 4) Для уничтожения танка достаточно одного попадания…
Разработка подхода к программированию «искусственного интеллекта» в позиционных играх на основе игры «Точки»
1.2 Позиционная игра «Точки». Правила игры
Игра «Точки» представляет собой упрощенный вариант японской игры «Го». Два игрока на клетчатом поле заданного размера ставят по очереди точки своего цвета. Игрок стремится окружить точки противника замкнутым контуром. Точки и территория…
Создание игры «Сапёр»
1.1 Правила игры и алгоритм работы
Рассмотрим правила игры «Сапер». Классический «Сапер» — игра для одного игрока, в которой игрок пытается найти все мины. Если игрок ошибается, то он проигрывает. Цель игрока — найти все мины. Поле для игры состоит из 5 строчек и 5 столбцов…
Как создать игру «Змейка» в Котлине
Иллюстрация Павла Кадыша
Предисловие
Хотели бы вы создать захватывающий мир с нуля и позволить другим людям исследовать его? Это поистине волшебная сила, и я хотел приобрести ее с раннего детства. Собственно, именно поэтому я выбрал информатику.
Разработка игр — одно из самых важных дел в моей жизни. В играх нет заранее установленных правил или ограничений, единственным ограничением является ваше собственное воображение.Вы можете создавать целые миры с нуля, и они будут выглядеть и вести себя именно так, как вы хотите. Это дает вам мощный способ выразить свои идеи и поделиться ими с другими людьми.
Я не профессиональный разработчик игр. Зачем? Оказалось, что есть много других полезных вещей, которые могут делать компьютеры, и, к сожалению, создание видеоигр — это не работа, о которой мечтали. В студенческие годы я сделал много игр, и это помогло мне устроиться на первую работу в области программирования, но сейчас создание игр — лишь одно из моих хобби.Это означает, что я не лучший человек, чтобы давать советы, как стать профессиональным разработчиком игр. Это руководство предназначено для таких любителей, как я, которые просто хотят изучить базовые концепции разработки игр с использованием Kotlin.
Содержание
Форма искусства или развлечения?
На мой взгляд, разработка игр — это искусство. Игры могут рассказать вам интересную историю, как это делает хорошая книга, но на самом деле вы можете увидеть эту историю именно так, как того хотел автор.В фильмах лучше визуальные эффекты, но они линейны, поэтому вы можете быть только наблюдателем, который не имеет никакого влияния на происходящее. Это может быть хорошо, но ограничивает. Кроме того, фильмы идут в фиксированном темпе, и это одна из причин, почему некоторые книги намного лучше фильмов, несмотря на отсутствие визуальных эффектов: фильмы часто искусственно укорачивают, чтобы соответствовать стандартам кинотеатра, или искусственно расширяют, чтобы показывать больше рекламы.
Общие проблемы
Самая распространенная проблема, с которой может столкнуться инди-разработчик игр, — это отсутствие мотивации для завершения игры.Вот несколько советов, как сохранять мотивацию:
Дело не в инструментах
Невозможно овладеть всем, изучение каждого аспекта разработки игр может занять годы, но не обязательно начинать писать игры как хобби. Многие новички пытаются использовать низкоуровневые библиотеки, такие как OpenGL, DirectX или Vulkan, в своих первых играх. Это то, что должен знать профессиональный разработчик игр, но в большинстве случаев Canvas API будет лучшим выбором, поскольку его гораздо проще использовать.Вы можете сэкономить много времени и нервов, если правильно подберете инструменты.
Время — важный фактор
Многие люди пытаются написать свою собственную MMORPG или думают, что они могут сравниться с лучшими играми в индустрии с точки зрения возможностей. Давайте проясним: даже если у вас есть необходимые навыки и мотивация, никто не сможет сделать такую игру в разумные сроки. Чтобы создать типичную игру ААА, нужны сотни людей и несколько лет, а иногда и более десяти лет. У вас, как у инди-разработчика игр или небольшой команды, не так много времени и ресурсов, но в небольшом размере есть преимущества, которые вы можете использовать.Хотя создать гигантский игровой мир и тонны красивой графики для вас практически невозможно, вы можете больше экспериментировать с игровым процессом. Крупные компании слишком беспокоятся о своих продажах, и это ограничивает их возможности экспериментировать с игровым процессом. Если у вас нет инвесторов, которым можно угодить, это дает вам большое преимущество.
Сделай это достаточно хорошо
Графика — важная часть современных игр. Это здорово, если вы можете написать чистый и красивый код, но игроки этого не увидят.Если ваша игра выглядит плохо, это может отпугнуть игроков и даже самих авторов. Намного веселее работать над тем, что выглядит красиво и чисто. Типичная инди-игра не требует большого количества графики, но графика очень важна, чтобы поддерживать вашу мотивацию и делать игру более увлекательной.
Двигатель
Мы не будем использовать какой-либо конкретный игровой движок, гораздо интереснее создать игровой движок с нуля, и это также поможет нам лучше понять, как видеоигры работают под капотом.Мы собираемся создать клон игры Snake, потому что его относительно легко реализовать, что делает его хорошей игрой для начала.
Исходный код
Вы можете проверить полный исходный код здесь. Это может помочь вам понять конфигурацию проекта и увидеть конечный результат, которого вы должны ожидать, если будете следовать этому руководству.
Создание сцены
Каждая игра индивидуальна, но, к счастью для нас, многие видеоигры имеют общий набор компонентов и шаблонов. Есть вещи, которые могут быть специфичными для определенного жанра, но на достаточно высоком уровне все игры более или менее одинаковы и все они сделаны из одних и тех же компонентов.
Иллюстрация Маноса Гкикаса
Некоторые из этих компонентов:
Экран
— игрок должен видеть, что происходит в игре, а использование экрана — удобный способ обеспечить визуальную обратную связь. Это может быть экран телевизора, экран компьютера, экран мобильного телефона или даже гарнитура VR. Технически в гарнитуре VR есть два экрана, но нам не о чем беспокоиться, большинство требуемых настроек выполняется автоматически аппаратным обеспечением гарнитуры.Контроллер
— это устройство, которое можно использовать для взаимодействия с видеоиграми. В качестве контроллеров можно использовать многие устройства: клавиатуры, компьютерные мыши, джойстики и множество более экзотических устройств, таких как штурвалы или контроллеры истребителей.Игровые объекты
— все, что существует в игре, является игровым объектом. Некоторые объекты статичны, например стены, а некоторые более динамичны, например игровой персонаж игрока или его друзья и враги.
Класс сцены
Начнем с реализации компонента экрана. Типичное настольное приложение состоит из нескольких окон, и каждое окно имеет свою цель, и разделение задач обычно выгодно как для пользователей приложения, так и для разработчиков. Пользователи приложений могут лучше сосредоточиться и чувствовать себя менее перегруженными, поскольку для каждой конкретной задачи есть отдельное окно, и разработчики могут разделить логику приложения между этими экранами, что упрощает обслуживание базы кода.
Видеоигры по умолчанию не имеют оконной системы, но это не значит, что мы не можем ее реализовать. Я думаю, нам не следует использовать название «окно», когда мы делаем игру. «Уровень» кажется удачным названием, многие игры действительно выглядят как серия уровней, но, на мой взгляд, это название недостаточно абстрактное. Например, главное меню или экран настроек визуально отделены от остальной части игры, и неудобно рассматривать их как «уровни», но они также имеют много общего с остальными экранами: они обычно что-то отображают и обрабатывают входные события от игровых контроллеров.Так что давайте назовем это сценой, вам будет легче понять, что здесь может происходить любое действие, если есть что показать игроку. Очевидно, мы не хотим, чтобы игрок смотрел на пустую сцену, это совсем не весело.
Вот наш Scene
class:
импорт java.awt.Graphics2D
импорт java.util.concurrent.TimeUnit
абстрактный класс Scene (val game: Game) {
абстрактное забавное обновление (
timePassed: долго,
timeUnits: TimeUnit = TimeUnit.Наносекунды
)
абстрактная забавная рисовка (графика: Graphics2D)
}
Как это работает?
Метод update
должен вызываться каждый раз, когда мы хотим, чтобы сцена обновляла свое состояние, но для выполнения обновления сцена должна знать, сколько времени прошло с момента последнего вызова. Вот почему мы требуем от вызывающего абонента предоставить нам значение timePassed
. Например, если на сцене движется машина, невозможно определить, как далеко она должна двигаться, если мы не знаем ее скорость и время, прошедшее с момента последнего обновления.Обновление , методы
и draw
следует вызывать не менее 30 раз в секунду, чтобы убедиться, что все игровые объекты будут перемещаться плавно. Вот почему если вы попытаетесь запустить ресурсоемкую игру на старом оборудовании, это будет выглядеть как слайд-шоу. Некоторые компьютеры могут быть не в состоянии обновлять и рисовать игровые сцены достаточно быстро, чтобы игра выглядела «в реальном времени».
Единственная цель метода обновления
— синхронизировать «игровое время» с «реальным временем». Нас не волнует, что конкретная игра делает с этой информацией, нам просто нужно сообщить игре, сколько времени прошло с момента последнего вызова.
Обновление состояния игры важно, но нам все еще нужен способ его отображения, чтобы сделать его видимым для игрока. Вот почему мы ввели метод draw
, который должен вызываться сразу после обновления
.
Заключение
Теперь у нас есть способ разделить наши игры на набор сцен, но это только один из основных игровых компонентов, которые нам нужны. Далее мы собираемся создать компонент, который будет обрабатывать данные с устройств ввода.Приятно наблюдать за движением объектов, но еще веселее иметь возможность управлять ими.
Обработка входных событий
Контроллеры— это устройства, которые могут регистрировать действия игроков, и мы хотим, чтобы наши игроки могли изменять ход игры. В более технических терминах это означает, что каждая игра имеет состояние, а игровые контроллеры — это устройства, которые можно использовать для изменения этого состояния.
Иллюстрация Павла Кадыша
Существует множество различных устройств ввода: типичные консольные игры, как правило, полагаются на геймпады, авиасимуляторы или гоночные симуляторы могут поддерживать свои собственные уникальные контроллеры, но мы пишем игру для ПК, поэтому мы будем придерживаться клавиатуры: самого популярного устройства ввода. используется для взаимодействия с персональными компьютерами.
Имеет смысл также поддерживать компьютерную мышь и трекпад, но нам на самом деле не нужны эти устройства для управления змейкой, поэтому мы можем сделать это позже. Это также сделает код немного проще, и всегда лучше не добавлять ненужной сложности.
Входной класс
Давайте добавим новый класс и назовем его Input
импорт java.awt.event.KeyEvent
импорт java.awt.event.KeyListener
class Input: KeyListener {
частные события val = mutableListOf ()
переопределить fun keyTyped (event: KeyEvent) {
// Нас не интересуют подобные мероприятия
}
override fun keyPressed (event: KeyEvent) {
synchronized (this) {
события + = Событие.KeyPressed (событие)
}
}
override fun keyReleased (event: KeyEvent) {
synchronized (this) {
events + = Event.KeyReleased (событие)
}
}
fun takeEvents (): List {
synchronized (this) {
val ConsmedEvents = events.toList ()
events.clear ()
возврат потребляемых событий
}
}
sealed class Event {
класс данных KeyPressed (данные val: KeyEvent): Event ()
класс данных KeyReleased (данные val: KeyEvent): Event ()
}
}
Как это работает?
Давайте подробнее рассмотрим наш класс Input
.Он реализует интерфейс KeyListener
, который позволяет получать уведомления о любых событиях клавиатуры, происходящих внутри окна игры.
Есть 3 метода, которые мы должны прослушать, чтобы реализовать интерфейс KeyListener
:
-
keyTyped
— нам эти события не нужны, поэтому мы их игнорируем -
keyPressed
— этот метод вызывается каждый раз, когда игрок нажимает кнопку на клавиатуре. Мы хотим знать об этом, потому что нам нужно сохранить это событие для последующей обработки -
keyReleased
— игрок отпустил кнопку, она может оказаться ценной, поэтому нам тоже нужно ее сохранить.Иногда мы хотим сделать что-то, пока нажата определенная кнопка, поэтому нам нужно знать, когда начинать и когда прекращать такие действия (подумайте о педали газа или что-то в этом роде)
Обратите внимание, что мы просто сохраняем эти события для дальнейшего использования, поэтому мы ожидаем, что какой-то другой компонент действительно отреагирует на них. Я назвал это потреблением событий, потому что как только внешний компонент считывает доступные события, они исчезают. Это проще, потому что это помогает нам избежать обработки одного и того же события дважды, потому что оно никогда не вернется дважды, но также предполагает, что у нас есть один потребитель, иначе такая модель не будет работать предсказуемым образом.
Зачем синхронизировать?
Вы, наверное, заметили, что весь код, касающийся коллекции событий, помещен внутри синхронизированных блоков
. Причина в том, что наш игровой цикл использует свой собственный поток, но наше игровое окно является частью библиотеки Swing, которая использует другой поток, поэтому мы должны быть особенно осторожны с этим. Мы бы хотели избежать одновременной модификации коллекции, так как она время от времени приводит к сбою нашей игры или вызывает множество странных и трудно отслеживаемых побочных эффектов.
Так для чего именно используются блоки synchronized
? Они гарантируют, что код внутри них никогда не будет использоваться более чем одним потоком одновременно. Мы также можем использовать это ключевое слово для нескольких методов внутри класса, и это гарантирует, что только один поток может получить доступ к любому из этих методов в любой момент. Это, конечно, приводит к некоторым недостаткам производительности, но это гарантирует, что наш класс будет правильно работать в параллельной среде, и мы действительно можем использовать более продвинутые методы синхронизации, но мы не будем использовать их в этом руководстве, чтобы код был как можно более простым. .
Заключение
Теперь у нас есть классы Scene
и Input
. В следующей части мы создадим ключевой элемент нашего мини-движка — класс Game
. Он будет использовать и связывать воедино код, который мы создали ранее.
Игровой цикл
В каждой игре есть игровой цикл. Это простой цикл, который должен быть знаком каждому программисту. Мы можем представить игру как последовательность статических изображений, меняющихся достаточно быстро, чтобы создать иллюзию движения.Цель игрового цикла — продолжать генерировать новые кадры до тех пор, пока цикл длится, и, как и в любом другом цикле, он не должен длиться вечно, поэтому у нас должен быть какой-то способ разорвать этот цикл, обычно когда игрок решает выйти из игра.
Иллюстрация Джереми Перкинса
Реализация
Мы собираемся создать класс под названием Game
, который обернет игровой цикл, а также будет содержать все важные игровые компоненты, такие как модуль обработки ввода и текущая сцена.
У этого класса будет два метода:
-
play
— этот метод активирует игровой цикл, чтобы он мог начать создание новых кадров. -
пауза
— этот метод переведет игру в неактивное состояние. Игровой цикл должен быть остановлен, что означает, что игра перестает создавать новые кадры.
Вот код:
импорт kotlinx.coroutines. *
импорт kotlinx.coroutines.swing.Swing
import java.awt.Canvas
импорт java.awt.Dimension
импортировать java.awt.Graphics2D
class Game (val screenSize: Dimension): Canvas () {
var сцена: Сцена? = ноль
val input = Input ()
приватный var gameLoop: Job? = ноль
в этом {
size = screenSize
addKeyListener (ввод)
}
fun play () {
если (gameLoop! = null) вернуть
gameLoop = GlobalScope.launch {
var lastIterationTime = System.nanoTime ()
while (isActive) {
val scene = scene?: продолжить
val now = Система.nanoTime ()
val timePassed = now - lastIterationTime
lastIterationTime = сейчас
scene.update (timePassed)
withContext (Dispatchers.Swing) {
scene.draw (bufferStrategy.drawGraphics как Graphics2D)
bufferStrategy.show ()
}
}
}
}
fun pause () = runBlocking {
gameLoop? .cancel ()
gameLoop? .join ()
gameLoop = нуль
}
}
Как это работает?
Как видите, класс Game
расширяет Canvas
, который является частью библиотеки AWT
(Abstract Window Toolkit).Мы можем думать о Canvas
как о пустом пространстве для рисования, которое мы будем использовать для рисования наших игровых сцен, и поскольку наш класс Game
также является Canvas
, мы можем легко разместить его внутри любого окна AWT
.
Конструктор Game
принимает единственный параметр: screenSize
, он должен указывать, сколько места (в пикселях) наша игра хочет занять. Класс Game
также имеет ссылки на текущую сцену, модуль ввода и игровой цикл.Методы play
и pause
используются для управления жизненным циклом игры.
Самая интересная часть этого класса — это содержимое игрового цикла, поэтому давайте рассмотрим его более подробно:
gameLoop = GlobalScope.launch {
var lastIterationTime = System.nanoTime ()
while (isActive) {
val scene = scene?: продолжить
val now = System.nanoTime ()
val timePassed = now - lastIterationTime
lastIterationTime = сейчас
место действия.обновление (timePassed)
withContext (Dispatchers.Swing) {
scene.draw (bufferStrategy.drawGraphics как Graphics2D)
bufferStrategy.show ()
}
}
}
Прежде чем мы перейдем к циклу while
, мы должны инициализировать переменную с именем lastIterationTime
. Эта переменная содержит время предыдущей итерации, и нам нужно знать его, чтобы узнать, сколько времени прошло с момента рендеринга последнего кадра.
Сам цикл будет выполняться до тех пор, пока включающая сопрограмма не станет активной.Вызов метода pause
сделает эту сопрограмму неактивной, поэтому она остановит цикл, и код внутри нее перестанет повторяться.
В петле
Первое, что делает цикл, — это вычисляет, сколько времени прошло с последней итерации, и оно может варьироваться от компьютера к компьютеру. Значение timePassed
будет больше на более медленных ПК и меньше на самых быстрых. Очевидно, что чем ниже, тем лучше, но игроки вряд ли заметят разницу, если игровой цикл может выполнять не менее 30 итераций в секунду.Было бы даже разумно ограничить максимальное количество кадров 60 в секунду, чтобы убедиться, что мы не тратим больше вычислительной мощности, чем нам действительно нужно, чтобы игра работала плавно.
Теперь, когда мы знаем, сколько времени прошло с предыдущей итерации, мы можем вызвать методы update
и draw
, чтобы обновить состояние игры и отобразить это состояние на экране. Мы также можем получить объект Graphics2D
из нашего Canvas
, который может использоваться нашими сценами для выполнения различных операций рисования.
И последний шаг, который должен сделать наш цикл, — это вызвать метод BufferStrategy.show
, чтобы уведомить другие компоненты пользовательского интерфейса о том, что кадр готов к отображению.
Заключение
Теперь у нас есть игровой цикл, модуль ввода и класс Scene
для отображения наших кадров. Все это связано и управляется классом Game
. Единственное, чего не хватает нашему маленькому фреймворку, — это фактическое окно. Игра должна жить в окне, и это то, что мы собираемся реализовать дальше.
Игровая фабрика
У нас уже есть набор компонентов для управления игровым циклом, рисования на экране и обработки входных событий, так зачем нам что-то еще? Две ключевые вещи все еще отсутствуют:
- Нам нужно место для создания экземпляра нашей игры
- Нашей игре нужно окно, поэтому мы должны его предоставить
Иллюстрация Билла Оксфорда
Давайте добавим класс GameFactory
, который будет выполнять эти задачи за нас:
импорт java.awt.Dimension
импорт javax.swing.WindowConstants
импортировать java.awt.BorderLayout
импорт javax.swing.JFrame
object GameFactory {
весело создавать (
screenSize: Размер,
windowTitle: Строка
): Game {
val game = Игра (размер экрана)
JFrame (). Apply {
title = windowTitle
isVisible = true
layout = BorderLayout ()
добавить (игра)
pack ()
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
setLocationRelativeTo (нуль)
}
игра.createBufferStrategy (2)
game.requestFocus ()
вернуться в игру
}
}
Как это работает?
Этот класс принимает в качестве аргументов размер экрана и заголовок окна. Оба они могут отличаться от игры к игре, поэтому нам не следует жестко кодировать ее, чтобы сделать эту фабрику многоразовой.
Наша игра не может появиться на экране, если нет окна для ее размещения. В приведенном выше коде для создания игрового окна используется JFrame
. Мы также должны убедиться, что игра видна и что размер окна по умолчанию не изменяется.Единственная строка, которая кажется немного странной, — это setLocationRelativeTo (null)
, и это просто означает, что мы хотим, чтобы наше игровое окно располагалось прямо в центре экрана компьютера.
Последний шаг — создать буферную стратегию и запросить фокус ввода, чтобы наша игра могла получать события ввода с клавиатуры.
Запуск игры
Давайте создадим новый файл и назовем его Main.kt
, который будет служить точкой входа в нашу игру. Вот код:
импорт java.awt.Dimension
fun main () {
val game = GameFactory.create (
screenSize = Размер (660, 660),
windowTitle = "Змея"
)
game.play ()
}
Теперь мы можем запустить наш игровой движок и посмотреть, работает ли он, так что не стесняйтесь делать это. Вы должны увидеть пустое белое окно в центре экрана.
Заключение
Наш «игровой движок» запущен и работает, а это значит, что мы можем начать его использовать. Наша следующая цель — создать простую сцену, чтобы продемонстрировать, как рисовать на экране и обрабатывать вводимые пользователем данные.
Главное меню
В большинстве игр есть главное меню, потому что мы не всегда можем знать, что игрок хочет делать, когда запускает игру. Игрок может захотеть начать новую игру, загрузить сохраненные игровые данные или изменить настройки игры, но у нас будет очень простое меню в игре «Змейка», в котором будет только одна опция: начать новую игру.
Главное меню — это просто сцена
, поэтому в нем уже есть возможность рисовать на экране, а также обрабатывать ввод данных пользователем.Давайте сначала посмотрим весь код:
импорт java.awt.Color
import java.awt.Font
импортировать java.awt.Graphics2D
import java.awt.event.KeyEvent
импорт java.util.concurrent.TimeUnit
class MainMenuScene (игра: Game): Scene (игра) {
private val primaryFont = Font ("По умолчанию", Font.BOLD, 30)
private val secondaryFont = Font ("По умолчанию", Font.PLAIN, 20)
переопределить забавное обновление (timePassed: Long, timeUnits: TimeUnit) {
game.input.consumeEvents (). forEach {
если (это Input.Event.KeyPressed && it.data.keyCode == KeyEvent.VK_ENTER) {
game.scene = GameScene (игра)
}
}
}
override fun draw (graphics: Graphics2D) {
graphics.apply {
color = Color.black
fillRect (0, 0, game.screenSize.width, game.screenSize.height)
font = primaryFont
color = Color.white
val name = "Змея"
drawString (
название,
game.screenSize.width / 2 - fontMetrics.stringWidth (имя) / 2,
game.screenSize.height / 2 - 50
)
font = secondaryFont
color = Color.gray
val message = "Нажмите Enter, чтобы продолжить"
drawString (
сообщение,
game.screenSize.width / 2 - fontMetrics.stringWidth (сообщение) / 2,
game.screenSize.height / 2 + 50
)
}
}
}
Недвижимость
Давайте исследуем все свойства, объявленные в этом классе:
private val primaryFont = Font ("По умолчанию", Font.ЖИРНЫЙ, 30)
private val secondaryFont = Font ("По умолчанию", Font.PLAIN, 20)
Оба свойства имеют один и тот же тип: Шрифт
. Мы могли бы объявить их в методе draw
, потому что это единственный метод, который их использует, но мы должны помнить, что метод draw
обычно вызывается несколько раз в секунду, поэтому каждый раз инициализировать сложные объекты крайне расточительно. вызывается метод.
Обновление
переопределить забавное обновление (timePassed: Long, timeUnits: TimeUnit) {
игра.input.consumeEvents (). forEach {
если (это Input.Event.KeyPressed && it.data.keyCode == KeyEvent.VK_ENTER) {
game.scene = GameScene (игра)
}
}
}
Этот метод довольно прост, потому что единственное, что нам нужно сделать, это просканировать все нажатые клавиши и проверить, не является ли какая-либо из них клавишей ввода. В случае нажатия клавиши ввода мы должны перейти к следующему экрану. Вы можете создать пустую сцену и назвать ее GameScene
, чтобы избежать ошибок компиляции.
Тираж
Давайте посмотрим на метод розыгрыша
:
переопределить забавное рисование (графика: Graphics2D) {
graphics.apply {
color = Color.black
fillRect (0, 0, game.screenSize.width, game.screenSize.height)
font = primaryFont
color = Color.white
val name = "Змея"
drawString (
название,
game.screenSize.width / 2 - fontMetrics.stringWidth (имя) / 2,
game.screenSize.height / 2 - 50
)
font = secondaryFont
color = Цвет.серый
val message = "Нажмите Enter, чтобы продолжить"
drawString (
сообщение,
game.screenSize.width / 2 - fontMetrics.stringWidth (сообщение) / 2,
game.screenSize.height / 2 + 50
)
}
}
В первой строке задается цвет Color.black
, что означает, что все, что мы собираемся рисовать дальше, будет черного цвета. Это, конечно, не относится к изображениям, но повлияет на цвет геометрических примитивов, а также на шрифты.
Вторая строка просто заполняет экран заданным ранее цветом.
Следующие два блока более сложны, но они просто повторяют один и тот же шаблон дважды:
- Установить шрифт
- Установить цвет
- Нарисовать текст
Мы используем разные шрифты для названия игры и для подсказки, но мы просто повторяем одни и те же шаги, разница только в значениях.
Тестирование
Давайте проверим эту сцену, чтобы убедиться, что она работает.Теперь мы можем назначить его нашему экземпляру Game
в Main.kt
:
пакет com.bubelov.snake
импорт com.bubelov.snake.engine.GameFactory
импорт com.bubelov.snake.scene.MainMenuScene
импортировать java.awt.Dimension
fun main (args: Array ) {
val screenSize = Размер (660, 660)
val game = GameFactory.create (размер экрана)
game.scene = MainMenuScene (игра)
game.play ()
}
А теперь запустим нашу игру. Вы должны увидеть следующий экран:
Заключение
Теперь мы знаем основы создания сцен, а также у нас есть игровое меню.Далее мы собираемся определить игровую модель.
Модель
Нам нужен слой модели, чтобы определить, какие объекты будут существовать в нашей игре. Мы можем определить любой объект, но поскольку мы работаем над игрой в змейку, имеет смысл начать с самого необходимого: змейки и яблока. Не стесняйтесь добавлять больше объектов, таких как бонусы, если вы думаете, что это сделает игру более интересной.
Иллюстрация Свена Мике
Змея Модель
Как можно изобразить змею? Как вы, возможно, уже знаете, традиционная змея выглядела как цепочка блоков, которая постоянно движется в определенном направлении.Каждая часть змеи имеет свое собственное положение, и это положение меняется при движении змеи.
Давайте создадим новый класс и назовем его SnakeBodyPart
:
класс данных SnakeBodyPart (
var x: Int,
var y: Int
)
Этот класс представляет собой одну часть змеи. Обычно в начале игры змея довольно короткая, но становится длиннее, когда ест яблоки. Каждое яблоко, съеденное нашей змеей, добавляет к своему телу еще одну часть. Единственное, что мы хотим знать о каждой части тела, — это ее положение.
Нужно ли нам что-то еще, кроме списка частей тела, чтобы описать змею? Оказывается, змея немного умнее, чем сумма ее частей. По крайней мере, он должен иметь направление и двигаться в соответствии с выбранным направлением.
Давайте создадим класс Snake
, который будет координировать движение всех частей тела:
класс Змея (
startX: Int,
startY: Int,
var direction: Direction = Direction.RIGHT
) {
val body = mutableListOf ()
val head от ленивого {body [0]}
в этом {
body + = SnakeBodyPart (startX, startY)
body + = SnakeBodyPart (
x = startX - направление.deltaX (),
y = startY - direction.deltaY ()
)
body + = SnakeBodyPart (
x = startX - direction.deltaX () * 2,
y = startY - direction.deltaY () * 2
)
}
fun move () {
for (i in body.size - 1 downTo 1) {
val current = body [i]
val (x, y) = тело [i - 1]
current.x = x
current.y = y
}
head.x = head.x + direction.deltaX ()
head.y = head.y + direction.deltaY ()
}
}
Этот класс обрабатывает создание змеи в определенном месте, а также перемещает ее в любом определенном направлении.Единственная отсутствующая часть — это Direction
enum:
enum class Direction {
Вверх,
ВЕРНО,
ВНИЗ,
СЛЕВА;
fun deltaX (): Int {
вернуться когда (это) {
ВЛЕВО -> -1
ВПРАВО -> 1
иначе -> 0
}
}
fun deltaY (): Int {
вернуться когда (это) {
ВВЕРХ -> 1;
ВНИЗ -> -1;
иначе -> 0;
}
}
}
Обратите внимание, что у нас есть 2 вспомогательных метода для вычисления того, как конкретное направление влияет на координаты x и y змеи.Например, направление ВВЕРХ даст deltaY = -1
и deltaX = 0
.
Apple Модель
Модель яблока очень простая. Единственное, что нам нужно знать, это положение яблока, которое можно описать как пару целых чисел (x и y):
класс данных Apple (
значение x: Int,
val y: Int
)
Заключение
Теперь у нас есть модель, которая описывает все объекты, которые могут существовать в нашем игровом мире. Затем мы разместим эти объекты на игровом экране и заставим их взаимодействовать с игроком и друг с другом.
Игровая сцена
Теперь, когда у нас есть все необходимые компоненты, мы можем начать работать над самой игровой механикой. В этом посте мы собираемся создать класс GameScene
, главную сцену, где происходит большая часть действия.
Иллюстрация Маноса Гкикаса
Механизм
Давайте подумаем о движении змеи. Когда он должен двигаться? Нам нужны реальные числа. Самый простой способ переместить змейку — вызывать ее метод move
при каждом обновлении сцены.Это может показаться хорошей идеей, но давайте подумаем, сколько раз этот метод должен вызываться? Ответ: мы не знаем и не можем знать, скорее всего, будет слишком часто звонить. Это не только зависит от машины к машине, но также может вызываться сотни раз в секунду, что приведет к слишком быстрому перемещению змеи, что сделает нашу игру неиграбельной.
Наш игровой мир — это просто сетка квадратов, поэтому квадрат кажется большой единицей расстояния. Как мы можем описать скорость? Единицы измерения расстояния в секунду звучат разумно, поэтому давайте уточним наши требования:
Змея должна двигаться с фиксированной скоростью 1 квадрат за 300 миллисекунд, примерно 3 квадрата в секунду.Скорость можно увеличивать по ходу игры, смело играйте с этим параметром.
Как мы можем добиться такого поведения? Я предлагаю сделать предположение, что сцена будет обновляться быстрее, чем 3 раза в секунду, поэтому нам нужно перемещать змейку при некоторых обновлениях, но оставлять ее неподвижной во время других вызовов того же метода.
В основном нам нужен таймер. Давайте добавим новый класс и назовем его Timer
:
class Timer (длительность частного значения: Long) {
частная переменная оставшееся время = продолжительность
забавное обновление (timePassed: Long) {
оставшееся время - = время прошло
}
весело timeIsUp () = Оставшееся время <= 0
fun reset () {
оставшееся время = продолжительность
}
}
Этот класс также имеет метод обновления, как и наша сцена, но он содержит только оставшееся время до его запуска, поэтому мы можем продолжать его обновлять, но он не будет запущен, пока не придет время.Если время истекло, мы должны выполнить отложенное событие и сбросить таймер (если мы хотим, чтобы он повторялся).
Игровая сцена
Похоже, теперь у нас есть все для реализации игровой сцены, давайте добавим новый класс и назовем его GameScene
:
импорт java.awt.Color
импортировать java.awt.Graphics2D
import java.awt.event.KeyEvent
импорт java.util.concurrent.TimeUnit
class GameScene (игра: Game): Scene (игра) {
private val snake = Змея (
startX = WORLD_WIDTH / 2,
startY = WORLD_HEIGHT / 2
)
private lateinit var яблоко: яблоко
private val snakeMoveTimer = Таймер (TimeUnit.МИЛЛИСЕКУНД в Нано (300))
в этом {
местоApple ()
}
переопределить забавное обновление (timePassed: Long, timeUnits: TimeUnit) {
if (gameIsOver ()) {
// game.scene = GameOverScene (game) TODO Реализовать позже
возвращение
}
processInput ()
snakeMoveTimer.update (timePassed)
if (snakeMoveTimer.timeIsUp ()) {
snake.move ()
val head = snake.head
if (head.x <1) {
head.x = WORLD_WIDTH
}
если (глава.x> WORLD_WIDTH) {
head.x = 1
}
if (head.y <1) {
head.y = WORLD_HEIGHT
}
if (head.y> WORLD_HEIGHT) {
head.y = 1
}
if (head.x == apple.x && head.y == apple.y) {
val body = snake.body
val lastPart = body [body.size - 1]
body.add (SnakeBodyPart (lastPart.x, lastPart.y))
местоApple ()
}
snakeMoveTimer.сброс()
}
}
override fun draw (graphics: Graphics2D) {
graphics.apply {
color = Color.black
fillRect (0, 0, game.width, game.height)
drawSnake (это)
drawApple (это)
}
}
private fun processInput () {
for (событие в game.input.consumeEvents ()) {
when (event) {
это Input.Event.KeyPressed -> {
когда (event.data.keyCode) {
KeyEvent.VK_UP -> змейка.direction = Direction.UP
KeyEvent.VK_RIGHT -> snake.direction = Direction.RIGHT
KeyEvent.VK_DOWN -> snake.direction = Направление.DOWN
KeyEvent.VK_LEFT -> snake.direction = Direction.LEFT
}
}
}
}
}
приватное развлечение drawSnake (графика: Graphics2D) {
graphics.apply {
color = Color.green
snake.body.forEach {часть ->
fillRect (
часть.x * CELL_SIZE - CELL_SIZE,
game.screenSize.height - part.y * CELL_SIZE,
CELL_SIZE,
CELL_SIZE
)
}
}
}
приватное развлечение drawApple (графика: Graphics2D) {
graphics.apply {
color = Color.red
fillRect (
apple.x * CELL_SIZE - CELL_SIZE,
game.screenSize.height - apple.y * CELL_SIZE,
CELL_SIZE,
CELL_SIZE
)
}
}
частное место для развлеченийApple () {
var x = (1 + (Мат.случайный () * МИР_ШИНА)). toInt ()
var y = (1 + (Math.random () * WORLD_HEIGHT)). toInt ()
while (! isCellEmpty (x, y)) {
if (x
if (index> 0 && part.x == snake.head.x && part.y == snake.head.y) {
вернуть истину
}
}
вернуть ложь
}
companion object {
const val WORLD_WIDTH = 12
const val WORLD_HEIGHT = 12
const val CELL_SIZE = 55
}
}
Давайте рассмотрим этот код строка за строкой, чтобы лучше понять, что происходит:
private val snake = Змея (
startX = WORLD_WIDTH / 2,
startY = WORLD_HEIGHT / 2
)
private lateinit var яблоко: яблоко
private val snakeMoveTimer = Таймер (TimeUnit.МИЛЛИСЕКУНД в Нано (300))
в этом {
местоApple ()
}
Здесь мы просто создаем экземпляры нашей змейки и яблока и настраиваем таймер движения змейки, чтобы убедиться, что она не будет работать слишком быстро.
переопределить забавное обновление (timePassed: Long, timeUnits: TimeUnit) {
if (gameIsOver ()) {
// game.scene = GameOverScene (game) TODO Реализовать позже
возвращение
}
processInput ()
snakeMoveTimer.update (timePassed)
if (snakeMoveTimer.timeIsUp ()) {
snake.move ()
val head = змея.голова
if (head.x <1) {
head.x = WORLD_WIDTH
}
if (head.x> WORLD_WIDTH) {
head.x = 1
}
if (head.y <1) {
head.y = WORLD_HEIGHT
}
if (head.y> WORLD_HEIGHT) {
head.y = 1
}
if (head.x == apple.x && head.y == apple.y) {
val body = snake.body
val lastPart = body [body.size - 1]
body.add (SnakeBodyPart (lastPart.x, lastPart.y))
местоApple ()
}
snakeMoveTimer.сброс()
}
}
Эта часть поинтереснее. Первое, что нам нужно делать при каждом обновлении, - это убедиться, что в игру по-прежнему можно играть. Если игра окончена (змея съела себя), нам нужно перейти на сцену GameOver
, которая будет реализована в следующем посте.
Следующим шагом является обработка пользовательского ввода. После этого нам нужно обновить таймер движения змейки и проверить, не пора ли переместить змейку. В случае, если нам нужно переместить змейку, мы также должны убедиться, что она остается в пределах экрана.Вот почему нам нужны эти 4 проверки положения головы.
Мы также должны проверить, достигла ли змея яблока. В этом случае нам нужно разместить другое яблоко где-нибудь на игровом экране, а также удлинить длину змейки.
Перейдем к следующему способу:
переопределить забавное рисование (графика: Graphics2D) {
graphics.apply {
color = Color.black
fillRect (0, 0, game.width, game.height)
drawSnake (это)
drawApple (это)
}
}
Здесь ничего интересного, мы просто заливаем экран черным цветом и рисуем змейку и яблоко.Вперед:
private fun processInput () {
for (событие в game.input.consumeEvents ()) {
when (event) {
это Input.Event.KeyPressed -> {
когда (event.data.keyCode) {
KeyEvent.VK_UP -> snake.direction = Direction.UP
KeyEvent.VK_RIGHT -> snake.direction = Direction.RIGHT
KeyEvent.VK_DOWN -> snake.direction = Направление.DOWN
KeyEvent.VK_LEFT -> змейка.direction = Direction.LEFT
}
}
}
}
}
Здесь мы анализируем нажатые клавиши и устанавливаем направление змейки в соответствии с вводом пользователя.
приватное развлечение drawSnake (графика: Graphics2D) {
graphics.apply {
color = Color.red
snake.body.forEach {часть ->
fillRect (
part.x * CELL_SIZE - CELL_SIZE,
game.screenSize.height - part.y * CELL_SIZE,
CELL_SIZE,
CELL_SIZE
)
}
}
}
приватное развлечение drawApple (графика: Graphics2D) {
графика.применять {
color = Color.green
fillRect (
apple.x * CELL_SIZE - CELL_SIZE,
game.screenSize.height - apple.y * CELL_SIZE,
CELL_SIZE,
CELL_SIZE
)
}
}
Эти методы используются для рисования игровых объектов. Как видите, мы можем просто заполнить прямоугольники разными цветами, чтобы различать тело змеи и яблоки: тело змеи зеленое, а яблоки красные.
частное место развлеченийApple () {
var x = (1 + (Мат.случайный () * МИР_ШИНА)). toInt ()
var y = (1 + (Math.random () * WORLD_HEIGHT)). toInt ()
while (! isCellEmpty (x, y)) {
if (x
Этот блок может быть немного сложнее для понимания.По сути, нам нужно найти пустую ячейку, чтобы разместить здесь новое яблоко, но местоположение нельзя предсказать, поэтому мы должны начать со случайной точки. Если эта точка пуста - это все, но если это не так, нам нужно сканировать нашу игровую сетку построчно, чтобы найти пустую ячейку. В этом случае мы должны использовать первую пустую ячейку, найденную при сканировании.
private fun gameIsOver (): Boolean {
if (snake.body.size == WORLD_WIDTH * WORLD_HEIGHT) {
вернуть истину
}
змея. тело.forEachIndexed {индекс, часть ->
if (index> 0 && part.x == snake.head.x && part.y == snake.head.y) {
вернуть истину
}
}
вернуть ложь
}
Что такое состояние "игра окончена"? Есть 2 возможных исхода:
- выигрыш (все пространство заняла змея)
- проигрывает (змея пыталась съесть себя)
Это именно то, что мы проверяем.
Запуск игры
Давай запустим нашу игру и попробуем поиграть.Вы должны увидеть такую картинку:
Не забудьте раскомментировать переход экрана в MainMenuScene
.
Заключение
Наша игра почти готова, нам просто нужно добавить сцену «игра окончена» и подумать над вещами, которые мы реализовали до сих пор.
Завершение игры
Иллюстрация Мэтта Ботсфорда
Игра окончена
Для завершения нашей игры нужна еще одна сцена: GameOverScene
, давайте добавим ее в наш проект:
импорт java.awt.Color
import java.awt.Font
импортировать java.awt.Font.BOLD
импортировать java.awt.Graphics2D
import java.awt.event.KeyEvent
импорт java.util.concurrent.TimeUnit
class GameOverScene (игра: Game): Scene (игра) {
переопределить забавное обновление (timePassed: Long, timeUnits: TimeUnit) {
game.input.consumeEvents (). forEach {
когда это) {
это Input.Event.KeyPressed -> {
когда (it.data.keyCode) {
KeyEvent.VK_ENTER -> game.scene = GameScene (игра)
}
}
}
}
}
override fun draw (graphics: Graphics2D) {
графика.применять {
color = Color.black
fillRect (0, 0, game.screenSize.width, game.screenSize.height)
font = Font («По умолчанию», ЖИРНЫЙ, 16)
color = Color.white
val message = "Нажмите , чтобы начать новую игру"
val messageBounds = fontMetrics.getStringBounds (сообщение, это)
val messageWidth = messageBounds.width.toInt ()
val messageHeight = messageBounds.height.toInt ()
drawString (
сообщение,
игра.screenSize.width / 2 - ширина сообщения / 2,
game.screenSize.height / 2 - messageHeight / 2
)
}
}
}
Здесь происходит 2 основных этапа:
- Сканирование пользовательского ввода
- Отрисовка текста подсказки в центре нашей новой сцены
Давайте рассмотрим эти шаги один за другим:
override fun update (nanosecondsPassed: Long) {
game.input.consumeEvents (). forEach {
когда это) {
это Ввод.Event.KeyPressed -> {
когда (it.data.keyCode) {
KeyEvent.VK_ENTER -> game.scene = GameScene (игра)
}
}
}
}
}
Этот шаг довольно прост, нам нужно просмотреть все входные события, чтобы узнать, была ли нажата клавиша ENTER. Нажатие клавиши ENTER означает, что мы должны перейти к GameScene
и перезапустить нашу игру.
Перейдем к следующему шагу:
переопределить забавное рисование (графика: Graphics2D) {
графика.применять {
color = Color.black
fillRect (0, 0, game.screenSize.width, game.screenSize.height)
font = Font («По умолчанию», ЖИРНЫЙ, 16)
color = Color.white
val message = "Нажмите , чтобы начать новую игру"
val messageBounds = fontMetrics.getStringBounds (сообщение, это)
val messageWidth = messageBounds.width.toInt ()
val messageHeight = messageBounds.height.toInt ()
drawString (
сообщение,
game.screenSize.width / 2 - ширина сообщения / 2,
игра.screenSize.height / 2 - messageHeight / 2
)
}
}
Первые 2 строки отвечают за заливку экрана черным цветом. Следующие две строки просто инициализируют шрифт, который мы хотим использовать для рисования текста, а остальной код отвечает за его рисование. К счастью для нас, Java SDK предоставляет нам метод getStringBounds
, который может предсказать размер текста, который мы собираемся нарисовать. Используя эти показатели, мы можем разместить наш текст в центре экрана:
Заключение
В этой серии статей мы рассмотрели основы разработки игр на Kotlin, а также создали полностью игровую игру про змейку.
В разработке игр есть гораздо больше, чем просто это. Вот шаги, которые я рекомендую, если вы хотите пойти дальше (порядок не имеет значения):
Узнайте, как создавать графику (растровую, векторную, 3D-объекты, преобразованные в 2D-изображения, не имеет значения).
Узнайте, как создавать и добавлять звуковые эффекты и музыку в вашу игру.
Изучите игровой движок, лучший выбор зависит от вашего опыта программирования. Я бы порекомендовал Game Maker для новичков, LibGDX для людей с хорошими навыками программирования и что-то вроде Unity, если вы хотите продать свою игру или продолжить карьеру в разработке игр.
Скопируйте несколько успешных игр с простой механикой. Это может научить вас многому в искусстве создания игр.
Не будьте к себе слишком строги. Есть множество вещей, которым нужно научиться, но это того стоит, только если вам нравится процесс!
Snake Game - Сыграйте в классическую онлайн-игру «Змейка»
Мы живем в мире, наводненном цифровыми гаджетами, и большинство из них поддерживают видеоигры. Сотни видеоигр выпускаются каждый год, но очень мало игр, которые проходят проверку временем.
Snake Game - одна из видеоигр, получивших широкую популярность, вероятно, намного превосходящую ожидания создателей этой игры.
Очень сложно найти человека, который никогда не играл в эту игру, потому что ее вариации доступны практически на каждом электронном устройстве.
Когда игрок играет в змейку, он управляет змейкой, которая ползает по сложной стене, которая обычно является границей экрана.
В некоторых играх есть препятствия внутри составной стены, тогда как в обычной версии никаких препятствий между ними нет.
В разных частях игровой зоны появятся новые кубики и бонусные лакомства, и стратегия состоит в том, чтобы съесть все это, обеспечивая плавное движение змеи.
Движение змеи не должно ограничиваться препятствием или ее собственным телом. В начале игры управлять движением змеи очень легко, но когда ваша змея ест различные предметы, размещенные на экране, змеи становятся больше, и навигация по змее становится очень сложной.
Существуют разные уровни, и когда игрок переходит на более высокий уровень, змея движется быстрее, что создает дополнительные трудности для навигации по ней.
Имея опыт и правильные игровые стратегии, игроки могут «кормить» своих змей в очень большой степени, и тело змеи будет покрывать более половины всего экрана.
Однако эта игра требует большого мастерства, особенно способности принимать решения. Успешный игрок в змейку - тот, кто может принимать правильные решения и реализовывать их за доли секунды.
Игра развивалась в середине 1970-х годов и широко использовалась как аркадная игра.Благодаря простой графике и удобному интерфейсу, эта игра привлекла как игроков начального уровня, так и профессиональных игроков, поскольку игра удовлетворяла потребности игроков с различным уровнем навыков.
Главная интересная особенность игры заключается в том, что змея всегда находится в движении после начала игры. Игрок может только контролировать движение змеи, но не может остановить ее движение.
Из-за этого игроку нужно было уделять постоянное внимание на протяжении всей игры, что принесло игре большую популярность.Когда популярность возросла, Trefonas, американский производитель игр для микрокомпьютеров, представил микрокомпьютерную версию snake в 1978 году.
В игру играли на микрокомпьютере TRS-80 и тепло встретили энтузиасты, которые искали простые, но сложные игры.
BBC Micro, микрокомпьютеры, разработанные Acom Computers для Британской радиовещательной корпорации, были оснащены змейкой. Игра была цветной версией, и BBC micro показала различные уровни скорости, когда длина змеи достигает определенного уровня.
Управлять длинной змеей, движущейся с относительно увеличивающейся скоростью в игре, было непростой задачей, и многим игрокам просто понравилась игра из-за того, что в ней требовалось сосредоточенное мышление.
В мобильных телефонах Nokia эта игра была одной из игр по умолчанию, и, несомненно, это одна из самых значительных вех в истории игры. Даже Nokia не ожидала, что игра получит такую широкую популярность, потому что было много игр для мобильных телефонов с более сложной темой игрового процесса.
Однако большинство людей предпочли придерживаться Snake Game на мобильных телефонах, а некоторые предполагают, что это могло даже способствовать продажам Nokia.
Эксперты предполагают, что мобильные телефоны открывали дорогу людям разных возрастных групп, чтобы получить удовольствие от игр с электроникой, а в змейку играть было проще всего. Многие эксперты считают, что это самая убедительная причина популярности этой игры.
Игра «Змейка» оставалась на мобильных телефонах Nokia очень долго, даже после того, как они стали цветными из-за своей популярности.Даже сегодня на некоторых телефонах есть эта игра с небольшими изменениями по сравнению с исходной версией.
Даже первые разработчики этой игры не смогли создать успешный эталон в истории аркадных игр. Есть пара игр, которые превзошли все ожидания и стали бессмертными, примером которых является Snake. Гремлин, который представил змею как аркадную игру, не смог оказать сколько-нибудь заметного влияния на рынке видеоигр даже после перехода в индустрию консольных игр.
Компания была объединена с Sega и позже оказалась перед банкротством.Тем не менее, Гремлин всегда может гордиться своим чудесным творением, которое собрало миллионы фанатов по всему миру.
AxySnake - Домашняя страница
Обзор игры
AxySnake - это трехмерная игра в жанре экшн, основанная на известной игре «Змейка». Дав классической Змеи новое измерение, мы дали ей новую жизнь. Трехмерные графические эффекты усиливаются стереозвуком! В игру включено множество замечательных музыкальных треков.Пугающие монстры и разнообразные бонусы, выполненные в полном 3D, выводят старую игру на совершенно новый уровень. Полный дистрибутив AxySnake включает 6 типов игр. Всего в игре 80 различных уровней!
Щелкните изображение, чтобы просмотреть полноразмерный снимок экрана в новом всплывающем окне.
Правила игры
Каждая игра состоит из нескольких уровней. Для прохождения уровня необходимо накормить змею и заставить ее вырасти до определенной длины.Когда змея достигнет необходимой длины, ваш друг принесет вам ключ на следующий уровень. Змея должна съесть этот ключ, чтобы открылась дверь на следующий уровень! Не ешьте камни. Они вредны для здоровья змеи. Избегайте темных участков леса, где прячутся опасные монстры. Быть внимательным. На разных уровнях обитают опасные насекомые. Собирая призы, вы можете увеличить свои жизни, свой счет и время.
Системные требования
Скачать Обзор
Вы можете БЕСПЛАТНО загрузить пробную версию AxySnake. Программа представляет собой самораспаковывающийся исполняемый файл «AxySnake.exe (размер файла около 4,5 МБ).
Скачанную программу необходимо настроить. Просто запустите его, чтобы начать процесс установки.Следовать инструкциям. Удачи в нашей игре!
Обзор регистрации
AxySnake распространяется на условно-бесплатной основе. Вы можете зарегистрировать свою копию по цене 10 долларов США.
После подтверждения платежа мы вышлем вам регистрационный ключ, который снимает все ограничения в игре. Ваша регистрация действительна для всех будущих версий AxySnake.
Онлайн-заказ
Вы можете приобрести AxySnake онлайн.Оплата кредитной картой - самый быстрый и простой способ. Все регистрации используют протоколы SECURE. Третья сторона не может перехватить информацию о вашей кредитной карте.
Нажмите эту кнопку, чтобы купить AxySnake онлайн |
Также онлайн-заказ поддерживает следующие способы оплаты: PayPal, банковский / банковский перевод, чек / денежный перевод, факс, счет-фактура.
Заказы по телефону
При заказе по телефону взимается дополнительная плата в размере 3 долларов США.00 долларов США за обработку заказа. НЕ используйте эти номера телефонов для вопросов технической поддержки (только для целей заказа).
Вы можете круглосуточно позвонить в Центр обслуживания клиентов по телефону
.1-877-353-7297 (США и Канада; бесплатно)
+ 1-952-646-5331 (Другие страны; обычный)
Подготовьте данные вашей кредитной карты.
Название продукта: AxySnake.
Артикул: 4174-5.
и nbsp
Играть в змейку онлайн бесплатно
Добро пожаловать в гулпер.io, современное сочетание классической змейки и игр Tron lightcycles, но на этот раз против реальных игроков. Он запускается прямо в вашем браузере, поэтому вам не нужно ничего скачивать - просто введите свой ник и играйте! В этой игре вы управляете скользящей змеей или червяком, или как вы это называете; Ваша цель - собрать как можно больше энергии и не позволять другим вас останавливать! Избегайте столкновений, будьте умны со своей стратегией, объединяйтесь с другими игроками и пробейтесь к вершине глобальной таблицы лидеров!Как играть
Элементы управления следующие: переместите мышь, чтобы изменить направление движения, и удерживайте левую кнопку мыши или клавишу пробела, чтобы увеличить скорость.По мере того как вы ускоряетесь, ваша длина постепенно уменьшается, как и ваш счет.Игра также адаптирована для игры на устройствах с сенсорным экраном: проведите пальцем, чтобы перемещаться, и нажмите в любом месте другим пальцем и удерживайте, чтобы ускориться.
Комнаты
Вы можете присоединиться или создать конкретную комнату, введя ее имя в поле ввода «Псевдоним» после самого псевдонима и символа @ (например, Player @ MyRoom). Если указанная комната существует на автоматически выбранном игровом сервере, и комната еще не заполнена, вы присоединитесь к ней.Есть два типа комнат: общие и частные. Общие комнаты создаются автоматически, и игрок присоединяется к ним, только когда вводит ник.
Игроки могут создавать отдельные комнаты для игры со своими друзьями, вводя то же имя комнаты после символа @. Если достаточное количество игроков попытается присоединиться к частной комнате в течение определенного промежутка времени, частная комната будет создана, и ожидающие игроки присоединятся к ней; в противном случае они автоматически присоединятся к одной из публичных комнат (той же самой) на игровом сервере.