Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity
Всем привет. Сегодня решил продолжить описывать технические статьи для Unity-разработчиков и на этот раз кратко пробежаться по теме реактивного программирования.
Что за зверь это ваше реактивное программирование?
Начнем с того, что реактивное программирование - это несколько иной подход к реализации вашего программного обеспечения. Конечно, никто не запрещает вам совмещать различные подходы (ООП, ECS и RX), однако нужно понимать что, зачем и почему.
Реактивное программирование строится на взаимодействии с потоками данных. Вам нужно уяснить, что все что вы будете делать - это манипулировать потоками.
Представим себе такую ситуацию - что у нас есть игрок, который может получать урон от противника, а UI - будет выводить этот урон. В базовом понимании у нас есть методы для работы с этим внутри объектов и мы по ссылкам будем обращаться к примеру от игрока к UI. В реактивном же подходе - мы создаем поток данных (к примеру здоровье игрока) у которого будет свой таймлайн, во время которого значения здоровья будет меняться и вызывать событие об этом, а все подписчики - получать новое событие.
Один простой пример, который показывает как работают потоки в реактивном программировании - это посмотреть на математические примерчики.
В стандартном примере без реактивного программирования, где:
A = B + C; B = 12; C = 12; A = 24;
И если мы изменим значение B - то при дальнейшем использовании A - оно останется неизменным.
Но в реактивном программировании, если мы изменим значение B - то значение A автоматически пересчитается (как в формулах в Excel). Таким образом, если мы сделаем B = 20, то в обычном подходе A остается 24, а в реактивном A станет = 32.
Что еще умеют потоки?
Помимо рассылки событий - вы можете манипулировать с потоками для фильтрации данных, обрабатывать ошибки и отменять их. Это отличает их от обычных Event-Bus.
В основе потоков стоят наблюдатели - которые отслеживают изменения потока и оповещают об этом слушателей.
Объединение потоков
Для того, чтобы группировать наши данные - мы можем объединять потоки и работать с единым потоком при помощи фильтраций. Эти фильтрации могут быть сделаны банальным сравнением, либо через LINQ (однако производительность LINQ - это отдельная тема и злоупотреблять им не стоит).
Объединенный поток - включает в себя общий таймлайн с событиями об изменении отдельных данных в потоке.
В чем плюсы и минусы работы с потоками?
Как и у любого подхода - в работе с потоками есть свои плюсы и минусы. Я выделил для себя несколько пунктов в обеих колонках.
Преимущества работы с потоками:
- Уменьшает связность и клей в проекте, а так же количество кода;
- Повышает отказоустойчивость системы - поскольку при возникновении ошибок в одном из классов, он не нарушит работу системы в целом, а просто перестанет обновлять данные в потоке;
- Высокий контроль над данными и событиями;
- Может сочетаться с ООП и ECS подходами;
Минусы работы с потоками:
- Сложен в понимании для новичков, требует определенного мышления;
- В некоторых случаях (как например в Unity) могут потребоваться Rx расширения (как например UniRx или самописные решения), которые в свою очередь могут накладывать свои ограничения;
- В идеале - требует понимания асинхронной разработки приложений, поскольку эти подходы часто пересекаются;
- Сложности в отладки многоуровневых реактивных полей при разработке проектов;
Базовые реализации в Unity
Допустим мы хотим обновление здоровья игрока при получении урона. Самый простой вариант - пробросить в класс игрока ссылку на UI и обновлять её. Однако это создает клей - и один из способов избавления от него - воспользоваться реактивными полями и UniRx.
Воспользовавшись потоками мы можем подписаться на обновление здоровья игрока и, если что-то пойдет не так - мы просто не будем получать новое значение. Так же мы абстрагируемся от реализаций внутри класса игрока, подписываясь лишь на его данные.
И теперь выносим UI отдельно от класса Player:
Зачем могут понадобиться потоки и Rx в ваших Unity проектах?
Работа с потоками - отличный инструмент. В Unity для этого существует UniRx и другие реализации.
В целом можно выделить следующие цели работы с потоками:
- Для того, чтобы не запрашивать данные у владельца, а оперировать лишь данными (к примеру, для работы с моделями в MVVM);
- Фильтровать данные в потоке, чтобы не получать лишнее;
- Подписываться на изменения и получать их;
- Работать с асинхронными задачами;
- Подписываться на ошибки работы в потоках;
- Манипулировать данными в потоке без изменения конкретных данных;
- Снизить связность в коде и уменьшить клей;
- Когда нам нужно реагировать на изменения между потоками;
В каких случаях стоит отказаться от потоков:
- Когда мы часто обращаемся к записи потоков с разных источников (по большей части касается UniRx);
- Когда это не оправдано (создавать поток для того, чтобы работать с ним в единственном классе. В таком случае проще обращаться на прямую);
- Когда нам не нужно реагировать на изменения;
Что еще умеет UniRX?
UniRx довольно мощное расширение для реактивного программирования и включает в себя большой набор функционала:
- Поддержку LINQ;
- Работу с сетью;
- Группировку потоков;
- Отладку ошибок и прогресса;
- Поддержку корутин;
- Поддержку MultiThreading;
- Кастомные триггеры;
- Интеграцию с uGui;
- MV(R)P паттерн;
Важно! UniRx плохо работает с очередями изменений, поэтому ваши подписчики могут получать события не в том порядке, в котором должны. Правильно проектируйте ваши приложения, чтобы избежать этого.
Еще один пример реактивного программирования, но в ECS:
Итого
Работать с потоками полезно, когда вы оперируете большим количеством данных и вам нужно получать их состояние при изменении. Так же это полезно для реагирования на изменения, к примеру чтобы коэффицент атаки игрока менялся автоматически при изменении здоровья.
Rx полезен, когда мы работаем с UI, но может нанести вред, если мы бездумно будем использовать его везде (как и впринципе со всем, что есть в программировании).
Полезные ссылки для изучения:
А вы пользуетесь реактивными расширениями и для чего? Будет интересно узнать о вашем опыте, в особенности о проблемах Rx в ваших проектах.
Выглядит как обычные подписки + timeline. Зачем использование таймлайна в обычных данных таких как хп игрока? Как мы используем эту временную переменную по сравнению с обычной подпиской?
>Что еще умеют потоки?>Помимо рассылки событий - вы можете манипулировать с потоками для фильтрации данных, обрабатывать ошибки и отменять их. Это отличает их от обычных Event-Bus.
И ни слова о том как потоки обрабатывают ошибки. А это единственное отличие от простых ивентовов, которое я увидел из всей статьи. Зачем городить всю эту сложность с таймлайнами если все те же проблемы (связанность кода и отказоустойчивость) решаются обычными подписками на ивенты без таймлайна?
Эвент это эвент. А поток можно использовать как поле класса, с которым можно делать все тем же действия и его значение обновится автоматически и разошлется всем подписчикам. Таким образом поток может быть внутри потока и при обновлении первого - автоматически обновится второй и разошлется всем подписчикам без необходимости делать Invoke события.
Статья хорошая респект.
"LINQ (однако производительность LINQ - это отдельная тема и злоупотреблять им не стоит)."
*надо знать как и где можно использовать, но если разработчик не знает где нужно использовать, то лучше не использовать)
Но тут открываются еще некоторые аспекты, как-то принято говорить что профи используют такой подход, а новички нет, так например часто думают работодатели. Но все наоборот, используют его обычно разработчики "новой волны", а старые разработчики держаться от него по дальше :)
Так с ходу не обдумывая какие проблемы такие эксперименты могут повлечь:
Понижение стабильности проекта
Проблема с компиляцией, или в кое где это вообще может на заработать
Повышение сложности проекта
Увеличивается порог входа для новых разработчиков
Время идет все меняется, может и сейчас что-то поменялось уже)
Это инструмент наверняка полезный, если профессионал знает где и как его использовать.
Комментарий недоступен
Впринципе как и все инструменты надо юзать с умом 🥲🤣
т.е. это ивенты
А как это внутри работает, C# events?