![](https://habrastorage.org/getpro/habr/upload_files/66b/bfe/682/66bbfe6821556020815efaba30f473d0.png)
Задача разработчика — показать пользователю, как живут и умирают цифровые клетки. Автор воспользовался React и её хуками: управление состоянием и возможность абстрагироваться от логики, связанной с состоянием, позволяют легко читать и понимать проект. Подробностями реализации и кодом на Github делимся, пока у нас начинается курс по Frontend-разработке.
Правила игры
Вселенная игры — бесконечная двумерная ортогональная сетка квадратных клеток, каждая из которых находится в одном из двух возможных состояний: живая или мёртвая (или заселённая и незаселённая соответственно). Каждая клетка взаимодействует со своими восемью соседями — клетками, которые расположены рядом по горизонтали, по вертикали или по диагонали. На каждом шаге во времени происходят следующие переходы:
Любая живая клетка с менее чем двумя живыми соседями умирает от недонаселения.
Любая живая клетка с двумя или тремя живыми соседями переходит в следующее поколение.
Любая живая клетка с более чем тремя живыми соседями умирает, как при перенаселении.
Любая мёртвая клетка с тремя живыми соседями становится живой клеткой, как будто размножаясь.
Попробуйте моё приложение, а затем поговорим о его работе под капотом.
Структура данных, которую я решил использовать для представления ячеек, довольно проста. Вот массив объектов:
![Этот массив объектов представляет нашу сетку, а каждый объект представляет отдельную ячейку. Свойство alive — ключевое в игре Этот массив объектов представляет нашу сетку, а каждый объект представляет отдельную ячейку. Свойство alive — ключевое в игре](https://habrastorage.org/getpro/habr/upload_files/e30/f4d/0ec/e30f4d0ec0364c86927c7b6f94ddca5c.png)
Создаём компонент отображения Grid, он накладывается на массив сетки и генерирует индивидуальную ячейку для каждого объекта в массиве сетки:
![Grid создаёт для каждого объекта ячейки в массиве сетки ячейку — тег <div> Grid создаёт для каждого объекта ячейки в массиве сетки ячейку — тег <div>](https://habrastorage.org/getpro/habr/upload_files/236/3e2/825/2363e28256814ae6fa62a5873c6d8aec.png)
Видно, что и div
Grid, — контейнер сетки, и div ячеек имеют встроенные стили, смотрите строки 9 и 17. Значение стиля задаётся вспомогательной функцией. Причина такого подхода заключается в том, что он позволяет динамически изменять стили сетки и ячеек на основе передаваемых в функции данных.
gridSize
хранит размер сетки. У меня есть три размера по умолчанию: 15x15, 30x30 или 50x50. У разных размеров будут разные стили. Посмотрим на вспомогательные функции:
![Вспомогательная функция динамически изменяет количество места, отведённого каждому столбцу и строке сетки Вспомогательная функция динамически изменяет количество места, отведённого каждому столбцу и строке сетки](https://habrastorage.org/getpro/habr/upload_files/1d2/907/9d3/1d29079d3a8f42d98ae39f9106a453d1.png)
![Функция в cellSize возвращает ширину и высоту отдельной ячейки на основе gridSize. Функция в cellDisplay создаёт 3 случайных цвета, а затем проверяет, жива или мертва переданная ячейка. Если она живая, то динамически устанавливает размер ячейки и затем даёт ей случайный цвет; если мёртвая, то динамически устанавливает размер ячейки и задаёт чёрный фон Функция в cellSize возвращает ширину и высоту отдельной ячейки на основе gridSize. Функция в cellDisplay создаёт 3 случайных цвета, а затем проверяет, жива или мертва переданная ячейка. Если она живая, то динамически устанавливает размер ячейки и затем даёт ей случайный цвет; если мёртвая, то динамически устанавливает размер ячейки и задаёт чёрный фон](https://habrastorage.org/getpro/habr/upload_files/d3d/cfe/a55/d3dcfea55bb559d6f61207c6bb160401.png)
Теперь посмотрим на логику изменения ячейки в зависимости от поколения и на то, как подключены элементы управления игры. Вся логика, связанная с состоянием, а также то, как мы управляем внешним видом сетки в отдельном поколении, обрабатывается в пользовательском хуке useGrid
.
useGrid
содержит несколько вызовов useState
для отслеживания информации, которую мы используем и для итерации поколений, и для управления игрой:
![Состояние для отслеживания информации, которую мы будем использовать для итерации поколений и для управления игрой Состояние для отслеживания информации, которую мы будем использовать для итерации поколений и для управления игрой](https://habrastorage.org/getpro/habr/upload_files/076/c30/c92/076c30c92fecfb77d4db7b9aab4eeafc.png)
Сначала нужно узнать, есть ли в сетке комбинация ячеек, которые можно изменить. Соответствующая логика располагается во вспомогательной функции stepThroughAutomata
внутри useGrid
. Я приступил к составлению плана функции, используя методы решения задач Джорджа Пойа.
![План разработки stepThroughAutomata План разработки stepThroughAutomata](https://habrastorage.org/getpro/habr/upload_files/343/559/670/343559670bb662521abe44b8752da8e7.png)
Воплощаем план в жизнь:
![Шаг 1. Задайте переменную, чтобы определить, валидна ли клетка (возможна ли мутация на основе правил) Шаг 1. Задайте переменную, чтобы определить, валидна ли клетка (возможна ли мутация на основе правил)](https://habrastorage.org/getpro/habr/upload_files/fbd/1e7/d4b/fbd1e7d4bed0811374bb279ce5c64424.png)
![Шаг 2. Примените отображение к текущей сетке, сохранив результат как nextGeneration, и с помощью вспомогательной функции getNeighbors проверьте соседей текущей ячейки Шаг 2. Примените отображение к текущей сетке, сохранив результат как nextGeneration, и с помощью вспомогательной функции getNeighbors проверьте соседей текущей ячейки](https://habrastorage.org/getpro/habr/upload_files/0c5/f94/a8a/0c5f94a8a0f36a890751279171ff4c23.png)
![Шаг 3. Инициализируйте значение livingNeighbors значением 0, затем проверьте всех соседей текущей клетки, чтобы узнать, есть ли среди них живые. Для каждого живого соседа увеличьте значение livingNeighbors на 1 Шаг 3. Инициализируйте значение livingNeighbors значением 0, затем проверьте всех соседей текущей клетки, чтобы узнать, есть ли среди них живые. Для каждого живого соседа увеличьте значение livingNeighbors на 1](https://habrastorage.org/getpro/habr/upload_files/19f/3df/934/19f3df934c5658670c3b38a785f9a5ce.png)
![Шаг 4. Исходя из количества живых соседей текущей ячейки, проверьте правила игры и переключите текущую ячейку на живую, если она была мёртвой, на мёртвую, если она была живой, или на неизменную. Установите validGrid в true, если мутация выполнена: если это возможно, то сетка валидна Шаг 4. Исходя из количества живых соседей текущей ячейки, проверьте правила игры и переключите текущую ячейку на живую, если она была мёртвой, на мёртвую, если она была живой, или на неизменную. Установите validGrid в true, если мутация выполнена: если это возможно, то сетка валидна](https://habrastorage.org/getpro/habr/upload_files/8c0/253/16c/8c025316cdba056b1bf3608140f738e8.png)
![Шаг 5. Если сетка валидная, увеличьте значение счётчика поколений; в противном случае сообщите пользователю, что сетка невалидная. Наконец, установите сетку в качестве следующего поколения. Следующее поколение будет старой сеткой, но с изменениями на основе правил Шаг 5. Если сетка валидная, увеличьте значение счётчика поколений; в противном случае сообщите пользователю, что сетка невалидная. Наконец, установите сетку в качестве следующего поколения. Следующее поколение будет старой сеткой, но с изменениями на основе правил](https://habrastorage.org/getpro/habr/upload_files/3ac/db7/c01/3acdb7c01e1d7499f8d8f0a190a256d6.png)
И это всё! Переходим к управлению.
![Управление Управление](https://habrastorage.org/getpro/habr/upload_files/112/38d/630/11238d6302459eec4ad39536d98ee1d7.png)
Итак, первая кнопка здесь — «Step 1 Generation».
Реализовать кнопку довольно просто: у нас есть функция stepThroughAutomata
. А ниже видим компонент Controls
.
На строке 13 у нас есть первая кнопка. Мы просто добавляем свойство onClick
к этой кнопке и передаём в него stepThroughAutomata
:
![](https://habrastorage.org/getpro/habr/upload_files/96a/cc6/1a8/96acc61a8187cafef196117e43b28014.png)
На строке 22 у нас есть поле ввода, определяющее скорость итераций.
И, наконец, есть третья кнопка, значение которой — «Start» или «Stop» в зависимости от того, кликабельны ли отдельные ячейки. Если клетки кликабельны, то игра запущена. Если нет, игра не запущена.
Вы можете спросить: «Секундочку, когда я нажимаю кнопку запуска, функция stepThroughAutomata
не запускается?». Да! Метод JS setInterval
не очень хорошо работает с onClick
. Поэтому для этой функциональности нужно было создать собственный хук. Посмотрим, как он работает:
![GridContainer: чёрный ящик с магией GridContainer: чёрный ящик с магией](https://habrastorage.org/getpro/habr/upload_files/c62/b58/94c/c62b5894c1ffc3614dbd9978cce642b5.png)
Выше мы деструктурируем все данные из useGrid
, но прямо под этим кодом вызываем другой пользовательский хук — useInterval
с четырьмя параметрами. Это:
Функция обратного вызова (здесь —
stepThroughAutomata
).Время между вызовами передаваемой функции в миллисекундах. Значение
speedInput
по умолчанию — 500.Текущая сетка.
Булево значение, здесь
clickable
.
![useInterval и магия хуков. useInterval и магия хуков.](https://habrastorage.org/getpro/habr/upload_files/9f2/cb4/35f/9f2cb435f98a7a7ab314970b8394b7c2.png)
Мы создали хук useInterval
потому, что встроенная функция setInterval не всегда хорошо сочетается с тем, как React перерисовывает компоненты.
Нам нужен способ узнать, что сетка меняется, и, следовательно, сетка должна повторно отобразиться, а мы должны быть уверены, что она последовательно изменяется каждые n
миллисекунд. Узнать это мы можем с помощью встроенного хука useRef
. Сначала инициализируем savedCallback
как ссылку.
Теперь воспользуемся useEffect
, чтобы установить текущий savedCallback
в качестве переданного обратного вызова. Это связано с тем, как setInterval
подписывается на объект window
и отписывается от него.
Будем обновлять savedCallback.current
каждый раз, когда возвращаемое значение обратного вызова изменяется. Это должно происходить при каждом выполнении функции обратного вызова.
![Второй вызов useEffect внутри useInterval. Второй вызов useEffect внутри useInterval.](https://habrastorage.org/getpro/habr/upload_files/fb2/fa9/1a2/fb2fa91a2cdef798f50bb0befdcdaebe.png)
Переходим ко второму вызову useEffect
. Сначала проверим, истинно ли clickable
. Если это так, то не хочется запускать функцию внутри: такой запуск означает, что игра идёт прямо сейчас. Если clickable
— ложно, это означает, что игра запускается впервые. Поэтому быстро инициализируем функцию tick
, которая вызывает текущий сохранённый обратный вызов.
Сохраняем результат вызова setInterval
, передавая tick
и задержку, а затем сразу же отписываемся, используя анонимную функцию и выполняя clearInterval с передачей id
.
Замечательно, что передаваемый обратный вызов — это та же функция, которую мы используем для перебора по одному поколению за раз, так что алгоритм итерации полностью пригоден для повторного использования.
Резюме:
Хуки позволяют писать чистый код, который можно использовать повторно.
Определение соседей путём сплющивания сетки в вектор и выполнение математических операций для поиска соседей дают пространственную сложность
O(n)
.Встроенная функция повторного рендеринга в React позволяет создать бесшовное представление UI игры «Жизнь».
Продолжить изучение ReactJS вы сможете на наших курсах:
![](https://habrastorage.org/getpro/habr/upload_files/7b3/7ee/701/7b37ee701eed48a97c336ce245cb9324.png)
Другие профессии и курсы
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также:
Комментарии (2)
gmananton
23.11.2021 12:59-
Любая живая клетка с менее чем двумя живыми соседями умирает от перенаселения.
Наверное, тут опечатка. Надо "от недонаселения"
-
Saiv46
Это всё конечно красиво, но с производительностью будет беда.