学習記事一覧 · Unity本格入門

Unity本格入門:いきのこバトルで学ぶ所持品データとオーディオ

題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)

対象読者:第5回(UI)を読み、所持アイテムの保存効果音の鳴らし方をコードレベルで追いたい方
OwnedItemsData.csAudioManager.cs を中心に、JSON セーブ・シングルトン・Resources 読み込みを整理します。

前提第1回:タイトル・セーブデータSaveDataSavableSingletonBase)とは別実装ですが、JsonUtilityPlayerPrefs の考え方は共通です。第5回:UI とメニューItemsDialogItemButton と合わせて読むと流れがつながります。


記事の目次

ポイント一覧


この記事で扱う範囲

  • 全文掲載OwnedItemsData.csAudioManager.cs
  • 参照ItemButton のきのこ使用時(AudioManager.Instance.Play("eat") など)

所持アイテムの JSON 保存と効果音再生(シングルトン)のイメージ

説明(学習のヒント)JsonUtilityPlayerPrefs で所持データを永続化し、Resources から読み込んだ SEAudioManager から鳴らすイメージです。DontDestroyOnLoad による常駐も記事で整理します。


クラス関係(この記事で登場する範囲)

classDiagram direction TB class MonoBehaviour class OwnedItemsData class OwnedItem class AudioManager OwnedItemsData *-- OwnedItem : "ownedItems" MonoBehaviour <|-- AudioManager

説明(学習のヒント)*-- は「所持品リストが OwnedItemsData の一部」という合成。AudioManager はシーン上の MonoBehaviour として存在します。


UMLで整理する(シーケンス)

きのこ使用時に、所持の更新SE 再生セーブがどう繋がるかを表します。

sequenceDiagram participant Btn as ItemButton participant Own as OwnedItemsData participant AM as AudioManager Btn->>Own: "Use" Btn->>AM: "Play" Btn->>Own: "Save"

説明(学習のヒント):ボタン1回で「データ更新 → SE → 保存」の3つが続くので、順番を頭に入れるとデバッグしやすいです。


コードを全部見てみよう

OwnedItemsData.cs(全文)

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class OwnedItemsData
{
    private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";
    public static OwnedItemsData Instance
    {
        get
        {
            if (null == _instance)
            {
                _instance = PlayerPrefs.HasKey(PlayerPrefsKey)
                    ? JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey))
                    : new OwnedItemsData();
            }
            return _instance;
        }
    }
    private static OwnedItemsData _instance;
    public OwnedItem[] OwnedItems => ownedItems.ToArray();
    [SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
    private OwnedItemsData()
    {
    }
    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 (null == item)
        {
            item = new OwnedItem(type);
            ownedItems.Add(item);
        }
        item.Add(number);
    }
    public void Use(Item.ItemType type, int number = 1)
    {
        var item = GetItem(type);
        if (null == item || item.Number < number)
        {
            throw new Exception("アイテムが足りません");
        }
        item.Use(number);
    }
    public OwnedItem GetItem(Item.ItemType type)
    {
        return ownedItems.FirstOrDefault(x => x.Type == type);
    }
    [Serializable]
    public class OwnedItem
    {
        public Item.ItemType Type => type;
        public int Number => number;
        [SerializeField] private Item.ItemType type;
        [SerializeField] private int number;
        public OwnedItem(Item.ItemType type)
        {
            this.type = type;
        }
        public void Add(int addNumber = 1)
        {
            number += addNumber;
        }
        public void Use(int useNumber = 1)
        {
            number -= useNumber;
        }
    }
}

AudioManager.cs(全文)

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
public class AudioManager : MonoBehaviour
{
    [SerializeField] private AudioSource audioSource;
    private readonly Dictionary<string, AudioClip> _clips = new Dictionary<string, AudioClip>();
    public static AudioManager Instance { get; private set; }
    private void Awake()
    {
        if (null != Instance)
        {
            Destroy(gameObject);
            return;
        }
        DontDestroyOnLoad(gameObject);
        Instance = this;
        var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
        foreach (var clip in audioClips)
        {
            _clips.Add(clip.name, clip);
        }
    }
    public void Play(string clipName)
    {
        if (!_clips.ContainsKey(clipName))
        {
            throw new Exception("Sound " + clipName + " is not defined");
        }
        audioSource.clip = _clips[clipName];
        audioSource.Play();
    }
}

ポイント①:OwnedItemsDataInstance と private コンストラクタ

new を外部から抑制するためコンストラクタを private にし、初回アクセス時に PlayerPrefs から復元するか 空のリストで生成します。第1回の SaveData と同様、ゲーム全体でひとつの所持リストとして扱えます。


ポイント②:JSON と固定キーで PlayerPrefs に保存

SaveData はクラス名ハッシュでキーを分けましたが、こちらは OWNED_ITEMS_DATA 固定文字列です。設計の好みの違いとして覚えておけば十分です。


ポイント③:Add / Use / GetItem の流れ

  • GetItem で種類を検索、FirstOrDefault で無ければ null
  • Add は無ければ OwnedItemList に追加してから個数加算
  • Use は不足時に例外(ゲームロジック側で呼び出しを制御)

ポイント④:OwnedItem[Serializable]

JsonUtilityフィールドをシリアライズするため、[SerializeField] 付きの private フィールドと public プロパティの組み合わせで、保存とカプセル化の両立を図っています。


ポイント⑤:AudioManagerDontDestroyOnLoad

DontDestroyOnLoad(gameObject);

タイトルやメインをまたいでも AudioManager の GameObject を破棄しないため、BGM/SE を連続して鳴らしやすくします。プレハブをシーンに1つ置く想定です。


ポイント⑥:二重インスタンスの防止

if (null != Instance)
{
    Destroy(gameObject);
    return;
}

誤ってシーンに2つ置いた場合、後から来た方を破棄し、最初の Instance を維持します。SingletonMonoBehaviourInSceneBase は例外を投げる方式でしたが、DontDestroyOnLoad 系ではこのパターンもよく使われます。


ポイント⑦:Resources.LoadAll で SE を辞書化

var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
    _clips.Add(clip.name, clip);
}

Assets/Resources/2D_SE/ 以下のクリップを起動時に一括ロードし、**ファイル名(clip.name)**で Dictionary から引きます。大規模プロジェクトでは Addressables 等への移行も検討されますが、少数の SE をまとめる用途ではこの形でも扱いやすいです。


ポイント⑧:PlayItemButton からの呼び出し

ItemButton できのこを食べると AudioManager.Instance.Play("eat") が走り、MainSceneController.Instance.EatItem() で満腹を回復し、OwnedItemsData.Instance.Save() で保存、という一連の連携になります(第3回・第5回と接続)。


コードの流れを整理しよう

flowchart LR item["ItemButton OnClick"] item --> eat["Use / EatItem"] item --> se["AudioManager.Play"] item --> save["OwnedItemsData.Save"] boot["AudioManager Awake"] boot --> res["Resources 2D_SE"] boot --> dict["Dictionary clipName"]

説明(学習のヒント):下の3本は「きのこを使うとき」の流れ、上は「AudioManager が起動するとき」の流れです。Resources 読み込みとボタン操作は別のタイミングです。


自分でカスタマイズしてみよう!

挑戦①:SE ファイル名と Play の引数

Resources/2D_SE のファイル名を変えると、clip.name が変わるので、Play("eat") などの文字列と一致させる必要があります。

挑戦②:PlayerPrefs の削除でリセット

開発中は PlayerPrefs.DeleteKey("OWNED_ITEMS_DATA") で所持を初期化できます(エディターの PlayerPrefs 削除機能でも可)。


まとめ

  • OwnedItemsDataprivate コンストラクタ + Instance で所持リストを一元管理し、JSON + PlayerPrefs で永続化する
  • AudioManagerDontDestroyOnLoad でシーン間に残し、Resources で SE を辞書に載せて Play(string) する
  • UI(第5回)・プレイヤー(第3回)から Instance 経由で参照される、という横断的なサービスとして位置づけられる

いきのこバトルについては、第1〜6回で主要な読み筋をカバーしました。別の題材の記事を増やす場合は 目次 の「今後の題材」を参照してください。


最終更新:2026年4月