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

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

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


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

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

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





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

Итак, первая кнопка здесь — «Step 1 Generation».
Реализовать кнопку довольно просто: у нас есть функция stepThroughAutomata. А ниже видим компонент Controls.
На строке 13 у нас есть первая кнопка. Мы просто добавляем свойство onClick к этой кнопке и передаём в него stepThroughAutomata:

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

Выше мы деструктурируем все данные из useGrid, но прямо под этим кодом вызываем другой пользовательский хук — useInterval с четырьмя параметрами. Это:
Функция обратного вызова (здесь —
stepThroughAutomata).Время между вызовами передаваемой функции в миллисекундах. Значение
speedInputпо умолчанию — 500.Текущая сетка.
Булево значение, здесь
clickable.

Мы создали хук useInterval потому, что встроенная функция setInterval не всегда хорошо сочетается с тем, как React перерисовывает компоненты.
Нам нужен способ узнать, что сетка меняется, и, следовательно, сетка должна повторно отобразиться, а мы должны быть уверены, что она последовательно изменяется каждые n миллисекунд. Узнать это мы можем с помощью встроенного хука useRef. Сначала инициализируем savedCallback как ссылку.
Теперь воспользуемся useEffect, чтобы установить текущий savedCallback в качестве переданного обратного вызова. Это связано с тем, как setInterval подписывается на объект window и отписывается от него.
Будем обновлять savedCallback.current каждый раз, когда возвращаемое значение обратного вызова изменяется. Это должно происходить при каждом выполнении функции обратного вызова.

Переходим ко второму вызову useEffect. Сначала проверим, истинно ли clickable. Если это так, то не хочется запускать функцию внутри: такой запуск означает, что игра идёт прямо сейчас. Если clickable — ложно, это означает, что игра запускается впервые. Поэтому быстро инициализируем функцию tick, которая вызывает текущий сохранённый обратный вызов.
Сохраняем результат вызова setInterval, передавая tick и задержку, а затем сразу же отписываемся, используя анонимную функцию и выполняя clearInterval с передачей id.
Замечательно, что передаваемый обратный вызов — это та же функция, которую мы используем для перебора по одному поколению за раз, так что алгоритм итерации полностью пригоден для повторного использования.
Резюме:
Хуки позволяют писать чистый код, который можно использовать повторно.
Определение соседей путём сплющивания сетки в вектор и выполнение математических операций для поиска соседей дают пространственную сложность
O(n).Встроенная функция повторного рендеринга в React позволяет создать бесшовное представление UI игры «Жизнь».
Продолжить изучение ReactJS вы сможете на наших курсах:

Другие профессии и курсы
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также:
Комментарии (2)

gmananton
23.11.2021 12:59-
Любая живая клетка с менее чем двумя живыми соседями умирает от перенаселения.
Наверное, тут опечатка. Надо "от недонаселения"
-
Saiv46
Это всё конечно красиво, но с производительностью будет беда.