Примерно год назад я начал работать с этой библиотекой, а теперь настолько к ней привык, что не представляю как без нее можно обходиться. Так как многие знакомые разработчики на 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 позволяет превращать в поток множество вещей: инпут, цикл обновлений юнити, компоненты. А еще пропускать изменения, делать задержки и многие другие вещи (о многих из которых я даже не знаю). Но я хотел показать прелести каждодневного использования, а не увеличить когнитивную нагрузку. Думаю, желающие углубиться могут перейти по прикрепленным на туториалы ссылкам или пойти читать документацию.

Комментарии (15)


  1. IL_Agent
    00.00.0000 00:00

    А почему нельзя использовать обычный rx? (не знаток юнити)


    1. oblakooblako
      00.00.0000 00:00

      В unirx много расширений для удобства работы с классами unity.


  1. AxeFizik
    00.00.0000 00:00

    Я немного выпал из Unity разработки, но ведь UniRx больше не поддерживается и даже не работает в Unity 2022, разве нет?


    1. Ipashelovo Автор
      00.00.0000 00:00
      +1

      Проверил на 22.2.7. При первом приближении проблем не вижу. Проверял таким образом.

      public class ScoreView : MonoBehaviour
      {
        [SerializeField] private ScoreUpdatable _scoreUpdatable;
        [SerializeField] private TMP_Text _scoreCount;
        [SerializeField] private Button _button;
        [SerializeField] private Image _buttonLisener;
        private bool _imageState;
      
        private void Start()
        {
          _scoreUpdatable.Score.Subscribe(v => _scoreCount.text = v.ToString());
          _button.onClick.AsObservable().Subscribe(v =>
          {
            _imageState = !_imageState;
            _buttonLisener.color = _imageState ? Color.red : Color.yellow;
          });
        }    
      }
      
      public class ScoreUpdatable : MonoBehaviour
      {
        private ReactiveProperty<int> _score = new ReactiveProperty<int>();
        public IReactiveProperty<int> Score => _score;
      
        private IEnumerator Start()
        {
          while (enabled)
          {
            yield return new WaitForSeconds(1);
            _score.Value++;
          }
        }
      }


      1. AxeFizik
        00.00.0000 00:00

        Хорошо, если работает)

        Я, помнится, скачал какую-то самую новую версию Unity, поставил netstandard2.1, установил UniRx и получил конфликт Assemblies.

        Когда руки дойдут посмотрю ещё раз


  1. programm_err
    00.00.0000 00:00
    +3

    Небольшая подсказка. Вместо

    private List<IDisposable> _disposables = new List<IDisposable>();

    можно использовать класс CompositeDisposable для простоты. Будет что-то типа

    private CompositeDisposable _disposables = new CompositeDisposable();
    
    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()
    {
      _disposables.Dispose();
    }


    1. ZimM
      00.00.0000 00:00

      Благодарю! Не зная про CompositeDisposable, в результате написал свой аналог с другим именем :)


      1. Ipashelovo Автор
        00.00.0000 00:00

        Да, у нас тоже используется аналог. Но у CompositeDisposable есть серьезный минус: у него внутри есть bool _disposed, которая не позволяет переиспользовать список.

                public void Dispose()
                {
                    var currentDisposables = default(IDisposable[]);
                    lock (_gate)
                    {
                        if (!_disposed)
                        {
                            _disposed = true;
                            currentDisposables = _disposables.ToArray();
                            _disposables.Clear();
                            _count = 0;
                        }
                    }
        
                    if (currentDisposables != null)
                    {
                        foreach (var d in currentDisposables)
                            if (d != null)
                                d.Dispose();
                    }
                }

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


        1. programm_err
          00.00.0000 00:00

          У CompositeDisposable есть метод Clear, который как раз делает то, что вам надо - у всех IDisposable вызовет Dispose, затем почистит список, но при этом _disposed не выставит в true. Т.е. можете переиспользовать его сколько угодно раз.


          1. Ipashelovo Автор
            00.00.0000 00:00

            Это правда. На главной странице библиотеки в гитхабе даже есть об этом пункт. Но я люблю пользоваться подсказками умного Райдера, когда реализую IDisposable, а он в таком случае вызовет именно этот метод у CompositeDisposable при заполении метода. И очень просто забыть, что это надо обязательно поменять


  1. RedFox2020
    00.00.0000 00:00

    Я наверное не первый заметно, но автор не корректно отписался от событий


    1. Ipashelovo Автор
      00.00.0000 00:00

      Спасибо, поправил


  1. Popou
    00.00.0000 00:00

    Хотелось бы узнать о преимуществах UniRx. Просто я всегда использовал ReactiveUI + CommunityToolkit.Mvvm


    1. Ipashelovo Автор
      00.00.0000 00:00

      Как уже отмечали выше, дело в заточенности под юнити. Сами авторы библиотеки помечают это как "Unity-specific Extra Gems"

      // Unity's singleton UiThread Queue Scheduler
      Scheduler.MainThreadScheduler 
      ObserveOnMainThread()/SubscribeOnMainThread()
      
      // Global StartCoroutine runner
      MainThreadDispatcher.StartCoroutine(enumerator)
      
      // convert Coroutine to IObservable
      Observable.FromCoroutine((observer, token) => enumerator(observer, token)); 
      
      // convert IObservable to Coroutine
      yield return Observable.Range(1, 10).ToYieldInstruction(); // after Unity 5.3, before can use StartAsCoroutine()
      
      // Lifetime hooks
      Observable.EveryApplicationPause();
      Observable.EveryApplicationFocus();
      Observable.OnceApplicationQuit();


  1. pauldyatlov
    00.00.0000 00:00

    В примере с событиями в методе Dispose опечатка, -= вместо +=