目次
Reactive ProgramingをUnityに活用した話
この記事のメイン対象:
- UnityでObservable Patternを活用することに興味持っている人
- 何らかのパターンを使って設計・実装してみたい人
- クラスの責務や依存関係を整理したい人
Reactive Programmingとは
リアクティブプログラミング(Reactive Programming)はエンジニアにとって疎いことではありません。
簡単に解説すると、「非同期データストリームを使用したプログラミング」です。
Reactive Programingの主たることは:「Observable(観察可能な)シーケンスを使用して、非同期のイベントベースのプログラムを作成することです」
上記に記載したReactive Programmingの定義には、重要な言葉が1つあります – 「非同期」(Asynchronous)です。
データがストリームで非同期に発行されると通知されるため、メインプログラムフローとは独立しています。
データストリームを中心にプログラムを構成することで、非同期コードを記述します。
つまり、ストリームが新しいアイテムを発行したときに呼び出されるコードを記述します。
注意:
- Reactive ProgrammingはReactive Systemではありません。(Reactive Systemはこの記事で説明しないです)
- Reactive Programingのコード書き方が明確で読みやすいですが、諸刃の剣です。例えば、色々ロジックを1つ関数に結合してSubscribeやZipやSelectなどをすると読解性がなくす可能性もあります。バグ発生した場合、Debug作業が非常にやり辛いです。なので、自分勝手に実装しないでください!Reactive Programingを利用する時、コメントを書いたり、説明したり、ダイアグラムを描いたりください。
- Reactive Programing、基本はThread管理などが必要です、その管理ができないと不利なサイドエフェクト(side Effect)の痛みは測り知れないです。例えばdeadlock hunt, blockingなどの問題。
AsynchronousはMultithreadingではないです!!!!
多くの人は、マルチスレッドと非同期は同じものであると教えられていますが、そうではありません。
マルチスレッドは非同期の一種にすぎません
AsyncとMultithreadingの分別はサンプルで解説したほうが理解しやすいと思います。
例えば、あるレストランで「親子丼と味噌汁セット」の注文をするときでは:
- Synchronous: 「親子丼」を料理してから、「味噌汁」を料理します。
- Asynchronous – Single Thread (シェフが一人しかいない): 「親子丼」を料理して、完了するまでを待たずにタイマーをセットして、「味噌汁」を料理して、タイマーもセットする。タイマーが鳴るまで別の仕事をやります。上記の両方タスクが完了しましたら、「親子丼」と「味噌汁」を準備して、提供する。
- Asynchronous – Multithread (シェフが二人以上): 一人が「親子丼」を準備する、もう一人が「味噌汁」を準備する。全部の料理が上がるまで待ちます。
下記の図を見ればもっと分かると思います。
引用:https://www.baeldung.com/cs/async-vs-multi-threading
結論:
- マルチスレッドは非同期の一種にすぎません
- マルチスレッドはワーカーに関するものであり、非同期はタスクに関するものです。
UniRXとMVPパターン
MVPとは
MVPパターンはAndroidアプリやWeb開発者にとって、とてもおなじみものですね。
MVPはModel-View-Presenterの頭字語です。
このパターンの紹介はネットに記事がいっぱいありまして、更に詳細な解説もあるので、本記事では紹介しません。
参照URLだけを記載しております。
- Unityで学ぶMVPパターン ~ UniRxを使って体力Barを作成する ~
- Web出身のUnityエンジニアによる大規模ゲームの基盤設計
- MVPパターンによる設計アプローチ「あなたのアプリ報連相できてますか」
MVPの基本解説を上記の記事から持っております。
M、V、Pの説明
MVPのMはModel、VはView、PはPresenterをそれぞれ表します。
各々、下記の図のような役割を持ちます。
この設計では、PresenterがModelとViewを所持しています。
そのため、PresenterはModelとViewを知っているのですが、相互参照を避けるためにModelとViewはPresenterを知りません。
UniRXとは
UniRx (Reactive Extensions for Unity) は.NETの ReactiveExtensionsの再実装です
UniRxを使用すると、UnityでReactive Programmingを書くのがもっと簡単になります。
UniRxでReactive Programingを書くと、ロジックを分離できて依存関係が少なくなります。
こうすると、チーム内でテストや実装のタスクの分担を柔軟にできます。
では柔軟に分担するためのWorkflowを説明します。
今までは機能単位で分けて、エンジニア毎に実装タスクを持っています。
UniRXを使用した場合タスクをもっと分解することができます。
上記の図を見ると、2人で1つの機能を分担できることが分かると思います。
UIの反映はすべてUniRXを使って、UIの変更はデータの状態により反映される形で実装する。
Presenterは全然Viewの処理が知らなくても良い、Viewの関数だけよばれたら良い感じです。
例:(解説はコードに記載します)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using System; using UniRx; using UnityEngine; using UnityEngine.UI; /// <sumary> /// Viewのクラスです、このクラスはUnityのコンポーネントを持っています。 ///<sumary/> public class FeatureAView : MonoBehaviour { [SerializeField] private Button _buttonA; [SerializeField] private Image _imgA; [SerializeField] private Text _resultA; public IObservable<Unit> OnClickAsObservable => _buttonA.OnClickAsObservable(); public void UpdateImg(string imgPath) { var spr = Resources.Load<Sprite>(imgPath); _imgA.sprite = spr; } public void UpdateResultA(string result) { _resultA.text = result; // 表示エフェクトなどの処理もここに実装 // UIエンジニアが担当する部分です。 } } /// <sumary> /// Presenterのクラスです、UIComponetのViewクラスとデータのModelクラス ///<sumary/> public class FeatureAPresenter : MonoBehaviour { [SerializeField] private FeatureAView _view; private ReactiveProperty<FeatureAModel1> _model1 = new ReactiveProperty<FeatureAModel1>(); private ReactiveProperty<FeatureAModel2> _model2 = new ReactiveProperty<FeatureAModel2>(); private void Awake() { // Modelの変更の検知にViewの表示のCallbackを登録する _model1.Subscribe(x => { _view.UpdateResultA(x.ResultA.ToString()); }) .AddTo(this); _model2.Subscribe(x => { _view.UpdateImg(x.Member1); }) .AddTo(this); // ボタンの押すの検知に、Callbackを登録する。 _view.OnClickAsObservable.Subscribe(x => { OnClickChangeButton(); }).AddTo(this); } private void OnClickChangeButton() { _model1.Value.ResultA = 123445; // ボタンにクリックするの処理など。 // 伝統で書くならこの関数がPublicして、UnityEditorでDragAndDropする。 // RXを使ったら、Privateにして、Viewに登録すれば良い、全然UnityEditor上の設計は考えなくて良い、その仕事はUI担当者が考えてもらいます。 } } public class FeatureAModel1 { public int ResultA; public int ResultB; } public class FeatureAModel2 { public string Member1; public string Member2; } |
結論:
- システム開発では、依存関係をなくすことが非常に大事です。依存関係をなくすことができたら、メンテナンスコストやタスクアサインすることも楽になります。
- Reactive ProgramingはUnityの新しいDOTS技術に活用ことができると思います。これから、ゲーム開発をOOP(オブジェクト指向プログラミング)以外、DOP(データ指向プログラミング)も身についたほうが良いと思います。