Примерно год назад я начал работать с этой библиотекой, а теперь настолько к ней привык, что не представляю как без нее можно обходиться. Так как многие знакомые разработчики на Unity совершенно не верят моим восторженным отзывам, здесь я попробую на пальцах показать, как немного улучшить жизнь при работе с ежедневными задачами.
Введение
На Хабре есть два исчерпывающих туториала на тему реактивов в юнити: первая и вторая. Я же попробую ответить на вопрос «зачем, если можно все сделать событиями?»
События
Предположим, у игрока меняется здоровье и мана. А мы их все время показываем в UI.
Очевидный способ — подписка на события изменения этих самых здоровья и маны
public void Init(Player player)
{
_player = player;
_player.HealthChanged += SetHealth;
_player.ManaChanged += SetMana;
}
public void Dispose()
{
_player.HealthChanged -= SetHealth;
_player.ManaChanged -= SetMana;
}
А сам Player будет выглядеть как-то так
public class Player : IPlayer
{
private int _health;
private int _mana;
public int Health => _health;
public int Mana => _mana;
public event Action<int> HealthChanged;
public event Action<int> ManaChanged;
}
А если мы захотим добавить для игрока интерфейс, то придем к такому варианту
public interface IPlayer
{
public int Health { get; }
public int Mana { get; }
public event Action<int> HealthChanged;
public event Action<int> ManaChanged;
}
В чем проблема? В том, что при росте данных для показа надо будет каждый раз спускаться до метода Dispose и не забывать отписываться, а также создавать на каждое значение свой метод. Ведь я даже не могу создать n одинаковых вьюшек для показа значений
ReactiveProperty
Чем же поможет в таком случае реактивное поле? Оно позволяет совершать подписку и отписку в одном и том же месте, хотя диспоузом все равно придется воспользоваться.
private List<IDisposable> _disposables = new List<IDisposable>();
public void Init(Player player)
{
player.Health
//создаем подписку
.Subscribe(v => { _healthView.text = v.ToString(); })
//добавляем подпику в список очищаемых обьектов
.AddTo(_disposables);
player.Mana.Subscribe(v => { _manaView.text = v.ToString(); }).AddTo(_disposables);
}
public void Dispose()
{
foreach (var disposable in _disposables)
{
disposable.Dispose();
}
_disposables.Clear();
}
В чем прелесть? В том, что метод Subscribe, которым мы подписываемся на поток, возвращает IDisposable, а значит, мы можем сразу добавлять это в список, который потом будет очищаться. При этом можно не опасаться подписок c помощью лямбд.
Теперь можно глянуть на Player
public class Player : IPlayer
{
private ReactiveProperty<int> _health = new();
private ReactiveProperty<int> _mana = new();
public IReadOnlyReactiveProperty<int> Health => _health;
public IReadOnlyReactiveProperty<int> Mana => _mana;
}
Так как ReactiveProperty<T> реализует интерфейс IReadOnlyReactiveProperty<T>, мы можем оставлять его торчащим наружу, не опасаясь непрошенных изменений.
ReactiveCollection/ReactiveDictionary
То же самое что и реактивные поля, но здесь надо отдельно подписываться на каждое событие, связанное с коллекцией.
public void Init(IReadOnlyReactiveCollection<Player> players)
{
players
//говорим, что хотим отслеживать
.ObserveAdd()
//подписываемся
.Subscribe(v =>
{
//работаем с добавленным элементом
Debug.Log(v.Value);
}).AddTo(_disposables);
players.ObserveRemove().Subscribe(v => { Debug.Log(v.Value); }).AddTo(_disposables);
players.ObserveReplace().Subscribe(v =>
{
Debug.Log(v.OldValue);
Debug.Log(v.NewValue);
}).AddTo(_disposables);
}
А как же «все есть поток»?
UniRx позволяет превращать в поток множество вещей: инпут, цикл обновлений юнити, компоненты. А еще пропускать изменения, делать задержки и многие другие вещи (о многих из которых я даже не знаю). Но я хотел показать прелести каждодневного использования, а не увеличить когнитивную нагрузку. Думаю, желающие углубиться могут перейти по прикрепленным на туториалы ссылкам или пойти читать документацию.