技術資料: Unityにおけるアイテム管理システムの設計と実装(VContainerを使用)


目次

  1. はじめに
  2. システム概要
  3. 設計原則
  4. クラス構成
  1. デザインパターンの選択
  1. 実装詳細
  1. テスト戦略
  2. 利点と考慮点
  3. まとめ
  4. 参考資料

1. はじめに

本資料では、Unityを用いたゲーム開発におけるアイテム管理システムの設計と実装について詳細に説明します。特に、データの保存・読み出し機能を考慮したクラス分けと、シングルトンパターンおよび依存性注入(Dependency Injection)を実現するためにVContainerを採用する方法について焦点を当てます。

2. システム概要

ゲーム内でプレイヤーが所有するアイテムを管理するシステムは、以下の主要な機能を持ちます。

  1. データの管理: アイテムの追加、使用、取得。
  2. データの保存・読み出し: プレイヤーの進行状況を保持するためのデータ保存機能。
  3. グローバルアクセス: システム全体からアクセス可能な管理機能。

これらの機能を効率的かつ拡張性の高い形で実装するために、適切なクラス設計とデザインパターンの採用が重要です。

3. 設計原則

以下の設計原則に基づいてシステムを設計します。

  • 単一責任の原則 (Single Responsibility Principle): 各クラスは一つの責任のみを持つ。
  • オープン/クローズドの原則 (Open/Closed Principle): クラスは拡張に対して開かれ、修正に対して閉じられている。
  • 依存性逆転の原則 (Dependency Inversion Principle): 高水準モジュールは低水準モジュールに依存しない。両者は抽象に依存する。

4. クラス構成

システムは以下の主要なクラスから構成されます。

4.1. データモデルクラス

4.1.1. OwnedItem クラス

  • アイテムの種類と数量を保持するクラス。
  • データの基本的な構造を表現。

4.1.2. OwnedItemsData クラス

  • 所有する全てのアイテムを管理するクラス。
  • 複数のOwnedItemをリストとして保持し、アイテムの追加や取得などの操作を提供。

4.2. データストレージクラス

4.2.1. IOwnedItemsStorage インターフェース

  • データの保存と読み出しの契約を定義。

4.2.2. OwnedItemsStorage クラス

  • IOwnedItemsStorageを実装。
  • PlayerPrefsを使用したデータの保存・読み出しを担当。

4.3. データ管理クラス

4.3.1. OwnedItemsManager クラス

  • アイテムの追加、使用、取得などのビジネスロジックを担当。
  • データストレージクラスを通じてデータの保存・読み出しを行う。

5. デザインパターンの選択

5.1. シングルトンパターン

概要: クラスのインスタンスがアプリケーション全体で一つだけ存在することを保証し、そのインスタンスへのグローバルなアクセスを提供するパターン。

メリット:

  • グローバルアクセスの容易さ。
  • インスタンスの一貫性。

デメリット:

  • テストの難しさ。
  • 依存性の隠蔽。
  • 柔軟性の低下。

5.2. 依存性注入 (Dependency Injection)

概要: クラスの依存関係を外部から注入することで、クラス間の結合度を下げる手法。

メリット:

  • テストの容易さ。
  • 柔軟性の向上。
  • 依存関係の明確化。

デメリット:

  • 初期設定の複雑さ。
  • 適切な管理が必要。

5.3. シングルトンと依存性注入の併用

シングルトンの利便性と依存性注入の柔軟性を組み合わせることで、グローバルアクセスの利便性を維持しつつ、テストの容易性や柔軟性を確保する方法。

6. 実装詳細

以下に、各クラスの具体的な実装例を示します。

6.1. データモデルクラスの実装

6.1.1. OwnedItem クラスの実装

using System;
using UnityEngine;

[Serializable]
public class OwnedItem
{
    public Item.ItemType Type { get; private set; }
    public int Number { get; private set; }

    public OwnedItem(Item.ItemType type)
    {
        Type = type;
        Number = 0;
    }

    public void Add(int addNumber = 1)
    {
        Number += addNumber;
    }

    public void Use(int useNumber = 1)
    {
        if (Number < useNumber)
            throw new Exception("アイテムが足りません");
        Number -= useNumber;
    }
}

6.1.2. OwnedItemsData クラスの実装

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class OwnedItemsData
{
    /// <summary>
    /// 所持アイテム一覧を取得します。
    /// </summary>
    public IReadOnlyList<OwnedItem> OwnedItems => ownedItems.AsReadOnly();

    /// <summary>
    /// どのアイテムを何個所持しているかのリスト
    /// </summary>
    [SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();

    /// <summary>
    /// アイテムを追加します。
    /// </summary>
    /// <param name="item"></param>
    public void Add(OwnedItem item)
    {
        ownedItems.Add(item);
    }

    /// <summary>
    /// アイテムを取得します。
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public OwnedItem GetItem(Item.ItemType type)
    {
        return ownedItems.FirstOrDefault(x => x.Type == type);
    }
}

6.2. データストレージクラスの実装

6.2.1. IOwnedItemsStorage インターフェースの実装

public interface IOwnedItemsStorage
{
    OwnedItemsData Load();
    void Save(OwnedItemsData data);
}

6.2.2. OwnedItemsStorage クラスの実装

using UnityEngine;

public class OwnedItemsStorage : IOwnedItemsStorage
{
    private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";

    public OwnedItemsData Load()
    {
        if (PlayerPrefs.HasKey(PlayerPrefsKey))
        {
            string json = PlayerPrefs.GetString(PlayerPrefsKey);
            return JsonUtility.FromJson<OwnedItemsData>(json);
        }
        return new OwnedItemsData();
    }

    public void Save(OwnedItemsData data)
    {
        string json = JsonUtility.ToJson(data);
        PlayerPrefs.SetString(PlayerPrefsKey, json);
        PlayerPrefs.Save();
    }
}

6.3. データ管理クラスの実装

6.3.1. OwnedItemsManager クラスの実装

using System;
using System.Linq;
using System.Collections.Generic;

public class OwnedItemsManager
{
    private readonly IOwnedItemsStorage _storage;
    private OwnedItemsData _data;

    // シングルトン実装
    private static OwnedItemsManager _instance;
    public static OwnedItemsManager Instance
    {
        get
        {
            if (_instance == null)
            {
                // デフォルトのストレージ実装を使用
                _instance = new OwnedItemsManager(new OwnedItemsStorage());
                _instance.LoadData();
            }
            return _instance;
        }
    }

    // コンストラクタ
    public OwnedItemsManager(IOwnedItemsStorage storage)
    {
        _storage = storage;
    }

    // 所有アイテムの取得
    public IReadOnlyList<OwnedItem> OwnedItems => _data.OwnedItems;

    // アイテムの追加
    public void AddItem(Item.ItemType type, int number = 1)
    {
        var item = _data.GetItem(type);
        if (item == null)
        {
            item = new OwnedItem(type);
            _data.Add(item);
        }
        item.Add(number);
        SaveData();
    }

    // アイテムの使用
    public void UseItem(Item.ItemType type, int number = 1)
    {
        var item = _data.GetItem(type);
        if (item == null || item.Number < number)
        {
            throw new Exception("アイテムが足りません");
        }
        item.Use(number);
        SaveData();
    }

    // データの読み込み
    private void LoadData()
    {
        _data = _storage.Load();
    }

    // データの保存
    private void SaveData()
    {
        _storage.Save(_data);
    }
}

6.4. 依存性注入の導入(VContainerを使用)

VContainerは、Unity向けの高速で軽量な依存性注入ライブラリです。ここでは、VContainerを使用して依存性注入を実装する方法を説明します。

6.4.1. VContainerのインストール

Unityのパッケージマネージャーを使用してVContainerをインストールします。以下の手順で進めます:

  1. Unityエディターで、Window > Package Manager を開きます。
  2. 左上の + ボタンをクリックし、Add package by name… を選択します。
  3. パッケージ名に VContainer を入力し、Add をクリックします。

6.4.2. インストーラーの作成

VContainerでは、LifetimeScopeを継承したクラスを作成し、依存関係のバインディングを設定します。

using VContainer;
using VContainer.Unity;
using UnityEngine;

public class GameInstaller : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // インターフェースと実装のバインディング
        builder.Register<IOwnedItemsStorage, OwnedItemsStorage>(Lifetime.Singleton);
        builder.Register<OwnedItemsManager>(Lifetime.Singleton);
    }
}

6.4.3. GameControllerの修正

GameControllerクラスに依存性注入を導入します。VContainerを使用すると、コンストラクタインジェクションやフィールドインジェクションが可能です。ここではコンストラクタインジェクションを使用します。

using UnityEngine;
using VContainer;
using VContainer.Unity;
using System.Linq;

public class GameController : MonoBehaviour
{
    private OwnedItemsManager _ownedItemsManager;

    [Inject]
    public void Construct(OwnedItemsManager ownedItemsManager)
    {
        _ownedItemsManager = ownedItemsManager;
    }

    void Start()
    {
        // _ownedItemsManagerを使用して初期化や処理を行う
        _ownedItemsManager.AddItem(Item.ItemType.Potion, 5);
        var potion = _ownedItemsManager.OwnedItems.FirstOrDefault(x => x.Type == Item.ItemType.Potion);
        if (potion != null)
        {
            Debug.Log($"所持ポーション数: {potion.Number}");
        }
        else
        {
            Debug.Log("ポーションを所持していません。");
        }
    }

    // 他のメソッドで _ownedItemsManager を使用
}

6.4.4. シーンにインストーラーを配置

  1. Hierarchyビューで、空のゲームオブジェクトを作成し、GameInstallerスクリプトをアタッチします。例えば、GameInstallerという名前にします。
  2. GameControllerをシーン内の任意のゲームオブジェクトにアタッチします。

6.4.5. VContainerによる依存性注入の動作確認

上記の設定により、GameControllerOwnedItemsManagerのインスタンスをVContainerから受け取ります。これにより、OwnedItemsManagerが正しく初期化され、他のクラスからも同一のインスタンスにアクセスできるようになります。

7. テスト戦略

7.1. ユニットテスト

  • OwnedItemsManagerのメソッド(AddItem、UseItemなど)を対象に、期待される動作を確認するユニットテストを実装。
  • IOwnedItemsStorageのモックを作成し、データの保存・読み出しの動作をシミュレート。

7.2. インテグレーションテスト

  • OwnedItemsManagerOwnedItemsStorageの連携動作を検証。
  • 実際のPlayerPrefsを使用せず、テスト用のストレージ実装を注入してテスト。

7.3. テスト環境の整備

  • VContainerを使用する場合、テスト用のLifetimeScopeを設定し、依存関係を適切に注入できるようにする。

8. 利点と考慮点

8.1. 利点

  1. 責務の分離:
    • 各クラスが明確な責務を持つため、コードの理解と保守が容易。
  2. 柔軟性の向上:
    • データストレージの実装を容易に変更可能(例:PlayerPrefsからクラウドストレージへ)。
  3. テストの容易性:
    • 依存性注入により、モックを使用したテストが可能。
  4. 再利用性の向上:
    • データストレージや管理クラスが他のシステムでも再利用可能。

8.2. 考慮点

  1. 初期設定の複雑さ:
    • 依存性注入を導入することで、初期設定が複雑になる可能性。
  2. ランタイムオーバーヘッド:
    • DIフレームワークを使用する場合、若干のランタイムオーバーヘッドが発生する可能性。
  3. シングルトンの適用範囲の慎重な検討:
    • グローバルアクセスが必要な箇所にのみシングルトンを適用し、過剰な使用を避ける。

9. まとめ

本資料では、Unityにおけるアイテム管理システムの設計と実装について、責務の分離、シングルトンパターン、依存性注入の観点から詳細に説明しました。特に、VContainerを使用した依存性注入の導入により、コードの可読性、保守性、拡張性を向上させ、将来的な機能追加や変更にも柔軟に対応できるシステムを構築する方法を紹介しました。

依存性注入を適切に導入することで、テストの容易性やコードの柔軟性が向上し、品質の高いシステムを実現することが可能です。VContainerのようなDIフレームワークを活用することで、効率的に依存関係を管理し、堅牢なアーキテクチャを構築できます。

今後の開発においても、設計原則とデザインパターンを適切に活用し、品質の高いシステムを構築していくことが重要です。

10. 参考資料