TL; DR # 1: Ненавижу чтение? Посмотрите видео (пожалуйста, подпишитесь, если вам нравится контент)
TL; DR # 2: Просто хотите код? Возьмите его здесь: https://github.com/lepunk/react-native-videos/tree/master/Snake
Snake - классическая видеоигра, ставшая популярной благодаря телефонам Nokia в 90-х годах. Геймплей прост: вы управляете головой змеи, и ваша цель - съесть еду. Каждый раз, когда вы едите пищу, у змеи растет хвост. (в классической версии) Если ударишься о стену или ударишься собственным хвостом, ты умрешь.
Для меня эта игра вызывает особую ностальгию, так как это была первая игра, которую я успешно клонировал в Turbo Pascal в первый год обучения в средней школе. Я даже продал пару экземпляров одноклассникам. #выгода
Чтобы воссоздать эту игру, я решил использовать (насколько мне известно) единственный игровой движок, доступный в настоящее время для React Native: react-native-game-engine
Сам движок довольно прост, но он реализует основы, необходимые для разработки игры:
- Игровой цикл
- Система рендеринга сущностей
- Он обрабатывает события касания
- Он имеет возможность отправлять и получать настраиваемые события в движок и из него.
Установить двигатель просто:
npm install --save react-native-game-engine
Движок требует, чтобы вы определили две вещи:
- Набор сущностей, которые будут отображаться при каждом тике.
- Одна или несколько «систем». По сути, это инструкции, которые должны выполняться на каждом тике.
Наши организации
Змейка супер простая. Если задуматься, нам нужно реализовать всего 3 сущности.
Голова
«Голова» будет персонажем, которым управляет наш игрок. У него будут атрибуты xspeed, yspeed, position и size. В нашем приложении размер будет постоянным. position будет списком координат [x, y]. xspeed и yspeed могут принимать значения 1, 0 или -1. Если xspeed равен 1 или -1, тогда yspeed должен быть 0, и аналогично, если yspeed равен 1 или -1, тогда xspeed должно быть равно нулю (змейка не может идти по диагонали). Мы реализуем рендеринг Head в компоненте ‹Head /›
Хвост
Хвост будет представлять собой список блоков на экране, следующих за нашей головой. Этот хвост будет расти по мере того, как голова будет есть все больше и больше еды. Он будет иметь опору size (которая будет константой) и опору elements, которая изначально будет пустым списком, и по мере того, как Голова ест Еду, мы будем продолжать добавлять [x, y] координаты к нему.
Еда
Наша Еда будет размещена на игровой площадке случайным образом и останется на месте до тех пор, пока Голова не столкнется с ней. Как только это произойдет, мы должны повторно отрендерить его в другое случайное место. У него будет свойство position, которое будет списком координат [x, y], и size, которое будет константой.
Давай перейдем к этому
Мне нравится определять некоторые константы в файле Constants.js в своих приложениях, так что давайте сделаем это.
После этого давайте настроим наш index.js с основной сценой и некоторыми кнопками управления.
Хорошо, кое-что мы еще не обсуждали:
- Обратите внимание, что мы используем переменную состояния running и передаем ее GameEngine в качестве опоры. Будет очень полезно остановить игровой цикл, если пользователь умирает.
- Мы сохраняем ссылку на GameEngine в переменной экземпляра this.engine. Мы будем использовать это для вызова определенных методов в GameEngine.
- Мы передаем обработчик события onEvent в GameEngine. Этот метод будет выполняться каждый раз при отправке нового события с помощью GameEngine.dispatch. Диспетчеризация - это способ связи между основным приложением и игровым циклом. Мы будем генерировать разные типы событий.
- Обратите внимание на эти реквизиты в нашей сущности Head: nextMove: 10, updateFrequency: 10
Стандартный игровой цикл близок к 60 кадрам в секунду. Мы не хотим обновлять положение головы на каждом тике, поэтому мы будем использовать nextMove и updateFrequency, чтобы замедлить обновление. Подробнее об этом позже. - Я добавил несколько элементов управления на экране. Каждый из них будет генерировать событие с типом «move- *», которое наша система будет прослушивать и соответственно изменять x и yspeed головы.
Помимо этих моментов, я думаю, что код довольно понятен.
Рендеринг
Отрисовка наших сущностей довольно проста. Нам просто нужно создать компоненты для ‹Head /›, ‹Food /› и ‹Tail /›
Голова и Еда очень похожи. Они вполне могут быть одними и теми же компонентами с разными опорами для своего цвета. Но пока давайте будем простыми:
Все, что они делают, - это рендеринг квадрата с заданными координатами x и y. Не совсем ракетостроение.
Tail немного сложнее, так как он должен отображать несколько блоков, но я не думаю, что он требует слишком подробных объяснений:
И это все, что нам нужно для рендеринга наших сущностей.
GameLoop
С помощью response-native-game-engine вы можете определить несколько «систем». Эти системы будут вызываться при каждом «тике» с набором полезных параметров. По сути, они реализуют логику вашей игры.
Они всегда будут получать список сущностей, и при желании они могут получать касания, события и многое другое.
В нашем случае у нас будет только одна система, и мы назовем ее GameLoop.
Самая минимальная реализация будет выглядеть примерно так.
Круто, круто, круто, но ничего не делает. Давай заставим нашу змею двигаться
Хорошо, давайте распакуем это:
- Помните, как мы передали nextMove = 10 и updateFrequency = 10 нашей сущности Head? С каждым тиком мы уменьшаем значение nextMove на единицу. Как только он достигает 0, мы выполняем некоторые вычисления в среде. Таким образом, игра замедляется, чтобы двигаться каждые 10 тиков.
- Сначала мы проверяем, вышла ли голова за пределы игровой сетки. В таком случае мы отправляем событие с типом «игра окончена». Наш метод onEvent index.js прослушает это событие и остановит игру.
- Если голова находится внутри игровой сетки, мы обновляем ее позицию на xspeed и yspeed.
Удивительно, у нас что-то движется, но мы не можем это контролировать. Давайте исправим это:
Все в порядке. Помните, как мы отправляли событие типа «move- *» каждый раз, когда конечный пользователь нажимал одну из кнопок управления? Эти отправленные события передаются в наш игровой цикл. Все, что нам нужно сделать, это изменить x и yspeed в зависимости от ввода пользователя, и наша голова волшебным образом изменит направление.
Время поесть:
На этом этапе мы проверяем, совпадает ли положение нашей головы с положением еды. В таком случае мы делаем две вещи:
- Подготовьте положение еды к элементам нашего хвоста
- Создайте новую случайную позицию для нашей Еды, чтобы она появлялась в новом месте.
Удивительный. Вот только наш Хвост не следует за нашей Головой. Также нам нужно убедиться, что когда Голова попадает в один из элементов Хвоста, игра заканчивается:
Здесь мы просто добавляем текущую позицию головы к элементам хвоста, а затем удаляем последний элемент хвоста. Затем с помощью простого цикла for мы проверяем, не сталкивается ли голова с каким-либо элементом хвоста. В таком случае мы отправляем событие окончания игры.
Я также добавил закомментированный блок кода на случай, если вам нужны элементы управления смахиванием. Моя первоначальная идея заключалась в том, чтобы использовать это в качестве механизма управления, но на реальном устройстве это было очень неудобно, поэтому я отказался от этого.
Это все! Конечно, это всего лишь взлом, и его можно значительно улучшить. Не стесняйтесь присылать PR или форк репозитория GitHub: https://github.com/lepunk/react-native-videos/tree/master/Snake