{"id":4012,"url":"\/distributions\/4012\/click?bit=1&hash=5b9cad3f989520ad358a2237d28d1f12ecdc50cb8452456f27fcbce716b2c8f0","title":"\u041f\u043e\u044f\u0432\u0438\u043b\u0441\u044f \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u0441\u0442\u0435\u0441\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0440\u0438\u0435\u043b\u0442\u043e\u0440\u043e\u0432","buttonText":"","imageUuid":"","isPaidAndBannersEnabled":false}

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте 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.

public class Player : MonoBehaviour { [SerializeField] private Text playerHeal; [SerializeField] private Text playerArmor; public float Heal = 100f; public float Armor = 100f; public void ApplyDamage(float damage) { if(Armor >= damage) { Armor -= damage; damage = 0; }else if(Armor > 0){ damage -= Armor; Armor = 0; } Heal -= damage; playerHeal.text = Heal.ToString("N0") + "%"; playerArmor.text = Armor.ToString("N0") + "%"; } }

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

public class Player : MonoBehaviour { [SerializeField] private Text playerHeal; [SerializeField] private Text playerArmor; public ReactiveProperty<float> Heal = new ReactiveProperty<float>(100f); public ReactiveProperty<float> Armor = new ReactiveProperty<float>(100f); public void ApplyDamage(float damage) { if(Armor >= damage) { Armor -= damage; damage = 0; }else if(Armor > 0){ damage -= Armor; Armor = 0; } Heal -= damage; } }

И теперь выносим UI отдельно от класса Player:

public class UI : MonoBehaviour { [SerializeField] private Player player; [SerializeField] private Text playerHeal; [SerializeField] private Text playerArmor; private void Start() { player.Heal.SubscribeToText(playerHeal); player.Armor.SubscribeToText(playerArmor); } }

Зачем могут понадобиться потоки и Rx в ваших Unity проектах?

Работа с потоками - отличный инструмент. В Unity для этого существует UniRx и другие реализации.

В целом можно выделить следующие цели работы с потоками:

  • Для того, чтобы не запрашивать данные у владельца, а оперировать лишь данными (к примеру, для работы с моделями в MVVM);
  • Фильтровать данные в потоке, чтобы не получать лишнее;
  • Подписываться на изменения и получать их;
  • Работать с асинхронными задачами;
  • Подписываться на ошибки работы в потоках;
  • Манипулировать данными в потоке без изменения конкретных данных;
  • Снизить связность в коде и уменьшить клей;
  • Когда нам нужно реагировать на изменения между потоками;

В каких случаях стоит отказаться от потоков:

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

Что еще умеет UniRX?

UniRx довольно мощное расширение для реактивного программирования и включает в себя большой набор функционала:

  • Поддержку LINQ;
  • Работу с сетью;
  • Группировку потоков;
  • Отладку ошибок и прогресса;
  • Поддержку корутин;
  • Поддержку MultiThreading;
  • Кастомные триггеры;
  • Интеграцию с uGui;
  • MV(R)P паттерн;

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

Еще один пример реактивного программирования, но в ECS:

Итого

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

Rx полезен, когда мы работаем с UI, но может нанести вред, если мы бездумно будем использовать его везде (как и впринципе со всем, что есть в программировании).

Полезные ссылки для изучения:

А вы пользуетесь реактивными расширениями и для чего? Будет интересно узнать о вашем опыте, в особенности о проблемах Rx в ваших проектах.

Пользуетесь ли вы реактивными расширениями?
Да, использую
Нет, не использую
Не знаю, как это работает
Показать результаты
Переголосовать
Проголосовать
0
19 комментариев
Написать комментарий...
DroiD

Выглядит как обычные подписки + timeline. Зачем использование таймлайна в обычных данных таких как хп игрока? Как мы используем эту временную переменную по сравнению с обычной подпиской?

>Что еще умеют потоки?
>Помимо рассылки событий - вы можете манипулировать с потоками для фильтрации данных, обрабатывать ошибки и отменять их. Это отличает их от обычных Event-Bus.

И ни слова о том как потоки обрабатывают ошибки. А это единственное отличие от простых ивентовов, которое я увидел из всей статьи. Зачем городить всю эту сложность с таймлайнами если все те же проблемы (связанность кода и отказоустойчивость) решаются обычными подписками на ивенты без таймлайна?

Ответить
Развернуть ветку
Илья Сергеич
Автор

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

Ответить
Развернуть ветку
DroiD

Ну обычные реактивные поля. При чем тут таймлайны и потоки? ))
И где в этой схеме корректировка ошибок и как она происходит?

Ответить
Развернуть ветку
Илья Сергеич
Автор

В разрезе работы реактивного поля его можно представить как поток событий на едином таймлайне (не путать с потоками в асинке)

Ответить
Развернуть ветку
Vandallord

Статья хорошая респект.

"LINQ (однако производительность LINQ - это отдельная тема и злоупотреблять им не стоит)."
*надо знать как и где можно использовать, но если разработчик не знает где нужно использовать, то лучше не использовать)

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

Так с ходу не обдумывая какие проблемы такие эксперименты могут повлечь:
Понижение стабильности проекта
Проблема с компиляцией, или в кое где это вообще может на заработать
Повышение сложности проекта
Увеличивается порог входа для новых разработчиков
Время идет все меняется, может и сейчас что-то поменялось уже)

Это инструмент наверняка полезный, если профессионал знает где и как его использовать.

Ответить
Развернуть ветку
Аркадий Акакиевич
*надо знать как и где можно использовать, но если разработчик не знает где нужно использовать, то лучше не использовать)

Если разработчик не начнет использовать, то и не узнает, где нужно использовать.

Ответить
Развернуть ветку
Илья Сергеич
Автор

Впринципе как и все инструменты надо юзать с умом 🥲🤣

Ответить
Развернуть ветку
Mike Kozlov

А как это внутри работает, C# events?

Ответить
Развернуть ветку
Илья Сергеич
Автор

Реализаций много на самом деле. UniRx работает на паттерне Observer с экшнами, насколько я помню.

Ответить
Развернуть ветку
Mike Kozlov

Поговаривают, что это достаточно медленно.
Я могу ошибаться, не знаю, как это работает уровнем ниже, но наверно там меняется контекст очень много раз.

Ответить
Развернуть ветку
Артём Хромов

Зато можно убить блокировки напрочь.

Ответить
Развернуть ветку
Илья Сергеич
Автор

Думаю, смотря как использовать 🤣

Ответить
Развернуть ветку
Zubius

"Самый простой вариант - пробросить в класс игрока ссылку на UI и обновлять её. Однако это создает клей - и один из способов избавления от него " - пробросить в класс UI ссылку на игрока?

А как дебажить кто изменил значение если, например, несколько сущностей будут подписаны на изменение ReactiveProperty?

Ответить
Развернуть ветку
Илья Сергеич
Автор

Бряки можно поставить и будет видно откуда вызов пошел

Ответить
Развернуть ветку
Олег Щеглов

Мощно

Ответить
Развернуть ветку
Евгений Онянов

Был клей Игрок -> UI, стал клей UI -> Игрок. В чем профит, если и там и там жесткая зависимость?

Ответить
Развернуть ветку
[email protected]

Проблема была в том, что игрок был завязан на определенной реализации UI. С использованием UniRx теперь мы можем использовать любой вывод.

Ответить
Развернуть ветку
Mark Mazitov

Как я понял, это расширение для того, чтобы самому Observable<T> не писать. Конец. Хотя там ещё говорится об асинхронности, но я про это ничего не знаю :)

В общем, просто поля-события. Может, сам пример автора какой-то неправильный, для чайников в программировании.

public System.Action<float> OnHealthUpdate = () => {}
public float Health;

превращается в

public ReactiveProperty<float> Health = new ReactiveProperty<float>();

Ответить
Развернуть ветку
AntonioModer

т.е. это ивенты

Ответить
Развернуть ветку
Читать все 19 комментариев
null