Как сделать «Жизнь» на хуках React

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Задача разработчика — показать пользователю, как живут и умирают цифровые клетки. Автор воспользовался React и её хуками: управление состоянием и возможность абстрагироваться от логики, связанной с состоянием, позволяют легко читать и понимать проект. Подробностями реализации и кодом на Github делимся, пока у нас начинается курс по Frontend-разработке.


Правила игры

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

  • Любая живая клетка с менее чем двумя живыми соседями умирает от перенаселения.

  • Любая живая клетка с двумя или тремя живыми соседями переходит в следующее поколение.

  • Любая живая клетка с более чем тремя живыми соседями умирает, как при перенаселении.

  • Любая мёртвая клетка с тремя живыми соседями становится живой клеткой, как будто размножаясь.

Попробуйте моё приложение, а затем поговорим о его работе под капотом.

Структура данных, которую я решил использовать для представления ячеек, довольно проста. Вот массив объектов:

Этот массив объектов представляет нашу сетку, а каждый объект представляет отдельную ячейку. Свойство alive — ключевое в игре
Этот массив объектов представляет нашу сетку, а каждый объект представляет отдельную ячейку. Свойство alive — ключевое в игре

Создаём компонент отображения Grid, он накладывается на массив сетки и генерирует индивидуальную ячейку для каждого объекта в массиве сетки:

Grid создаёт для каждого объекта ячейки в массиве сетки ячейку — тег <div>
Grid создаёт для каждого объекта ячейки в массиве сетки ячейку — тег <div>

Видно, что и div Grid, — контейнер сетки, и div ячеек имеют встроенные стили, смотрите строки 9 и 17. Значение стиля задаётся вспомогательной функцией. Причина такого подхода заключается в том, что он позволяет динамически изменять стили сетки и ячеек на основе передаваемых в функции данных.

gridSize хранит размер сетки. У меня есть три размера по умолчанию: 15x15, 30x30 или 50x50. У разных размеров будут разные стили. Посмотрим на вспомогательные функции:

Вспомогательная функция динамически изменяет количество места, отведённого каждому столбцу и строке сетки
Вспомогательная функция динамически изменяет количество места, отведённого каждому столбцу и строке сетки
Функция в cellSize возвращает ширину и высоту отдельной ячейки на основе gridSize. Функция в cellDisplay создаёт 3 случайных цвета, а затем проверяет, жива или мертва переданная ячейка. Если она живая, то динамически устанавливает размер ячейки и затем даёт ей случайный цвет; если мёртвая, то динамически устанавливает размер ячейки и задаёт чёрный фон
Функция в cellSize возвращает ширину и высоту отдельной ячейки на основе gridSize. Функция в cellDisplay создаёт 3 случайных цвета, а затем проверяет, жива или мертва переданная ячейка. Если она живая, то динамически устанавливает размер ячейки и затем даёт ей случайный цвет; если мёртвая, то динамически устанавливает размер ячейки и задаёт чёрный фон

Теперь посмотрим на логику изменения ячейки в зависимости от поколения и на то, как подключены элементы управления игры. Вся логика, связанная с состоянием, а также то, как мы управляем внешним видом сетки в отдельном поколении, обрабатывается в пользовательском хуке useGrid.

useGrid содержит несколько вызовов useState для отслеживания информации, которую мы используем и для итерации поколений, и для управления игрой:

Состояние для отслеживания информации, которую мы будем использовать для итерации поколений и для управления игрой
Состояние для отслеживания информации, которую мы будем использовать для итерации поколений и для управления игрой

Сначала нужно узнать, есть ли в сетке комбинация ячеек, которые можно изменить. Соответствующая логика располагается во вспомогательной функции stepThroughAutomata внутри useGrid. Я приступил к составлению плана функции, используя методы решения задач Джорджа Пойа.

План разработки stepThroughAutomata
План разработки stepThroughAutomata

Воплощаем план в жизнь:

Шаг 1. Задайте переменную, чтобы определить, валидна ли клетка (возможна ли мутация на основе правил)
Шаг 1. Задайте переменную, чтобы определить, валидна ли клетка (возможна ли мутация на основе правил)
Шаг 2. Примените отображение к текущей сетке, сохранив результат как nextGeneration, и с помощью вспомогательной функции getNeighbors проверьте соседей текущей ячейки
Шаг 2. Примените отображение к текущей сетке, сохранив результат как nextGeneration, и с помощью вспомогательной функции getNeighbors проверьте соседей текущей ячейки
Шаг 3. Инициализируйте значение livingNeighbors значением 0, затем проверьте всех соседей текущей клетки, чтобы узнать, есть ли среди них живые. Для каждого живого соседа увеличьте значение livingNeighbors на 1
Шаг 3. Инициализируйте значение livingNeighbors значением 0, затем проверьте всех соседей текущей клетки, чтобы узнать, есть ли среди них живые. Для каждого живого соседа увеличьте значение livingNeighbors на 1
Шаг 4. Исходя из количества живых соседей текущей ячейки, проверьте правила игры и переключите текущую ячейку на живую, если она была мёртвой, на мёртвую, если она была живой, или на неизменную. Установите validGrid в true, если мутация выполнена: если это возможно, то сетка валидна
Шаг 4. Исходя из количества живых соседей текущей ячейки, проверьте правила игры и переключите текущую ячейку на живую, если она была мёртвой, на мёртвую, если она была живой, или на неизменную. Установите validGrid в true, если мутация выполнена: если это возможно, то сетка валидна
Шаг 5. Если сетка валидная, увеличьте значение счётчика поколений; в противном случае сообщите пользователю, что сетка невалидная. Наконец, установите сетку в качестве следующего поколения. Следующее поколение будет старой сеткой, но с изменениями на основе правил
Шаг 5. Если сетка валидная, увеличьте значение счётчика поколений; в противном случае сообщите пользователю, что сетка невалидная. Наконец, установите сетку в качестве следующего поколения. Следующее поколение будет старой сеткой, но с изменениями на основе правил

И это всё! Переходим к управлению.

Управление
Управление

Итак, первая кнопка здесь — «Step 1 Generation».

Реализовать кнопку довольно просто: у нас есть функция stepThroughAutomata. А ниже видим компонент Controls.

На строке 13 у нас есть первая кнопка. Мы просто добавляем свойство onClick к этой кнопке и передаём в него stepThroughAutomata:

На строке 22 у нас есть поле ввода, определяющее скорость итераций.

И, наконец, есть третья кнопка, значение которой — «Start» или «Stop» в зависимости от того, кликабельны ли отдельные ячейки. Если клетки кликабельны, то игра запущена. Если нет, игра не запущена.

Вы можете спросить: «Секундочку, когда я нажимаю кнопку запуска, функция stepThroughAutomata не запускается?». Да! Метод JS setInterval не очень хорошо работает с onClick. Поэтому для этой функциональности нужно было создать собственный хук. Посмотрим, как он работает:

GridContainer: чёрный ящик с магией
GridContainer: чёрный ящик с магией

Выше мы деструктурируем все данные из useGrid, но прямо под этим кодом вызываем другой пользовательский хук — useInterval с четырьмя параметрами. Это:

  1. Функция обратного вызова (здесь — stepThroughAutomata).

  2. Время между вызовами передаваемой функции в миллисекундах. Значение speedInput по умолчанию — 500.

  3. Текущая сетка.

  4. Булево значение, здесь clickable.

useInterval и магия хуков.
useInterval и магия хуков.

Мы создали хук useInterval потому, что встроенная функция setInterval не всегда хорошо сочетается с тем, как React перерисовывает компоненты. 

Нам нужен способ узнать, что сетка меняется, и, следовательно, сетка должна повторно отобразиться, а мы должны быть уверены, что она последовательно изменяется каждые n миллисекунд. Узнать это мы можем с помощью встроенного хука useRef. Сначала инициализируем savedCallback как ссылку.

Теперь воспользуемся useEffect, чтобы установить текущий savedCallback в качестве переданного обратного вызова. Это связано с тем, как setInterval подписывается на объект window и отписывается от него.

Будем обновлять savedCallback.current каждый раз, когда возвращаемое значение обратного вызова изменяется. Это должно происходить при каждом выполнении функции обратного вызова.

Второй вызов useEffect внутри useInterval.
Второй вызов useEffect внутри useInterval.

Переходим ко второму вызову useEffect. Сначала проверим, истинно ли clickable. Если это так, то не хочется запускать функцию внутри: такой запуск означает, что игра идёт прямо сейчас. Если clickable — ложно, это означает, что игра запускается впервые. Поэтому быстро инициализируем функцию tick, которая вызывает текущий сохранённый обратный вызов.

Сохраняем результат вызова setInterval, передавая tick и задержку, а затем сразу же отписываемся, используя анонимную функцию и выполняя clearInterval с передачей id.

Замечательно, что передаваемый обратный вызов — это та же функция, которую мы используем для перебора по одному поколению за раз, так что алгоритм итерации полностью пригоден для повторного использования.

Резюме:

  • Хуки позволяют писать чистый код, который можно использовать повторно.

  • Определение соседей путём сплющивания сетки в вектор и выполнение математических операций для поиска соседей дают пространственную сложность O(n).

  • Встроенная функция повторного рендеринга в React позволяет создать бесшовное представление UI игры «Жизнь».

Код на Github.

Продолжить изучение ReactJS вы сможете на наших курсах:

  • Узнать подробности акции

  • Профессия Fullstack-разработчик на Python (15 месяцев)

  • Профессия Data Scientist (24 месяца)

Другие профессии и курсы

Data Science и Machine Learning

  • Профессия Data Scientist

  • Профессия Data Analyst

  • Курс «Математика для Data Science»

  • Курс «Математика и Machine Learning для Data Science»

  • Курс по Data Engineering

  • Курс «Machine Learning и Deep Learning»

  • Курс по Machine Learning

Python, веб-разработка

  • Профессия Fullstack-разработчик на Python

  • Курс «Python для веб-разработки»

  • Профессия Frontend-разработчик

  • Профессия Веб-разработчик

Мобильная разработка

  • Профессия iOS-разработчик

  • Профессия Android-разработчик

Java и C#

  • Профессия Java-разработчик

  • Профессия QA-инженер на JAVA

  • Профессия C#-разработчик

  • Профессия Разработчик игр на Unity

От основ — в глубину

  • Курс «Алгоритмы и структуры данных»

  • Профессия C++ разработчик

  • Профессия Этичный хакер

А также:

  • Курс по DevOps

  • Все курсы

Источник: https://habr.com/ru/company/skillfactory/blog/590783/


Интересные статьи

Интересные статьи

Привет всем!  Как всегда ярко, мощно и динамично отгремел The Standoff, отшумели насыщенные PHDays10, и мы занялись разбором накопившихся завалов из отложенных дел. На волне впечатлений от междун...
В данной статье приведены примеры вопросов и задач по React Hooks для собеседования Читать далее
Прежде чем мы начнём разговор о способах импорта в веб-проекты библиотеки React, покажу современные способы выполнения этой операции и использования хука useState: // Глобальный по...
Для создания интерфейсов React рекомендует использовать композицию и библиотеки по управлению состоянием (state management libraries) для построения иерархий компонентов. Однако при сложных патте...
«Если суть работы программиста в автоматизации работы других людей, то почему моя работа так мало автоматизирована» — думал я, копируя в очередной раз всю необходимую в проекте обвязку для добавл...