Unity アイテム管理システムとリアルタイム難易度・出現率調整チュートリアル(最終版)

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

  • ScriptableObject によるアイテムデータ管理
    各アイテムの Prefab、出現重み、個別落下速度、そしてアイテムタイプ(Enum)をエディタ上で管理します。
  • DifficultySettings によるパラメータ管理
    ゲーム進行に合わせた生成間隔、出現率、時間の閾値などを DifficultySettings アセットで管理し、GameDirector で適用します。
  • 抽象基底クラスと IItemEffect インターフェースによる共通処理と個別効果の分離
    共通の落下処理は ItemBase に集約し、各アイテムは IItemEffect 経由で独自の効果(スコア更新など)を実装します。
  • ファクトリ/レジストリパターンを用いた重み付きランダム生成
    ItemFactory が登録された ItemData リストから、spawnWeight による重み付きランダム抽出でアイテムを生成します。
  • ScoreManager によるスコア管理
    各アイテムの効果は ScoreManager 経由でスコア更新されます。
  • Enum によるアイテムタイプ管理
    アイテム識別には文字列リテラルではなく Enum(ItemType)を使用することで、保守性と安全性を向上させます。
  • BasketController による衝突処理
    衝突時にアイテムの効果を呼び出し、サウンドを再生します。

以下、各コードファイルとシーン設定の手順を示します。
すべて Allman スタイル(開き括弧「{」が行頭)で記述されています。


1. プロジェクトのセットアップ

  1. Unity で新規プロジェクトを作成します。
  2. シーンにカメラ、ライト、UI 用の TextMeshPro コンポーネントを配置します。
    • タイマー表示用の UI オブジェクトは「Time」という名前にしてください。
    • スコア表示用の UI オブジェクトは ScoreManager に設定します。
  3. ゲーム内でアイテムを受け取る「Basket」オブジェクトを作成し、後述の BasketController をアタッチします。

2. 各コードファイルの作成

2-1. 共通インターフェース

IItemEffect.cs

using UnityEngine;

public interface IItemEffect
{
    // 衝突時にアイテム固有の効果を適用する
    void ApplyEffect();
    // 衝突時に再生するサウンドを返す
    AudioClip GetAudioClip();
}

2-2. アイテムタイプ Enum の定義

ItemType.cs

public enum ItemType
{
    Apple,
    Bomb
}

2-3. アイテム定義(ScriptableObject)

ItemData.cs
各アイテムの情報(Prefab、出現重み、個別落下速度、アイテムタイプ)をエディタ上で設定します。

using UnityEngine;

[CreateAssetMenu(menuName = "Item/ItemData")]
public class ItemData : ScriptableObject
{
    public string itemName;       // 参考用の名称
    public GameObject prefab;     // 出現用のPrefab
    public float spawnWeight;     // 出現確率に用いる重み
    public float dropSpeed;       // 個別の落下速度
    public ItemType itemType;     // Enum によるアイテムタイプ
}

【Tips】
Assets 内で右クリック → Create → Item → ItemData を選び、Apple 用、Bomb 用などのアセットを作成してください。


2-4. DifficultySettings(難易度パラメータ管理)

DifficultySettings.cs
ゲーム進行に合わせた生成間隔、出現率、時間の閾値などをまとめた ScriptableObject です。

using UnityEngine;

[CreateAssetMenu(menuName = "Settings/DifficultySettings")]
public class DifficultySettings : ScriptableObject
{
    public float initialTime = 30.0f;

    // 各ステージへの移行閾値(秒)
    // Stage1: time >= stage2Threshold
    // Stage2: stage3Threshold <= time < stage2Threshold
    // Stage3: stage4Threshold <= time < stage3Threshold
    // Stage4: time < stage4Threshold
    public float stage2Threshold = 23f;
    public float stage3Threshold = 12f;
    public float stage4Threshold = 4f;

    // 各ステージの生成間隔
    public float intervalStage1 = 1.0f;      // 23~30 秒
    public float intervalStage2 = 0.8f;      // 12~23 秒
    public float intervalStage3 = 0.5f;      // 4~12 秒
    public float intervalStage4 = 0.3f;      // 0~4 秒
    public float gameOverInterval = 10000.0f; // ゲームオーバー時

    // 各ステージの出現率設定(spawnWeight)
    public float bombWeightStage1 = 2f;
    public float appleWeightStage1 = 8f;

    public float bombWeightStage2 = 4f;
    public float appleWeightStage2 = 6f;

    public float bombWeightStage3 = 6f;
    public float appleWeightStage3 = 4f;

    public float bombWeightStage4 = 0f;
    public float appleWeightStage4 = 1f;
}

2-5. 共通処理・個別効果のための抽象基底クラス

ItemBase.cs
共通の落下処理を実装し、各アイテムは IItemEffect 経由で独自の効果を定義します。

using UnityEngine;

public abstract class ItemBase : MonoBehaviour, IItemEffect
{
    protected float dropSpeed = -0.03f;

    // アイテムの落下速度を設定するメソッド
    public void SetDropSpeed(float speed)
    {
        dropSpeed = speed;
    }

    // 共通の落下処理
    protected virtual void Update()
    {
        transform.Translate(0, dropSpeed, 0);
        if (transform.position.y < -1.0f)
        {
            Destroy(gameObject);
        }
    }

    // IItemEffect の実装は各サブクラスで定義
    public abstract void ApplyEffect();
    public abstract AudioClip GetAudioClip();
}

2-6. アイテム生成のファクトリ/レジストリ

ItemFactory.cs
登録された ItemData リストから、重み付きランダム抽出でアイテムを生成します。

using UnityEngine;
using System.Collections.Generic;

public static class ItemFactory
{
    // ItemDataManager により登録される 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);
                // ItemBase を持つ場合、設定された dropSpeed を反映
                ItemBase itemBase = instance.GetComponent<ItemBase>();
                if (itemBase != null)
                {
                    itemBase.SetDropSpeed(data.dropSpeed);
                }
                return instance;
            }
        }
        return null;
    }
}

2-7. アイテムデータ登録用マネージャー

以下のいずれかの方法で ItemData を登録します。

方法 1: Inspector から手動登録

ItemDataManager.cs

using UnityEngine;
using System.Collections.Generic;

public class ItemDataManager : MonoBehaviour
{
    // Inspector で登録する ItemData のリスト
    public List<ItemData> registeredItemDatas = new List<ItemData>();

    void Awake()
    {
        // 登録されたデータを ItemFactory に設定
        ItemFactory.itemDataList = new List<ItemData>(registeredItemDatas);
    }
}

【設定手順】
シーン内に空の GameObject を作成し、「ItemDataManager」と名付け、上記スクリプトをアタッチ。
Inspector で各 ItemData アセットをドラッグ&ドロップして登録。

方法 2: Resources フォルダから自動読み込み

ItemDataManager.cs

using UnityEngine;
using System.Collections.Generic;

public class ItemDataManager : MonoBehaviour
{
    void Awake()
    {
        // Resources/ItemData フォルダ内のすべての ItemData を読み込み
        ItemData[] itemDatas = Resources.LoadAll<ItemData>("ItemData");
        ItemFactory.itemDataList = new List<ItemData>(itemDatas);
    }
}

【設定手順】
プロジェクトに「Resources/ItemData」フォルダを作成し、各 ItemData アセットを配置。
シーン内に空の GameObject を作成し、「ItemDataManager」と名付け、上記スクリプトをアタッチ。


2-8. アイテム生成コンポーネント

ItemGenerator.cs
一定間隔でアイテムを生成し、出現位置をランダムに設定します。

using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    float span = 1.0f;   // 生成間隔
    float delta = 0;

    // 生成間隔を外部から変更可能に
    public void SetParameter(float span)
    {
        this.span = span;
    }

    void Update()
    {
        delta += Time.deltaTime;
        if (delta > span)
        {
            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);
            }
        }
    }
}

2-9. 各アイテムの個別実装例

AppleItem.cs
Apple の効果としてスコアに 100 加算します。

using UnityEngine;

public class AppleItem : ItemBase
{
    public AudioClip appleSE;

    public override void ApplyEffect()
    {
        // Apple の効果:スコアに 100 加算
        ScoreManager.Instance.AddScore(100);
    }

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

BombItem.cs
Bomb の効果としてスコアを 2 で割ります。

using UnityEngine;

public class BombItem : ItemBase
{
    public AudioClip bombSE;

    public override void ApplyEffect()
    {
        // Bomb の効果:スコアを 2 で割る
        ScoreManager.Instance.DivideScore(2);
    }

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

2-10. スコア管理クラス

ScoreManager.cs
スコア更新・表示を行うシングルトンパターンのクラスです。

using UnityEngine;
using TMPro;

public class ScoreManager : MonoBehaviour
{
    public static ScoreManager Instance { get; private set; }
    private int score = 0;
    public TextMeshProUGUI scoreText; // スコア表示用 UI

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

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

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

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

【設定】
シーン内に ScoreManager 用の GameObject を作成し、Inspector で scoreText に対応する TextMeshProUGUI を設定します。


2-11. ゲーム進行管理&難易度調整

GameDirector.cs
DifficultySettings を参照して、生成間隔および出現率(spawnWeight)を動的に調整します。
時間の閾値も DifficultySettings によりパラメータ化しています。

using UnityEngine;
using TMPro;

public class GameDirector : MonoBehaviour
{
    public DifficultySettings difficultySettings; // Inspector で設定
    GameObject timerText;
    float time;
    GameObject generator;

    void Start()
    {
        time = difficultySettings.initialTime;
        timerText = GameObject.Find("Time");
        generator = GameObject.Find("ItemGenerator");
    }

    void Update()
    {
        time -= Time.deltaTime;
        if (time < 0)
        {
            time = 0;
            // ゲームオーバー時はアイテム生成を停止
            generator.GetComponent<ItemGenerator>().SetParameter(difficultySettings.gameOverInterval);
        }
        else if (time < difficultySettings.stage4Threshold)
        {
            generator.GetComponent<ItemGenerator>().SetParameter(difficultySettings.intervalStage4);
            AdjustSpawnRates(difficultySettings.bombWeightStage4, difficultySettings.appleWeightStage4);
        }
        else if (time < difficultySettings.stage3Threshold)
        {
            generator.GetComponent<ItemGenerator>().SetParameter(difficultySettings.intervalStage3);
            AdjustSpawnRates(difficultySettings.bombWeightStage3, difficultySettings.appleWeightStage3);
        }
        else if (time < difficultySettings.stage2Threshold)
        {
            generator.GetComponent<ItemGenerator>().SetParameter(difficultySettings.intervalStage2);
            AdjustSpawnRates(difficultySettings.bombWeightStage2, difficultySettings.appleWeightStage2);
        }
        else // time >= stage2Threshold
        {
            generator.GetComponent<ItemGenerator>().SetParameter(difficultySettings.intervalStage1);
            AdjustSpawnRates(difficultySettings.bombWeightStage1, difficultySettings.appleWeightStage1);
        }

        timerText.GetComponent<TextMeshProUGUI>().text = time.ToString("F1");
    }

    // 出現率(spawnWeight)の調整メソッド(Enum を使用)
    void AdjustSpawnRates(float bombWeight, float appleWeight)
    {
        foreach (var data in ItemFactory.itemDataList)
        {
            if (data.itemType == ItemType.Bomb)
            {
                data.spawnWeight = bombWeight;
            }
            else if (data.itemType == ItemType.Apple)
            {
                data.spawnWeight = appleWeight;
            }
        }
    }
}

【設定】
シーン上に「Time」という名前の UI オブジェクト(TextMeshProUGUI)を作成し、タイマー表示用として利用してください。
Inspector で DifficultySettings アセットをドラッグ&ドロップして割り当てます。


2-12. バスケット(衝突処理)

BasketController.cs
アイテムと衝突した際、アイテムの効果を呼び出し、サウンドを再生します。

using UnityEngine;

public class BasketController : MonoBehaviour
{
    public AudioSource aud;
    GameObject director;

    void Start()
    {
        Application.targetFrameRate = 60;
        aud = GetComponent<AudioSource>();
        director = GameObject.Find("GameDirector");
    }

    void OnTriggerEnter(Collider other)
    {
        // IItemEffect を実装しているか確認し、あれば効果を適用
        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);
            }
        }
    }
}

3. シーンへの配置と設定

  1. ItemDataManager
    • シーン内に空の GameObject を作成し、名前を「ItemDataManager」とします。
    • 上記 ItemDataManager.cs をアタッチし、Inspector から各 ItemData アセットを登録するか、Resources 配置の方法で設定します。
  2. ItemGenerator
    • 空の GameObject を作成し、名前を「ItemGenerator」として ItemGenerator.cs をアタッチします。
  3. GameDirector
    • 空の GameObject を作成し、名前を「GameDirector」として GameDirector.cs をアタッチします。
    • Inspector で DifficultySettings アセットを割り当てます。
  4. ScoreManager
    • シーン内に ScoreManager 用の GameObject を作成し、 ScoreManager.cs をアタッチします。
    • Inspector で scoreText に対応する TextMeshProUGUI コンポーネントを設定します。
  5. BasketController
    • バスケット用の GameObject に BasketController.cs と AudioSource、Collider(Trigger)を設定します。
  6. UI の設定
    • 「Time」という名前の UI オブジェクト(TextMeshProUGUI)を作成し、GameDirector によりタイマー表示に利用。
    • ScoreManager によるスコア表示用 UI も設定してください。

まとめ

  • データ駆動設計
    ScriptableObject(ItemData、DifficultySettings)を利用して、各アイテムや難易度パラメータをエディタ上で管理し、柔軟な拡張性を実現しました。
  • 共通処理と個別効果の分離
    ItemBase に共通の落下処理を実装し、AppleItemBombItemIItemEffect 経由で独自の効果(ScoreManager 経由のスコア更新)を実装しています。
  • ファクトリ/レジストリパターン
    ItemFactory が登録された ItemData リストから重み付きランダム抽出でアイテムを生成し、各アイテムに個別の落下速度を反映します。
  • リアルタイム難易度調整
    GameDirector が DifficultySettings を参照して、生成間隔および出現率(spawnWeight)を時間経過に合わせて動的に変更します。
    さらに、時間の閾値も DifficultySettings によりパラメータ化され、ハードコーディングを排除しています。
  • Enum によるアイテムタイプ管理
    アイテムの識別に文字列リテラルではなく Enum(ItemType)を使用することで、保守性と安全性を向上させています。
  • 衝突処理
    BasketController がアイテムとの衝突時に効果を呼び出し、サウンドを再生します。

このチュートリアルに沿って実装すれば、柔軟にパラメータ調整が可能な、拡張性・保守性に優れたアイテム管理システムが構築できます。