Задача разработчика — показать пользователю, как живут и умирают цифровые клетки. Автор воспользовался 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
Это всё конечно красиво, но с производительностью будет беда.