はじめに
UniRxにはReactiveCommand
とAsyncReactiveCommand
という機構が用意されています。
こちらはICommandインタフェースの考えをベースに作られており、「処理を実行できるかどうか」をIObservable<bool>
を使って許可したり禁止したりできるようにすることができるようになっています。
とりあえず、詳しい使い方を見てみましょう。
ReactiveCommand
概要
ReactiveCommand
はIObservable<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が入力されている状態で、ReactiveCommand
のExecute()
を実行することでOnNext
がそこから発行されます。
- イメージ図(falseのとき)
- イメージ図(trueのとき)
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!");
}
}
Toggleの状態に応じてButtonのクリックイベントを実行するかどうかに加え、Buttonそのもののinteractive状態も同時に制御してくれるようになりました。
AsyncReactiveCommand
AsyncReactiveCommand
はReactiveCommand
を非同期処理に対応させたものになります。
だいたいの挙動は似ているのですが、大きく違う点として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);
});
}
}
少しわかりにくいかもしれませんが、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);
});
}
}
このように、AsyncReactiveCommand
に共通のIReactiveProperty<bool>
を渡すことで、Buttonをグルーピングしてまとめて状態の管理ができるようになりました。
まとめ
ReactiveCommand
の方はちょっと使いみちがあまりない感じがしますが、AsyncReactiveCommand
の方は汎用性が高くかなり便利に使える気がします。
特にUIにバインドすることで真価を発揮するので、UI周りで困ったら使ってみるとよいのではないでしょうか。