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 с основной сценой и некоторыми кнопками управления.

Хорошо, кое-что мы еще не обсуждали:

  1. Обратите внимание, что мы используем переменную состояния running и передаем ее GameEngine в качестве опоры. Будет очень полезно остановить игровой цикл, если пользователь умирает.
  2. Мы сохраняем ссылку на GameEngine в переменной экземпляра this.engine. Мы будем использовать это для вызова определенных методов в GameEngine.
  3. Мы передаем обработчик события onEvent в GameEngine. Этот метод будет выполняться каждый раз при отправке нового события с помощью GameEngine.dispatch. Диспетчеризация - это способ связи между основным приложением и игровым циклом. Мы будем генерировать разные типы событий.
  4. Обратите внимание на эти реквизиты в нашей сущности Head: nextMove: 10, updateFrequency: 10
    Стандартный игровой цикл близок к 60 кадрам в секунду. Мы не хотим обновлять положение головы на каждом тике, поэтому мы будем использовать nextMove и updateFrequency, чтобы замедлить обновление. Подробнее об этом позже.
  5. Я добавил несколько элементов управления на экране. Каждый из них будет генерировать событие с типом «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