Help us understand the problem. What is going on with this article?

【UniRx】 ReactiveCommand/AsyncReactiveCommandについて

はじめに

UniRxにはReactiveCommandAsyncReactiveCommandという機構が用意されています。
こちらはICommandインタフェースの考えをベースに作られており、「処理を実行できるかどうか」をIObservable<bool>を使って許可したり禁止したりできるようにすることができるようになっています。

とりあえず、詳しい使い方を見てみましょう。

ReactiveCommand

概要

ReactiveCommandIObservable<bool>を使って、処理の実行の許可/不許可を制御することができます。
サンプルコードを見たほうが早いので、次のコードを御覧ください。

void Start()
{
    // 実行の許可/不許可を制御するIObservable<bool>
    ReactiveProperty<bool> gate = new BoolReactiveProperty(false);

    // gateを使って実行制御する
    var command = new ReactiveCommand((IObservable<bool>)gate);

    // command.Execute()を実行した時に、
    // 実行ができる状態ならOnNextが発行される
    command.Subscribe(_ =>
    {
        Debug.Log("実行した!");
    });

    // 実行を不許可にする
    gate.Value = false;

    Debug.Log("Execute()を呼び出します gate = false");

    // Executeを実行するとgateの状態を判定し、
    // trueならObservableにOnNextを流す
    // falseなら何もしない
    command.Execute();

    // 実行許可する
    gate.Value = true;

    Debug.Log("Execute()を呼び出します gate = true");

    command.Execute();
}
Execute()を呼び出します gate = false
Execute()を呼び出します gate = true
実行した!

ReactiveCommandは引数としてIObservable<bool>をとり、このObservableの値を用いてコマンドの実行許可/不許可の制御を行います。
ReactiveCommand自体が購読可能(Sunscribeできる)存在であり、trueが入力されている状態で、ReactiveCommandExecute()を実行することでOnNextがそこから発行されます。

  • イメージ図(falseのとき)

1.png

  • イメージ図(trueのとき)

2_.png

ToReactiveCommand

newしなくても、IObservable<bool>からToReactiveCommandで直接生成できます。

ReactiveProperty<bool> gate = new BoolReactiveProperty(false);
var command = gate.ToReactiveCommand();

任意のメッセージを発行する場合

ReactiveCommand<T>を使う。

void Start()
{
    ReactiveProperty<bool> gate = new BoolReactiveProperty(false);

    // string型を扱うReactiveCommand
    var command = gate.ToReactiveCommand<string>();

    command.Subscribe(x =>
    {
        Debug.Log(x);
    });

    // 実行を不許可にする
    gate.Value = false;

    command.Execute("ほげ");

    // 実行許可する
    gate.Value = true;

    command.Execute("ふが");
}
ふが

UIとの組み合わせ

ReactiveCommandは単体でも使えますが、uGUI要素と連携させることでより便利に使えるようになります。
uGUI要素にBindすることで、状態に応じてUIのinteractiveも制御してくれるようになります。

public class ReactiveCommandSample : MonoBehaviour
{
    [SerializeField] private Toggle _toggle;
    [SerializeField] private Button _button;
    [SerializeField] private Text _resultText;

    void Start()
    {
        // toggleの状態を使ったReactiveCommandを作成
        var command = _toggle.OnValueChangedAsObservable().ToReactiveCommand();

        // ButtonにBindする
        command.BindTo(_button);

        command.Subscribe(_ =>
        {
            _resultText.text += "Click!";
        });

        // BindToOnClickでまとめて定義もできる
        // command.BindToOnClick(_button, _ => _resultText.text += "Click!");
    }
}

rt1.gif

Toggleの状態に応じてButtonのクリックイベントを実行するかどうかに加え、Buttonそのもののinteractive状態も同時に制御してくれるようになりました。

AsyncReactiveCommand

AsyncReactiveCommandReactiveCommandを非同期処理に対応させたものになります。

だいたいの挙動は似ているのですが、大きく違う点としてSubscribeのスキーマが

IDisposable Subscribe(Func<T, IObservable<Unit>> asyncAction);

に変更されており、Execute()で呼び出された処理の実行後にIObservable<Unit>のreturnが必要になっています。

なぜこのような仕組みになっているのかは、使用例を見ていただくとわかると思います。

挙動

引数を指定せずにAsyncReactiveCommandを作成した場合は、Execute()を実行した際に自分自身で実行状態をfalseに切り替えます。
その上で、購読時に登録したIObservable<Unit>の完了(OnCompleted/OnError状態になる)を待ち受け、全て完了状態になったタイミングでtrueに戻すという仕組みになっています。

文章ではわかりにくいと思うので、実例を見てみましょう。

次のコードは、入力されたURLに対してHTTP通信を行い、結果を表示するという処理です。
その通信処理とButtonの状態をAsyncReactiveCommandを使って連結しています。

public class AsyncReactiveCommandSample : MonoBehaviour
{
    [SerializeField] private InputField _urlText;
    [SerializeField] private Button _button;
    [SerializeField] private Text _resultText;

    void Start()
    {
        var command = new AsyncReactiveCommand();

        command.BindToOnClick(_button, _ =>
        {
            return ObservableWWW.GetWWW(_urlText.text)
                    .ForEachAsync(www => _resultText.text = www.text);
        });
    }
}

rt2.gif

少しわかりにくいかもしれませんが、HTTP通信中はButtonがグレーアウトして無効化されています。
このように、AsyncReactiveCommandを使うことで非同期処理を実行しそれが終了するまで次のコマンドを実行しないといった処理が簡単に書くことができるようになります。つまり、正確な連打防止機構を作ることができるようになります。

ちなみに、1つのAsyncReactiveCommandを複数回Subscribeして非同期処理を並列で走らせた場合、すべての非同期処理が終わるまでAsyncReactiveCommandは有効状態に戻りません。

省略記法

この使い方をする場合、わざわざ自分でAsyncReactiveCommandをnewするのも冗長です。
UI要素から直接AsyncReactiveCommandを生成して利用する省略記法(BindToOnClick)が用意されているため、そちらを使ったほうがよいでしょう。

省略記法
_button.BindToOnClick(_ =>
{
    return ObservableWWW.GetWWW(_urlText.text)
        .ForEachAsync(www => _resultText.text = www.text);
});
比較(自分で定義する書き方)
var command = new AsyncReactiveCommand();

command.BindToOnClick(_button, _ =>
{
    return ObservableWWW.GetWWW(_urlText.text)
            .ForEachAsync(www => _resultText.text = www.text);
});

補足: ForEachAsync

ForEachAsyncは簡単に言ってしまえばDo() + AsUnitObservable()です。
非同期に処理を実行したいが、Subscribeまではしたくないといった場合に使うことができます。

ForEachAsync についての詳細についてはUniRx作者のneueccさんがブログにて紹介されています。

AsyncReactiveCommandを使ってUIをまとめて制御する

AsyncReactiveCommandにはIReactiveProperty<bool>を引数にとるパターンもあります。
こちらを使うことで、複数のAsyncReactiveCommandを共通のIReactivePropertyで制御するといったことができるようになります。

こちらもコードを見てもらったほうが早いです。

ボタンが押されたら各々のアクションを実行する
public class AsyncReactiveCommandSample2 : MonoBehaviour
{
    [SerializeField] private Button _wait1SecondButton;
    [SerializeField] private Button _wait2SecondsButton;
    [SerializeField] private Button _httpButton;
    [SerializeField] private Text _resultText;

    /// <summary>
    /// 全ボタンで共通して使うReactiveProperty
    /// </summary>
    private ReactiveProperty<bool> _sharedGate = new ReactiveProperty<bool>(true);

    void Start()
    {

        // 1秒待つボタン
        _wait1SecondButton.BindToOnClick(_sharedGate, _ =>
        {
            _resultText.text = "1秒待つよ";
            return Observable.Timer(TimeSpan.FromSeconds(1))
                .ForEachAsync(__ => _resultText.text = "1秒経ったよ");
        });

        // 2秒待つボタン
        _wait2SecondsButton.BindToOnClick(_sharedGate, _ =>
        {
            _resultText.text = "2秒待つよ";
            return Observable.Timer(TimeSpan.FromSeconds(2))
                .ForEachAsync(__ => _resultText.text = "2秒経ったよ");
        });

        // 通信するボタン
        _httpButton.BindToOnClick(_sharedGate, _ =>
        {
            _resultText.text = "通信するよ";
            return ObservableWWW.GetWWW("https://unity3d.com")
                .ForEachAsync(www => _resultText.text = www.text);
        });
    }
}

rt3.gif

このように、AsyncReactiveCommandに共通のIReactiveProperty<bool>を渡すことで、Buttonをグルーピングしてまとめて状態の管理ができるようになりました。

まとめ

ReactiveCommandの方はちょっと使いみちがあまりない感じがしますが、AsyncReactiveCommandの方は汎用性が高くかなり便利に使える気がします。
特にUIにバインドすることで真価を発揮するので、UI周りで困ったら使ってみるとよいのではないでしょうか。

参考文献

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away