(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]



UniRx(ユニアールエックス)の基本的な使い方と具体的な利用例【Unity】【UniRx】


このエントリーをはてなブックマークに追加


この記事でのバージョン
Unity 2018.3.4f1


はじめに

Unityを使っているとよく耳にするUniRxですが、

(以下のツイートは開発者さん本人のもの)



実は今まで一度も使ったことがなかったので、勉強ついでに記事にしてみました!

UniRx - Reactive Extensions for Unity - Asset Store


ただし、UniRxについての詳細を理解するという感じではなく、

「基本的な使い方」「具体的な利用例」といった軽く概要を掴むのが目的の記事です。


なお、最初は初心者向けの記事にしようかと思ったのですが、

それだとUniRx以外の解説が増えて逆に分かりづらくなりそうだったので、

今回はUnityやC#にある程度慣れたけど、UniRxは使ったことがない人向けの記事になっています。

(そもそもUnityやC#、プログラミングを始めたばかりの人にUniRxは難易度が高過ぎる)


具体的にはデリゲートやイベント、Observerパターン、インタフェース、

拡張メソッド、LINQあたりがなんとなくでも分かっていれば大丈夫だと思います。







UniRxとは

そもそもUniRxとはなんぞやという話からですが、

起源はMicrosoftResearchが開発したRx(Reactive Extensions)というC#用のライブラリらしいです。



RxはObserverパターンを元に設計された、非同期処理用のライブラリなのですが、

残念ながらこれをUnityでそのまま使う事は出来ませんでした。


そこでUnityでも使えるRxが作られました。それがUniRxです。

なお、PC / Mac / Android / iOS / WebGL / WindowsStoreなどに対応してるようです。


ちなみに、UniRx以外にもRxJavaやRxSwiftなど様々なRxあるので、




考え方や使い方を一度覚えてしまえば、色々な所で役に立ちそうです。


基本的な使い方

ここからはUniRxの基本的な使い方について説明していきます。


まず、UniRxはAsset StoreかGithubから入手できます。

インポート後、特に設定などは必要ありません。

UniRx - Reactive Extensions for Unity - Asset Store


ちなみに、アセットにはサンプルのコードが豊富に用意されています。


f:id:kan_kikuchi:20190317142843j:plain


Subject

UniRxを使う上でまず理解する必要があるのが、Subjectというクラスと、そのクラスに実装されている

処理を登録(購読)するSubscribe処理を実行するOnNextというメソッドです。

//引数にstringが渡せるSubjectを作成(intやboolなど他の型でもOK)
var sub = new Subject<string>();

//Subscribeを使って処理を登録する
sub.Subscribe(text => Debug.Log(text));

//"テキスト"というstringを渡して、処理を実行(ログが表示される)
sub.OnNext("テキスト");

f:id:kan_kikuchi:20190316093936j:plain


これだけだとデリゲートと大差ないですが、もちろんこれだけでは終わりません。


IObservable

先程、分かりやすいように「Subjectに実装されているメソッド」といいましたが、

正確には「Subjectが実装しているインターフェースのメソッド」です。

例えばSubscribeはIObservableというインターフェースのメソッドです。


つまり、IObservableだけ公開すれば、処理の登録部分だけを公開出来るという事でもあります。

(OnNextはIObservableのメソッドではないので)

/*SampleA*/
//引数にstringが渡せるSubject
private Subject<string> _subject = new Subject<string>();
    
//Subjectのうち、IObservableだけを公開し、処理を登録できるように
public IObservable<string> Observable{
  get { return _subject; }
}
/*SampleB*/
//Subscribeで処理を登録する事は出来るが、
_sampleA.Observable.Subscribe(text => Debug.Log(text));

//OnNextを使って処理を実行する事は出来ない。
//_sampleA.Observable.OnNext("テキスト");


これでもイベントと同じような感じですが、まだまだ続きます。


IObserver

Subscribeと同様にOnNextも別のインターフェース、IObserverのメソッドです。

最初は分かりやすいようにOnNextだけを例に挙げましたが、

IObserverにはOnErrorOnCompletedというメソッドもあります。

//引数にstringが渡せるSubjectを作成
var sub = new Subject<string>();

//onNextで通常時の処理、onErrorでエラー時の処理、onCompletedで終了時の処理を登録
sub.Subscribe(
  onNext:text => Debug.Log("テキスト! : " + text),
  onError:error => Debug.Log("エラー! : " + error),
  onCompleted:() => Debug.Log("完了!")
);


OnNextと同じようにOnCompletedを使う事で、登録した処理を実行できますが、

それに加えて、それ以降の処理を止める(実行しない)事にもなります。

(IDisposableを使えば、onCompletedの処理をせずに止める事も可能)

sub.OnNext("テキスト1");
sub.OnCompleted();
sub.OnNext("テキスト2");//実行されない

f:id:kan_kikuchi:20190316112117j:plain


OnErrorについても同様です。

ただし、エラーと言ってもコンソールにエラー表示を出すわけではありません。

sub.OnNext("テキスト1");
sub.OnError(new Exception("例外だよ"));
sub.OnNext("テキスト2");//実行されない

f:id:kan_kikuchi:20190316112157j:plain


ここらへんから便利そうな空気が漂ってきた感じがなくもないですが、まだ序の口です。


オペレータ

UniRxにはオペレータというLINQのような便利なメソッドが用意されています。

オペレータは基本的にIObservableの拡張メソッドとして実装されており、

Subscribeで登録する処理の前に別の処理を追加するような感じで使います。


例えば、Whereを使うとその処理を実行するかの判定(フィルタリング)を追加出来ますし、

Selectを使うとデータ(Subject<string>ならstring)を任意の形に加工する事が出来ます。

//引数にstringが渡せるSubjectを作成
var sub = new Subject<string>();

//Subscribeでログの処理を追加、その前にWhereとSelectの処理も設定
sub.Where(text => text.Length < 10) //10文字未満の時だけ、これ以降の処理をする
  .Select(text => text + text[0])   //1文字目を最後尾に追加
  .Subscribe(text => Debug.Log(text));

//処理の実行
sub.OnNext("テキストテキストテキストテキスト"); //10文字以上なので、ログは表示されない(Selectも実行されない)
sub.OnNext("テキスト");                  //10文字未満なので、1文字目(テ)を最後尾に追加したログ(テキストテ)が表示される

f:id:kan_kikuchi:20190316113951j:plain


ちなみにオペレータの処理でエラーが出た場合、onErrorで登録された処理が実行されます。

(onErrorを登録してない場合は普通のエラーログが出る)

また、OnErrorを実行した時と同じように、それ以降の処理は実行出来ません。

//引数にstringが渡せるSubjectを作成
var sub = new Subject<string>();

//Subscribeでログの処理を追加、その前にWhereとSelectの処理も設定
sub.Where(text => text.Length < 10) //10文字未満の時だけ、これ以降の処理をする
  .Select(text => text + text[0])   //1文字目を最後尾に追加
  .Subscribe(
    onNext :text => Debug.Log(text), 
    onError:error => Debug.Log("エラー! : " + error)
);

//処理の実行
sub.OnNext("");       //1文字目がないので、Selectのtext[0]でエラーが出る
sub.OnNext("テキスト"); //実行されない

f:id:kan_kikuchi:20190317142712j:plain


そしてこのオペレータ、めちゃめちゃ種類がある(=色んな事が簡単に出来る)んです。



ここまで来れば、だいぶ便利な気がしてきたのではないでしょうか。


具体的な利用例

UniRxの基本的な使い方や、便利さ加減がなんとなく分かってきたとしても、

やはり気になるのは「具体的に何が出来るの?何が利点なの?」という事だと思います。

(書き方が統一されて、コードが読みやすくなるという利点も、もちろんありますが)


なので、ここからは具体的な利用例について説明してきます。

なお、言葉やUniRxのコードだけで説明すると分かりづらいと思うので、

UniRxを使わない場合のコードと比較してみました。


遅延処理

「1秒後に音を鳴らす」という感じで、

間を開けて何かの処理を行う「遅延処理」というのはゲームではよくあります。

こういう場合、Unityではコルーチンを使うのが一般的かと思います。

(Invokeを使う手もあるが、今回は割愛)

private void Start(){
  //2秒後にログを表示実行する
  StartCoroutine(DelayMethod(2f, () =>{
    Debug.Log("2秒遅れて実行");
  }));
}
    
//渡された処理を指定時間後に実行する
private IEnumerator DelayMethod(float waitTime, Action action){
  //指定時間待つ
  yield return new WaitForSeconds(waitTime);

  //処理を実行
  action();
}

f:id:kan_kikuchi:20190316122945g:plain


これがUniRxならなんと1行で書けてしまう上に、

MonoBehaviourを継承してないクラスからでも実行可能です。

(StartCoroutineはMonoBehaviourを継承してないと使えない)

Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe(_=>Debug.Log("2秒遅れて実行"));


なお、SubjectやIObservableなしで、いきなりSubscribeしてるのが変に見えるかもしれませんが、

Observable.Timerがオペレータの一種(ObservableはStaticクラス)で、

ここでIObservableを生成しているので、Subscribeが実行出来るという感じです。

//2秒後に処理が実行されるIObservableを作成
IObservable<long> observable = Observable.Timer(TimeSpan.FromSeconds(2));

//処理を登録
observable.Subscribe(_=>Debug.Log("2秒遅れて実行"));



ボタンを押した時の処理

uGUIのButtonは、ボタンを押した処理をonClick.AddListenerを使う事で簡単に設定出来ますが、

例えば「押した数が偶数回の時だけ処理を行う」みたいな場合はひと工夫必要になります。

//ボタンを押した回数
private int _clickCount = 0;
 
private void Start(){
  //ボタンが押した時のイベントに処理を追加
  GetComponent<Button>().onClick.AddListener (Click);
}

//ボタンを押したときの処理
private void Click(){
  //押した回数を加算し、偶数の時だけログを出すように
  _clickCount++;
  if (_clickCount % 2 == 0){
     Debug.Log("偶数!");
  }
}

f:id:kan_kikuchi:20190316133401g:plain


こんな処理も、UniRxならたった1行で実装出来ちゃいます。(分かりやすいように改行はしてます)

GetComponent<Button>()
  .OnClickAsObservable()
  .Buffer(2) //2回分の処理をまとめて行う
  .Subscribe(_=>Debug.Log("偶数!"));


これまたSubjectもIObservableもありませんが、今回はUniRxが追加したButtonの拡張メソッド、

OnClickAsObservableでIObservable化したクリックのイベントを生成しているので、

Subscribeが実行出来るという感じです。(Bufferはオペレータ)

//IObservable化したクリックのイベントを生成
IObservable<Unit> observable = GetComponent<Button>().OnClickAsObservable();
//IObservable<Unit> observable = GetComponent<Button>().onClick.AsObservable();//この書き方でも同じ

//ボタンを押したときの処理を追加
observable.Buffer(2).Subscribe(_=>Debug.Log("偶数!"));



値の監視

「値が変更されたらUIを更新したい」みたいな事はよくありますが、

そういう場合はイベントを使うと比較的簡単に実装出来ます。

/*SampleA*/
//変更を監視する値
private int _value = 0;

//値が変更された時に実行されるイベント
public event Action<int> ChangedValue = delegate{};
    
private void Start(){
  //てきとうに値を変更
  SetValue(1);
  SetValue(1); 
  SetValue(2); 
  SetValue(2); 
  SetValue(1); 
}

//値を設定する
private void SetValue(int value){
  //同じ値が来た場合は設定しないし、イベントも実行しない
  if (_value == value){
    return;
  }

  _value = value;
  ChangedValue(_value);
}
/*SampleB*/
[SerializeField]
private SampleA _sampleA = null;
    
private void Awake(){
  //値が変更されたらログを出すように
  _sampleA.ChangedValue += (value) => Debug.Log(value);
}

f:id:kan_kikuchi:20190316142401j:plain


これをUniRxを使って実装すると以下の通り。

/*SampleA*/
//変更を監視する値
private ReactiveProperty<int> _valueReactiveProperty = new ReactiveProperty<int>(0);

//ReactivePropertyのうち、IObservableだけを公開し、処理を登録できるように
public IObservable<int> Observable{
  get { return _valueReactiveProperty; }
}
    
private void Start(){
  //てきとうに値を変更
  SetValue(1);
  SetValue(1); 
  SetValue(2); 
  SetValue(2); 
  SetValue(1); 
}

//値を設定する
private void SetValue(int value){
  _valueReactiveProperty.Value = value;
}
/*SampleB*/
[SerializeField]
private SampleA _sampleA = null;
    
private void Awake(){
  //値が変更されたらログを出すように
  _sampleA.Observable.Subscribe(count => Debug.Log(count));
}

f:id:kan_kikuchi:20190316142911j:plain


やはりSubjectはありませんが、今回はIObservableがあります。

お察しの通り、ReactivePropertyがIObservableを実装しているのです。


このReactivePropertyは値を保持しており、Valueから取得や変更が行えます。

また、値の代入時に元の値と異なっていればイベントが自動で実行されるので、

値が変化したかを判定する必要がありませんし、イベントの実行忘れもなくなります。

//_valueReactiveProperty.Valueとvalueが違う値ならイベントが自動で実行される
_valueReactiveProperty.Value = value;


今回はintの例ですが、もちろんfloatやboolなどにも使えます。


なお、画像を見て分かる通り、処理の登録時に初期値である0でイベントが実行されています。

UIの更新時など、たいていの場合は最初の更新も必要になるため、これも地味に嬉しかったりします。


ちなみに、ここまで色々な使い方を示すためにわざとSubjectを使わない例だけを挙げていますが、

「実際に使う時もSubjectを全く使わない」というわけではないので、ご注意を。


欠点や問題点

利点だけでなく、欠点や問題点も書いておかなければフェアではないのですが、

使い始めたばかりで、正直よく分かってはいないので、参考になりそうなものを載せておきます。

  • 33:55あたりからUniRxを使ったプロジェクトでダメだった点(UniRx開発者自身の講演)



  • 45ページからRxの問題点


一応、ざっくりまとめてると以下のような感じ。

  • 学習コストが高い
  • Rxを徹底して使いすぎると、コードが追いづらくなる?(リアクティブスパゲッティ?)
  • 正しく使わないと、致命的なメモリの消費や負荷の増大に繋がる



おわりに

使いこなせれば、かなり便利なのは間違いないですが、

そうなるには結構時間が必要そうなアセット(?)でした。


ただ、思っていたより、使い始める事自体は簡単だったので、

とりあえず使いながら諸々を覚えていこうかと思います。


なお、今回紹介した以外にも色々便利な使い方や落とし穴があると思うので、

良いネタを見つけ次第、随時記事にしていきます。