チュートリアル:Unity でのアイテム管理システムの作成

1. はじめに

このチュートリアルでは、Unity で以下の内容を学びます:

  • データ管理の基本
    プレイヤーの持つアイテム情報を保存し、読み込むために PlayerPrefs と JSON を利用する方法
  • シングルトンパターンの活用
    ゲーム内でいつでも同じデータにアクセスするための設計
  • アニメーションの実装
    DOTween を使ってオブジェクト(アイテム)の出現時の演出を行う方法
  • 衝突検知と処理
    プレイヤーがアイテムに触れたときに、適切な処理を行い、データに反映させる方法

2. プロジェクトの準備

  1. Unity のセットアップ:
    まず、Unity Hub から新規プロジェクトを作成します。2D や 3D プロジェクトでも構いませんが、ここでは 3D プロジェクトを前提とします。
  2. 必要なパッケージ:
    • DOTween (Demigiant DOTween) をインポートします。DOTween は無料で使える強力なアニメーションライブラリです。
    • DOTween インポート後、セットアップウィザードの指示に従って基本設定を行ってください。
  3. スクリプト作成:
    Assets フォルダー内に Scripts フォルダーを作成し、以下のようなスクリプトファイルを作成します。
    • OwnedItemsData.cs
    • Item.cs
    • GameManager.cs(シーン上のアイテムを初期化するためのスクリプト)

3. OwnedItemsData クラスの作成

このクラスは、プレイヤーが所有するアイテム情報(どのアイテムを何個持っているか)を管理します。ゲーム終了後もデータを保存するため、PlayerPrefs を利用し、JSON によるデータシリアライズ・デシリアライズを行います。

シングルトンパターンとデータの永続化

  • シングルトンパターンの目的:
    ゲーム全体で必ず同じデータ(=1つのインスタンス)にアクセスできるようにします。
    これにより、データの不整合や複数個所での更新ミスを防ぐことができます。
  • PlayerPrefs と JSON の利用:
    データを JSON 形式に変換し、PlayerPrefs に文字列として保存します。再起動時はこの文字列を読み込んでデータを復元します。

コード例:

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

[Serializable]
public class OwnedItemsData
{
    // 保存先となるキーを定義
    private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";

    // シングルトンインスタンス
    private static OwnedItemsData _instance;
    public static OwnedItemsData Instance
    {
        get
        {
            if (_instance == null)
            {
                // 既存のデータがあれば復元し、なければ新規作成する
                _instance = PlayerPrefs.HasKey(PlayerPrefsKey)
                    ? JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey))
                    : new OwnedItemsData();
            }
            return _instance;
        }
    }

    // 内部でアイテム情報をリストで管理
    [SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();

    // アイテム一覧を配列で取得できるプロパティ
    public OwnedItem[] OwnedItems { get { return ownedItems.ToArray(); } }

    // コンストラクタを private にすることで外部から新規作成できないようにする
    private OwnedItemsData() { }

    // 現在のデータ状態を JSON に変換して PlayerPrefs に保存
    public void Save()
    {
        var jsonString = JsonUtility.ToJson(this);
        PlayerPrefs.SetString(PlayerPrefsKey, jsonString);
        PlayerPrefs.Save();
    }

    // 特定のアイテムを追加する処理
    public void Add(Item.ItemType type, int number = 1)
    {
        var 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)
    {
        var item = GetItem(type);
        if (item == null || item.Number < number)
        {
            throw new Exception("アイテムが足りません");
        }
        item.Use(number);        
    }

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

    /// <summary>
    /// アイテムの個別情報を管理する内部クラス
    /// </summary>
    [Serializable]
    public class OwnedItem
    {
        [SerializeField] private Item.ItemType type;
        public Item.ItemType Type { get { return type; } }

        [SerializeField] private int number;
        public int Number { get { return number; } }

        // コンストラクタ: アイテムの種類を指定して生成。初期の個数は 0。
        public OwnedItem(Item.ItemType type)
        {
            this.type = type;
        }

        // アイテム数を追加
        public void Add(int number = 1)
        {
            this.number += number;
        }

        // アイテム数を減少
        public void Use(int number = 1)
        {
            this.number -= number;
        }
    }
}

解説ポイント

  • シングルトン実装:
    Instance プロパティ内で if (_instance == null) のチェックを行い、初回のみ PlayerPrefs のデータを読み込む、または新規作成しています。
  • PlayerPrefs 保存:
    Save() メソッドで JsonUtility.ToJson(this) を使ってクラス全体を文字列化し、保存後は PlayerPrefs.Save() を呼び出して変更を確定させています。
  • アイテムの追加と使用:
    Add()Use() メソッドにより、アイテムの数値を動的に変更でき、取得は GetItem() で行います。

4. Item クラスの作成

次に、実際にシーン上に配置される「アイテム」オブジェクトのスクリプトを作成します。このクラスは、アイテムの出現アニメーション、プレイヤーとの衝突時の取得処理などを担当します。

アイテムの種類と基本設定

  • ItemType 列挙体:
    木、石、投げオノなど、アイテムの種類を列挙して管理します。
  • RequireComponent 属性:
    このクラスを利用するゲームオブジェクトには必ず Collider コンポーネントが必要であることを保証します。

コード例:

using DG.Tweening;
using UnityEngine;

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

    // インスペクター上で設定可能なアイテム種類
    [SerializeField] private ItemType type;

DOTween を使ったアニメーション

  • Initialize() メソッド:
    アイテムがシーン上に出現するとき、アニメーションで動きやスケールの変化をつけます。
  • アニメーションの内容:
    • 初めは Collider を無効にし、出現アニメーション中は当たり判定が働かないようにします。
    • 出現時に、アイテムの位置を少しランダムにずらした後、元のサイズに弾むように拡大表示します。

コード例 (Initialize メソッド):

    /// <summary>
    /// 初期化処理
    /// - 出現アニメーションを実行
    /// - アニメーション完了まではColliderを無効化
    /// </summary>
    public void Initialize()
    {
        Collider colliderCache = GetComponent<Collider>();
        colliderCache.enabled = false;

        // 出現位置を現在の位置からランダムにずらす
        Transform transformCache = transform;
        Vector3 dropPosition = transformCache.localPosition +
                               new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));

        // DOTween の Sequence を使って連続アニメーションを組む
        Sequence sequence = DOTween.Sequence();

        // 移動アニメーション: 0.5秒でランダムな位置へ移動
        sequence.Append(transformCache.DOLocalMove(dropPosition, 0.5f));

        // スケールアニメーション: ゼロから元の大きさへ、Ease.OutBounce の効果付き
        Vector3 defaultScale = transformCache.localScale;
        transformCache.localScale = Vector3.zero;
        sequence.Append(transformCache.DOScale(defaultScale, 0.5f).SetEase(Ease.OutBounce));

        // アニメーション完了後に Collider を再度有効化
        sequence.OnComplete(() =>
        {
            colliderCache.enabled = true;
        });
    }

プレイヤーとの衝突でアイテム取得

  • OnTriggerEnter() メソッド:
    Collider を使ってプレイヤーとの衝突を検知します。
  • 処理の流れ:
  1. 衝突した相手が「Player」タグであるかを確認
  2. 該当アイテムを OwnedItemsData に追加し、保存
  3. 現在の所持アイテムの状態をログに出力
  4. アイテムオブジェクトをシーンから削除(Destroy)して取得完了

コード例 (OnTriggerEnter メソッド):

    /// <summary>
    /// プレイヤーとの衝突判定
    /// </summary>
    /// <param name="other">衝突した Collider</param>
    private void OnTriggerEnter(Collider other)
    {
        // タグが "Player" でなければ処理しない
        if (!other.CompareTag("Player")) return;

        // アイテムをプレイヤーの所持データに追加し、保存する
        OwnedItemsData.Instance.Add(type);
        OwnedItemsData.Instance.Save();

        // 現在の所持アイテム状態をログに表示
        foreach (var item in OwnedItemsData.Instance.OwnedItems)
        {
            Debug.Log($"{item.Type}を{item.Number}個所持");
        }

        // 取得済みのアイテムオブジェクトをシーン上から破棄
        Destroy(gameObject);
    }
}

コード全体 (Item.cs)

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 colliderCache = GetComponent<Collider>();
        colliderCache.enabled = false;

        Transform transformCache = transform;
        Vector3 dropPosition = transformCache.localPosition +
                               new Vector3(Random.Range(-1f, 1f), 0f, Random.Range(-1f, 1f));

        Sequence sequence = DOTween.Sequence();
        sequence.Append(transformCache.DOLocalMove(dropPosition, 0.5f));

        Vector3 defaultScale = transformCache.localScale;
        transformCache.localScale = Vector3.zero;
        sequence.Append(transformCache.DOScale(defaultScale, 0.5f).SetEase(Ease.OutBounce));

        sequence.OnComplete(() => { colliderCache.enabled = true; });
    }

    private void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag("Player")) return;

        OwnedItemsData.Instance.Add(type);
        OwnedItemsData.Instance.Save();

        foreach (var item in OwnedItemsData.Instance.OwnedItems)
        {
            Debug.Log($"{item.Type}を{item.Number}個所持");
        }

        Destroy(gameObject);
    }
}

5. GameManager からの統合と動作確認

実際にシーン上でこれらのスクリプトが正しく動作するか確認するために、簡単な GameManager スクリプトを作成します。このスクリプトでは、シーン内のアイテムを探し出し、各アイテムの初期化を呼び出します。

GameManager.cs の例:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // シーン内に存在する全ての Item コンポーネントを探して初期化する
        Item[] items = FindObjectsOfType<Item>();
        foreach (Item item in items)
        {
            item.Initialize();
        }
    }
}

シーンの設定

  1. アイテムオブジェクトの配置:
    シーン内に数個のアイテムオブジェクトを配置します。各オブジェクトには、Item.cs のスクリプトがアタッチされていることと、適切な Collider(例: Box Collider や Sphere Collider)がついている必要があります。
  2. Player オブジェクトの作成:
    プレイヤーオブジェクトを作り、タグを「Player」に設定します。また、プレイヤー側にも Collider および Rigidbody(衝突判定のため)を設定してください。
  3. GameManager の配置:
    シーン上に空の GameObject を作成し、GameManager.cs をアタッチします。これにより、シーン開始時にアイテムの Initialize が呼ばれます。

6. まとめと学びのポイント

このチュートリアルでは、Unity で簡単なアイテム管理システムを構築する方法を学びました。ポイントは以下の通りです。

  • データ管理:
    • OwnedItemsData クラスを通じて、プレイヤーが取得したアイテムの種類と数を管理し、PlayerPrefs と JSON によってデータの永続化を実現しました。
    • シングルトンパターンにより、どこからでも同じインスタンスへアクセスできる仕組みを作りました。
  • ゲームオブジェクトとしてのアイテム:
    • Item クラスでは、DOTween を利用してアイテムの出現時にアニメーション(位置と大きさの変更)を実行し、自然な演出を加えました。
    • Collider による衝突検知で、プレイヤーがアイテムに接触した際にアイテムの取得処理が行われます。
  • 統合:
    • GameManager を利用してシーン上のすべてのアイテムを初期化することで、各オブジェクトが正しく動作する環境を整えました。

このシステムをベースに、さらに発展させてアイテムの種類を増やしたり、UI と連携させて取得状況を画面表示したりするなど、さまざまな拡張が可能です。最初はこの基本形をしっかり理解し、実際に動かしてみることで、Unity におけるデータ管理やアニメーション、衝突処理の概念を深く学んでいただけるでしょう。


UML図

クラス図

説明

  • OwnedItemsData クラス
    シングルトンとして実装され、プレイヤーが所持するアイテムをリスト(ownedItems)として管理しています。
    ※メソッドには、Save()Add()Use()GetItem() などがあります。
  • OwnedItem クラス (内部クラス)
    各アイテムの種類と個数 (type と number) を管理し、Add() や Use() によりその個数を操作します。
  • Item クラス
    シーン上に配置される実際のアイテムオブジェクトのコンポーネントです。出現アニメーション(Initialize())や、プレイヤーとの衝突処理(OnTriggerEnter())が実装されています。
  • GameManager クラス
    シーン内の全アイテムを見つけ出し、Initialize() を呼び出すなど、システム全体の統合処理を担います。

アイテム取得シーケンス図

プレイヤーがアイテムに衝突して取得する際の処理の流れ(アイテムオブジェクトの OnTriggerEnter 呼び出しから、OwnedItemsData へのアイテム追加、保存、ログ出力、そしてアイテム破棄まで)を示しています。

アイテム初期化アクティビティ図

Item クラスの Initialize() メソッド内で行われる処理を表しています。ここでは、Collider の無効化、DOTween を使ったアニメーションの開始と完了後の Collider の再有効化までの流れを確認できます。

アイテム管理システムコンポーネント図

説明

  • シーケンス図:
    プレイヤーがアイテムに衝突して取得する一連の処理を時系列で表現します。各処理(アイテムの衝突判定、OwnedItemsData への追加と保存、そしてアイテムオブジェクトの削除)の関係が明確になります。
  • アクティビティ図:
    アイテムの初期化処理の流れ(Collider の無効化→DOTween を使ったアニメーション→アニメーション完了後の Collider 再有効化)を視覚的に理解できます。
  • コンポーネント図:
    システムの主要モジュール間の依存関係や連携(プレイヤーがアイテムを取得する、GameManager による初期化など)が把握しやすくなります。

C#,Unity

Posted by hidepon