拡張性・柔軟性を考慮した Unity アイテム管理システム チュートリアル

このチュートリアルでは、以下の要素を実装します。

  • 外部設定ファイルによるパラメータ管理
    → JSON ファイルからゲーム全体やアイテムのパラメータを読み込みます。
  • イベント駆動アーキテクチャ
    → アイテム取得時などにイベントを発行し、ScoreManager やその他のリスナーが処理します。
  • 依存性注入(DI)
    → 簡易 DI コンテナを用い、各コンポーネント間の依存性を外部から注入します。
  • コンポジションベースの設計
    → 落下処理などの機能を専用コンポーネント(FallingBehavior)として分離し、アイテムにアタッチします。
  • 状態管理システム
    → ゲーム進行に応じたパラメータ変更をステートマシンで管理します。
  • モジュール化/プラグイン対応
    → 新たなアイテムや機能をコアに影響を与えずに追加できる設計の考え方も取り入れます。

以下、各コードとその説明をステップごとに示します。


【Step 1】外部設定ファイルの用意と ConfigManager の作成

(1) 外部設定ファイル (Resources/GameConfig.json)

Assets/Resources フォルダに以下の JSON ファイルを配置します。

{
    "spawnInterval": 1.0,
    "defaultDropSpeed": -0.03,
    "itemSettings": [
        {
            "itemName": "Apple",
            "spawnWeight": 1.0,
            "dropSpeed": -0.03
        },
        {
            "itemName": "Bomb",
            "spawnWeight": 0.5,
            "dropSpeed": -0.05
        }
    ]
}

(2) ConfigManager.cs

JSON を読み込み、グローバル設定やアイテム設定を管理します。

using UnityEngine;
using System.Collections.Generic;

public class ConfigManager : MonoBehaviour
{
    public static ConfigManager Instance { get; private set; }
    public float SpawnInterval { get; private set; } = 1.0f;
    public float DefaultDropSpeed { get; private set; } = -0.03f;
    public List<ItemConfig> ItemConfigs { get; private set; } = new List<ItemConfig>();

    [System.Serializable]
    public class ItemConfig
    {
        public string itemName;
        public float spawnWeight;
        public float dropSpeed;
    }

    [System.Serializable]
    public class ConfigData
    {
        public float spawnInterval;
        public float defaultDropSpeed;
        public List<ItemConfig> itemSettings;
    }

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(this.gameObject);
            return;
        }
        Instance = this;
        LoadConfig();
    }

    void LoadConfig()
    {
        TextAsset jsonText = Resources.Load<TextAsset>("GameConfig");
        if (jsonText != null)
        {
            ConfigData data = JsonUtility.FromJson<ConfigData>(jsonText.text);
            SpawnInterval = data.spawnInterval;
            DefaultDropSpeed = data.defaultDropSpeed;
            ItemConfigs = data.itemSettings;
        }
        else
        {
            Debug.LogWarning("GameConfig.json not found in Resources.");
        }
    }
}

【Tips】

  • ConfigManager をシーン内の空オブジェクトにアタッチしてください。

【Step 2】依存性注入(DI)の簡易コンテナ

DIContainer.cs

各主要コンポーネント(ConfigManager、ScoreManager、GameStateMachine など)を登録し、依存性解決を支援します。

using UnityEngine;
using System.Collections.Generic;

public static class DIContainer
{
    private static Dictionary<System.Type, Object> container = new Dictionary<System.Type, Object>();

    public static void Register<T>(T instance) where T : Object
    {
        container[typeof(T)] = instance;
    }

    public static T Resolve<T>() where T : Object
    {
        if (container.TryGetValue(typeof(T), out Object instance))
        {
            return instance as T;
        }
        Debug.LogWarning("DIContainer: No instance registered for " + typeof(T));
        return null;
    }
}

【設定】

  • 各コンポーネントの Awake() で DIContainer.Register(this) を呼び出すか、専用の初期化スクリプトで登録します。

【Step 3】イベント駆動のための ItemEventManager

ItemEventManager.cs

アイテム取得時などのイベントを発行するためのクラスです。

using UnityEngine;
using System;

public static class ItemEventManager
{
    // アイテム取得イベント。引数はアイテム名や得点変化など必要に応じて変更
    public static event Action<string> OnItemCollected;

    public static void ItemCollected(string itemName)
    {
        OnItemCollected?.Invoke(itemName);
    }
}

【Step 4】コンポジションによる落下処理コンポーネント

FallingBehavior.cs

アイテムの落下処理を独立したコンポーネントとして実装します。

using UnityEngine;

public class FallingBehavior : MonoBehaviour
{
    public float dropSpeed = -0.03f;

    void Update()
    {
        transform.Translate(0, dropSpeed, 0);
        if (transform.position.y < -1.0f)
        {
            Destroy(gameObject);
        }
    }

    public void SetDropSpeed(float speed)
    {
        dropSpeed = speed;
    }
}

【Step 5】共通インターフェースとアイテムデータ(ScriptableObject)

IItemEffect.cs

アイテム取得時の効果を定義するインターフェースです。

using UnityEngine;

public interface IItemEffect
{
    // アイテム取得時に効果を実行する
    void ApplyEffect();
    // 再生するサウンドを返す
    AudioClip GetAudioClip();
}

ItemData.cs

ScriptableObject を使って、各アイテムの基本情報を管理します。

using UnityEngine;

[CreateAssetMenu(menuName = "Item/ItemData")]
public class ItemData : ScriptableObject
{
    public string itemName;
    public GameObject prefab;
    public float spawnWeight;
    public float dropSpeed;
}

【Tips】

  • エディタから「Create → Item → ItemData」で Apple や Bomb のデータを作成し、Prefab、spawnWeight、dropSpeed を設定してください。

【Step 6】ItemBase と各アイテムの個別実装

ItemBase.cs

個別効果の実装用の抽象クラスです。
※ 落下処理は FallingBehavior で行うため、ここでは効果部分に注力します。

using UnityEngine;

public abstract class ItemBase : MonoBehaviour, IItemEffect
{
    // イベント駆動により、アイテム名を後から通知できるように
    public string itemName;

    public abstract void ApplyEffect();
    public abstract AudioClip GetAudioClip();
}

AppleItem.cs

using UnityEngine;

public class AppleItem : ItemBase
{
    public AudioClip appleSE;

    public override void ApplyEffect()
    {
        // イベントを発行して、ScoreManager などのリスナーが反応
        ItemEventManager.ItemCollected(itemName);
        // 追加効果(必要に応じて)を実装
    }

    public override AudioClip GetAudioClip()
    {
        return appleSE;
    }
}

BombItem.cs

using UnityEngine;

public class BombItem : ItemBase
{
    public AudioClip bombSE;

    public override void ApplyEffect()
    {
        ItemEventManager.ItemCollected(itemName);
    }

    public override AudioClip GetAudioClip()
    {
        return bombSE;
    }
}

【ポイント】

  • AppleItem や BombItem は、直接 ScoreManager を呼び出すのではなく、イベントを発行します。
  • リスナー(ScoreManager など)がアイテム名に応じた処理を行います。

【Step 7】ファクトリ/レジストリと DI を利用したアイテム生成

ItemFactory.cs

ConfigManager から読み込んだ設定や、ScriptableObject に登録した ItemData(または外部から登録する方法)をもとに、重み付きランダムでアイテムを生成します。

using UnityEngine;
using System.Collections.Generic;

public static class ItemFactory
{
    // 外部設定やエディタで登録した ItemData リストを使用(どちらかの方法で管理)
    public static List<ItemData> itemDataList = new List<ItemData>();

    public static GameObject CreateRandomItem()
    {
        float totalWeight = 0;
        foreach (ItemData data in itemDataList)
        {
            totalWeight += data.spawnWeight;
        }

        float randomPoint = Random.Range(0, totalWeight);
        float current = 0;
        foreach (ItemData data in itemDataList)
        {
            current += data.spawnWeight;
            if (randomPoint <= current)
            {
                GameObject instance = GameObject.Instantiate(data.prefab);

                // コンポジションによる落下処理の設定
                FallingBehavior falling = instance.GetComponent<FallingBehavior>();
                if (falling != null)
                {
                    falling.SetDropSpeed(data.dropSpeed);
                }

                // ItemBase にアイテム名を設定
                ItemBase item = instance.GetComponent<ItemBase>();
                if (item != null)
                {
                    item.itemName = data.itemName;
                }
                return instance;
            }
        }
        return null;
    }
}

【Step 8】アイテム生成コンポーネントと状態管理

GameStateMachine.cs

ゲームの進行状況に応じて、生成間隔やその他のパラメータを変更するためのシンプルな状態管理例です。

using UnityEngine;

public class GameStateMachine : MonoBehaviour
{
    public float currentSpawnInterval;
    private float gameTime = 30.0f;

    void Update()
    {
        gameTime -= Time.deltaTime;
        if (gameTime < 0)
        {
            gameTime = 0;
        }

        // 状態に応じた生成間隔の変更例
        if (gameTime > 20)
        {
            currentSpawnInterval = ConfigManager.Instance.SpawnInterval;
        }
        else if (gameTime > 10)
        {
            currentSpawnInterval = ConfigManager.Instance.SpawnInterval * 0.8f;
        }
        else
        {
            currentSpawnInterval = ConfigManager.Instance.SpawnInterval * 0.6f;
        }
    }

    public float GetSpawnInterval()
    {
        return currentSpawnInterval;
    }
}

ItemGenerator.cs

GameStateMachine から生成間隔を取得し、一定間隔でアイテムを生成します。

using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    float delta = 0;
    GameStateMachine stateMachine;

    void Start()
    {
        stateMachine = DIContainer.Resolve<GameStateMachine>();
        if (stateMachine == null)
        {
            Debug.LogWarning("GameStateMachine is not registered in DIContainer.");
        }
    }

    void Update()
    {
        float spawnInterval = (stateMachine != null) ? stateMachine.GetSpawnInterval() : ConfigManager.Instance.SpawnInterval;
        delta += Time.deltaTime;
        if (delta > spawnInterval)
        {
            delta = 0;
            GameObject item = ItemFactory.CreateRandomItem();
            if (item != null)
            {
                // アイテムの出現位置をランダムに設定
                float x = Random.Range(-1, 2);
                float z = Random.Range(-1, 2);
                item.transform.position = new Vector3(x, 4, z);
            }
        }
    }
}

【Step 9】スコア管理とイベントリスニング

ScoreManager.cs

DI で登録し、ItemEventManager のイベントを購読してスコア更新を行います。

using UnityEngine;
using TMPro;

public class ScoreManager : MonoBehaviour
{
    public static ScoreManager Instance { get; private set; }
    private int score = 0;
    public TextMeshProUGUI scoreText; // Inspector で割り当て

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(this.gameObject);
            return;
        }
        Instance = this;
        DIContainer.Register<ScoreManager>(this);
    }

    void OnEnable()
    {
        ItemEventManager.OnItemCollected += OnItemCollected;
    }

    void OnDisable()
    {
        ItemEventManager.OnItemCollected -= OnItemCollected;
    }

    // イベントリスナー:アイテム名に応じたスコア処理を実装
    void OnItemCollected(string itemName)
    {
        if (itemName == "Apple")
        {
            AddScore(100);
        }
        else if (itemName == "Bomb")
        {
            DivideScore(2);
        }
    }

    public void AddScore(int value)
    {
        score += value;
        UpdateScoreText();
    }

    public void DivideScore(int divisor)
    {
        score /= divisor;
        UpdateScoreText();
    }

    void UpdateScoreText()
    {
        if (scoreText != null)
        {
            scoreText.text = score.ToString() + " point";
        }
    }
}

【Step 10】ゲーム進行管理とバスケット(衝突処理)

GameDirector.cs

シンプルなタイマー管理例です。

using UnityEngine;
using TMPro;

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    float time = 30.0f;

    void Start()
    {
        timerText = GameObject.Find("Time");
    }

    void Update()
    {
        time -= Time.deltaTime;
        if (time < 0)
        {
            time = 0;
        }
        timerText.GetComponent<TextMeshProUGUI>().text = time.ToString("F1");
    }
}

BasketController.cs

衝突時に、アイテムの効果を実行し、サウンドを再生します。

using UnityEngine;

public class BasketController : MonoBehaviour
{
    public AudioSource aud;

    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void OnTriggerEnter(Collider other)
    {
        IItemEffect effect = other.gameObject.GetComponent<IItemEffect>();
        if (effect != null)
        {
            aud.PlayOneShot(effect.GetAudioClip());
            effect.ApplyEffect();
        }
        Destroy(other.gameObject);
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                float x = Mathf.RoundToInt(hit.point.x);
                float z = Mathf.RoundToInt(hit.point.z);
                transform.position = new Vector3(x, 0, z);
            }
        }
    }
}

【Step 11】DI の登録例(初期化スクリプト)

各コンポーネント(例えば、GameStateMachine や ConfigManager など)は Awake() で DIContainer.Register<T>(this) を呼び出すか、専用の初期化スクリプトで一括登録してください。
例:

using UnityEngine;

public class Initializer : MonoBehaviour
{
    void Awake()
    {
        // すでに各コンポーネントは自ら Awake() で登録済みなら不要です。
        // 例として GameStateMachine の登録
        GameStateMachine stateMachine = FindObjectOfType<GameStateMachine>();
        if (stateMachine != null)
        {
            DIContainer.Register<GameStateMachine>(stateMachine);
        }
    }
}

【設定】

  • Initializer をシーン内の空オブジェクトにアタッチします。

【まとめ】

このチュートリアルでは、

  1. 外部設定ファイル
    → GameConfig.json と ConfigManager により、ゲーム全体やアイテムのパラメータを外部から管理。
  2. イベント駆動アーキテクチャ
    → ItemEventManager を通じて、アイテム取得時の処理を ScoreManager などのリスナーが受け取る仕組みを実装。
  3. 依存性注入
    → DIContainer を使い、コンポーネント間の依存性を明示的に管理。
  4. コンポジションベースの設計
    → FallingBehavior などの機能コンポーネントを分離し、柔軟に再利用可能に。
  5. 状態管理システム
    → GameStateMachine により、ゲーム進行に応じた生成間隔の変更などを実装。
  6. モジュール化/プラグイン対応
    → 各機能を分離して実装することで、新たなアイテムや機能(例:プラグイン形式での追加)が容易に拡張できる設計となっています。

この設計により、将来的な仕様変更や拡張にも柔軟に対応できる、拡張性・柔軟性に優れたシステムを実現できます。
各コードファイルを適切にプロジェクトへ追加し、シーン上のオブジェクトと連携させて動作を確認してください。