Skip to content

テンプレから学ぶUnityライブラリの使い方

Elise edited this page Jul 26, 2024 · 1 revision

テンプレで使っているUnityライブラリの解説を書きたい、真似したい!


テンプレのデバッグ画面。BattleArea0のボタンを押して水色かオレンジ色の領域にカードを出したりしています。 これがどう実装されているか解説したいです。 image

DIコンテナの生成

まず、VContainerの資料のようにデバッグ用のLifetimeScopeを作って1つのDIコンテナを生成したいです

BattleDebugLifetimeScope.cs

using App.BattleDebug.Data;
using App.BattleDebug.Presenters;
using App.BattleDebug.UseCases;
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace App.BattleDebug
{
    public class BattleDebugLifetimeScope : LifetimeScope
    {
        [SerializeField] private BattleCardDebugger _BattlePlayerCardDebugger;
        [SerializeField] private BattleDebugDeckPresenter _BattleDebugPresenter;
        [SerializeField] private BattleDebugBattleAreaPresenter _BattleDebugBattleAreaPresenter;
        [SerializeField] private BattleDebugStageAreaPresenter _BattleDebugStageAreaPresenter;
        [SerializeField] private BattleDebugSupportAreaPresenter _BattleDebugSupportAreaPresenter;
        [SerializeField] private BattleDebugPhasePresenter _BattleDebugPhasePresenter;

        protected override void Configure(IContainerBuilder builder)
        {
            builder.RegisterInstance(_BattlePlayerCardDebugger);

            builder.RegisterComponent(_BattleDebugPresenter).AsImplementedInterfaces();
            builder.RegisterComponent(_BattleDebugBattleAreaPresenter).AsImplementedInterfaces();
            builder.RegisterComponent(_BattleDebugStageAreaPresenter).AsImplementedInterfaces();
            builder.RegisterComponent(_BattleDebugSupportAreaPresenter).AsImplementedInterfaces();
            builder.RegisterComponent(_BattleDebugPhasePresenter).AsImplementedInterfaces();

            builder.RegisterEntryPoint<BattleDebugUseCase>();
            builder.RegisterEntryPoint<BattleDebugPlayerCardUseCase>();
            builder.RegisterEntryPoint<BattleDebugPhaseUseCase>();
        }
    }
}

注目したいのは名前空間(namespace)もちゃんと定義していることと、DIコンテナを作るにはLifetimeScopeを継承する必要があること。

あとは注入するためにConfigure関数を使って Register してますが、RegisterInstanceRegisterComponentRegisterEntryPointとかいろいろ使ってます。 それぞれ注入することは同じですが、対象と目的によって使う関数が違くなります。それはここのwikiで確認できます。

  • BattleCardDebugger(名前は_BattlePlayerCardDebugger)はScriptableObjectなのでそのものを参照したいからRegisterInstanceになる。

  • BattleDebugDeckPresenterBattleDebugBattleAreaPresenterはそれぞれスクリプトのコンポネントなのでRegisterComponentを使ってます。また、それぞれのスクリプトのInterfaceも参照したいので.AsImplementedInterfacesをつけている。(SerializeFieldで該当しているスクリプトコンポネントを事前に入れています)

  • BattleDebugUseCaseBattleDebugPlayerCardUseCaseはまだわからないんですが、RegisterEntryPointを使っています。これはUnityのStartUpdateと同じ機能を持つようにするためです。

実際にBattleDebugUseCase.csのファイルを見てみましょう。 クラスがVContainerのInterfaceであるIInitializableを継承してます。これによって実行されるInitialize()関数はここを参照するとStart()より早く実行されるらしいです。 なので、Start()が実行される前に参照関係を構築しています。(IDisposableは知ってる方教えてください... UniRxみたいですが...)

BattleDebugUseCase.csを続けて見ましょう

namespace App.BattleDebug.UseCases
{
    public class BattleDebugUseCase : IInitializable, IDisposable
    {
        private readonly IBattleDebugDeckPresenter _BattleDebugDeckPresenter;
        private readonly IBattleDebugBattleAreaPresenter _DebugBattleAreaPresenter;
        ...
        private readonly CompositeDisposable _Disposables = new();

        [Inject]
        public BattleDebugUseCase(
            IBattleDebugDeckPresenter battleDebugDeckPresenter,
            IBattleDebugBattleAreaPresenter debugBattleAreaPresenter,
            ...
        )
        {
            _BattleDebugDeckPresenter = battleDebugDeckPresenter;
            _DebugBattleAreaPresenter = debugBattleAreaPresenter;
            ..
        }
        ...
}

ここでは参照関係を構築したので持ってきたいから[Inject]を書いてコンストラクタpublic BattleDebugUseCase()からもらってます。 それぞれのInterface(Iから始めるもの)は上記したように.AsImplementedInterfacesから一緒にすべて注入されていました。 これら全部先作ったDIコンテナが自動的に探してやってくれます。

ですので、基本的にはLifetimeScopeをつけたDIコンテナで参照関係を構築して、上のようにもらったらいい感じかもです。


UniRxについて

ここまで見たら次はIBattleDebugDeckPresenter.csを見ましょう。

using System;
using UniRx;

namespace App.BattleDebug.Interfaces.Presenters
{
    public interface IBattleDebugDeckPresenter
    {
        IObservable<Unit> OnRequestDrawCard { get; }
        IObservable<Unit> OnRequestInitialDraw { get; }
        IObservable<Unit> OnRequestMulligan { get; }
    }
}

(わからないのたくさん。。。)

これはBattleDebugDeckPresenter.csで使うためのInterfaceです。 実際BattleDebugDeckPresenter.csではIBattleDebugDeckPresenterを継承しています。Interfaceは事前に使う関数を名前だけ定義しておくみたいなものです(正確ではない)。 みんなが調べてくれ~

重要なのはIObservable<Unit>です。これはUniRxの機能です(導入しなきゃ)。これを説明する前にイベントの話をしなきゃいけません。

イベントはボタンで一番多く使われます。ボタンがクリックされたとき、クリックされた(OnClick)というイベントが発生し、そのイベントを聞くオブジェクトは 登録された関数は実行します。UniRxはこのイベント機能をもっと操作しやすいようにしたものです。

具体的な説明はUniRx入門を読んでくださいね

UniRxの簡略な説明

UniRxにおいてイベントSubjectが担当します。このSubjectは実行したい関数を事前に受け取って、メッセージがきたとき(イベントが起こったとき) 登録しといた関数を実行します。

Subjectは2つのInterfaceからなっており、IObserverIObservableです。

  • IObserverは3つの関数OnCompletedOnErrorOnNextからなっています。OnCompletedはメッセージの発行(関数の実行)が完了したことを通知する関数、 OnErrorは発生したエラーを通知するメッセージを発行する関数、そして、OnNextは登録されている関数にメッセージを渡して実行する関数です。
  • IObservableSubscribeの1つの関数からなっています。Subscribeは関数を登録する関数です。

それぞれの書き方は上のリンクを見てください。簡単な例を載せておきます。

var Subject<string> subject = new Subject<string>(); // メッセージの型は string

subject
    .Select(str => int.Parse(str)) // 受け取った文字列を int型に変換、このようなメッセージの変換には Selectを使う
    .Subscribe( // Subscribeで関数を登録
        x => Debug.Log("Success : " + x), // 成功したときそのまま x を出力
        ex => Debug.Log("Error : " + ex) // int.Parse(str)が失敗したときエラーを出力
    );

subject.OnNext("2");
subject.OnNext("Hello"); // エラー

さて、IBattleDebugDeckPresenter.csに戻りましょう。ここでは

IObservable<Unit> OnRequestDrawCard { get; }

と書いています。ここで Unit は特殊な型で、「どの意味も持たない」を意味します。つまり、何でも良いってこどです。 また、これはIObservableですので、今度 Subject<Unit> に Subscribe される予定ですね。


ボタンを押したらカードを引いてみよう

以上までの説明からボタンを押したらカードを引く機能を作ってみましょう。 まず、ボタンから

`BattleDebugDeckPresenter.cs のコードを見ましょう。

[SerializeField] private Button _DrawButton;

private readonly Subject<Unit> _OnRequestDrawCard = new();
public IObservable<Unit> OnRequestDrawCard => _OnRequestDrawCard;
// public IObservable<Unit> OnRequestDrawCard { get { return _OnRequestDrawCard; }} と一緒

private readonly CompositeDisposable _Disposables = new();

public void Initialize()
{
    _DrawButton.OnClickAsObservable()
        .Subscribe(_ => _OnRequestDrawCard.OnNext(Unit.Default))
        .AddTo(_Disposables);
}

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

カードを引く部分だけ持ってきました。まず、ボタンの一つのイベントですので、UniRxからOnClickAsObservableメソッドが使えます。

.Subscribe(_ => _OnRequestDrawCard.OnNext(Unit.Default))

と書くことで、ボタンが押されたら「_OnRequestDrawCardが実行する関数」を実行するようにすることができます。 なので、_OnRequestDrawCardというSubjectに実行する関数を登録(Subscribe)する必要があります。SOLID原則によって、他のスクリプトで書きたいですよね。 外部からSubjectに関数を登録したいときは

private readonly Subject<Unit> _OnRequestDrawCard = new();
public IObservable<Unit> OnRequestDrawCard => _OnRequestDrawCard;
// public IObservable<Unit> OnRequestDrawCard { get { return _OnRequestDrawCard; }} と一緒

のように、IObservableのプロパティを使います。外部からはこのIObservableに登録することになります。

BattleDebugUseCase.cs を見ましょう。

private readonly IBattleDebugDeckPresenter _BattleDebugDeckPresenter;
private readonly IPlayerDeckUseCase _PlayerDeckUseCase;

[Inject]
public BattleDebugUseCase(
    IBattleDebugDeckPresenter battleDebugDeckPresenter,
    IPlayerDeckUseCase playerDeckUseCase,
    ...)
{
    _BattleDebugDeckPresenter = battleDebugDeckPresenter;
    _PlayerDeckUseCase = playerDeckUseCase;
    ...
} 

public void Initialize()
{
    _BattleDebugDeckPresenter.OnRequestDrawCard
        .Subscribe(_ => _PlayerDeckUseCase.DrawCard())
        .AddTo(_Disposables);
}

まず、DIコンテナから IBattleDebugDeckPresenter を持ってきました。Initialize()を見ると、

    _BattleDebugDeckPresenter.OnRequestDrawCard
        .Subscribe(_ => _PlayerDeckUseCase.DrawCard())
        .AddTo(_Disposables);

となっています。上記したように外部からは OnRequestDrawCardに登録するのでこんな形になっています。ここでは、 _PlayerDeckUseCase.DrawCard()を実行するようになっていますね。確認しましょう。

IPlayerDeckUseCase.cs

namespace App.Battle.Interfaces.UseCases
{
    public interface IPlayerDeckUseCase
    {
        void Build();
        void InitialDraw();
        bool DrawCard();
        void Mulligan();
    }
}

はい。ちゃんとあります。ここまでのことを整理しましょう。

  • Button には _OnRequestDrawCard.OnNext() という関数を登録しました。これはボタンが押されたら「_OnRequestDrawCard というイベント(Subject)が登録されている関数」を実行せよということです。
  • _OnRequestDrawCardに関数を登録するために OnRequestDrawCard プロパティーを使いました。OnRequestDrawCard には、_PlayerDeckUseCase.DrawCard()が登録されました。

まぁぷよぷよの連鎖みたいになってます。

ここで最後の疑問が残ると思います。 _PlayerDeckUseCase は Interface であって、実際のDrawCard()の関数の中身はわからないのになんでこれでいいのか?と。 これをVContainerが解決してくれます。

BattleLifetimeScope.cs

builder.RegisterEntryPoint<PlayerDeckUseCase>().As<IPlayerDeckUseCase>();

この部分です。これによって、自動的にPlayerDeckUseCaseが渡されます。これによって、Interfaceにだけ注目できます。

以上の一連の図

クラス図
  • Inheritance : 継承
  • Register... : DIコンテナに登録
  • Inject : 注入
  • Parent(Layer) : 親
classDiagram
    class BattleDebugLifetimeScope{
        <<DI Container>>
    }
    class BattleLifetimeScope{
        <<DI Container>>
    }
    class IBattleDebugDeckPresenter{
        <<interface>>
    }
    class IPlayerDeckUseCase{
        <<interface>>
    }
    class BattleDebugDeckPresenter
    class BattleDebugUseCase
    class PlayerDeckUseCase

    BattleLifetimeScope<..IPlayerDeckUseCase : RegisterEntryPoint As IPlayerDeckUseCase
    BattleDebugLifetimeScope<..IBattleDebugDeckPresenter : RegisterEntryPoint
    BattleLifetimeScope*..BattleDebugLifetimeScope : Parent(Layer)
    IBattleDebugDeckPresenter<|--BattleDebugDeckPresenter : Inheritance
    IPlayerDeckUseCase<|--PlayerDeckUseCase : Inheritance
    BattleDebugUseCase<..BattleDebugLifetimeScope : Inject
    BattleDebugUseCase<..BattleLifetimeScope : Inject

    class BattleDebugLifetimeScope{
        configure()
    }
    class BattleDebugDeckPresenter{
        -Button _DrawButton
        -Subject~Unit~ _OnRequestDrawCard
        +IObservable~Unit~ OnRequestDrawCard => _OnRequestDrawCard
    }
    class BattleDebugUseCase{
        -IBattleDebugDeckPresenter _BattleDebugDeckPresenter
        -IPlayerDeckUseCase _PlayerDeckUseCase
    }
    class IBattleDebugDeckPresenter{
        IObservable~Unit~ OnRequestDrawCard
        IObservable~Unit~ OnRequestInitialDraw
        IObservable~Unit~ OnRequestMulligan
    }
    class IPlayerDeckUseCase{
        void Build()
        void InitialDraw()
        bool DrawCard()
        void Mulligan()
    }
    class PlayerDeckUseCase{
        void Build()
        void InitialDraw()
        bool DrawCard()
        void Mulligan()
    }
Loading
ボタンの操作で起こることのフローチャート
flowchart LR
    Player[Player]
    Button[DrawCard Button]
    DrawCard["DrawCard()"]
    OnRequestDrawCard

    subgraph _OnRequestDrawCard
    OnRequestDrawCard -->|"Observable"| DrawCard
    end

    Player -->|"Button Clicked(Observer)"| Button
    Button -->|"Observable -> Observer"| OnRequestDrawCard
Loading
Clone this wiki locally