C# eventのラップと、簡略・単純化したUniRx/R3の機能を提供するUnity/C#用ライブラリです。
- C# eventをベースにUniRxに準拠したAPI
- StreamやScheduler、非同期など複雑な機能を廃して基本的な機能のみを集約
基本的な構文のサンプルは以下の通りです。
public IPublisher<int> OnDamage => _onDamage;
private Publisher<int> _onDamage = default;
private bool _isInvincible = false;
private IDisposableSubscription _subscription = default;
private void Start() {
_onDamage = new Publisher<int>();
_subscription = OnDamage
.OnCondition(() => !_isInvincible)
.Subscribe(value => {
Debug.Log(value);
});
}
private void OnDestroy() {
_subscription.Dispose();
_onDamage.Dispose();
}
private void Damage(int value) {
_onDamage.Invoke(value);
}- eventの代わりに Publisher<T> を生成する
- 生成した Publisher<T> を、 IPublisher<T> として公開する
- IPublisher<T> に対して Subscribe() をして購読する
- Subscribeの戻り値を Dispose() するか、AddTo(_disposable) のようにして Disposables に追加、そのDisposablesを Dispose() して購読解除
public IReactiveProperty<int> Hp => _hp;
private ReactiveProperty<int> _hp = default;
private Disposables _disposables = default;
private void Start() {
_disposables = new Disposables();
_hp = new ReactiveProperty<int>(100).AddTo(_disposables);
Hp.OnValueChanged
.Subscribe(value => Debug.Log(value))
.AddTo(_disposables);
}
private void OnDestroy() {
_disposables.Dispose();
}
private void Damage(int value) {
_hp.Value - value;
}
private void Heal(int value) {
int minimum = Mathf.Min(100, _hp.Value + value);
_hp.SetForceNotify(minimum);
}-
ReactiveProperty<T> を生成する
同時に初期値も設定可能 -
生成した ReactiveProperty<T> を IReactiveProperty<T> として公開する
-
購読、購読解除は Publisher<T> と同様
-
アクセサ/メソッド 効果 .Value 値が現在と違えば発火 .SetForceNotify() どんな値でも強制発火 .SetWithoutNotify() どんな値でも発火しない のいずれかで値を変更する
- Disposables を生成する
- Subscribe() の戻り値である IDisposableSubscription を Add して登録する
もしくは IDisposableSubscription に対して .AddTo(_disposables) のように記述して登録する - OnDestroy() や IDisposable.Dispose() において Dispose() する
- Window -> Package Manager から Package Manager を開く
- 「+」ボタン -> Add package from git URL を選択
- 以下を入力してインストール
- RuntimeフォルダをそのままUnityプロジェクトでお使いのスクリプトフォルダに入れてください
UniRxの(R)3歩前に使うライブラリをテーマに、eventとUniRx/R3の中間、橋渡しといった位置付けで、教育/学習目的として制作しています。
C# eventを使っていて購読解除のはん雑さや、参照の追いにくさという痛みを感じている。
かといってUniRx/R3に手を出すにはまだ早い気がするし、機能が多すぎて手に余る...。
といった方のためのライブラリです。
C#のeventは「購読したら購読解除」がワンセットになっていますが、例えばこのように
[SerializeField] private MyPublisher _publisher = default;
private void Start() {
_publisher.OnEvent += DoSomething;
}
private void OnDestroy() {
_publisher.OnEvent -= DoSomething;
}
private void DoSomething() { }同じような式を書かなくてはならず、また + と - を取り違えやすい、そもそも購読解除自体を忘れがち...ということもあります。
また、購読が増えてくると誰が何を購読しているか管理がとても難しくなります。
eventは基本的に
public event Action OnEvent = delegate { }という風に記述しますが、Visual StudioのIntelliSenseにおいて「n件の参照」といった形で誰から参照されているのかを見ることができません。
そこで、
private Publisher<int> _publisher = default;
private IDisposableSubscription _subscription = default;
private void Start() {
_publisher = new Publisher<int>();
_subscription = _publisher.Subscribe(DoSomeThing);
}
private void OnDestroy() {
_subscription.Dispose();
}
private void DoSomething(int value) { }というラッパを書き、
- eventの購読と解除をクラスに抽象化し、メソッドを呼ぶことで購読の解除をしよう。
- さらにeventをクラス(インターフェース)で公開することでIntelliSenseなどで参照を追えるようにしよう。
というのが基本的な考え方です。
そこに、UniRx/R3の便利な機能とAPIの方式を一部導入して、eventとUniRx/R3の中間となるライブラリを目指したのがXqLuaになります。
それならいっそUniRxを使えばいいのでは?と思うかもしれませんが、初学者にとってUniRxやR3は手に余ることが多いです。
「強そうだから」「LLMがそう言っていたから」使うのではなく、まずはeventから始めて、上述の痛みを感じたところでXqLuaを使って、最終的にUniRxを使い始めてほしいと思います。
個人開発や学生開発、ハッカソンなど小規模開発には十分に耐えられると思います。
しかし、それ以上は保証できませんので、UniRx/R3を勉強して導入することをおすすめします。
特にスレッドをまたいだ処理に関しては対応していないので、なおさら乗り換えるべきだと思います。
| XqLua | UniRx/R3 | |
|---|---|---|
| eventベース | ○ | × |
| Observable | × | ○ |
| Subject | Publisher | Subject |
| Operator | 最低限 | 豊富で便利 |
| Scheduler | × | ○ |
| 非同期系 | 一部のみ | 豊富で便利 |
| スレッド間処理 | 非対応 | 対応 |
C#のeventに相当するクラスです。
基本的にはUniRx/R3のSubjectを模しており、Subscribe()によって購読、Invoke()によって発火することができます。
IPublisher<T> を公開することで本家と同じく購読のみ許すプロパティを公開できます。
event Action (引数がない、タイミングを取るだけのevent)に相当するPublisherを作る場合は、 Publisher<Empty> として生成してください。
発火するときは .Invoke(Empty.Default); で発火できます。
Void は UniRx の Unit に当たります。
引数が空なのでEmptyと、直感的になるような名前を採用しています。
購読解除は、Subscribeの戻り値である IDisposableSubscription を Dispose() することによって解除できます。
この購読解除を忘れるとリークします。
具体的には、内部のeventにメソッドが登録されたままになってしまい、予想外のところで発火してしまう、メソッドがGCに引っかからないなど不都合が生じます。
忘れないようにDispose()をクセ付けるか、後述のDisposablesを使って確実に購読解除しましょう。
実際にUniRxにおいて初学者のうちはSubjectやSubscribeのDisposeを忘れがちなので、XqLuaでもそれに準拠して「PublisherをnewしたらDisposeする」「SubscribeしたらDisposeする」のサイクルで実装しています。
IPublisher<T>をAwaitableSubscription<T>() で購読することで、発火を1回だけ待つことのできるAwaitable<T>を生成することができます。 この購読は自動で解除されるので、Disposeする必要はありません。
もともと存在するeventをラップするためのクラスです。
UniRxではFromEventに相当します。
EventPublisher.FromEvent<T>() を、購読と購読解除のメソッドを引数に与えて呼ぶと IPublisher<T> を生成することができます。
public event Action<int> OnEvent = delegate { }
public void Start() {
_disposables = new Disposables();
IPublisher<int> eventPublisher = EventPublisher<int>.FromEvent(
method => OnEvent += method,
method => OnEvent -= method
);
}生成した IPublisher<T> は、前述のPublisherと同じように購読とOperatorの登録をすることができます。
ここで生成したIPublisher<T>をDisposeする必要はありません。 本家 Observable.FromEvent も Dispose が実装されていないのでそれに準拠してます。
重要なのは、Subscribeしたときの戻り値であるIDisposableSubscriptionのDispose()を忘れるとリークすることですのでそちらには注意してください。
UniRx/R3のOperatorを模したクラス群です。
PublisherかほかのOperatorに続いてチェーンメソッドを記述することで、どんな条件の時にeventの発火を受け取りたいかをフィルタリングすることができます。
短絡評価を実装しており、Skip(1).Take(1) としても、Skipが消費されたときTakeは消費されません。
使用頻度の高そうなOperatorだけ抽出して実装しています。
なお、完全に互換性があるわけではなくあくまで同じような動作をする、といった位置付けです。
| Operator | 説明 | 引数 | UniRx/R3 |
|---|---|---|---|
| OnCondition | 条件を満たした時だけ発火を通す | Func<bool> | Where |
| OnValueIs | 流れてきた値が条件を満たした時だけ発火を通す | Func<T, bool> | Where |
| OnValueChanged | 値が変わった時だけ発火を通す | なし | Distinct |
| Skip | 指定回数だけ最初の発火を無視する | int | Skip |
| Take | 指定回数の分だけ発火を通す | int | Take |
| WithInterval | 一度発火を通したら指定秒数の間無視する 時間の基準は引数によってSystem.DateTime基準(デフォルト)か、Unity.Time基準かを変更可能※ |
float, (TimeMode.e_timeMode) | ThrottleFirst |
| WithDelay | 一定時間後に発火させる | float | Delay |
| ConvertTo | 値(TPrev)を別の値(TNext)に変換する | Func<TPrev, TNext> | Select |
| WithDebugLog | 流れてきた値をログに出力する | なし | Debug(UniRx) |
| WithMessage | メッセージをログに出力する メッセージ中に { } があると、現在の値に変換される |
string | Do(UniRx) |
※System.DateTimeはUnityEngine.Time.timeScaleの影響を受けず、逆にUnity.Timeだと受ける
UniRxユーザやLinqに慣れている方からすると多少違和感のある名前ですが、Select, Where, ThrottleFirstなど一般的には直感的でないと思うので、動作を想像しやすい名前を採用しました。
なお、BaseOperatorを継承すれば本家のように新しいOperatorを実装することもできます。
UniRx/R3にも存在するReactivePropertyを簡略化したクラスです。
機能を「値を監視し、変更があれば発火する」だけに絞っています。
Publisherと同じく、IReactiveProperty<T> を公開することができます。
また、ReactivePropertyを購読したとき、その購読に向けて即座に発火します。
たとえば
_reactiveProperty.Subscribe(value => Debug.Log(value)).AddTo(_disposables);としたとき、Subscribeを呼んだ時点でDebug.Log(value)が実行されます。
この挙動はUniRxでも同等で、この発火を受け取りたくない場合は.Skip(1)のOperatorを付けることで最初の発火を無視することができます。
なお、.Skip(1)の例のように、Operatorは消費されるので注意してください。
同じくUniRx/R3に存在するCompositeDisposableを模したクラスです。 IDisposableSubscriptionをAdd()で登録し、Dispose() でまとめてリソースの破棄、つまり購読解除することができます。
本家と同じようにAddTo()で登録できる拡張メソッドを用意しています。
また、内部ではIDisposableを管理しています。
そのため、IDisposableを持ったクラスはすべて登録することができ、IDisposableSubscriptionはIDisposableを持っているためもちろん登録できます。
なおIDisposableSubscriptionは「購読の戻り値に意味がある」ことを強調するために特別な実装をしています。
なお、実装は非同期に対応していませんので注意してください。
以下の方法で、起きやすいエラーを日本語で表示するデバッグモードを設定できます。
- Edit -> Project Settings を開く
- Player -> Other Settings -> Script Compilation -> Scripting Define Symbols を開く
XQLUA_DEBUGを追加する
具体的には
- XqLua側でハンドルできるエラーの検知
- Disposeが必要な購読などが行われたときに記録を行い、Dispose漏れがあれば警告 を実装しています。
Q. eventとの違いは?
A.
購読をクラスで表現しているので購読解除をDisposeで管理できます。
Operatorによって「どんな条件の時に値を受け取りたいか」というフィルタリングも可能です。
Q. UniRxの代わりになりますか?
A.
なりません。
最低限の機能だけを実装しているので、不便を感じた場合はUniRxを勉強してみてください。
とりすーぷ(toRisouP)さんの資料がおすすめです。
Q. Luaという言語と関係はありますか?
A.
関係ありません。
テーマの「UniRxの(R)3歩前に使うライブラリ」というところで
- UniRxをシーザー暗号で-(R)3すると、XqlUa
- その単語の区切り(3-2)をR(everse)するとXqLua(2-3)
という偶然です。
公開するので一応MITライセンスをつけています。
なお、学生さんの場合は使ったことを教えていただけると私がすごく喜びます。