-
Notifications
You must be signed in to change notification settings - Fork 0
テンプレから学ぶUnityライブラリの使い方
テンプレで使っているUnityライブラリの解説を書きたい、真似したい!
テンプレのデバッグ画面。BattleArea0のボタンを押して水色かオレンジ色の領域にカードを出したりしています。 これがどう実装されているか解説したいです。
DIコンテナの生成
まず、VContainerの資料のようにデバッグ用のLifetimeScopeを作って1つのDIコンテナを生成したいです
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 してますが、RegisterInstance
やRegisterComponent
やRegisterEntryPoint
とかいろいろ使ってます。
それぞれ注入することは同じですが、対象と目的によって使う関数が違くなります。それはここのwikiで確認できます。
-
BattleCardDebugger
(名前は_BattlePlayerCardDebugger
)はScriptableObjectなのでそのものを参照したいからRegisterInstance
になる。 -
BattleDebugDeckPresenter
とBattleDebugBattleAreaPresenter
はそれぞれスクリプトのコンポネントなのでRegisterComponent
を使ってます。また、それぞれのスクリプトのInterfaceも参照したいので.AsImplementedInterfaces
をつけている。(SerializeFieldで該当しているスクリプトコンポネントを事前に入れています) -
BattleDebugUseCase
、BattleDebugPlayerCardUseCase
はまだわからないんですが、RegisterEntryPoint
を使っています。これはUnityのStart
やUpdate
と同じ機能を持つようにするためです。
実際に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からなっており、IObserver
とIObservable
です。
-
IObserver
は3つの関数OnCompleted
、OnError
、OnNext
からなっています。OnCompleted
はメッセージの発行(関数の実行)が完了したことを通知する関数、OnError
は発生したエラーを通知するメッセージを発行する関数、そして、OnNext
は登録されている関数にメッセージを渡して実行する関数です。 -
IObservable
はSubscribe
の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()
を実行するようになっていますね。確認しましょう。
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が解決してくれます。
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()
}
ボタンの操作で起こることのフローチャート
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