UniRxを使ったデバッグボタンの作り方 - CASru-GAME/TeamGameDevBootcamp GitHub Wiki

1. Unityのボタンの動作

Unityに慣れている皆さんはボタンをどう実装するかは知っていると思います。

大体は次の通りです。 CanvasからButtonを用意して、そのボタンが何の動作をするかを記述するスクリプトを作ります。 そして、そのスクリプトをButtonOnClick()にメソッドとして張ります。

image

このようにインスペクタ上で張ることもできますが、スクリプト上で追加することもできます。

Button button;
[SerializeField] GameObject obj1;

void Start()
{
    button = GetComponent<Button>(); // まずボタンを取得
    button.onClick.AddListener(func1); // ボタンの OnClick() に func1 関数を追加
}

void func1()
{
    obj1.GetComponent<Obj1Script>().func(); // obj1 の中のスクリプトの関数を実行
}

2. デバッグ機能のレイヤー構成

しかし、今回のプロジェクトではこの実装方法では問題があります。

今回のプロジェクトでのレイヤー構成とデバッグの目的に注目してください。レイヤー構成的に、UseCaseレイヤーに「デバッグでやりたい機能」を記述して、Presenterレイヤーに「その機能の実行に必要な補助的な動作」を記述します。しかし、上の実装方法では両方のレイヤーの役割が一つのスクリプトで実行されています。

もっと具体的には、

  • デバッグでやりたい機能は「fun1() の中身を実行する」です。上のスクリプトではfun1()関数を作ってそれをやっています。

  • その機能の実行に必要な補助的な動作は「ボタンが押されたらその機能を実行する」です。上のスクリプトでは button.onClick.AddListener でそれをやっています(これは正確ではないですが、詳細は省略します)。

なので、これをSOLID原則とDDDのレイヤーアーキテクチャに合うように修正しましょう。

3. UniRx とは

3.1 イベントの仕組み

このときに、UniRx が非常に役立ちます。UniRx は「イベント処理」と「非同期処理」を使いやすくするライブラリです。今回は「イベント処理」に注目します。

イベントとは、簡略に説明すると、何らかのことが起きたとき(ボタンではクリックされたとき)、そのイベントに事前に登録されていた関数を実行するような仕組みです。ボタンは以下にようになっています。

image

  • 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);
}

image

メッセージを送る必要がない(つまり、引数が存在しない)場合もあります。この時は引数として Unit を使います。

Subject<Unit> subject = new();
subject.subscribe(_ => UnityEngine.Debug.Log("Hello!"));
subject.OnNext(Unit.Default);

OnNext にも Unit.Default を渡す必要があることには注意しましょう。

3.2 IObserverとIObservable

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);

3.3 Dispose

(ここは自分も完全にわかってないです。)

イベント(Subject)は使い終わった後破棄しないとメモリリークが起こります。これを防ぐためにDisposeを使う必要があります。

大体は対象のオブジェクトが破壊されたら破棄したいので、このときはAddToを使います。

Subject<Unit> subject = new();
private readonly CompositeDisposable _disposables = new();

subject
  .subscribe( ~~~ )
  .AddTo(_disposables);

public void Dispose()
{
    _disposables.Dispose();
}

IDisposable を継承する必要があります!!!

4. 実際にデバッグボタンを作ろう

これまでの内容からデバッグボタンを作りましょう。ボタンの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>();

とする必要があります。

以上で終わりです。いかかでしたか?

⚠️ **GitHub.com Fallback** ⚠️