【Unity】DI(VContainer)を使ったMVPパターンサンプル

MVP(Model-View-Presenter)パターンは、MVC(Model-View-Controller)パターンから派生したもので、アプリケーションのプレゼンテーション層をロジックから分離することを目的としています。これにより、コードがよりモジュラーになり、テストやメンテナンスが容易になります。

MVPとは

UnityとVContainerを使用してMVPパターンを実装する場合、Model、View、Presenterのために別々のコンポーネントを作成することになります

  1. Model(モデル): アプリケーションのデータとビジネスロジックを表します。Unityでは、プレイヤーの健康状態やスコアなどのデータと、プレイヤーがダメージを受けたときに何が起こるかなどのロジックを保持するクラスまたは一連のクラスになります。
  2. View(ビュー): データをプレイヤーに提示する責任があります。Unityでは、UI要素から、モデルの状態を視覚的に表すゲームオブジェクトまで、様々なものがこれに該当します。
  3. Presenter(プレゼンター): モデルとビューの間の仲介者として機能します。ビューからのイベントをリスンし、それに応じてモデルを更新し、モデルが変更されたときにビューを更新します。

依存性注入(DI)とは

依存性注入(DI: Dependency Injection)は、ソフトウェアエンジニアリングの設計パターンの一つで、クラスの依存関係をそのクラス自身で作成するのではなく、外部の設定(例えば、別のクラスやフレームワーク)から受け取る方法です。このパターンは、ソフトウェアの柔軟性、再利用性、テストのしやすさを向上させることを目的としています。

依存性注入を利用することで、クラスは自分が依存しているオブジェクトを直接生成する必要がなくなります。その代わりに、依存しているオブジェクトは外部から「注入」されます。これにより、クラス間の結合度が低下し、コンポーネントの交換やモックオブジェクトを用いた単体テストが容易になるなど、多くの利点があります。

依存性注入は、主に3つの方法で行われます:

  1. コンストラクターインジェクション:依存オブジェクトをクラスのコンストラクターを通じて渡します。
  2. セッターインジェクション:セッターメソッドまたはプロパティを通じて依存オブジェクトを渡します。
  3. インターフェースインジェクション:依存オブジェクトを注入するための専用のインターフェースを使用します。

依存性注入をサポートするために、多くのプログラミング言語やフレームワークでは、DIコンテナやIoC(Inversion of Control: 制御の反転)コンテナと呼ばれるツールを提供しています。これらのツールは、依存関係の解決、オブジェクトのライフサイクル管理、設定の外部化などを行うことで、開発者が依存性注入を容易に利用できるようにするものです。

VContainerを使ってMVPを実現する

VContainerはUnity向けの依存性注入(DI)ライブラリであり、これらのコンポーネント間の依存関係の管理を助けます。VContainerを使用することで、モデルとビューの参照を持つプレゼンターを、それらを密結合せずにより簡単にインスタンス化できます。これにより、コードがよりモジュラーになり、テスト可能になります。

UnityでのVContainerを使用したMVPの実装の基本的な例は以下の通りです:

  • VContainerの設定: DIコンテナをセットアップするLifetimeScopeコンポーネントを定義します。このコンポーネントでは、必要なモデル、ビュー、プレゼンターを登録し、それらの依存関係を指定します。
  • Model(モデル): ゲームのデータとロジックのためのクラスを作成します。
  • View(ビュー): ゲームのUIと視覚的表現のためのUnity GameObjectsを作成し、ビューが受け取るデータに基づいてビジュアルをどのように更新するかを定義するスクリプトを付けます。
  • Presenter(プレゼンター): モデルとビューの間のやり取りを仲介するクラスを作成します。VContainerを使用して、モデルとビューへの依存関係をプレゼンターに注入します。
  • すべてをまとめる: LifetimeScopeで、VContainerにプレゼンター、モデル、ビューを登録し、VContainerが依存関係のインスタンス化と注入を管理できるようにします。

このアプローチは、依存関係の管理においてVContainerの強みを活かし、Unityプロジェクト内でMVPパターンを実装することを容易にします。これにより、関心の分離がクリーンになり、コードの保守性が向上します。

より詳細な例や実装については、VContainerのドキュメントやUnity特有のMVPパターン実装に関するリソースを探索することをお勧めします。

サンプル

ボタンとテキストを配置し、ボタンをクリックするとカウントがインクリメントされるシンプルなプロジェクトをサンプルとして見ていきましょう

実行結果

構成

オブジェクト、コンポーネントについての細かな説明を省いています
ここで、参考にされる方は自ら学習済みと考え、その様にしました

GameInstaller ゲームオブジェクト

MVPのすべてをまとめる役割があります
GameLifeTimeScopeスクリプトを実行するためのコンテナ(入れ物)になります

Canvasゲームオブジェクト

View(UI)機能を提供します
ボタン、テキストを1つずつ持ちます
また、コントロールするためのViewスクリプトがアタッチされています

コード

シーンにスクリプトとしてアタッチされているのは、GameLifeTimeScopeとViewになります

using UnityEngine;
using VContainer;
using VContainer.Unity;

// ゲームのライフタイムスコープとその設定を定義
public class GameLifeTimeScope : LifetimeScope
{
    [SerializeField] private View view;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterEntryPoint<Presenter>();
        builder.Register<Model>(Lifetime.Singleton).AsSelf();
        builder.RegisterComponent(view);
    }
}

コードは、Unity用の依存性注入ライブラリであるVContainerを使用してゲームのライフタイムスコープを定義する方法を示しています。コードの各部分が何をするかを説明します:

名前空間のインポート:

  • UnityEngine: この名前空間はUnityのコア機能の一部です。UnityのAPIにアクセスするために使用され、ゲームオブジェクト、コンポーネント、その他のゲーム開発に不可欠な機能へのアクセスを提供します。
  • VContainer: この名前空間は、VContainer依存性注入フレームワークを指している可能性があります。依存性注入は、クラスとその依存関係の間で制御の反転を実現するために使用される技術です。
  • VContainer.Unity: この名前空間は、VContainerをUnityに統合するためのもので、Unityの環境内で依存性注入を容易にします。

GameLifeTimeScopeクラスの定義:

  • GameLifeTimeScopeクラスは、VContainerのLifetimeScopeクラスから派生したクラスです。これは、依存性注入のためのオブジェクトのライフタイムを定義するVContainerのクラスです。VContainerとUnityのコンテキストでは、ライフタイムスコープはしばしば、オブジェクトが作成、使用、廃棄されるゲームシーンやゲームの特定の部分を表します。

フィールド宣言:

  • [SerializeField] private View view;: この行は、View型(カスタムまたはUnity型、ここでは指定されていません)のプライベートフィールドを宣言し、[SerializeField]でマークされています。この属性により、プライベートであるにもかかわらず、viewフィールドはUnityエディターから割り当てることができます。このパターンは、Unityでフィールドをカプセル化しながらもエディターで編集可能にするために一般的に使用されます。

Configureメソッド:

  • protected override void Configure(IContainerBuilder builder): このメソッドは、LifetimeScopeクラスからのConfigureメソッドをオーバーライドします。依存関係がVContainerフレームワーク内でどのように登録されるかを定義する場所です。IContainerBuilderインターフェースは、型とインスタンスを登録し、それらがどのように解決され、注入されるかを設定するためのメソッドを提供します。
  • builder.RegisterEntryPoint<Presenter>();: Presenterクラスをエントリポイントとして登録します。VContainerでは、エントリポイントは実行が始まるクラスで、通常は初期化ロジックを含んでいます。
  • builder.Register<Model>(Lifetime.Singleton).AsSelf();: Modelクラスをシングルトンライフタイムで登録し、VContainerはライフタイムスコープ全体でModelの単一インスタンスを作成および維持します。AsSelfは、Modelクラスが自身の型で解決されることを示します。
  • builder.RegisterComponent(view);: このメソッドは、先に見たviewコンポーネント(フィールド)をVContainerに登録し、他のクラスに依存性として注入できるようにします。

この設定は、依存性注入をUnityで使用する際のシンプルかつ強力な例を効果的に示しています。GameLifeTimeScopeクラスは、ゲームの特定の部分における様々なオブジェクトとその依存関係のライフタイムを設定し、管理する責任を持っています。依存性注入は、より構造化された、保守しやすい方法で複雑な依存関係を管理するのに役立ち、ゲームのアーキテクチャをよりクリーンでスケーラブルにします。

// ゲームのデータモデル、特にスコアリングシステムを管理
public class Model
{
    private int score;

    public int AddScore()
    {
        score++;
        return score;
    }
}

このコードは、ゲームのデータモデルの一部としてスコアリングシステムを管理するクラスの実装を示しています。具体的には、ゲーム内でのプレイヤーのスコアを追跡し、更新する機能を提供します。コードの各部分が何をするかを詳細に説明します:

クラス定義:

  • Model: このクラスは、ゲームのデータモデル、特にスコアリングシステムの管理を担当します。プレイヤーのスコアを追跡し、それを更新するメソッドを提供することで、ゲームのビジネスロジックをカプセル化しています。

フィールド宣言:

  • private int score;: これはプライベートフィールドで、ゲーム内のプレイヤーの現在のスコアを保持します。プライベートアクセス修飾子により、このフィールドはModelクラス内からのみアクセス可能であり、外部から直接変更されることはありません。

メソッド定義:

  • public int AddScore(): このメソッドは、プレイヤーのスコアを1点増加させ、更新されたスコアを返します。スコアのインクリメント(score++)は、ゲーム内で何らかの目標を達成したり、特定のアクションを完了したりした結果として呼び出されることが想定されています。このメソッドは公開されているため、Modelクラスのインスタンスを持つ他のクラス(例えばゲームのロジックを管理するコントローラーやプレゼンター)がスコアを更新するために使用できます。

このModelクラスの設計により、スコアリングシステムのロジックが一箇所に集中され、ゲームの他の部分とは独立しています。これにより、ゲームのデータモデルがより維持しやすく、拡張しやすいものになります。また、プレイヤーのスコアを更新する際の一貫性と正確性が保証され、ゲーム開発におけるベストプラクティスが実践されています。

using TMPro; // 必要なライブラリのみをinclude
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class View : MonoBehaviour
{
    [SerializeField] private Button actionButton; // Inspectorから割り当てるためのSerializeField属性
    [SerializeField] private TextMeshProUGUI scoreText; // Inspectorから割り当て

    // ボタンリスナーのセットアップをより明確に
    public void SetupActionButtonListener(UnityAction action)
    {
        actionButton.onClick.AddListener(action);
    }

    // スコア表示の更新メソッド名を明確に
    public void UpdateScoreDisplay(int score)
    {
        scoreText.text = score.ToString();
    }
}

このコードは、Unityを使用したゲーム開発におけるユーザーインターフェイス(UI)の構築と操作に関する実装例です。具体的には、ボタンのイベントリスナーのセットアップとスコア表示の更新を行うViewクラスを定義しています。以下に、コードの各部分が担う役割について説明します:

名前空間のインポート:

  • TMPro: TextMeshProを利用するために必要な名前空間です。TextMeshProは、高品質なテキストレンダリングを実現するためのUnityのアドオンです。
  • UnityEngine: Unityの基本機能を提供する名前空間です。この名前空間には、ゲームオブジェクトやコンポーネントの操作に必要なクラスが含まれています。
  • UnityEngine.Events: Unityのイベントシステムにアクセスするための名前空間です。イベントの登録やリスナーの追加に使用します。
  • UnityEngine.UI: UnityのUIシステムを使用するための名前空間です。ボタンやテキストなど、UI関連のコンポーネントの操作に必要です。

クラス定義:

  • View: MonoBehaviourを継承したクラスで、Unityのコンポーネントとして機能します。UI関連の操作を担当し、特定のUIイベント(ボタンクリックなど)に対する応答や、スコア表示の更新などの機能を提供します。

フィールド宣言:

  • [SerializeField] private Button actionButton: UnityのInspectorから割り当て可能なボタンコンポーネントへの参照です。SerializeField属性を使用することで、プライベート変数であってもInspector上で編集が可能になります。
  • [SerializeField] private TextMeshProUGUI scoreText: スコア表示用のTextMeshProUGUIコンポーネントへの参照です。これもSerializeFieldを使用してInspectorから割り当てます。

メソッド定義:

  • public void SetupActionButtonListener(UnityAction action): ボタンのクリックイベントに対するリスナー(コールバック関数)を設定するメソッドです。UnityAction型の引数を取り、actionButton.onClickイベントにリスナーとして追加します。これにより、ボタンがクリックされたときに指定されたアクションが実行されるようになります。
  • public void UpdateScoreDisplay(int score): スコア表示を更新するメソッドです。整数型のスコア値を受け取り、それを文字列に変換してscoreText.textに設定します。これにより、UI上のスコア表示が更新されます。

このViewクラスの設計は、UIコンポーネントとゲームロジックの分離を促進し、ゲームのプレゼンテーション層を効率的に管理することを目的としています。SerializeField属性を利用することで、プログラマだけでなく、デザイナーや他の開発チームメンバーもUnityエディタを通じて簡単にUIコンポーネントを設定・調整できるようになります。

using VContainer.Unity;

// ゲームのプレゼンテーションロジックを扱い、モデルとビューの橋渡しを行う
public class Presenter : IStartable
{
    private readonly Model model;
    private readonly View view;

    public Presenter(Model model, View view)
    {
        this.model = model;
        this.view = view;
    }

    public void Start()
    {
        view.SetupActionButtonListener(IncrementScore);
    }

    private void IncrementScore()
    {
        int newScore = model.AddScore();
        view.UpdateScoreDisplay(newScore);
    }
}

コードは、Unity用の依存性注入ライブラリであるVContainerを使用して、ゲームのプレゼンテーションロジックを担うPresenterクラス、およびその他の関連コンポーネントを管理する方法を示しています。以下は、コードの各部分が何をするかを説明したものです。

名前空間のインポート:

  • UnityEngine: Unityのコア機能へのアクセスを提供します。これにより、ゲームオブジェクト、コンポーネント、その他多くのゲーム開発に必要な機能を使用できます。
  • VContainer: 依存性注入フレームワークVContainerを指します。これは、クラス間の結合を緩和し、コードのテスタビリティと再利用性を向上させるために使用されます。
  • VContainer.Unity: VContainerをUnityの環境に統合するための名前空間です。Unityプロジェクト内で依存性注入を簡単に実施できるようにします。

Presenterクラスの定義:

  • IStartable: PresenterクラスがIStartableインターフェースを実装していることは、VContainerによって定義されたライフサイクルイベント(この場合はStartメソッド)を利用する意向を示しています。これにより、初期化ロジックをクリーンに実行できます。

フィールド宣言:

  • [SerializeField] private View view;: Unityエディターから割り当て可能なView型のプライベートフィールドです。[SerializeField]属性を使うことで、プライベートフィールドであってもUnityエディターからアクセスして設定することができます。

Configureメソッド:

  • 依存性の登録: Configureメソッドでは、IContainerBuilderインターフェースを使用して、PresenterModel、そしてViewコンポーネントを依存性として登録します。これにより、VContainerがこれらのクラスのインスタンスを必要に応じて生成し、適切な依存関係を注入する準備が整います。
    • builder.RegisterEntryPoint<Presenter>();は、アプリケーションの実行開始点としてPresenterクラスを登録します。
    • builder.Register<Model>(Lifetime.Singleton).AsSelf();は、Modelクラスをシングルトンとして登録し、アプリケーションのライフサイクル全体で1つのインスタンスを保持します。
    • builder.RegisterComponent(view);は、Viewコンポーネントを依存性として登録し、必要に応じて他のクラスへ注入できるようにします。

このコードは、VContainerを利用して、Unityプロジェクト内でクリーンな依存性注入パターンを実装する方法を効果的に示しています。Presenterクラスは、ゲームのプレゼンテーションロジックを担い、ModelViewの間の橋渡しを行うことで、アプリケーションの構造をよりクリーンにし、保守性と拡張性を向上させています。

メリット・デメリット

VContainerを使用してUnityでMVP(Model-View-Presenter)パターンを実装する際のメリットとデメリットについて説明します。

メリット

  1. 結合度の低減: MVPパターンは、モデル(Model)、ビュー(View)、プレゼンター(Presenter)の各層を明確に分離します。VContainerの依存性注入を使用することで、これらのコンポーネント間の結合度をさらに低下させ、コンポーネントの独立性を高めることができます。これは、大規模なプロジェクトやチームで作業する際に特に有益です。
  2. テストの容易性: MVPパターンによってロジックがプレゼンターに集中されるため、UIやデータモデルから独立してプレゼンターの単体テストが容易になります。VContainerを使用すると、テスト時にモックオブジェクトやスタブを注入しやすくなり、テスト駆動開発(TDD)を促進します。
  3. 再利用性の向上: ビジネスロジックをビューから分離することで、同じロジックを異なるビューで再利用しやすくなります。また、VContainerを使用すると、プロジェクト全体で再利用可能なサービスやコンポーネントを簡単に定義し、管理することができます。
  4. 保守性と拡張性の向上: 各層が独立しているため、一部のコンポーネントを修正または拡張しても、他の部分に与える影響が少なくなります。VContainerによる明確な依存性管理も、コードベースの理解と修正を容易にします。

デメリット

  1. 学習曲線: MVPパターンとVContainerの依存性注入の両方には、学習が必要です。これらの概念に不慣れな開発者にとっては、導入の障壁となる可能性があります。
  2. 初期設定の複雑さ: 正しく設定するためには、初期の設計とセットアップに時間と労力がかかります。依存性注入コンテナの設定や、MVPパターンに従ったアーキテクチャの構築には、プロジェクトの初期段階での注意深い計画が必要です。
  3. ランタイムパフォーマンスへの影響: 依存性注入はランタイム時のパフォーマンスにわずかながら影響を与える可能性があります。特に、大量のオブジェクトを動的に生成する場合や、リフレクションを多用する場合には、パフォーマンスへの影響を考慮する必要があります。
  4. 過剰な抽象化のリスク: 各層を厳格に分離し、依存性注入を過剰に使用することで、コードベースが過度に複雑化し、逆に管理が困難になることがあります。必要以上の抽象化は避け、プロジェクトの規模や要件に応じた適切な設計を心がける必要があります。

MVPパターンとVContainerの利用は、プロジェクトの要件、チームの経験、および将来の拡張性に基づいて検討する必要があります。正しく適用されれば、プロジェクトの品質、保守性、およびスケーラビリティを大幅に向上させることができます。

MVPを意識しない、シンプルな実装

MVPを採用するかどうかは、プロジェクトの規模、これからの拡張の有無などを考慮して選択します

単純な実現で良いのであれば、MVPにこだわる必要はないでしょう

次に、最初に提示した実行結果以上を求めないシンプルな実装で良い場合のサンプルをしまします

TextMeshProのテキストの値をインクリメントして表示するサンプルコードをご紹介します。このサンプルでは、ボタンがクリックされるたびにテキストの数値が1ずつ増加します。TextMeshProを使用する場合は、プロジェクトにTextMeshProを事前にインポートしておく必要があります。以下のステップとコードを参考にしてください。

必要なステップ:

  1. TextMeshProのテキストをシーンに追加: Unityエディタで、HierarchyビューにTextMeshPro – Textを追加します。これは、メニューの「GameObject > UI > Text – TextMeshPro」を選択することで行えます。
  2. ボタンをシーンに追加: 同様に、「GameObject > UI > Button」を選択してボタンを追加します。
  3. 新しいC#スクリプトを作成: 「IncrementTextValue」という名前の新しいC#スクリプトを作成します。
  4. スクリプトを編集してロジックを追加: 下記のサンプルコードをスクリプトに追加します。
  5. スクリプトをUIコンポーネントにアタッチ: スクリプトをTextMeshProのテキストオブジェクトにアタッチし、スクリプトのInspectorビューでボタンのOnClick()イベントにこのスクリプトのIncrementValueメソッドを割り当てます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro; // TextMeshProを使うために必要

public class IncrementTextValue : MonoBehaviour
{
    public TextMeshProUGUI textDisplay; // TextMeshProのテキストを参照するための変数
    private int currentValue = 0; // 現在の値

    // ボタンがクリックされた時に呼ばれるメソッド
    public void IncrementValue()
    {
        currentValue++; // 値をインクリメント
        textDisplay.text = currentValue.ToString(); // テキストを更新
    }
}

このコードは、ボタンがクリックされるたびにcurrentValueの値を1増やし、その新しい値をTextMeshProのテキストに表示します。textDisplay変数は、InspectorからTextMeshProのテキストコンポーネントを割り当てることで、どのテキストを更新するか指定します。

C#

Posted by hidepon