Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

Всем привет. Сегодня решил продолжить описывать технические статьи для Unity-разработчиков и на этот раз кратко пробежаться по теме реактивного программирования.

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

Что за зверь это ваше реактивное программирование?

Начнем с того, что реактивное программирование - это несколько иной подход к реализации вашего программного обеспечения. Конечно, никто не запрещает вам совмещать различные подходы (ООП, ECS и RX), однако нужно понимать что, зачем и почему.

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

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

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

Представим себе такую ситуацию - что у нас есть игрок, который может получать урон от противника, а UI - будет выводить этот урон. В базовом понимании у нас есть методы для работы с этим внутри объектов и мы по ссылкам будем обращаться к примеру от игрока к UI. В реактивном же подходе - мы создаем поток данных (к примеру здоровье игрока) у которого будет свой таймлайн, во время которого значения здоровья будет меняться и вызывать событие об этом, а все подписчики - получать новое событие.

Один простой пример, который показывает как работают потоки в реактивном программировании - это посмотреть на математические примерчики.

В стандартном примере без реактивного программирования, где:

A = B + C; B = 12; C = 12; A = 24;

И если мы изменим значение B - то при дальнейшем использовании A - оно останется неизменным.

Но в реактивном программировании, если мы изменим значение B - то значение A автоматически пересчитается (как в формулах в Excel). Таким образом, если мы сделаем B = 20, то в обычном подходе A остается 24, а в реактивном A станет = 32.

Что еще умеют потоки?

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

В основе потоков стоят наблюдатели - которые отслеживают изменения потока и оповещают об этом слушателей.

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

Объединение потоков

Для того, чтобы группировать наши данные - мы можем объединять потоки и работать с единым потоком при помощи фильтраций. Эти фильтрации могут быть сделаны банальным сравнением, либо через LINQ (однако производительность LINQ - это отдельная тема и злоупотреблять им не стоит).

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

Объединенный поток - включает в себя общий таймлайн с событиями об изменении отдельных данных в потоке.

В чем плюсы и минусы работы с потоками?

Как и у любого подхода - в работе с потоками есть свои плюсы и минусы. Я выделил для себя несколько пунктов в обеих колонках.

Преимущества работы с потоками:

  • Уменьшает связность и клей в проекте, а так же количество кода;
  • Повышает отказоустойчивость системы - поскольку при возникновении ошибок в одном из классов, он не нарушит работу системы в целом, а просто перестанет обновлять данные в потоке;
  • Высокий контроль над данными и событиями;
  • Может сочетаться с ООП и ECS подходами;

Минусы работы с потоками:

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

Базовые реализации в 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") + "%"; } }
Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

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

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

И теперь выносим 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 и другие реализации.

Коротко о реактивном программировании в геймдеве. Что это, зачем нужно и как применить в контексте Unity

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

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

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

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

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

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

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

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

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

Итого

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

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

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

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

Пользуетесь ли вы реактивными расширениями?
Да, использую
Нет, не использую
Не знаю, как это работает
3434
25 комментариев

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

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

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

5
Ответить

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

Ответить

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

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

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

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

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

2
Ответить

Комментарий недоступен

2
Ответить

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

Ответить

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

Ответить

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

Ответить