Unity におけるアイテム管理システムの概要およびコード解説資料

この資料は、Unity を用いて開発されたゲーム内での「所持アイテム管理」と「フィールド上でのアイテム表現」の仕組みについて、コードの詳細とその役割を初学者向けに解説したものです。ここでは、システム全体の構造、各クラスの機能、ならびに主要な技術要素について説明します。


1. 全体概要

  • 目的:
    ゲーム内でプレイヤーが獲得したアイテム情報を管理し、実際のフィールド上では出現や取得ができるようにする。
  • 主な機能:
    • 所持品の管理:
      プレイヤーがどのアイテムを何個持っているかを一元管理し、セーブ・ロードできる仕組み。
    • アイテムの演出と取得:
      フィールドに出現したアイテムがアニメーションと共に現れ、プレイヤーと接触することで取得される機能。

2. OwnedItemsData クラスの詳細

2.1. シングルトンパターンの採用

  • 目的:
    ゲーム中に一つだけ存在すべき所持品データを、どこからでも同じインスタンスにアクセスできるようにするため。
  • 実装のポイント:
    • Instance プロパティを利用して、最初にアクセスされたときに PlayerPrefs から既存のデータを読み込み、なければ新しく作成。
    • コンストラクタが private になっており、外部から new できない設計となっている。

2.2. アイテムデータの保存と更新

  • 保存機能 (Save メソッド):
    オブジェクトを JSON 形式に変換して PlayerPrefs に保存することで、ゲーム終了後もデータが保持される。
  • アイテム追加 (Add メソッド):
    指定したアイテムの種類と個数を追加。既に存在すれば個数を増やし、存在しない場合は新しいエントリーとしてリストに追加する。
  • アイテム消費 (Use メソッド):
    指定個数だけアイテムを使用(消費)する。所持個数が不足している場合は例外を投げる仕組み。

2.3. 内部クラス OwnedItem

  • 役割:
    各アイテムごとの種類と個数を保持するシンプルなデータモデル。
  • 提供されるメソッド:
    • Add: 個数を増加させる。
    • Use: 個数を減少させる。

3. Item クラスの詳細

3.1. アイテムの種類定義

  • ItemType 列挙体:
    • Wood(木)
    • Stone(石)
    • ThrowAxe(投げオノ)
      ※ 注:投げオノは木と石を組み合わせることで作成する仕様が示唆されています。

3.2. 初期化処理 (Initialize メソッド)

  • 目的:
    フィールド上に出現する際のアニメーション演出を実現するために、アイテムの位置・サイズの変化を制御する。
  • 処理の流れ:
  1. Collider の一時無効化:
    アニメーション中にプレイヤーとの不要な衝突を防止するために、一時的にコライダーを無効化します。
  2. 位置のランダムなドロップ:
    現在の位置から若干ランダムな位置へ移動するアニメーションを実施し、自然な出現感を演出。
  3. サイズアニメーション:
    アイテムの大きさをゼロから元のスケールまで変化させることで、ポップで目立つエフェクトを付与。イージング(Ease.OutBounce)により弾むような効果が実現されます。
  4. Collider の再有効化:
    アニメーション完了後に再びコライダーを有効にし、プレイヤーとの接触を可能にします。

3.3. プレイヤーとの接触処理 (OnTriggerEnter メソッド)

  • 目的:
    プレイヤーがアイテムに触れた際、アイテムを所持品として追加(取得)するための処理を準備する。
  • 具体的な挙動:
    • プレイヤーかどうかをタグ("Player")で判定します。
    • 触れた際に、実際の所持品への追加処理はコメントアウトされており、今後の実装予定となっています。
    • アイテム取得後、Destroy(gameObject) によりシーンから対象オブジェクトを削除します。

4. 重要ポイント

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

  • 利点:
    ゲーム内で一つだけ必要なデータを管理でき、どのクラスからも同じデータにアクセスできるため、データの整合性が保たれます。

4.2. PlayerPrefs と JSON の利用

  • 説明:
    PlayerPrefs は Unity における簡易的なデータ保存機能で、JSON 化することで複雑なデータ(リストなど)も保存できるようになります。

4.3. DOTween を利用したアニメーション

  • 特徴:
    DOTween はオブジェクトの位置、スケール、回転などを滑らかに変化させるライブラリです。
    初期化処理でアニメーションを設定することで、視覚的に魅力的な出現効果を実現しています。

4.4. Collider と OnTriggerEnter の活用

  • 役割:
    ゲームオブジェクトの接触判定により、特定の条件(例:プレイヤーだけ)でのみイベントを発生させることができます。
    これにより、ゲーム内でのアイテム取得やその他のインタラクションが可能となります。

5. まとめ

今回のコードは、所持品のデータ管理フィールド上のアイテム演出という、ゲームの基幹部分となるシステムの実装例です。

  • OwnedItemsData クラス は、シングルトンパターンを活用してプレイヤーの所持アイテムを一元管理し、JSON 化してデータ保存を実現しています。
  • Item クラス は、DOTween によるアニメーションや Collider を用いたプレイヤーとの接触検出で、実際のアイテムの出現と取得処理を担います。

これらの技術を理解することで、初学者の方でもより複雑なゲームシステムの構築に挑戦しやすくなります。各要素がどのように連携するかを学ぶことで、自分のプロジェクトへの応用も進めやすくなるでしょう。

参考 リファクタリング

以下は、元のコードの機能はそのままに、より見通しがよく、初学者でも理解しやすいようにリファクタリングした例と、その解説です。


リファクタリング後のコード例

OwnedItemsData クラス

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

[Serializable]
public class OwnedItemsData
{
    // PlayerPrefs に保存するためのキー
    private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";

    private static OwnedItemsData _instance;

    // シングルトンインスタンスを取得するプロパティ
    public static OwnedItemsData Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = LoadData();
            }
            return _instance;
        }
    }

    // PlayerPrefsからデータを読み込むか、新しく生成する
    private static OwnedItemsData LoadData()
    {
        if (PlayerPrefs.HasKey(PlayerPrefsKey))
        {
            return JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey));
        }
        return new OwnedItemsData();
    }

    // 所持しているアイテムのリスト(内部ではListで管理)
    [SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();

    // 外部からは配列として取得できるようにする
    public OwnedItem[] OwnedItems => ownedItems.ToArray();

    // コンストラクタは private にして外部から new できないようにする
    private OwnedItemsData() { }

    // 現在のデータをJSON化してPlayerPrefsに保存する
    public void Save()
    {
        string json = JsonUtility.ToJson(this);
        PlayerPrefs.SetString(PlayerPrefsKey, json);
        PlayerPrefs.Save();
    }

    // 指定されたアイテムを所持品リストに追加する
    public void Add(Item.ItemType type, int number = 1)
    {
        OwnedItem item = GetItem(type);
        if (item == null)
        {
            item = new OwnedItem(type);
            ownedItems.Add(item);
        }
        item.Add(number);
    }

    // 指定されたアイテムを使用(消費)する
    public void Use(Item.ItemType type, int number = 1)
    {
        OwnedItem item = GetItem(type);
        if (item == null || item.Number < number)
        {
            throw new Exception("アイテムが足りません: " + type);
        }
        item.Use(number);
    }

    // 指定された種類のアイテム情報を取得する
    public OwnedItem GetItem(Item.ItemType type)
    {
        return ownedItems.FirstOrDefault(x => x.Type == type);
    }

    // 所持アイテムの情報を管理する内部クラス
    [Serializable]
    public class OwnedItem
    {
        [SerializeField] private Item.ItemType type;
        [SerializeField] private int number = 0;  // 初期は0からスタート

        // 外部からはプロパティを通して値を取得
        public Item.ItemType Type => type;
        public int Number => number;

        // コンストラクタ
        public OwnedItem(Item.ItemType type)
        {
            this.type = type;
        }

        // アイテム数を追加する
        public void Add(int amount)
        {
            number += amount;
        }

        // アイテム数を減らす(使用する)
        public void Use(int amount)
        {
            number -= amount;
        }
    }
}

Item クラス

using DG.Tweening;
using UnityEngine;

[RequireComponent(typeof(Collider))]
public class Item : MonoBehaviour
{
    // アイテムの種類を定義する列挙体
    public enum ItemType
    {
        Wood,    // 木
        Stone,   // 石
        ThrowAxe // 投げオノ(木と石で作る!)
    }

    [SerializeField] private ItemType type;

    // アイテム出現時のアニメーションなどの初期化処理
    public void Initialize()
    {
        // アニメーション中はColliderを無効にする(誤操作防止)
        var colliderComponent = GetComponent<Collider>();
        colliderComponent.enabled = false;

        // オブジェクトのドロップ位置をランダムに決定する
        Vector3 dropPosition = transform.localPosition +
                               new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f));
        transform.DOLocalMove(dropPosition, 0.5f);

        // オブジェクトのサイズを0から元の大きさに変えるアニメーション
        Vector3 originalScale = transform.localScale;
        transform.localScale = Vector3.zero;
        transform.DOScale(originalScale, 0.5f)
            .SetEase(Ease.OutBounce)
            .OnComplete(() =>
            {
                // アニメーション終了後、Colliderを再度有効化する
                colliderComponent.enabled = true;
            });
    }

    // 他のオブジェクト(主にプレイヤー)との接触を検知する
    private void OnTriggerEnter(Collider other)
    {
        // 接触対象がプレイヤーでなければ何もしない
        if (!other.CompareTag("Player")) return;

        // プレイヤーの所持品にアイテムを追加する
        OwnedItemsData.Instance.Add(type);

        // アイテム取得後、オブジェクトをシーンから削除する
        Destroy(gameObject);
    }
}

リファクタリングのポイントと解説

1. コードの整理と分離

  • 責務の分離
    • OwnedItemsData クラス: プレイヤーの所持しているアイテムの管理、保存、更新を行うクラスです。
    • Item クラス: ゲーム内に存在するアイテム自体の動作(アニメーション、衝突判定など)を担当しています。
      それぞれのクラスが1つの責務に集中することで、コードが理解しやすくなります。

2. シングルトンパターンの明確化

  • インスタンスの生成処理の切り出し
    以前はInstanceプロパティ内に処理がまとまっていましたが、LoadData()というプライベートメソッドに分けることで「データの読み込み」と「インスタンス生成」の処理を分離し、コードの読みやすさを向上させました。

3. プロパティとラムダ式の活用

  • シンプルな書き方
    外部からリストを取得する部分は、ラムダ式を使って一行で記述しています。これにより、冗長なコードが減り、初学者でも一目で意味がわかるようになっています。

4. 初期値の明示

  • 初期値を明確に設定
    内部クラス OwnedItemnumber0 で初期化することで、初めてアイテムが追加された時の状態が明確になります。これにより、どのアイテムも初めは0個からスタートするというルールが分かりやすくなります。

5. エラーメッセージの改善

  • 具体的なエラーメッセージ
    アイテムが不足している場合、どのアイテムが不足しているのかをエラーメッセージに含めることで、デバッグ時にどこが問題かを把握しやすくしています。

6. アニメーションと衝突処理の整頓

  • アニメーション処理の順序整理
    Item クラスの Initialize() メソッドでは、DOTween を使って出現アニメーションを実施しています。
    • 最初にColliderを無効化して、アニメーション中に誤ってアイテムが取得されないようにしています。
    • アニメーション完了後に再度Colliderを有効にすることで、プレイヤーがアイテムを取得できる状態に戻しています。
  • プレイヤーとの接触のチェック
    OnTriggerEnter メソッドでは、タグチェックで対象がプレイヤーかどうかを判定し、プレイヤーの場合のみアイテムを追加する処理を行っています。これにより、意図しないオブジェクトとの反応を避けられます。

まとめ

今回のリファクタリングでは、各クラスが担う役割を明確に分離し、読みやすく保守しやすいコードに改善しました。

  • 責務ごとに整理: プレイヤーの所持品管理とアイテムの動作を分ける
  • メソッドの切り出し: インスタンス生成時の処理やデータ読み込みを専用のメソッドに分ける
  • 簡潔な記述: ラムダ式やプロパティを活用し、コードをすっきりとまとめる

こうしたリファクタリングの考え方は、コードの規模が大きくなったときや、複数人での開発時にとても重要になります。

C#,Unity

Posted by hidepon