Примерно год назад я начал работать с этой библиотекой, а теперь настолько к ней привык, что не представляю как без нее можно обходиться. Так как многие знакомые разработчики на 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)
AxeFizik
00.00.0000 00:00Я немного выпал из Unity разработки, но ведь UniRx больше не поддерживается и даже не работает в Unity 2022, разве нет?
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++; } } }
AxeFizik
00.00.0000 00:00Хорошо, если работает)
Я, помнится, скачал какую-то самую новую версию Unity, поставил netstandard2.1, установил UniRx и получил конфликт Assemblies.
Когда руки дойдут посмотрю ещё раз
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(); }
ZimM
00.00.0000 00:00Благодарю! Не зная про CompositeDisposable, в результате написал свой аналог с другим именем :)
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(); } }
Тогда как наш под капотом работает точно так же, как и в примере в посте, то есть при диспоузе он просто диспоузит все элементы, а потом чистится
programm_err
00.00.0000 00:00У CompositeDisposable есть метод Clear, который как раз делает то, что вам надо - у всех IDisposable вызовет Dispose, затем почистит список, но при этом _disposed не выставит в true. Т.е. можете переиспользовать его сколько угодно раз.
Ipashelovo Автор
00.00.0000 00:00Это правда. На главной странице библиотеки в гитхабе даже есть об этом пункт. Но я люблю пользоваться подсказками умного Райдера, когда реализую IDisposable, а он в таком случае вызовет именно этот метод у CompositeDisposable при заполении метода. И очень просто забыть, что это надо обязательно поменять
Popou
00.00.0000 00:00Хотелось бы узнать о преимуществах UniRx. Просто я всегда использовал ReactiveUI + CommunityToolkit.Mvvm
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();
IL_Agent
А почему нельзя использовать обычный rx? (не знаток юнити)
oblakooblako
В unirx много расширений для удобства работы с классами unity.