UniRxを使ったデバッグボタンの作り方 - CASru-GAME/TeamGameDevBootcamp GitHub Wiki
Unityに慣れている皆さんはボタンをどう実装するかは知っていると思います。
大体は次の通りです。
Canvas
からButton
を用意して、そのボタンが何の動作をするかを記述するスクリプトを作ります。
そして、そのスクリプトをButton
のOnClick()
にメソッドとして張ります。
このようにインスペクタ上で張ることもできますが、スクリプト上で追加することもできます。
Button button;
[SerializeField] GameObject obj1;
void Start()
{
button = GetComponent<Button>(); // まずボタンを取得
button.onClick.AddListener(func1); // ボタンの OnClick() に func1 関数を追加
}
void func1()
{
obj1.GetComponent<Obj1Script>().func(); // obj1 の中のスクリプトの関数を実行
}
しかし、今回のプロジェクトではこの実装方法では問題があります。
今回のプロジェクトでのレイヤー構成とデバッグの目的に注目してください。レイヤー構成的に、UseCase
レイヤーに「デバッグでやりたい機能」を記述して、Presenter
レイヤーに「その機能の実行に必要な補助的な動作」を記述します。しかし、上の実装方法では両方のレイヤーの役割が一つのスクリプトで実行されています。
もっと具体的には、
-
デバッグでやりたい機能は「
fun1()
の中身を実行する」です。上のスクリプトではfun1()
関数を作ってそれをやっています。 -
その機能の実行に必要な補助的な動作は「ボタンが押されたらその機能を実行する」です。上のスクリプトでは
button.onClick.AddListener
でそれをやっています(これは正確ではないですが、詳細は省略します)。
なので、これをSOLID原則とDDDのレイヤーアーキテクチャに合うように修正しましょう。
このときに、UniRx が非常に役立ちます。UniRx は「イベント処理」と「非同期処理」を使いやすくするライブラリです。今回は「イベント処理」に注目します。
イベントとは、簡略に説明すると、何らかのことが起きたとき(ボタンではクリックされたとき)、そのイベントに事前に登録されていた関数を実行するような仕組みです。ボタンは以下にようになっています。
-
AddListener()
で実行した関数を事前に登録する -
OnClick()
でボタンが押されたとき登録されていた関数を実行する。
UniRx はこれをもっと柔軟にします。UniRx ではボタンみたいなイベントを Subject と呼びます。
また、AddListener()
みたいに関数を登録することを Subscribe
と呼び、OnClick()
みたいに登録されている関数を実行することを OnNext
と呼びます。
UniRx の OnNext
は関数を実行する時に メッセージ (関数の引数) も送ることができます。
例でみてみましょう。
Subject<string> subject = new(); // Subject を作成する。メッセージ(引数)の型は string
subject.subscribe(msg => echo(msg)); // subjectに関数を登録。メッセージとして msg をもらったら echo(msg)を登録する
subject.OnNext("Hello!"); // メッセージ(引数)を "Hello!" として登録されている関数を実行する
void echo(string message) // 登録する関数
{
UnityEngine.Debug.Log("echo : " + message);
}
メッセージを送る必要がない(つまり、引数が存在しない)場合もあります。この時は引数として Unit
を使います。
Subject<Unit> subject = new();
subject.subscribe(_ => UnityEngine.Debug.Log("Hello!"));
subject.OnNext(Unit.Default);
OnNext
にも Unit.Default
を渡す必要があることには注意しましょう。
SubjectはIObserver
インターフェースとIObservable
インターフェースの2つを実装しています。
ここは詳細まで説明すると複雑になるので以下の点だけ覚えておきましょう!
-
IObserver
にはOnNext()
が定義されている -
IObservable
にはSubscribe()
が定義されている
なので以下のような書き方ができます。
private Subject<Unit> _sayHello = new(); // イベント _sayHello を定義
public IObservable<Unit> SayHello => _sayHello; // _sayHello の IObservable を SayHello とする。
// これで SayHello に Subscribe することと _sayHello に subscribe するのは同じになる
SayHello.subscribe(_ => UnityEngine.Debug.Log("Hello!"));
_sayHello.OnNext(Unit.Default);
(ここは自分も完全にわかってないです。)
イベント(Subject)は使い終わった後破棄しないとメモリリークが起こります。これを防ぐためにDispose
を使う必要があります。
大体は対象のオブジェクトが破壊されたら破棄したいので、このときはAddTo
を使います。
Subject<Unit> subject = new();
private readonly CompositeDisposable _disposables = new();
subject
.subscribe( ~~~ )
.AddTo(_disposables);
public void Dispose()
{
_disposables.Dispose();
}
IDisposable
を継承する必要があります!!!
これまでの内容からデバッグボタンを作りましょう。ボタンのIObservable
に対応する機能として
UniRxでは OnClickAsObservable()
が用意されているのでこれを使います。
Presenter.cs
[SerializeField] private Button _button;
private readonly Subject<Unit> _useFunc = new();
public IObservable<Unit> UseFunc => _useFunc;
private readonly CompositeDisposable _disposables = new();
// ボタンが押されたらあ _useFunc に登録されている関数が実行されるようにする
public void Initialize()
{
_button.OnClickAsObservable() // ボタンの IObservable
.Subscribe(_ => _useFunc.OnNext(Unit.Default))
.AddTo(_disposables);
}
public void Dispose()
{
_disposables.Dispose();
}
UseCase.cs
private readonly IObj1Script _Obj1Script;
private readonly IPresenter _Presenter;
private readonly CompositeDisposable _disposables = new();
// VContainer を使って _Obj1Script, _Presenter に注入する
// Presenter の UseFunc に関数を登録
public void Initialize()
{
_Presenter.UseFunc
.Subscribe(_ => _Obj1Script.Func());
.AddTo(_disposables);
}
public void Dispose()
{
_disposables.Dispose();
}
ちなみに LifetimeScope
にな
[SerializeField] private Presenter _Presenter
builder.RegisterComponent<_Presenter>.AsImplementedInterfaces();
builder.RegisterEntryPoint<UseCase>();
とする必要があります。
以上で終わりです。いかかでしたか?